+ );
+ }
+});
diff --git a/src/components/structures/login/PostRegistration.js b/src/components/structures/login/PostRegistration.js
new file mode 100644
index 0000000000..5af8d37dd8
--- /dev/null
+++ b/src/components/structures/login/PostRegistration.js
@@ -0,0 +1,80 @@
+/*
+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('../../../index');
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+
+module.exports = React.createClass({
+ displayName: 'PostRegistration',
+
+ propTypes: {
+ onComplete: React.PropTypes.func.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ avatarUrl: null,
+ errorString: null,
+ busy: false
+ };
+ },
+
+ componentWillMount: function() {
+ // There is some assymetry between ChangeDisplayName and ChangeAvatar,
+ // as ChangeDisplayName will auto-get the name but ChangeAvatar expects
+ // the URL to be passed to you (because it's also used for room avatars).
+ var cli = MatrixClientPeg.get();
+ this.setState({busy: true});
+ var self = this;
+ cli.getProfileInfo(cli.credentials.userId).done(function(result) {
+ self.setState({
+ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
+ busy: false
+ });
+ }, function(error) {
+ self.setState({
+ errorString: "Failed to fetch avatar URL",
+ busy: false
+ });
+ });
+ },
+
+ render: function() {
+ var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
+ var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
+ return (
+
+
+
+
+
+
+ Set a display name:
+
+ Upload an avatar:
+
+
+ {this.state.errorString}
+
+
+
+ );
+ }
+});
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
new file mode 100644
index 0000000000..0cb1a87752
--- /dev/null
+++ b/src/components/structures/login/Registration.js
@@ -0,0 +1,248 @@
+/*
+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('../../../index');
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+var dis = require('../../../dispatcher');
+var Signup = require("../../../Signup");
+var ServerConfig = require("../../views/login/ServerConfig");
+var RegistrationForm = require("../../views/login/RegistrationForm");
+var CaptchaForm = require("../../views/login/CaptchaForm");
+
+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
+ });
+ },
+
+ _getRegisterContentJsx: function() {
+ var currStep = this.registerLogic.getStep();
+ var registerStep;
+ switch (currStep) {
+ case "Register.COMPLETE":
+ break; // NOP
+ 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.
+
+ );
+ }
+});
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js
new file mode 100644
index 0000000000..5c4887955b
--- /dev/null
+++ b/src/components/views/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('../../../index');
+
+/**
+ * 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 (
+
+
+
+ );
+ }
+});
diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js
new file mode 100644
index 0000000000..39f9dc4594
--- /dev/null
+++ b/src/components/views/login/ServerConfig.js
@@ -0,0 +1,161 @@
+/*
+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 Modal = require('../../../Modal');
+var sdk = require('../../../index');
+
+/**
+ * A pure UI component which displays the HS and IS to use.
+ */
+module.exports = React.createClass({
+ displayName: 'ServerConfig',
+
+ propTypes: {
+ onHsUrlChanged: React.PropTypes.func,
+ onIsUrlChanged: React.PropTypes.func,
+ defaultHsUrl: React.PropTypes.string,
+ defaultIsUrl: React.PropTypes.string,
+ withToggleButton: React.PropTypes.bool,
+ delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged
+ },
+
+ getDefaultProps: function() {
+ return {
+ onHsUrlChanged: function() {},
+ onIsUrlChanged: function() {},
+ withToggleButton: false,
+ delayTimeMs: 0
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ hs_url: this.props.defaultHsUrl,
+ is_url: this.props.defaultIsUrl,
+ original_hs_url: this.props.defaultHsUrl,
+ original_is_url: this.props.defaultIsUrl,
+ // no toggle button = show, toggle button = hide
+ configVisible: !this.props.withToggleButton
+ }
+ },
+
+ onHomeserverChanged: function(ev) {
+ this.setState({hs_url: ev.target.value}, function() {
+ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
+ this.props.onHsUrlChanged(this.state.hs_url);
+ });
+ });
+ },
+
+ onIdentityServerChanged: function(ev) {
+ this.setState({is_url: ev.target.value}, function() {
+ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
+ this.props.onIsUrlChanged(this.state.is_url);
+ });
+ });
+ },
+
+ _waitThenInvoke: function(existingTimeoutId, fn) {
+ if (existingTimeoutId) {
+ clearTimeout(existingTimeoutId);
+ }
+ return setTimeout(fn.bind(this), this.props.delayTimeMs);
+ },
+
+ getHsUrl: function() {
+ return this.state.hs_url;
+ },
+
+ getIsUrl: function() {
+ return this.state.is_url;
+ },
+
+ onServerConfigVisibleChange: function(ev) {
+ this.setState({
+ configVisible: ev.target.checked
+ });
+ },
+
+ showHelpPopup: function() {
+ var ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
+ Modal.createDialog(ErrorDialog, {
+ title: 'Custom Server Options',
+ description:
+ You can use the custom server options to log into other Matrix
+ servers by specifying a different Home server URL.
+
+ This allows you to use Vector with an existing Matrix account on
+ a different Home server.
+
+
+ You can also set a custom Identity server but this will affect
+ people's ability to find you if you use a server in a group other
+ than the main Matrix.org group.
+ ,
+ button: "Dismiss",
+ focus: true
+ });
+ },
+
+ render: function() {
+ var serverConfigStyle = {};
+ serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
+
+ var toggleButton;
+ if (this.props.withToggleButton) {
+ toggleButton = (
+