diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss
index d7606ecea9..1c9ce724d1 100644
--- a/res/css/views/settings/_EmailAddresses.scss
+++ b/res/css/views/settings/_EmailAddresses.scss
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss
index 7aaef2a56b..507b07334e 100644
--- a/res/css/views/settings/_PhoneNumbers.scss
+++ b/res/css/views/settings/_PhoneNumbers.scss
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -36,6 +37,15 @@ limitations under the License.
margin-left: 5px;
}
+.mx_ExistingPhoneNumber_verification {
+ display: inline-flex;
+ align-items: center;
+
+ .mx_Field {
+ margin: 0 0 0 1em;
+ }
+}
+
.mx_PhoneNumbers_input {
display: flex;
align-items: center;
diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
similarity index 94%
rename from src/components/views/settings/EmailAddresses.js
rename to src/components/views/settings/account/EmailAddresses.js
index 1bb41ae8b5..c13d2b4e0f 100644
--- a/src/components/views/settings/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,14 +17,14 @@ 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 * as Email from "../../../email";
-import AddThreepid from "../../../AddThreepid";
-const sdk = require('../../../index');
-const Modal = require("../../../Modal");
+import {_t} from "../../../../languageHandler";
+import MatrixClientPeg from "../../../../MatrixClientPeg";
+import Field from "../../elements/Field";
+import AccessibleButton from "../../elements/AccessibleButton";
+import * as Email from "../../../../email";
+import AddThreepid from "../../../../AddThreepid";
+const sdk = require('../../../../index');
+const Modal = require("../../../../Modal");
/*
TODO: Improve the UX for everything in here.
@@ -163,7 +164,7 @@ export default class EmailAddresses extends React.Component {
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
- task.addEmailAddress(email, true).then(() => {
+ task.addEmailAddress(email, false).then(() => {
this.setState({continueDisabled: false});
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
similarity index 94%
rename from src/components/views/settings/PhoneNumbers.js
rename to src/components/views/settings/account/PhoneNumbers.js
index 4ebc2a20a6..236a4e7587 100644
--- a/src/components/views/settings/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,14 +17,14 @@ 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");
+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.
@@ -160,7 +161,7 @@ export default class PhoneNumbers extends React.Component {
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
- task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => {
+ task.addMsisdn(phoneCountry, phoneNumber, false).then((response) => {
this.setState({continueDisabled: false, verifyMsisdn: response.msisdn});
}).catch((err) => {
console.error("Unable to add phone number " + phoneNumber + " " + err);
@@ -224,7 +225,7 @@ export default class PhoneNumbers extends React.Component {
{_t("A text message has been sent to +%(msisdn)s. " +
- "Please enter the verification code it contains", { msisdn: msisdn })}
+ "Please enter the verification code it contains.", { msisdn: msisdn })}
{this.state.verifyError}
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js
new file mode 100644
index 0000000000..7862eda61e
--- /dev/null
+++ b/src/components/views/settings/discovery/EmailAddresses.js
@@ -0,0 +1,248 @@
+/*
+Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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 sdk from '../../../../index';
+import Modal from '../../../../Modal';
+import IdentityAuthClient from '../../../../IdentityAuthClient';
+import AddThreepid from '../../../../AddThreepid';
+
+/*
+TODO: Improve the UX for everything in here.
+It's very much placeholder, but it gets the job done. The old way of handling
+email addresses in user settings was to use dialogs to communicate state, however
+due to our dialog system overriding dialogs (causing unmounts) this creates problems
+for a sane UX. For instance, the user could easily end up entering an email address
+and receive a dialog to verify the address, which then causes the component here
+to forget what it was doing and ultimately fail. Dialogs are still used in some
+places to communicate errors - these should be replaced with inline validation when
+that is available.
+*/
+
+/*
+TODO: Reduce all the copying between account vs. discovery components.
+*/
+
+export class EmailAddress extends React.Component {
+ static propTypes = {
+ email: PropTypes.object.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ const { bound } = props.email;
+
+ this.state = {
+ verifying: false,
+ addTask: null,
+ continueDisabled: false,
+ bound,
+ };
+ }
+
+ async changeBinding({ bind, label, errorTitle }) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const { medium, address } = this.props.email;
+
+ const task = new AddThreepid();
+ this.setState({
+ verifying: true,
+ continueDisabled: true,
+ addTask: task,
+ });
+
+ try {
+ // XXX: Unfortunately, at the moment we can't just bind via the HS
+ // in a single operation, at it will error saying the 3PID is in use
+ // even though it's in use by the current user. For the moment, we
+ // work around this by removing the 3PID from the HS and re-adding
+ // it with IS binding enabled.
+ // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052
+ await MatrixClientPeg.get().deleteThreePid(medium, address);
+ await task.addEmailAddress(address, bind);
+ this.setState({
+ continueDisabled: false,
+ bound: bind,
+ });
+ } catch (err) {
+ console.error(`Unable to ${label} email address ${address} ${err}`);
+ this.setState({
+ verifying: false,
+ continueDisabled: false,
+ addTask: null,
+ });
+ Modal.createTrackedDialog(`Unable to ${label} email address`, '', ErrorDialog, {
+ title: errorTitle,
+ description: ((err && err.message) ? err.message : _t("Operation failed")),
+ });
+ }
+ }
+
+ onRevokeClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.changeBinding({
+ bind: false,
+ label: "revoke",
+ errorTitle: _t("Unable to revoke sharing for email address"),
+ });
+ }
+
+ onShareClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.changeBinding({
+ bind: true,
+ label: "share",
+ errorTitle: _t("Unable to share email address"),
+ });
+ }
+
+ onContinueClick = async (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ this.setState({ continueDisabled: true });
+ try {
+ await this.state.addTask.checkEmailLinkClicked();
+ this.setState({
+ addTask: null,
+ continueDisabled: false,
+ verifying: false,
+ });
+ } catch (err) {
+ this.setState({ continueDisabled: false });
+ if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Unable to verify email address: " + err);
+ Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
+ title: _t("Unable to verify email address."),
+ description: ((err && err.message) ? err.message : _t("Operation failed")),
+ });
+ }
+ }
+ }
+
+ render() {
+ const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+ const { address } = this.props.email;
+ const { verifying, bound } = this.state;
+
+ let status;
+ if (verifying) {
+ status =
+ {_t("Check your inbox, then click Continue")}
+
+ {_t("Continue")}
+
+ ;
+ } else if (bound) {
+ status =
+ {_t("Revoke")}
+ ;
+ } else {
+ status =
+ {_t("Share")}
+ ;
+ }
+
+ return (
+
+ {address}
+ {status}
+
+ );
+ }
+}
+
+export default class EmailAddresses extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ loaded: false,
+ emails: [],
+ };
+ }
+
+ async componentWillMount() {
+ const client = MatrixClientPeg.get();
+ const userId = client.getUserId();
+
+ const { threepids } = await client.getThreePids();
+ const emails = threepids.filter((a) => a.medium === 'email');
+
+ if (emails.length > 0) {
+ // TODO: Handle terms agreement
+ // See https://github.com/vector-im/riot-web/issues/10522
+ const authClient = new IdentityAuthClient();
+ const identityAccessToken = await authClient.getAccessToken();
+
+ // Restructure for lookup query
+ const query = emails.map(({ medium, address }) => [medium, address]);
+ const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
+
+ // Record which are already bound
+ for (const [medium, address, mxid] of lookupResults.threepids) {
+ if (medium !== "email" || mxid !== userId) {
+ continue;
+ }
+ const email = emails.find(e => e.address === address);
+ if (!email) continue;
+ email.bound = true;
+ }
+ }
+
+ this.setState({ emails });
+ }
+
+ render() {
+ let content;
+ if (this.state.emails.length > 0) {
+ content = this.state.emails.map((e) => {
+ return
;
+ });
+ } else {
+ content =
+ {_t("Discovery options will appear once you have added an email above.")}
+ ;
+ }
+
+ return (
+
+ {content}
+
+ );
+ }
+}
diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js
new file mode 100644
index 0000000000..3930277aea
--- /dev/null
+++ b/src/components/views/settings/discovery/PhoneNumbers.js
@@ -0,0 +1,267 @@
+/*
+Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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 sdk from '../../../../index';
+import Modal from '../../../../Modal';
+import IdentityAuthClient from '../../../../IdentityAuthClient';
+import AddThreepid from '../../../../AddThreepid';
+
+/*
+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 PhoneNumber extends React.Component {
+ static propTypes = {
+ msisdn: PropTypes.object.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ const { bound } = props.msisdn;
+
+ this.state = {
+ verifying: false,
+ verificationCode: "",
+ addTask: null,
+ continueDisabled: false,
+ bound,
+ };
+ }
+
+ async changeBinding({ bind, label, errorTitle }) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const { medium, address } = this.props.msisdn;
+
+ const task = new AddThreepid();
+ this.setState({
+ verifying: true,
+ continueDisabled: true,
+ addTask: task,
+ });
+
+ try {
+ // XXX: Unfortunately, at the moment we can't just bind via the HS
+ // in a single operation, at it will error saying the 3PID is in use
+ // even though it's in use by the current user. For the moment, we
+ // work around this by removing the 3PID from the HS and re-adding
+ // it with IS binding enabled.
+ // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052
+ await MatrixClientPeg.get().deleteThreePid(medium, address);
+ // XXX: Sydent will accept a number without country code if you add
+ // a leading plus sign to a number in E.164 format (which the 3PID
+ // address is), but this goes against the spec.
+ // See https://github.com/matrix-org/matrix-doc/issues/2222
+ await task.addMsisdn(null, `+${address}`, bind);
+ this.setState({
+ continueDisabled: false,
+ bound: bind,
+ });
+ } catch (err) {
+ console.error(`Unable to ${label} phone number ${address} ${err}`);
+ this.setState({
+ verifying: false,
+ continueDisabled: false,
+ addTask: null,
+ });
+ Modal.createTrackedDialog(`Unable to ${label} phone number`, '', ErrorDialog, {
+ title: errorTitle,
+ description: ((err && err.message) ? err.message : _t("Operation failed")),
+ });
+ }
+ }
+
+ onRevokeClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.changeBinding({
+ bind: false,
+ label: "revoke",
+ errorTitle: _t("Unable to revoke sharing for phone number"),
+ });
+ }
+
+ onShareClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.changeBinding({
+ bind: true,
+ label: "share",
+ errorTitle: _t("Unable to share phone number"),
+ });
+ }
+
+ onVerificationCodeChange = (e) => {
+ this.setState({
+ verificationCode: e.target.value,
+ });
+ }
+
+ onContinueClick = async (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ this.setState({ continueDisabled: true });
+ const token = this.state.verificationCode;
+ try {
+ await this.state.addTask.haveMsisdnToken(token);
+ this.setState({
+ addTask: null,
+ continueDisabled: false,
+ verifying: false,
+ verifyError: null,
+ verificationCode: "",
+ });
+ } 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")});
+ }
+ }
+ }
+
+ render() {
+ const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+ const Field = sdk.getComponent('elements.Field');
+ const { address } = this.props.msisdn;
+ const { verifying, bound } = this.state;
+
+ let status;
+ if (verifying) {
+ status =
+
+ {_t("Please enter verification code sent via text.")}
+
+ {this.state.verifyError}
+
+
+ ;
+ } else if (bound) {
+ status =
+ {_t("Revoke")}
+ ;
+ } else {
+ status =
+ {_t("Share")}
+ ;
+ }
+
+ return (
+
+ +{address}
+ {status}
+
+ );
+ }
+}
+
+export default class PhoneNumbers extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ loaded: false,
+ msisdns: [],
+ };
+ }
+
+ async componentWillMount() {
+ const client = MatrixClientPeg.get();
+ const userId = client.getUserId();
+
+ const { threepids } = await client.getThreePids();
+ const msisdns = threepids.filter((a) => a.medium === 'msisdn');
+
+ if (msisdns.length > 0) {
+ // TODO: Handle terms agreement
+ // See https://github.com/vector-im/riot-web/issues/10522
+ const authClient = new IdentityAuthClient();
+ const identityAccessToken = await authClient.getAccessToken();
+
+ // Restructure for lookup query
+ const query = msisdns.map(({ medium, address }) => [medium, address]);
+ const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
+
+ // Record which are already bound
+ for (const [medium, address, mxid] of lookupResults.threepids) {
+ if (medium !== "msisdn" || mxid !== userId) {
+ continue;
+ }
+ const msisdn = msisdns.find(e => e.address === address);
+ if (!msisdn) continue;
+ msisdn.bound = true;
+ }
+ }
+
+ this.setState({ msisdns });
+ }
+
+ render() {
+ let content;
+ if (this.state.msisdns.length > 0) {
+ content = this.state.msisdns.map((e) => {
+ return
;
+ });
+ } else {
+ content =
+ {_t("Discovery options will appear once you have added a phone number above.")}
+ ;
+ }
+
+ return (
+
+ {content}
+
+ );
+ }
+}
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 32dcf4721c..a9c010b6b4 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,8 +18,6 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
-import EmailAddresses from "../../EmailAddresses";
-import PhoneNumbers from "../../PhoneNumbers";
import Field from "../../../elements/Field";
import * as languageHandler from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
@@ -111,6 +110,9 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderAccountSection() {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
+ const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
+ const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
+
const passwordChangeForm = (
+ {_t("Email addresses")}
+
+
+ {_t("Phone numbers")}
+
+
+ );
+ }
+
_renderManagementSection() {
// TODO: Improve warning text for account deactivation
return (
@@ -189,6 +206,9 @@ export default class GeneralUserSettingsTab extends React.Component {
{this._renderAccountSection()}
{this._renderLanguageSection()}
{this._renderThemeSection()}
+