diff --git a/res/css/_components.scss b/res/css/_components.scss index 89573ee865..b26998b9d3 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -131,6 +131,7 @@ @import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; +@import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/tabs/_GeneralSettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss new file mode 100644 index 0000000000..2700798519 --- /dev/null +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -0,0 +1,54 @@ +/* +Copyright 2019 New Vector 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. +*/ + +.mx_ExistingPhoneNumber { + margin-bottom: 5px; +} + +.mx_ExistingPhoneNumber_delete { + margin-right: 5px; + cursor: pointer; + vertical-align: middle; +} + +.mx_ExistingPhoneNumber_address { + vertical-align: middle; +} + +.mx_ExistingPhoneNumber_promptText { + margin-right: 10px; +} + +.mx_ExistingPhoneNumber_confirmBtn { + margin-right: 5px; +} + +.mx_PhoneNumbers_new .mx_Field input { + width: calc(100% - 20px); +} + +.mx_PhoneNumbers_input { + display: flex; + align-items: center; +} + +.mx_PhoneNumbers_input > .mx_Field { + flex-grow: 1; +} + +.mx_PhoneNumbers_country { + width: 80px; +} diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/PhoneNumbers.js new file mode 100644 index 0000000000..c4b34f7cf3 --- /dev/null +++ b/src/components/views/settings/PhoneNumbers.js @@ -0,0 +1,246 @@ +/* +Copyright 2019 New Vector 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 PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import AddThreepid from "../../../AddThreepid"; +import CountryDropdown from "../auth/CountryDropdown"; +const sdk = require('../../../index'); +const Modal = require("../../../Modal"); + +/* +TODO: Improve the UX for everything in here. +This is a copy/paste of EmailAddresses, mostly. + */ + +// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic + +export class ExistingPhoneNumber extends React.Component { + static propTypes = { + msisdn: PropTypes.object.isRequired, + onRemoved: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + verifyRemove: false, + }; + } + + _onRemove = (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({verifyRemove: true}); + }; + + _onDontRemove = (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({verifyRemove: false}); + }; + + _onActuallyRemove = (e) => { + e.stopPropagation(); + e.preventDefault(); + + MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => { + return this.props.onRemoved(this.props.msisdn); + }).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to remove contact information: " + err); + Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { + title: _t("Unable to remove contact information"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + }; + + render() { + if (this.state.verifyRemove) { + return ( +
+ + {_t("Are you sure?")} + + + {_t("Yes")} + + + {_t("No")} + +
+ ); + } + + return ( +
+ {_t("Remove")} + +{this.props.msisdn.address} +
+ ); + } +} + +export default class PhoneNumbers extends React.Component { + constructor() { + super(); + + this.state = { + msisdns: [], + verifying: false, + verifyError: false, + verifyMsisdn: "", + addTask: null, + continueDisabled: false, + phoneCountry: "", + }; + } + + componentWillMount(): void { + const client = MatrixClientPeg.get(); + + client.getThreePids().then((addresses) => { + this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')}); + }); + } + + _onRemoved = (address) => { + this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)}); + }; + + _onAddClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + + if (!this.refs.newPhoneNumber) return; + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const phoneNumber = this.refs.newPhoneNumber.value; + const phoneCountry = this.state.phoneCountry; + + const task = new AddThreepid(); + this.setState({verifying: true, continueDisabled: true, addTask: task}); + + task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => { + this.setState({continueDisabled: false, verifyMsisdn: response.msisdn}); + }).catch((err) => { + console.error("Unable to add phone number " + phoneNumber + " " + err); + this.setState({verifying: false, continueDisabled: false, addTask: null}); + Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, { + title: _t("Error"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + }; + + _onContinueClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({continueDisabled: true}); + const token = this.refs.newPhoneNumberCode.value; + this.state.addTask.haveMsisdnToken(token).then(() => { + this.setState({ + msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}], + addTask: null, + continueDisabled: false, + verifying: false, + verifyMsisdn: "", + verifyError: null, + }); + this.refs.newPhoneNumber.value = ""; + }).catch((err) => { + this.setState({continueDisabled: false}); + if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify phone number: " + err); + Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, { + title: _t("Unable to verify phone number."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } else { + this.setState({verifyError: _t("Incorrect verification code")}); + } + }); + }; + + _onCountryChanged = (e) => { + this.setState({phoneCountry: e.iso2}); + }; + + render() { + const existingPhoneElements = this.state.msisdns.map((p) => { + return ; + }); + + let addVerifySection = ( + + {_t("Add")} + + ); + if (this.state.verifying) { + const msisdn = this.state.verifyMsisdn; + addVerifySection = ( +
+
+ {_t("A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", {msisdn: msisdn})} +
+ {this.state.verifyError} +
+
+ + + {_t("Continue")} + + +
+ ); + } + + return ( +
+ {existingPhoneElements} +
+
+ + +
+ {addVerifySection} +
+
+ ); + } +} diff --git a/src/components/views/settings/tabs/GeneralSettingsTab.js b/src/components/views/settings/tabs/GeneralSettingsTab.js index 0b6a88d652..44237bacfb 100644 --- a/src/components/views/settings/tabs/GeneralSettingsTab.js +++ b/src/components/views/settings/tabs/GeneralSettingsTab.js @@ -23,6 +23,7 @@ import {MatrixClient} from "matrix-js-sdk"; import { DragDropContext } from 'react-beautiful-dnd'; import ProfileSettings from "../ProfileSettings"; import EmailAddresses from "../EmailAddresses"; +import PhoneNumbers from "../PhoneNumbers"; const sdk = require('../../../../index'); const Modal = require("../../../../Modal"); @@ -105,6 +106,9 @@ export default class GeneralSettingsTab extends React.Component { {_t("Email addresses")} + + {_t("Phone numbers")} + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 34b1e201c0..5b320a57c7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -413,6 +413,8 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", + "Verification code": "Verification code", + "Phone Number": "Phone Number", "Profile picture": "Profile picture", "Upload profile picture": "Upload profile picture", "Display Name": "Display Name", @@ -425,6 +427,7 @@ "Account": "Account", "Set a new account password...": "Set a new account password...", "Email addresses": "Email addresses", + "Phone numbers": "Phone numbers", "Language and region": "Language and region", "Theme": "Theme", "Account management": "Account management",