diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js new file mode 100644 index 000000000..8fda406d4 --- /dev/null +++ b/src/components/login/Registration.js @@ -0,0 +1,266 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); + +var sdk = require('matrix-react-sdk'); +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var dis = require('matrix-react-sdk/lib/dispatcher'); +var ServerConfig = require("./ServerConfig"); +var RegistrationForm = require("./RegistrationForm"); +var CaptchaForm = require("matrix-react-sdk/lib/components/login/CaptchaForm"); +var Signup = require("matrix-react-sdk/lib/Signup"); +var MIN_PASSWORD_LENGTH = 6; + +module.exports = React.createClass({ + displayName: 'Registration', + + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + clientSecret: React.PropTypes.string, + sessionId: React.PropTypes.string, + registrationUrl: React.PropTypes.string, + idSid: React.PropTypes.string, + hsUrl: React.PropTypes.string, + isUrl: React.PropTypes.string, + // registration shouldn't know or care how login is done. + onLoginClick: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + busy: false, + errorText: null, + enteredHomeserverUrl: this.props.hsUrl, + enteredIdentityServerUrl: this.props.isUrl + }; + }, + + componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + // attach this to the instance rather than this.state since it isn't UI + this.registerLogic = new Signup.Register( + this.props.hsUrl, this.props.isUrl + ); + this.registerLogic.setClientSecret(this.props.clientSecret); + this.registerLogic.setSessionId(this.props.sessionId); + this.registerLogic.setRegistrationUrl(this.props.registrationUrl); + this.registerLogic.setIdSid(this.props.idSid); + this.registerLogic.recheckState(); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + componentDidMount: function() { + // may have already done an HTTP hit (e.g. redirect from an email) so + // check for any pending response + var promise = this.registerLogic.getPromise(); + if (promise) { + this.onProcessingRegistration(promise); + } + }, + + onHsUrlChanged: function(newHsUrl) { + this.registerLogic.setHomeserverUrl(newHsUrl); + }, + + onIsUrlChanged: function(newIsUrl) { + this.registerLogic.setIdentityServerUrl(newIsUrl); + }, + + onAction: function(payload) { + if (payload.action !== "registration_step_update") { + return; + } + this.forceUpdate(); // registration state has changed. + }, + + onFormSubmit: function(formVals) { + var self = this; + this.setState({ + errorText: "", + busy: true + }); + this.onProcessingRegistration(this.registerLogic.register(formVals)); + }, + + // Promise is resolved when the registration process is FULLY COMPLETE + onProcessingRegistration: function(promise) { + var self = this; + promise.done(function(response) { + if (!response || !response.access_token) { + console.warn( + "FIXME: Register fulfilled without a final response, " + + "did you break the promise chain?" + ); + // no matter, we'll grab it direct + response = self.registerLogic.getCredentials(); + } + if (!response || !response.user_id || !response.access_token) { + console.error("Final response is missing keys."); + self.setState({ + errorText: "There was a problem processing the response." + }); + return; + } + self.props.onLoggedIn({ + userId: response.user_id, + homeserverUrl: self.registerLogic.getHomeserverUrl(), + identityServerUrl: self.registerLogic.getIdentityServerUrl(), + accessToken: response.access_token + }); + self.setState({ + busy: false + }); + }, function(err) { + if (err.message) { + self.setState({ + errorText: err.message + }); + } + self.setState({ + busy: false + }); + console.log(err); + }); + }, + + onFormValidationFailed: function(errCode) { + var errMsg; + switch (errCode) { + case "RegistrationForm.ERR_PASSWORD_MISSING": + errMsg = "Missing password."; + break; + case "RegistrationForm.ERR_PASSWORD_MISMATCH": + errMsg = "Passwords don't match."; + break; + case "RegistrationForm.ERR_PASSWORD_LENGTH": + errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; + break; + default: + console.error("Unknown error code: %s", errCode); + errMsg = "An unknown error occurred."; + break; + } + this.setState({ + errorText: errMsg + }); + }, + + onCaptchaLoaded: function(divIdName) { + this.registerLogic.tellStage("m.login.recaptcha", { + divId: divIdName + }); + this.setState({ + busy: false // requires user input + }); + }, + + // TODO: + // This should really be a different component which MatrixChat then + // instantiates rather than having it pollute registration logic. There is + // no reason to wedge them together here. This function is currently NOT CALLED. + _getPostRegisterJsx: function() { + var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); + return ( +
+ Set a display name: + + Upload an avatar: + + +
+ ); + }, + + _getRegisterContentJsx: function() { + var currStep = this.registerLogic.getStep(); + var registerStep; + switch (currStep) { + case "Register.COMPLETE": + return; // this._getPostRegisterJsx(); + case "Register.START": + case "Register.STEP_m.login.dummy": + registerStep = ( + + ); + break; + case "Register.STEP_m.login.email.identity": + registerStep = ( +
+ Please check your email to continue registration. +
+ ); + break; + case "Register.STEP_m.login.recaptcha": + registerStep = ( + + ); + break; + default: + console.error("Unknown register state: %s", currStep); + break; + } + var busySpinner; + if (this.state.busy) { + var Spinner = sdk.getComponent("atoms.Spinner"); + busySpinner = ( + + ); + } + return ( +
+

Create an account

+ {registerStep} +
{this.state.errorText}
+ {busySpinner} + + + I already have an account + +
+ ); + }, + + render: function() { + return ( +
+
+
+ vector +
+ {this._getRegisterContentJsx()} +
+
+ ); + } +}); diff --git a/src/components/login/RegistrationForm.js b/src/components/login/RegistrationForm.js new file mode 100644 index 000000000..3f9fb6ae1 --- /dev/null +++ b/src/components/login/RegistrationForm.js @@ -0,0 +1,126 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var sdk = require('matrix-react-sdk') + +/** + * A pure UI component which displays a registration form. + */ +module.exports = React.createClass({ + displayName: 'RegistrationForm', + + propTypes: { + defaultEmail: React.PropTypes.string, + defaultUsername: React.PropTypes.string, + showEmail: React.PropTypes.bool, + minPasswordLength: React.PropTypes.number, + onError: React.PropTypes.func, + onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise + }, + + getDefaultProps: function() { + return { + showEmail: false, + minPasswordLength: 6, + onError: function(e) { + console.error(e); + } + }; + }, + + getInitialState: function() { + return { + email: this.props.defaultEmail, + username: this.props.defaultUsername, + password: null, + passwordConfirm: null + }; + }, + + onSubmit: function(ev) { + ev.preventDefault(); + + var pwd1 = this.refs.password.value.trim(); + var pwd2 = this.refs.passwordConfirm.value.trim() + + var errCode; + if (!pwd1 || !pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISSING"; + } + else if (pwd1 !== pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH"; + } + else if (pwd1.length < this.props.minPasswordLength) { + errCode = "RegistrationForm.ERR_PASSWORD_LENGTH"; + } + if (errCode) { + this.props.onError(errCode); + return; + } + + var promise = this.props.onRegisterClick({ + username: this.refs.username.value.trim(), + password: pwd1, + email: this.refs.email.value.trim() + }); + + if (promise) { + ev.target.disabled = true; + promise.finally(function() { + ev.target.disabled = false; + }); + } + }, + + render: function() { + var emailSection, registerButton; + if (this.props.showEmail) { + emailSection = ( + + ); + } + if (this.props.onRegisterClick) { + registerButton = ( + + ); + } + + return ( +
+
+ {emailSection} +
+ +
+ +
+ +
+ {registerButton} +
+
+ ); + } +}); diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js deleted file mode 100644 index 5f88d5905..000000000 --- a/src/controllers/templates/Register.js +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -var extend = require('matrix-react-sdk/lib/extend'); -var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); -var BaseRegisterController = require('matrix-react-sdk/lib/controllers/templates/Register.js'); - -var RegisterController = {}; -extend(RegisterController, BaseRegisterController); - -RegisterController.onRegistered = function(user_id, access_token) { - MatrixClientPeg.replaceUsingAccessToken( - this.state.hs_url, this.state.is_url, user_id, access_token - ); - - this.setState({ - step: 'profile', - busy: true - }); - - var self = this; - var cli = MatrixClientPeg.get(); - cli.getProfileInfo(cli.credentials.userId).done(function(result) { - self.setState({ - avatarUrl: result.avatar_url, - busy: false - }); - }, - function(err) { - console.err(err); - self.setState({ - busy: false - }); - }); -}; - -RegisterController.onAccountReady = function() { - if (this.props.onLoggedIn) { - this.props.onLoggedIn(); - } -}; - -module.exports = RegisterController; diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index 45d60f730..a2b8d8cd3 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -85,6 +85,5 @@ skin['organisms.UserSettings'] = require('./views/organisms/UserSettings'); skin['organisms.ViewSource'] = require('./views/organisms/ViewSource'); skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage'); skin['pages.MatrixChat'] = require('./views/pages/MatrixChat'); -skin['templates.Register'] = require('./views/templates/Register'); -module.exports = skin; \ No newline at end of file +module.exports = skin; diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 1a2ea288b..81268d5ad 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -23,7 +23,10 @@ var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/Matri var dis = require('matrix-react-sdk/lib/dispatcher'); var Matrix = require("matrix-js-sdk"); + var ContextualMenu = require("../../../../ContextualMenu"); +var Login = require("../../../../components/login/Login"); +var Registration = require("../../../../components/login/Registration"); var config = require("../../../../../config.json"); module.exports = React.createClass({ @@ -102,12 +105,15 @@ module.exports = React.createClass({ this.showScreen("register"); }, + onLoginClick: function() { + this.showScreen("login"); + }, + render: function() { var LeftPanel = sdk.getComponent('organisms.LeftPanel'); var RoomView = sdk.getComponent('organisms.RoomView'); var RightPanel = sdk.getComponent('organisms.RightPanel'); var UserSettings = sdk.getComponent('organisms.UserSettings'); - var Register = sdk.getComponent('templates.Register'); var CreateRoom = sdk.getComponent('organisms.CreateRoom'); var RoomDirectory = sdk.getComponent('organisms.RoomDirectory'); var MatrixToolbar = sdk.getComponent('molecules.MatrixToolbar'); @@ -172,14 +178,17 @@ module.exports = React.createClass({ ); } else if (this.state.screen == 'register') { return ( - + onLoggedIn={this.onLoggedIn} + onLoginClick={this.onLoginClick} /> ); } else { - var Login = require("../../../../components/login/Login"); return ( -
-
-
-
-
- - - -
- -
-
- -
- - ); - // XXX: clearly these should be separate organisms - case 'stage_m.login.email.identity': - return ( -
- Please check your email to continue registration. -
- ); - case 'stage_m.login.recaptcha': - return ( -
- This Home Server would like to make sure you are not a robot -
-
- ); - } - }, - - registerContent: function() { - if (this.state.busy) { - var Loader = sdk.getComponent("atoms.Spinner"); - return ( - - ); - } else if (this.state.step == 'profile') { - var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); - var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); - return ( -
- Set a display name: - - Upload an avatar: - - -
- ); - } else { - return ( -
-

Create an account

- {this.componentForStep(this.state.step)} -
{this.state.errorText}
- I already have an account -
- ); - } - }, - - onBadFields: function(bad) { - var keys = Object.keys(bad); - var strings = []; - for (var i = 0; i < keys.length; ++i) { - switch (bad[keys[i]]) { - case this.FieldErrors.PasswordMismatch: - strings.push("Passwords don't match"); - break; - case this.FieldErrors.Missing: - strings.push("Missing "+keys[i]); - break; - case this.FieldErrors.TooShort: - strings.push(keys[i]+" is too short"); - break; - case this.FieldErrors.InUse: - strings.push(keys[i]+" is already taken"); - break; - case this.FieldErrors.Length: - strings.push(keys[i] + " is not long enough."); - break; - default: - console.error("Unhandled FieldError: %s", bad[keys[i]]); - break; - } - } - var errtxt = strings.join(', '); - this.setState({ - errorText: errtxt - }); - }, - - render: function() { - return ( -
-
-
- vector -
- {this.registerContent()} -
-
- ); - } -});