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 (
+
+
+
+{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}
+
+
+
+ );
+ }
+
+ return (
+
+ {existingPhoneElements}
+
+
+ );
+ }
+}
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",