diff --git a/src/AddThreepid.js b/src/AddThreepid.js index d6a1d58aa0..c89de4f5fa 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,11 +52,36 @@ class AddThreepid { }); } + /** + * Attempt to add a msisdn threepid. This will trigger a side-effect of + * sending a test message to the provided phone number. + * @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in + * @param {string} phoneNumber The national or international formatted phone number to add + * @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server + * @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken(). + */ + addMsisdn(phoneCountry, phoneNumber, bind) { + this.bind = bind; + return MatrixClientPeg.get().requestAdd3pidMsisdnToken( + phoneCountry, phoneNumber, this.clientSecret, 1, + ).then((res) => { + this.sessionId = res.sid; + return res; + }, function(err) { + if (err.errcode == 'M_THREEPID_IN_USE') { + err.message = "This phone number is already in use"; + } else if (err.httpStatus) { + err.message = err.message + ` (Status ${err.httpStatus})`; + } + throw err; + }); + } + /** * Checks if the email link has been clicked by attempting to add the threepid - * @return {Promise} Resolves if the password was reset. Rejects with an object + * @return {Promise} Resolves if the email address was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why - * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". + * the request failed. */ checkEmailLinkClicked() { var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; @@ -73,6 +99,29 @@ class AddThreepid { throw err; }); } + + /** + * Takes a phone number verification code as entered by the user and validates + * it with the ID server, then if successful, adds the phone number. + * @return {Promise} Resolves if the phone number was added. Rejects with an object + * with a "message" property which contains a human-readable message detailing why + * the request failed. + */ + haveMsisdnToken(token) { + return MatrixClientPeg.get().submitMsisdnToken( + this.sessionId, this.clientSecret, token, + ).then((result) => { + if (result.errcode) { + throw result; + } + const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; + return MatrixClientPeg.get().addThreePid({ + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: identityServerDomain + }, this.bind); + }); + } } module.exports = AddThreepid; diff --git a/src/component-index.js b/src/component-index.js index c83c0dbb11..d6873c6dfd 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -229,6 +229,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar); import views$rooms$UserTile from './components/views/rooms/UserTile'; views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile); +import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber'; +views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber); import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar'; views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar); import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName'; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 91ecc4fb0f..0cb120019e 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -139,6 +140,7 @@ module.exports = React.createClass({ componentWillMount: function() { this._unmounted = false; + this._addThreepid = null; if (PlatformPeg.get()) { q().then(() => { @@ -321,12 +323,16 @@ module.exports = React.createClass({ UserSettingsStore.setEnableNotifications(event.target.checked); }, - onAddThreepidClicked: function(value, shouldSubmit) { + _onAddEmailEditFinished: function(value, shouldSubmit) { if (!shouldSubmit) return; + this._addEmail(); + }, + + _addEmail: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var email_address = this.refs.add_threepid_input.value; + var email_address = this.refs.add_email_input.value; if (!Email.looksValid(email_address)) { Modal.createDialog(ErrorDialog, { title: "Invalid Email Address", @@ -334,10 +340,10 @@ module.exports = React.createClass({ }); return; } - this.add_threepid = new AddThreepid(); + this._addThreepid = new AddThreepid(); // we always bind emails when registering, so let's do the // same here. - this.add_threepid.addEmailAddress(email_address, true).done(() => { + this._addThreepid.addEmailAddress(email_address, true).done(() => { Modal.createDialog(QuestionDialog, { title: "Verification Pending", description: "Please check your email and click on the link it contains. Once this is done, click continue.", @@ -352,7 +358,7 @@ module.exports = React.createClass({ description: "Unable to add email address" }); }); - ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); + ReactDOM.findDOMNode(this.refs.add_email_input).blur(); this.setState({email_add_pending: true}); }, @@ -391,8 +397,8 @@ module.exports = React.createClass({ }, verifyEmailAddress: function() { - this.add_threepid.checkEmailLinkClicked().done(() => { - this.add_threepid = undefined; + this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid = null; this.setState({ phase: "UserSettings.LOADING", }); @@ -811,30 +817,35 @@ module.exports = React.createClass({ ); }); - var addThreepidSection; + let addEmailSection; if (this.state.email_add_pending) { - addThreepidSection = ; + addEmailSection = ; } else if (!MatrixClientPeg.get().isGuest()) { - addThreepidSection = ( -
+ addEmailSection = ( +
+ onValueChanged={ this._onAddEmailEditFinished } />
- Add + Add
); } - threepidsSection.push(addThreepidSection); + const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber'); + const addMsisdnSection = ( + + ); + threepidsSection.push(addEmailSection); + threepidsSection.push(addMsisdnSection); var accountJsx; diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js new file mode 100644 index 0000000000..3a348393aa --- /dev/null +++ b/src/components/views/settings/AddPhoneNumber.js @@ -0,0 +1,172 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; + +import sdk from '../../../index'; +import AddThreepid from '../../../AddThreepid'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import Modal from '../../../Modal'; + + +export default WithMatrixClient(React.createClass({ + displayName: 'AddPhoneNumber', + + propTypes: { + matrixClient: React.PropTypes.object.isRequired, + onThreepidAdded: React.PropTypes.func, + }, + + getInitialState: function() { + return { + busy: false, + phoneCountry: null, + phoneNumber: "", + msisdn_add_pending: false, + }; + }, + + componentWillMount: function() { + this._addThreepid = null; + this._addMsisdnInput = null; + this._unmounted = false; + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + _onPhoneCountryChange: function(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + }, + + _onPhoneNumberChange: function(ev) { + this.setState({ phoneNumber: ev.target.value }); + }, + + _onAddMsisdnEditFinished: function(value, shouldSubmit) { + if (!shouldSubmit) return; + this._addMsisdn(); + }, + + _onAddMsisdnSubmit: function(ev) { + ev.preventDefault(); + this._addMsisdn(); + }, + + _collectAddMsisdnInput: function(e) { + this._addMsisdnInput = e; + }, + + _addMsisdn: function() { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + this._addThreepid = new AddThreepid(); + // we always bind phone numbers when registering, so let's do the + // same here. + this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => { + this._promptForMsisdnVerificationCode(resp.msisdn); + }).catch((err) => { + console.error("Unable to add phone number: " + err); + let msg = err.message; + Modal.createDialog(ErrorDialog, { + title: "Error", + description: msg, + }); + }).finally(() => { + if (this._unmounted) return; + this.setState({msisdn_add_pending: false}); + }).done(); + this._addMsisdnInput.blur(); + this.setState({msisdn_add_pending: true}); + }, + + _promptForMsisdnVerificationCode:function (msisdn, err) { + if (this._unmounted) return; + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); + let msgElements = [ +
A text message has been sent to +{msisdn}. + Please enter the verification code it contains
+ ]; + if (err) { + let msg = err.error; + if (err.errcode == 'M_THREEPID_AUTH_FAILED') { + msg = "Incorrect verification code"; + } + msgElements.push(
{msg}
); + } + Modal.createDialog(TextInputDialog, { + title: "Enter Code", + description:
{msgElements}
, + button: "Submit", + onFinished: (should_verify, token) => { + if (!should_verify) { + this._addThreepid = null; + return; + } + if (this._unmounted) return; + this.setState({msisdn_add_pending: true}); + this._addThreepid.haveMsisdnToken(token).then(() => { + this._addThreepid = null; + this.setState({phoneNumber: ''}); + if (this.props.onThreepidAdded) this.props.onThreepidAdded(); + }).catch((err) => { + this._promptForMsisdnVerificationCode(msisdn, err); + }).finally(() => { + if (this._unmounted) return; + this.setState({msisdn_add_pending: false}); + }).done(); + } + }); + }, + + render: function() { + const Loader = sdk.getComponent("elements.Spinner"); + if (this.state.msisdn_add_pending) { + return ; + } else if (this.props.matrixClient.isGuest()) { + return
; + } + + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + // XXX: This CSS relies on the CSS surrounding it in UserSettings as its in + // a tabular format to align the submit buttons + return ( +
+
+
+
+
+ + +
+
+
+ +
+
+ ); + } +}))