From 8e8b27c893ab8797e6100ecdfca37b73ccfe3aba Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:40:31 +0000 Subject: [PATCH 01/12] Add RegistrationForm UI component and new Registration wire component Hook it up to MatrixChat instead of the existing logic (this breaks reg). WIP. --- src/components/login/Registration.js | 147 +++++++++++++++++++++ src/components/login/RegistrationForm.js | 126 ++++++++++++++++++ src/skins/vector/views/pages/MatrixChat.js | 20 ++- 3 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 src/components/login/Registration.js create mode 100644 src/components/login/RegistrationForm.js diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js new file mode 100644 index 000000000..a1ebe57ce --- /dev/null +++ b/src/components/login/Registration.js @@ -0,0 +1,147 @@ +/* +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 ServerConfig = require("./ServerConfig"); +var RegistrationForm = require("./RegistrationForm"); + +module.exports = React.createClass({ + displayName: 'Registration', + + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + registerLogic: React.PropTypes.any.isRequired, + // 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.registerLogic.getHomeserverUrl(), + enteredIdentityServerUrl: this.props.registerLogic.getIdentityServerUrl() + }; + }, + + componentWillMount: function() { + + }, + + onHsUrlChanged: function(newHsUrl) { + this.props.registerLogic.setHomeserverUrl(newHsUrl); + this.forceUpdate(); // registration state may have changed. + }, + + onIsUrlChanged: function(newIsUrl) { + this.props.registerLogic.setIdentityServerUrl(newIsUrl); + this.forceUpdate(); // registration state may have changed. + }, + + onFormSubmit: function(formVals) { + console.log("Form vals: %s", formVals); + }, + + onFormValidationFailed: function(errCode) { + console.error("Ruh roh: %s", errCode); + }, + + _getRegisterContentJsx: function() { + var currState = this.props.registerLogic.getState(); + var registerStep; + switch (currState) { + case "Register.COMPLETE": + return this._getPostRegisterJsx(); + case "Register.START": + 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 = ( +
+ This Home Server would like to make sure you are not a robot +
+
+ ); + break; + default: + console.error("Unknown register state: %s", currState); + break; + } + return ( +
+

Create an account

+ {registerStep} + +
{this.state.errorText}
+ + I already have an account + +
+ ); + }, + + _getPostRegisterJsx: function() { + var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); + return ( +
+ Set a display name: + + Upload an avatar: + + +
+ ); + }, + + 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/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 5ed96c376..9c8872e01 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 ContextualMenu = require("../../../../ContextualMenu") +var Login = require("../../../../components/login/Login"); +var Registration = require("../../../../components/login/Registration"); +var Signup = require("matrix-react-sdk/lib/Signup"); module.exports = React.createClass({ displayName: 'MatrixChat', @@ -93,12 +96,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'); @@ -159,15 +165,23 @@ module.exports = React.createClass({ ); } else if (this.state.screen == 'register') { + /* return ( + ); */ + return ( + ); } else { - var Login = require("../../../../components/login/Login"); return ( ); From b4c0625961c3c95302d0cc666e237af7b59eecae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 15:32:44 +0000 Subject: [PATCH 02/12] Show validation errors --- src/components/login/Registration.js | 53 +++++++++++++++------- src/skins/vector/views/pages/MatrixChat.js | 11 +++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index a1ebe57ce..c44d13685 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -22,6 +22,7 @@ var sdk = require('matrix-react-sdk'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var ServerConfig = require("./ServerConfig"); var RegistrationForm = require("./RegistrationForm"); +var MIN_PASSWORD_LENGTH = 6; module.exports = React.createClass({ displayName: 'Registration', @@ -61,7 +62,40 @@ module.exports = React.createClass({ }, onFormValidationFailed: function(errCode) { - console.error("Ruh roh: %s", 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 + }); + }, + + _getPostRegisterJsx: function() { + var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); + return ( +
+ Set a display name: + + Upload an avatar: + + +
+ ); }, _getRegisterContentJsx: function() { @@ -74,7 +108,7 @@ module.exports = React.createClass({ registerStep = ( ); @@ -117,21 +151,6 @@ module.exports = React.createClass({ ); }, - _getPostRegisterJsx: function() { - var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); - var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); - return ( -
- Set a display name: - - Upload an avatar: - - -
- ); - }, - render: function() { return (
diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 51de92a7b..85506c213 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -176,12 +176,17 @@ module.exports = React.createClass({ /> ); */ return ( + var registerLogic = new Signup.Register( + config.default_hs_url, config.default_is_url + ); + registerLogic.setClientSecret(this.state.register_client_secret); + registerLogic.setSessionId(this.state.register_session_id); + registerLogic.setRegistrationUrl(this.props.registrationUrl); + registerLogic.setIdSid(this.state.register_id_sid); + registerLogic={registerLogic} /> ); } else { return ( From 5424567a66b9a1ecc6a1808abce4bb9d85e0e547 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:15:20 +0000 Subject: [PATCH 03/12] Hook up onFormSubmit to make registration (dummy only) work again. --- src/components/login/Registration.js | 46 +++++++++++++++++++--- src/skins/vector/views/pages/MatrixChat.js | 12 +++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index c44d13685..05960c610 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -20,6 +20,7 @@ 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 MIN_PASSWORD_LENGTH = 6; @@ -44,7 +45,11 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); }, onHsUrlChanged: function(newHsUrl) { @@ -57,8 +62,38 @@ module.exports = React.createClass({ this.forceUpdate(); // registration state may have changed. }, + onAction: function(payload) { + if (payload.action !== "registration_step_update") { + return; + } + this.forceUpdate(); + }, + onFormSubmit: function(formVals) { - console.log("Form vals: %s", formVals); + var self = this; + this.props.registerLogic.register(formVals).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.props.registerLogic.getCredentials(); + } + self.props.onLoggedIn({ + userId: response.user_id, + homeserverUrl: self.props.registerLogic.getHomeserverUrl(), + identityServerUrl: self.props.registerLogic.getIdentityServerUrl(), + accessToken: response.access_token + }); + }, function(err) { + if (err.message) { + self.setState({ + errorText: err.message + }); + } + console.log(err); + }); }, onFormValidationFailed: function(errCode) { @@ -99,12 +134,13 @@ module.exports = React.createClass({ }, _getRegisterContentJsx: function() { - var currState = this.props.registerLogic.getState(); + var currStep = this.props.registerLogic.getStep(); var registerStep; - switch (currState) { + switch (currStep) { case "Register.COMPLETE": return this._getPostRegisterJsx(); case "Register.START": + case "Register.STEP_m.login.dummy": registerStep = (

Create an account

{registerStep} +
{this.state.errorText}
-
{this.state.errorText}
I already have an account diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 85506c213..7dafff91f 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -175,14 +175,14 @@ module.exports = React.createClass({ registrationUrl={this.props.registrationUrl} /> ); */ - return ( - var registerLogic = new Signup.Register( + var registerLogic = new Signup.Register( config.default_hs_url, config.default_is_url ); - registerLogic.setClientSecret(this.state.register_client_secret); - registerLogic.setSessionId(this.state.register_session_id); - registerLogic.setRegistrationUrl(this.props.registrationUrl); - registerLogic.setIdSid(this.state.register_id_sid); + registerLogic.setClientSecret(this.state.register_client_secret); + registerLogic.setSessionId(this.state.register_session_id); + registerLogic.setRegistrationUrl(this.props.registrationUrl); + registerLogic.setIdSid(this.state.register_id_sid); + return ( Date: Wed, 18 Nov 2015 17:46:17 +0000 Subject: [PATCH 04/12] Load the Recaptcha script if we have a container for it This is complex enough that the Registration component shouldn't have to care about it, so it should probably be split into a pure UI component. --- src/components/login/Registration.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 05960c610..ea121ba02 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -48,25 +48,41 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); }, + componentDidUpdate: function() { + // Just putting a script tag into the returned jsx doesn't work, annoyingly, + // so we do this instead. + var self = this; + if (this.refs.recaptchaContainer) { + console.log("Loading recaptcha script..."); + var scriptTag = document.createElement('script'); + window.mx_on_recaptcha_loaded = function() { + console.log("Loaded recaptcha script."); + self.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); + }; + scriptTag.setAttribute( + 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" + ); + this.refs.recaptchaContainer.appendChild(scriptTag); + } + }, + componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, onHsUrlChanged: function(newHsUrl) { this.props.registerLogic.setHomeserverUrl(newHsUrl); - this.forceUpdate(); // registration state may have changed. }, onIsUrlChanged: function(newIsUrl) { this.props.registerLogic.setIdentityServerUrl(newIsUrl); - this.forceUpdate(); // registration state may have changed. }, onAction: function(payload) { if (payload.action !== "registration_step_update") { return; } - this.forceUpdate(); + this.forceUpdate(); // registration state has changed. }, onFormSubmit: function(formVals) { From eaafc11064b6daa976d635706ad68e8bf120db9d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 13:44:11 +0000 Subject: [PATCH 05/12] Factor out Captcha UI --- src/components/login/CaptchaForm.js | 68 ++++++++++++++++++++++ src/components/login/Registration.js | 37 +++++------- src/skins/vector/views/pages/MatrixChat.js | 4 +- 3 files changed, 85 insertions(+), 24 deletions(-) create mode 100644 src/components/login/CaptchaForm.js diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js new file mode 100644 index 000000000..81e5136ce --- /dev/null +++ b/src/components/login/CaptchaForm.js @@ -0,0 +1,68 @@ +/* +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 captcha form. + */ +module.exports = React.createClass({ + displayName: 'CaptchaForm', + + propTypes: { + onCaptchaLoaded: React.PropTypes.func.isRequired + }, + + getDefaultProps: function() { + return { + onCaptchaLoaded: function() { + console.error("Unhandled onCaptchaLoaded"); + } + }; + }, + + componentDidMount: function() { + // Just putting a script tag into the returned jsx doesn't work, annoyingly, + // so we do this instead. + console.log("CDM"); + var self = this; + if (this.refs.recaptchaContainer) { + console.log("Loading recaptcha script..."); + var scriptTag = document.createElement('script'); + window.mx_on_recaptcha_loaded = function() { + console.log("Loaded recaptcha script."); + self.props.onCaptchaLoaded(); + }; + scriptTag.setAttribute( + 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" + ); + this.refs.recaptchaContainer.appendChild(scriptTag); + } + }, + + render: function() { + // FIXME: Tight coupling with the div id and SignupStages.js + return ( +
+ This Home Server would like to make sure you are not a robot +
+
+ ); + } +}); \ No newline at end of file diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index ea121ba02..38cc51b7b 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -23,6 +23,7 @@ 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("./CaptchaForm"); var MIN_PASSWORD_LENGTH = 6; module.exports = React.createClass({ @@ -48,24 +49,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); }, - componentDidUpdate: function() { - // Just putting a script tag into the returned jsx doesn't work, annoyingly, - // so we do this instead. - var self = this; - if (this.refs.recaptchaContainer) { - console.log("Loading recaptcha script..."); - var scriptTag = document.createElement('script'); - window.mx_on_recaptcha_loaded = function() { - console.log("Loaded recaptcha script."); - self.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); - }; - scriptTag.setAttribute( - 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" - ); - this.refs.recaptchaContainer.appendChild(scriptTag); - } - }, - componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, @@ -96,6 +79,14 @@ module.exports = React.createClass({ // no matter, we'll grab it direct response = self.props.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; + } + // TODO: do post-register stuff self.props.onLoggedIn({ userId: response.user_id, homeserverUrl: self.props.registerLogic.getHomeserverUrl(), @@ -134,6 +125,11 @@ module.exports = React.createClass({ }); }, + onCaptchaLoaded: function() { + this.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); + }, + + // TODO: I wonder if this should actually be a different component... _getPostRegisterJsx: function() { var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); @@ -174,10 +170,7 @@ module.exports = React.createClass({ break; case "Register.STEP_m.login.recaptcha": registerStep = ( -
- This Home Server would like to make sure you are not a robot -
-
+ ); break; default: diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 6a5031db3..7198ec947 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -187,8 +187,8 @@ module.exports = React.createClass({ /> ); */ var registerLogic = new Signup.Register( - config.default_hs_url, config.default_is_url - ); + config.default_hs_url, config.default_is_url + ); registerLogic.setClientSecret(this.state.register_client_secret); registerLogic.setSessionId(this.state.register_session_id); registerLogic.setRegistrationUrl(this.props.registrationUrl); From e700a5a21942ff7b95434a083dedd4c49690e823 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 13:58:52 +0000 Subject: [PATCH 06/12] Add TODO on post register logic --- src/components/login/Registration.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 38cc51b7b..627f9b276 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -86,7 +86,6 @@ module.exports = React.createClass({ }); return; } - // TODO: do post-register stuff self.props.onLoggedIn({ userId: response.user_id, homeserverUrl: self.props.registerLogic.getHomeserverUrl(), @@ -129,7 +128,10 @@ module.exports = React.createClass({ this.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); }, - // TODO: I wonder if this should actually be a different component... + // 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'); From 2d481a630212d80e4861c25f76073a34e04e5742 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 14:17:18 +0000 Subject: [PATCH 07/12] Recheck registration state since we may be able to immediately do an HTTP hit if we've been given good QPs --- src/skins/vector/views/pages/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 7198ec947..ef0231579 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -193,6 +193,7 @@ module.exports = React.createClass({ registerLogic.setSessionId(this.state.register_session_id); registerLogic.setRegistrationUrl(this.props.registrationUrl); registerLogic.setIdSid(this.state.register_id_sid); + registerLogic.recheckState(); return ( Date: Thu, 19 Nov 2015 15:44:17 +0000 Subject: [PATCH 08/12] Instantiate Signup.Register in Registration component This has to be done rather than in MatrixChat because the render() calls will create new instances otherwise. Pass in all the strings the logic class requires to the Registration wire component. This isn't the "best" solution because unloading/reloading the Registration component will lose registration state which should be persisted. Ideally we'd DI from the top to ensure this can't happen (as opposed to relying on module globals...) --- src/components/login/CaptchaForm.js | 1 - src/components/login/Registration.js | 52 +++++++++++++++++----- src/skins/vector/views/pages/MatrixChat.js | 26 +++-------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js index 81e5136ce..f82ace8ec 100644 --- a/src/components/login/CaptchaForm.js +++ b/src/components/login/CaptchaForm.js @@ -40,7 +40,6 @@ module.exports = React.createClass({ componentDidMount: function() { // Just putting a script tag into the returned jsx doesn't work, annoyingly, // so we do this instead. - console.log("CDM"); var self = this; if (this.refs.recaptchaContainer) { console.log("Loading recaptcha script..."); diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 627f9b276..99b58ad14 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -24,6 +24,7 @@ var dis = require('matrix-react-sdk/lib/dispatcher'); var ServerConfig = require("./ServerConfig"); var RegistrationForm = require("./RegistrationForm"); var CaptchaForm = require("./CaptchaForm"); +var Signup = require("matrix-react-sdk/lib/Signup"); var MIN_PASSWORD_LENGTH = 6; module.exports = React.createClass({ @@ -31,7 +32,12 @@ module.exports = React.createClass({ propTypes: { onLoggedIn: React.PropTypes.func.isRequired, - registerLogic: React.PropTypes.any.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 }, @@ -40,25 +46,43 @@ module.exports = React.createClass({ return { busy: false, errorText: null, - enteredHomeserverUrl: this.props.registerLogic.getHomeserverUrl(), - enteredIdentityServerUrl: this.props.registerLogic.getIdentityServerUrl() + 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.props.registerLogic.setHomeserverUrl(newHsUrl); + this.registerLogic.setHomeserverUrl(newHsUrl); }, onIsUrlChanged: function(newIsUrl) { - this.props.registerLogic.setIdentityServerUrl(newIsUrl); + this.registerLogic.setIdentityServerUrl(newIsUrl); }, onAction: function(payload) { @@ -70,14 +94,20 @@ module.exports = React.createClass({ onFormSubmit: function(formVals) { var self = this; - this.props.registerLogic.register(formVals).done(function(response) { + 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.props.registerLogic.getCredentials(); + response = self.registerLogic.getCredentials(); } if (!response || !response.user_id || !response.access_token) { console.error("Final response is missing keys."); @@ -88,8 +118,8 @@ module.exports = React.createClass({ } self.props.onLoggedIn({ userId: response.user_id, - homeserverUrl: self.props.registerLogic.getHomeserverUrl(), - identityServerUrl: self.props.registerLogic.getIdentityServerUrl(), + homeserverUrl: self.registerLogic.getHomeserverUrl(), + identityServerUrl: self.registerLogic.getIdentityServerUrl(), accessToken: response.access_token }); }, function(err) { @@ -125,7 +155,7 @@ module.exports = React.createClass({ }, onCaptchaLoaded: function() { - this.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); + this.registerLogic.tellStage("m.login.recaptcha", "loaded"); }, // TODO: @@ -148,7 +178,7 @@ module.exports = React.createClass({ }, _getRegisterContentJsx: function() { - var currStep = this.props.registerLogic.getStep(); + var currStep = this.registerLogic.getStep(); var registerStep; switch (currStep) { case "Register.COMPLETE": diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index ef0231579..3d5856519 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -27,7 +27,6 @@ var Matrix = require("matrix-js-sdk"); var ContextualMenu = require("../../../../ContextualMenu") var Login = require("../../../../components/login/Login"); var Registration = require("../../../../components/login/Registration"); -var Signup = require("matrix-react-sdk/lib/Signup"); var config = require("../../../../../config.json"); module.exports = React.createClass({ @@ -178,27 +177,16 @@ module.exports = React.createClass({
); } else if (this.state.screen == 'register') { - /* - return ( - - ); */ - var registerLogic = new Signup.Register( - config.default_hs_url, config.default_is_url - ); - registerLogic.setClientSecret(this.state.register_client_secret); - registerLogic.setSessionId(this.state.register_session_id); - registerLogic.setRegistrationUrl(this.props.registrationUrl); - registerLogic.setIdSid(this.state.register_id_sid); - registerLogic.recheckState(); return ( + onLoginClick={this.onLoginClick} /> ); } else { return ( From bb6eeea0d8aa672fde93eac96e5fd8c9d905c786 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:08:25 +0000 Subject: [PATCH 09/12] Factor out div ID name to avoid tight coupling with logic class. --- src/components/login/CaptchaForm.js | 8 +++++--- src/components/login/Registration.js | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js index f82ace8ec..0bbf94883 100644 --- a/src/components/login/CaptchaForm.js +++ b/src/components/login/CaptchaForm.js @@ -19,6 +19,8 @@ limitations under the License. var React = require('react'); var sdk = require('matrix-react-sdk') +var DIV_ID = 'mx_recaptcha'; + /** * A pure UI component which displays a captcha form. */ @@ -26,7 +28,7 @@ module.exports = React.createClass({ displayName: 'CaptchaForm', propTypes: { - onCaptchaLoaded: React.PropTypes.func.isRequired + onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name }, getDefaultProps: function() { @@ -46,7 +48,7 @@ module.exports = React.createClass({ var scriptTag = document.createElement('script'); window.mx_on_recaptcha_loaded = function() { console.log("Loaded recaptcha script."); - self.props.onCaptchaLoaded(); + self.props.onCaptchaLoaded(DIV_ID); }; scriptTag.setAttribute( 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" @@ -60,7 +62,7 @@ module.exports = React.createClass({ return (
This Home Server would like to make sure you are not a robot -
+
); } diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 99b58ad14..54cbeeb08 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -154,8 +154,10 @@ module.exports = React.createClass({ }); }, - onCaptchaLoaded: function() { - this.registerLogic.tellStage("m.login.recaptcha", "loaded"); + onCaptchaLoaded: function(divIdName) { + this.registerLogic.tellStage("m.login.recaptcha", { + divId: divIdName + }); }, // TODO: From d372018e61e5203c30def00653a5dd57d725cad7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:47:14 +0000 Subject: [PATCH 10/12] Minor tweaks --- src/components/login/Registration.js | 2 +- src/skins/vector/views/pages/MatrixChat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 54cbeeb08..c721b0050 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -184,7 +184,7 @@ module.exports = React.createClass({ var registerStep; switch (currStep) { case "Register.COMPLETE": - return this._getPostRegisterJsx(); + return; // this._getPostRegisterJsx(); case "Register.START": case "Register.STEP_m.login.dummy": registerStep = ( diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 3d5856519..81268d5ad 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -24,7 +24,7 @@ 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 ContextualMenu = require("../../../../ContextualMenu"); var Login = require("../../../../components/login/Login"); var Registration = require("../../../../components/login/Registration"); var config = require("../../../../../config.json"); From f62312fbf378054beee855108d3693e0f138142b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:18:04 +0000 Subject: [PATCH 11/12] Remove old registration files. Move CaptchaForm to React SDK. --- src/components/login/CaptchaForm.js | 69 ------ src/components/login/Registration.js | 2 +- src/controllers/templates/Register.js | 58 ----- src/skins/vector/skindex.js | 3 +- src/skins/vector/views/templates/Register.js | 216 ------------------- 5 files changed, 2 insertions(+), 346 deletions(-) delete mode 100644 src/components/login/CaptchaForm.js delete mode 100644 src/controllers/templates/Register.js delete mode 100644 src/skins/vector/views/templates/Register.js diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js deleted file mode 100644 index 0bbf94883..000000000 --- a/src/components/login/CaptchaForm.js +++ /dev/null @@ -1,69 +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 React = require('react'); -var sdk = require('matrix-react-sdk') - -var DIV_ID = 'mx_recaptcha'; - -/** - * A pure UI component which displays a captcha form. - */ -module.exports = React.createClass({ - displayName: 'CaptchaForm', - - propTypes: { - onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name - }, - - getDefaultProps: function() { - return { - onCaptchaLoaded: function() { - console.error("Unhandled onCaptchaLoaded"); - } - }; - }, - - componentDidMount: function() { - // Just putting a script tag into the returned jsx doesn't work, annoyingly, - // so we do this instead. - var self = this; - if (this.refs.recaptchaContainer) { - console.log("Loading recaptcha script..."); - var scriptTag = document.createElement('script'); - window.mx_on_recaptcha_loaded = function() { - console.log("Loaded recaptcha script."); - self.props.onCaptchaLoaded(DIV_ID); - }; - scriptTag.setAttribute( - 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" - ); - this.refs.recaptchaContainer.appendChild(scriptTag); - } - }, - - render: function() { - // FIXME: Tight coupling with the div id and SignupStages.js - return ( -
- This Home Server would like to make sure you are not a robot -
-
- ); - } -}); \ No newline at end of file diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index c721b0050..f7a9be36f 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -23,7 +23,7 @@ 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("./CaptchaForm"); +var CaptchaForm = require("matrix-react-sdk/lib/components/login/CaptchaForm"); var Signup = require("matrix-react-sdk/lib/Signup"); var MIN_PASSWORD_LENGTH = 6; 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/templates/Register.js b/src/skins/vector/views/templates/Register.js deleted file mode 100644 index 8d6bbf42e..000000000 --- a/src/skins/vector/views/templates/Register.js +++ /dev/null @@ -1,216 +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 React = require('react'); - -var sdk = require('matrix-react-sdk') -var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg') - -var RegisterController = require('../../../../controllers/templates/Register') -var ServerConfig = require("../../../../components/login/ServerConfig"); - -var config = require('../../../../../config.json'); - -module.exports = React.createClass({ - displayName: 'Register', - mixins: [RegisterController], - - getInitialState: function() { - // TODO: factor out all localstorage stuff into its own home. - // This is common to Login, Register and MatrixClientPeg - var localStorage = window.localStorage; - var hs_url, is_url; - if (localStorage) { - hs_url = localStorage.getItem("mx_hs_url"); - is_url = localStorage.getItem("mx_is_url"); - } - - // make sure we have our MatrixClient set up whatever - // Useful for debugging only. - // MatrixClientPeg.replaceUsingUrls( - // hs_url || config.default_hs_url, - // is_url || config.default_is_url - // ); - - return { - customHsUrl: hs_url || config.default_hs_url, - customIsUrl: is_url || config.default_is_url, - serverConfigVisible: (hs_url && hs_url !== config.default_hs_url || - is_url && is_url !== config.default_is_url) - } - }, - - getRegFormVals: function() { - return { - email: this.refs.email.value.trim(), - username: this.refs.username.value.trim(), - password: this.refs.password.value.trim(), - confirmPassword: this.refs.confirmPassword.value.trim() - }; - }, - - getHsUrl: function() { - if (this.state.serverConfigVisible) { - return this.state.customHsUrl; - } else { - return config.default_hs_url; - } - }, - - getIsUrl: function() { - if (this.state.serverConfigVisible) { - return this.state.customIsUrl; - } else { - return config.default_is_url; - } - }, - - onServerConfigVisibleChange: function(ev) { - this.setState({ - serverConfigVisible: ev.target.checked - }); - }, - - onServerUrlChanged: function(newUrl) { - this.setState({ - customHsUrl: this.refs.serverConfig.getHsUrl(), - customIsUrl: this.refs.serverConfig.getIsUrl(), - }); - this.forceUpdate(); - }, - - onProfileContinueClicked: function() { - this.onAccountReady(); - }, - - componentForStep: function(step) { - switch (step) { - case 'initial': - var serverConfigStyle = {}; - serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none'; - 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()} -
-
- ); - } -}); From 3075c97baec941039297a2aea7772c668eb381e0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:27:21 +0000 Subject: [PATCH 12/12] Set busy Spinner --- src/components/login/Registration.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index f7a9be36f..8fda406d4 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -94,6 +94,10 @@ module.exports = React.createClass({ onFormSubmit: function(formVals) { var self = this; + this.setState({ + errorText: "", + busy: true + }); this.onProcessingRegistration(this.registerLogic.register(formVals)); }, @@ -122,12 +126,18 @@ module.exports = React.createClass({ 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); }); }, @@ -158,6 +168,9 @@ module.exports = React.createClass({ this.registerLogic.tellStage("m.login.recaptcha", { divId: divIdName }); + this.setState({ + busy: false // requires user input + }); }, // TODO: @@ -211,11 +224,19 @@ module.exports = React.createClass({ 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}