Refactor the various email/phone management UI into a single component (#12884)
* Refactor the various email/phone management UI into a single component These were basically the same component copied & pasted 3 times and tweaked to match the behaviour of each case. This de-dupes them into one component. This all could really benefit from playwright tests, but would require setting up a dummy ID server in the playwright tests. This is all legacy pre-MAS stuff so its questionable whether its worth the effort. * Basic test, remove old tests * Use different text to confirm remove & put headers back although the two texts are both 'Remove' in practice * Remove string This was never triggered anyway with sydent & synapse because they don't seem to agree on what error to return. In any case, I think it makes more sense for it to be consistent with the email path, ie. using a dialog. * Avoid nested forms * Snapshots * More snapshots * Test the hs side * Snapshots * Test IS bind/revoke * Test remove can be cancelled * Test unvalidated cases & fix phone error * Reset state between tests * Import useState directly * One more direct React import
This commit is contained in:
parent
de898d1b62
commit
4751c52d82
21 changed files with 1391 additions and 1981 deletions
|
@ -37,7 +37,7 @@
|
||||||
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
|
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
|
||||||
@import "./components/views/pips/_WidgetPip.pcss";
|
@import "./components/views/pips/_WidgetPip.pcss";
|
||||||
@import "./components/views/polls/_PollOption.pcss";
|
@import "./components/views/polls/_PollOption.pcss";
|
||||||
@import "./components/views/settings/_EmailAddressesPhoneNumbers.pcss";
|
@import "./components/views/settings/_AddRemoveThreepids.pcss";
|
||||||
@import "./components/views/settings/devices/_CurrentDeviceSection.pcss";
|
@import "./components/views/settings/devices/_CurrentDeviceSection.pcss";
|
||||||
@import "./components/views/settings/devices/_DeviceDetailHeading.pcss";
|
@import "./components/views/settings/devices/_DeviceDetailHeading.pcss";
|
||||||
@import "./components/views/settings/devices/_DeviceDetails.pcss";
|
@import "./components/views/settings/devices/_DeviceDetails.pcss";
|
||||||
|
|
|
@ -21,17 +21,29 @@ limitations under the License.
|
||||||
* tab sensibly and before I can refactor these components.
|
* tab sensibly and before I can refactor these components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_EmailAddressesPhoneNumbers_discovery_existing {
|
.mx_AddRemoveThreepids_existing {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EmailAddressesPhoneNumbers_discovery_existing_address,
|
.mx_AddRemoveThreepids_existing_address,
|
||||||
.mx_EmailAddressesPhoneNumbers_discovery_existing_promptText {
|
.mx_AddRemoveThreepids_existing_promptText {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EmailAddressesPhoneNumbers_discovery_existing_button {
|
.mx_AddRemoveThreepids_existing_button {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EmailAddressesPhoneNumbers_verify {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmailAddressesPhoneNumbers_existing_button {
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmailAddressesPhoneNumbers_verify_instructions {
|
||||||
|
flex: 1;
|
||||||
|
}
|
|
@ -271,9 +271,7 @@ export default class AddThreepid {
|
||||||
* with a "message" property which contains a human-readable message detailing why
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the request failed.
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
public async haveMsisdnToken(
|
public async haveMsisdnToken(msisdnToken: string): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
|
||||||
msisdnToken: string,
|
|
||||||
): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> {
|
|
||||||
const authClient = new IdentityAuthClient();
|
const authClient = new IdentityAuthClient();
|
||||||
|
|
||||||
if (this.submitUrl) {
|
if (this.submitUrl) {
|
||||||
|
@ -301,13 +299,14 @@ export default class AddThreepid {
|
||||||
id_server: getIdServerDomain(this.matrixClient),
|
id_server: getIdServerDomain(this.matrixClient),
|
||||||
id_access_token: await authClient.getAccessToken(),
|
id_access_token: await authClient.getAccessToken(),
|
||||||
});
|
});
|
||||||
|
return [true];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await this.makeAddThreepidOnlyRequest();
|
await this.makeAddThreepidOnlyRequest();
|
||||||
|
|
||||||
// The spec has always required this to use UI auth but synapse briefly
|
// The spec has always required this to use UI auth but synapse briefly
|
||||||
// implemented it without, so this may just succeed and that's OK.
|
// implemented it without, so this may just succeed and that's OK.
|
||||||
return;
|
return [true];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof MatrixError) || err.httpStatus !== 401 || !err.data || !err.data.flows) {
|
if (!(err instanceof MatrixError) || err.httpStatus !== 401 || !err.data || !err.data.flows) {
|
||||||
// doesn't look like an interactive-auth failure
|
// doesn't look like an interactive-auth failure
|
||||||
|
|
534
src/components/views/settings/AddRemoveThreepids.tsx
Normal file
534
src/components/views/settings/AddRemoveThreepids.tsx
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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, { useCallback, useRef, useState } from "react";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import {
|
||||||
|
IRequestMsisdnTokenResponse,
|
||||||
|
IRequestTokenResponse,
|
||||||
|
MatrixError,
|
||||||
|
ThreepidMedium,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../AddThreepid";
|
||||||
|
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import ErrorDialog, { extractErrorMessageFromError } from "../dialogs/ErrorDialog";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
import { looksValid as emailLooksValid } from "../../../email";
|
||||||
|
import CountryDropdown from "../auth/CountryDropdown";
|
||||||
|
import { PhoneNumberCountryDefinition } from "../../../phonenumber";
|
||||||
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
|
|
||||||
|
// Whether we're adding 3pids to the user's account on the homeserver or sharing them on an identity server
|
||||||
|
type TheepidControlMode = "hs" | "is";
|
||||||
|
|
||||||
|
interface ExistingThreepidProps {
|
||||||
|
mode: TheepidControlMode;
|
||||||
|
threepid: ThirdPartyIdentifier;
|
||||||
|
onChange: (threepid: ThirdPartyIdentifier) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExistingThreepid: React.FC<ExistingThreepidProps> = ({ mode, threepid, onChange, disabled }) => {
|
||||||
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
const client = useMatrixClientContext();
|
||||||
|
const bindTask = useRef<AddThreepid | undefined>();
|
||||||
|
|
||||||
|
const [isVerifyingBind, setIsVerifyingBind] = useState(false);
|
||||||
|
const [continueDisabled, setContinueDisabled] = useState(false);
|
||||||
|
const [verificationCode, setVerificationCode] = useState("");
|
||||||
|
|
||||||
|
const onRemoveClick = useCallback((e: ButtonEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsConfirming(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCancelClick = useCallback((e: ButtonEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsConfirming(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onConfirmRemoveClick = useCallback(
|
||||||
|
(e: ButtonEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
client
|
||||||
|
.deleteThreePid(threepid.medium, threepid.address)
|
||||||
|
.then(() => {
|
||||||
|
return onChange(threepid);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Unable to remove contact information: " + err);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("settings|general|error_remove_3pid"),
|
||||||
|
description: err && err.message ? err.message : _t("invite|failed_generic"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client, threepid, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeBinding = useCallback(
|
||||||
|
async ({ bind, label, errorTitle }: Binding) => {
|
||||||
|
try {
|
||||||
|
if (bind) {
|
||||||
|
bindTask.current = new AddThreepid(client);
|
||||||
|
setContinueDisabled(true);
|
||||||
|
if (threepid.medium === ThreepidMedium.Email) {
|
||||||
|
await bindTask.current.bindEmailAddress(threepid.address);
|
||||||
|
} else {
|
||||||
|
// 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 bindTask.current.bindMsisdn(null as unknown as string, `+${threepid.address}`);
|
||||||
|
}
|
||||||
|
setContinueDisabled(false);
|
||||||
|
setIsVerifyingBind(true);
|
||||||
|
} else {
|
||||||
|
await client.unbindThreePid(threepid.medium, threepid.address);
|
||||||
|
onChange(threepid);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`changeBinding: Unable to ${label} email address ${threepid.address}`, err);
|
||||||
|
setIsVerifyingBind(false);
|
||||||
|
setContinueDisabled(false);
|
||||||
|
bindTask.current = undefined;
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: errorTitle,
|
||||||
|
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, threepid, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRevokeClick = useCallback(
|
||||||
|
(e: ButtonEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
changeBinding({
|
||||||
|
bind: false,
|
||||||
|
label: "revoke",
|
||||||
|
errorTitle:
|
||||||
|
threepid.medium === "email"
|
||||||
|
? _t("settings|general|error_revoke_email_discovery")
|
||||||
|
: _t("settings|general|error_revoke_msisdn_discovery"),
|
||||||
|
}).then();
|
||||||
|
},
|
||||||
|
[changeBinding, threepid.medium],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onShareClick = useCallback(
|
||||||
|
(e: ButtonEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
changeBinding({
|
||||||
|
bind: true,
|
||||||
|
label: "share",
|
||||||
|
errorTitle:
|
||||||
|
threepid.medium === "email"
|
||||||
|
? _t("settings|general|error_share_email_discovery")
|
||||||
|
: _t("settings|general|error_share_msisdn_discovery"),
|
||||||
|
}).then();
|
||||||
|
},
|
||||||
|
[changeBinding, threepid.medium],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onContinueClick = useCallback(
|
||||||
|
async (e: ButtonEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setContinueDisabled(true);
|
||||||
|
try {
|
||||||
|
if (threepid.medium === ThreepidMedium.Email) {
|
||||||
|
await bindTask.current?.checkEmailLinkClicked();
|
||||||
|
} else {
|
||||||
|
await bindTask.current?.haveMsisdnToken(verificationCode);
|
||||||
|
}
|
||||||
|
setIsVerifyingBind(false);
|
||||||
|
onChange(threepid);
|
||||||
|
bindTask.current = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Unable to verify threepid:`, err);
|
||||||
|
|
||||||
|
let underlyingError = err;
|
||||||
|
if (err instanceof UserFriendlyError) {
|
||||||
|
underlyingError = err.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title:
|
||||||
|
threepid.medium === "email"
|
||||||
|
? _t("settings|general|email_not_verified")
|
||||||
|
: _t("settings|general|error_msisdn_verification"),
|
||||||
|
description:
|
||||||
|
threepid.medium === "email"
|
||||||
|
? _t("settings|general|email_verification_instructions")
|
||||||
|
: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error("Unable to verify email address: " + err);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("settings|general|error_email_verification"),
|
||||||
|
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setContinueDisabled(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[verificationCode, onChange, threepid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onVerificationCodeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setVerificationCode(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isConfirming) {
|
||||||
|
return (
|
||||||
|
<div className="mx_AddRemoveThreepids_existing">
|
||||||
|
<span className="mx_AddRemoveThreepids_existing_promptText">
|
||||||
|
{threepid.medium === ThreepidMedium.Email
|
||||||
|
? _t("settings|general|remove_email_prompt", { email: threepid.address })
|
||||||
|
: _t("settings|general|remove_msisdn_prompt", { phone: threepid.address })}
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onConfirmRemoveClick}
|
||||||
|
kind="danger_sm"
|
||||||
|
className="mx_AddRemoveThreepids_existing_button"
|
||||||
|
>
|
||||||
|
{_t("action|remove")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onCancelClick}
|
||||||
|
kind="link_sm"
|
||||||
|
className="mx_AddRemoveThreepids_existing_button"
|
||||||
|
>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVerifyingBind) {
|
||||||
|
if (threepid.medium === ThreepidMedium.Email) {
|
||||||
|
return (
|
||||||
|
<div className="mx_EmailAddressesPhoneNumbers_verify">
|
||||||
|
<span className="mx_EmailAddressesPhoneNumbers_verify_instructions">
|
||||||
|
{_t("settings|general|discovery_email_verification_instructions")}
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_EmailAddressesPhoneNumbers_existing_button"
|
||||||
|
kind="primary_sm"
|
||||||
|
onClick={onContinueClick}
|
||||||
|
disabled={continueDisabled}
|
||||||
|
>
|
||||||
|
{_t("action|complete")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="mx_EmailAddressesPhoneNumbers_verify">
|
||||||
|
<span className="mx_EmailAddressesPhoneNumbers_verify_instructions">
|
||||||
|
{_t("settings|general|msisdn_verification_instructions")}
|
||||||
|
</span>
|
||||||
|
<form onSubmit={onContinueClick} autoComplete="off" noValidate={true}>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
label={_t("settings|general|msisdn_verification_field_label")}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={continueDisabled}
|
||||||
|
value={verificationCode}
|
||||||
|
onChange={onVerificationCodeChange}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_AddRemoveThreepids_existing">
|
||||||
|
<span className="mx_AddRemoveThreepids_existing_address">{threepid.address}</span>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={mode === "hs" ? onRemoveClick : threepid.bound ? onRevokeClick : onShareClick}
|
||||||
|
kind={mode === "hs" || threepid.bound ? "danger_sm" : "primary_sm"}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{mode === "hs" ? _t("action|remove") : threepid.bound ? _t("action|revoke") : _t("action|share")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isMsisdnResponse(
|
||||||
|
resp: IRequestTokenResponse | IRequestMsisdnTokenResponse,
|
||||||
|
): resp is IRequestMsisdnTokenResponse {
|
||||||
|
return (resp as IRequestMsisdnTokenResponse).msisdn !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddThreepidSection: React.FC<{ medium: "email" | "msisdn"; disabled?: boolean; onChange: () => void }> = ({
|
||||||
|
medium,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const addTask = useRef<AddThreepid | undefined>();
|
||||||
|
const [newThreepidInput, setNewThreepidInput] = useState("");
|
||||||
|
const [phoneCountryInput, setPhoneCountryInput] = useState("");
|
||||||
|
const [verificationCodeInput, setVerificationCodeInput] = useState("");
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
|
const [continueDisabled, setContinueDisabled] = useState(false);
|
||||||
|
const [sentToMsisdn, setSentToMsisdn] = useState("");
|
||||||
|
|
||||||
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
|
const onPhoneCountryChanged = useCallback((country: PhoneNumberCountryDefinition) => {
|
||||||
|
setPhoneCountryInput(country.iso2);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onContinueClick = useCallback(
|
||||||
|
(e: ButtonEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!addTask.current) return;
|
||||||
|
|
||||||
|
setContinueDisabled(true);
|
||||||
|
|
||||||
|
const checkPromise =
|
||||||
|
medium === "email"
|
||||||
|
? addTask.current?.checkEmailLinkClicked()
|
||||||
|
: addTask.current?.haveMsisdnToken(verificationCodeInput);
|
||||||
|
checkPromise
|
||||||
|
.then(([finished]) => {
|
||||||
|
if (finished) {
|
||||||
|
addTask.current = undefined;
|
||||||
|
setIsVerifying(false);
|
||||||
|
setNewThreepidInput("");
|
||||||
|
onChange();
|
||||||
|
}
|
||||||
|
setContinueDisabled(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Unable to verify 3pid: ", err);
|
||||||
|
|
||||||
|
setContinueDisabled(false);
|
||||||
|
|
||||||
|
let underlyingError = err;
|
||||||
|
if (err instanceof UserFriendlyError) {
|
||||||
|
underlyingError = err.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
underlyingError instanceof MatrixError &&
|
||||||
|
underlyingError.errcode === "M_THREEPID_AUTH_FAILED"
|
||||||
|
) {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title:
|
||||||
|
medium === "email"
|
||||||
|
? _t("settings|general|email_not_verified")
|
||||||
|
: _t("settings|general|error_msisdn_verification"),
|
||||||
|
description: _t("settings|general|email_verification_instructions"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title:
|
||||||
|
medium == "email"
|
||||||
|
? _t("settings|general|error_email_verification")
|
||||||
|
: _t("settings|general|error_msisdn_verification"),
|
||||||
|
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, medium, verificationCodeInput],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNewThreepidInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewThreepidInput(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onAddClick = useCallback(
|
||||||
|
(e: React.FormEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!newThreepidInput) return;
|
||||||
|
|
||||||
|
// TODO: Inline field validation
|
||||||
|
if (medium === "email" && !emailLooksValid(newThreepidInput)) {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("settings|general|error_invalid_email"),
|
||||||
|
description: _t("settings|general|error_invalid_email_detail"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTask.current = new AddThreepid(client);
|
||||||
|
setIsVerifying(true);
|
||||||
|
setContinueDisabled(true);
|
||||||
|
|
||||||
|
const addPromise =
|
||||||
|
medium === "email"
|
||||||
|
? addTask.current.addEmailAddress(newThreepidInput)
|
||||||
|
: addTask.current.addMsisdn(phoneCountryInput, newThreepidInput);
|
||||||
|
|
||||||
|
addPromise
|
||||||
|
.then((resp: IRequestTokenResponse | IRequestMsisdnTokenResponse) => {
|
||||||
|
setContinueDisabled(false);
|
||||||
|
if (isMsisdnResponse(resp)) {
|
||||||
|
setSentToMsisdn(resp.msisdn);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(`Unable to add threepid ${newThreepidInput}`, err);
|
||||||
|
setIsVerifying(false);
|
||||||
|
setContinueDisabled(false);
|
||||||
|
addTask.current = undefined;
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: medium === "email" ? _t("settings|general|error_add_email") : _t("common|error"),
|
||||||
|
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client, phoneCountryInput, newThreepidInput, medium],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onVerificationCodeInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setVerificationCodeInput(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isVerifying && medium === "email") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{_t("settings|general|add_email_instructions")}</div>
|
||||||
|
<AccessibleButton onClick={onContinueClick} kind="primary" disabled={continueDisabled}>
|
||||||
|
{_t("action|continue")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (isVerifying) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{_t("settings|general|add_msisdn_instructions", { msisdn: sentToMsisdn })}
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<form onSubmit={onContinueClick} autoComplete="off" noValidate={true}>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
label={_t("settings|general|msisdn_verification_field_label")}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={disabled || continueDisabled}
|
||||||
|
value={verificationCodeInput}
|
||||||
|
onChange={onVerificationCodeInputChange}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onContinueClick}
|
||||||
|
kind="primary"
|
||||||
|
disabled={disabled || continueDisabled || verificationCodeInput.length === 0}
|
||||||
|
>
|
||||||
|
{_t("action|continue")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneCountry =
|
||||||
|
medium === "msisdn" ? (
|
||||||
|
<CountryDropdown
|
||||||
|
onOptionChange={onPhoneCountryChanged}
|
||||||
|
className="mx_PhoneNumbers_country"
|
||||||
|
value={phoneCountryInput}
|
||||||
|
disabled={isVerifying}
|
||||||
|
isSmall={true}
|
||||||
|
showPrefix={true}
|
||||||
|
/>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onAddClick} autoComplete="off" noValidate={true}>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
label={
|
||||||
|
medium === "email"
|
||||||
|
? _t("settings|general|email_address_label")
|
||||||
|
: _t("settings|general|msisdn_label")
|
||||||
|
}
|
||||||
|
autoComplete={medium === "email" ? "email" : "tel-national"}
|
||||||
|
disabled={disabled || isVerifying}
|
||||||
|
value={newThreepidInput}
|
||||||
|
onChange={onNewThreepidInputChange}
|
||||||
|
prefixComponent={phoneCountry}
|
||||||
|
/>
|
||||||
|
<AccessibleButton onClick={onAddClick} kind="primary" disabled={disabled}>
|
||||||
|
{_t("action|add")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AddRemoveThreepidsProps {
|
||||||
|
// Whether the control is for adding 3pids to the user's homeserver account or sharing them on an IS
|
||||||
|
mode: TheepidControlMode;
|
||||||
|
// Whether the control is for emails or phone numbers
|
||||||
|
medium: ThreepidMedium;
|
||||||
|
// The current list of third party identifiers
|
||||||
|
threepids: ThirdPartyIdentifier[];
|
||||||
|
// If true, the component is disabled and no third party identifiers can be added or removed
|
||||||
|
disabled?: boolean;
|
||||||
|
// Called when changes are made to the list of third party identifiers
|
||||||
|
onChange: () => void;
|
||||||
|
// If true, a spinner is shown instead of the component
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddRemoveThreepids: React.FC<AddRemoveThreepidsProps> = ({
|
||||||
|
mode,
|
||||||
|
medium,
|
||||||
|
threepids,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
isLoading,
|
||||||
|
}) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <InlineSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEmailElements = threepids.map((e) => {
|
||||||
|
return <ExistingThreepid mode={mode} threepid={e} onChange={onChange} key={e.address} disabled={disabled} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{existingEmailElements}
|
||||||
|
{mode === "hs" && <AddThreepidSection medium={medium} disabled={disabled} onChange={onChange} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -18,8 +18,6 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
import { ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||||
import { Alert } from "@vector-im/compound-web";
|
import { Alert } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import AccountEmailAddresses from "./account/EmailAddresses";
|
|
||||||
import AccountPhoneNumbers from "./account/PhoneNumbers";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import InlineSpinner from "../elements/InlineSpinner";
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
import SettingsSubsection from "./shared/SettingsSubsection";
|
import SettingsSubsection from "./shared/SettingsSubsection";
|
||||||
|
@ -27,6 +25,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
import { ThirdPartyIdentifier } from "../../../AddThreepid";
|
import { ThirdPartyIdentifier } from "../../../AddThreepid";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
import { AddRemoveThreepids } from "./AddRemoveThreepids";
|
||||||
|
|
||||||
type LoadingState = "loading" | "loaded" | "error";
|
type LoadingState = "loading" | "loaded" | "error";
|
||||||
|
|
||||||
|
@ -64,26 +63,28 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
||||||
|
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
useEffect(() => {
|
const updateThreepids = useCallback(async () => {
|
||||||
(async () => {
|
try {
|
||||||
try {
|
const threepids = await client.getThreePids();
|
||||||
const threepids = await client.getThreePids();
|
setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email));
|
||||||
setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email));
|
setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone));
|
||||||
setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone));
|
setLoadingState("loaded");
|
||||||
setLoadingState("loaded");
|
} catch (e) {
|
||||||
} catch (e) {
|
setLoadingState("error");
|
||||||
setLoadingState("error");
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const onEmailsChange = useCallback((emails: ThirdPartyIdentifier[]) => {
|
useEffect(() => {
|
||||||
setEmails(emails);
|
updateThreepids().then();
|
||||||
}, []);
|
}, [updateThreepids]);
|
||||||
|
|
||||||
const onMsisdnsChange = useCallback((msisdns: ThirdPartyIdentifier[]) => {
|
const onEmailsChange = useCallback(() => {
|
||||||
setPhoneNumbers(msisdns);
|
updateThreepids().then();
|
||||||
}, []);
|
}, [updateThreepids]);
|
||||||
|
|
||||||
|
const onMsisdnsChange = useCallback(() => {
|
||||||
|
updateThreepids().then();
|
||||||
|
}, [updateThreepids]);
|
||||||
|
|
||||||
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
|
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
|
||||||
|
|
||||||
|
@ -99,10 +100,13 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
||||||
error={_t("settings|general|unable_to_load_emails")}
|
error={_t("settings|general|unable_to_load_emails")}
|
||||||
loadingState={loadingState}
|
loadingState={loadingState}
|
||||||
>
|
>
|
||||||
<AccountEmailAddresses
|
<AddRemoveThreepids
|
||||||
emails={emails!}
|
mode="hs"
|
||||||
onEmailsChange={onEmailsChange}
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={emails!}
|
||||||
|
onChange={onEmailsChange}
|
||||||
disabled={!canMake3pidChanges}
|
disabled={!canMake3pidChanges}
|
||||||
|
isLoading={loadingState === "loading"}
|
||||||
/>
|
/>
|
||||||
</ThreepidSectionWrapper>
|
</ThreepidSectionWrapper>
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
@ -116,10 +120,13 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
||||||
error={_t("settings|general|unable_to_load_msisdns")}
|
error={_t("settings|general|unable_to_load_msisdns")}
|
||||||
loadingState={loadingState}
|
loadingState={loadingState}
|
||||||
>
|
>
|
||||||
<AccountPhoneNumbers
|
<AddRemoveThreepids
|
||||||
msisdns={phoneNumbers!}
|
mode="hs"
|
||||||
onMsisdnsChange={onMsisdnsChange}
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={phoneNumbers!}
|
||||||
|
onChange={onMsisdnsChange}
|
||||||
disabled={!canMake3pidChanges}
|
disabled={!canMake3pidChanges}
|
||||||
|
isLoading={loadingState === "loading"}
|
||||||
/>
|
/>
|
||||||
</ThreepidSectionWrapper>
|
</ThreepidSectionWrapper>
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
|
@ -1,303 +0,0 @@
|
||||||
/*
|
|
||||||
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 { ThreepidMedium, MatrixError } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
|
|
||||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
|
||||||
import Field from "../../elements/Field";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
|
||||||
import * as Email from "../../../../email";
|
|
||||||
import AddThreepid, { ThirdPartyIdentifier } from "../../../../AddThreepid";
|
|
||||||
import Modal from "../../../../Modal";
|
|
||||||
import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog";
|
|
||||||
|
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface IExistingEmailAddressProps {
|
|
||||||
email: ThirdPartyIdentifier;
|
|
||||||
onRemoved: (emails: ThirdPartyIdentifier) => void;
|
|
||||||
/**
|
|
||||||
* Disallow removal of this email address when truthy
|
|
||||||
*/
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IExistingEmailAddressState {
|
|
||||||
verifyRemove: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
|
|
||||||
public constructor(props: IExistingEmailAddressProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifyRemove: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDontRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onActuallyRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
MatrixClientPeg.safeGet()
|
|
||||||
.deleteThreePid(this.props.email.medium, this.props.email.address)
|
|
||||||
.then(() => {
|
|
||||||
return this.props.onRemoved(this.props.email);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to remove contact information: " + err);
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_remove_3pid"),
|
|
||||||
description: err && err.message ? err.message : _t("invite|failed_generic"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
if (this.state.verifyRemove) {
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_promptText">
|
|
||||||
{_t("settings|general|remove_email_prompt", { email: this.props.email.address })}
|
|
||||||
</span>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onActuallyRemove}
|
|
||||||
kind="danger_sm"
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
>
|
|
||||||
{_t("action|remove")}
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onDontRemove}
|
|
||||||
kind="link_sm"
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
>
|
|
||||||
{_t("action|cancel")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_address">
|
|
||||||
{this.props.email.address}
|
|
||||||
</span>
|
|
||||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm" disabled={this.props.disabled}>
|
|
||||||
{_t("action|remove")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
emails: ThirdPartyIdentifier[];
|
|
||||||
onEmailsChange: (emails: ThirdPartyIdentifier[]) => void;
|
|
||||||
/**
|
|
||||||
* Adding or removing emails is disabled when truthy
|
|
||||||
*/
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
verifying: boolean;
|
|
||||||
addTask: AddThreepid | null;
|
|
||||||
continueDisabled: boolean;
|
|
||||||
newEmailAddress: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EmailAddresses extends React.Component<IProps, IState> {
|
|
||||||
public constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifying: false,
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
newEmailAddress: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRemoved = (address: ThirdPartyIdentifier): void => {
|
|
||||||
const emails = this.props.emails.filter((e) => e !== address);
|
|
||||||
this.props.onEmailsChange(emails);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
this.setState({
|
|
||||||
newEmailAddress: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAddClick = (e: React.FormEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!this.state.newEmailAddress) return;
|
|
||||||
|
|
||||||
const email = this.state.newEmailAddress;
|
|
||||||
|
|
||||||
// TODO: Inline field validation
|
|
||||||
if (!Email.looksValid(email)) {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_invalid_email"),
|
|
||||||
description: _t("settings|general|error_invalid_email_detail"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = new AddThreepid(MatrixClientPeg.safeGet());
|
|
||||||
this.setState({ verifying: true, continueDisabled: true, addTask: task });
|
|
||||||
|
|
||||||
task.addEmailAddress(email)
|
|
||||||
.then(() => {
|
|
||||||
this.setState({ continueDisabled: false });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to add email address " + email + " " + err);
|
|
||||||
this.setState({ verifying: false, continueDisabled: false, addTask: null });
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_add_email"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onContinueClick = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ continueDisabled: true });
|
|
||||||
this.state.addTask
|
|
||||||
?.checkEmailLinkClicked()
|
|
||||||
.then(([finished]) => {
|
|
||||||
let newEmailAddress = this.state.newEmailAddress;
|
|
||||||
if (finished) {
|
|
||||||
const email = this.state.newEmailAddress;
|
|
||||||
const emails = [...this.props.emails, { address: email, medium: ThreepidMedium.Email }];
|
|
||||||
this.props.onEmailsChange(emails);
|
|
||||||
newEmailAddress = "";
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
verifying: false,
|
|
||||||
newEmailAddress,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to verify email address: ", err);
|
|
||||||
|
|
||||||
this.setState({ continueDisabled: false });
|
|
||||||
|
|
||||||
let underlyingError = err;
|
|
||||||
if (err instanceof UserFriendlyError) {
|
|
||||||
underlyingError = err.cause;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|email_not_verified"),
|
|
||||||
description: _t("settings|general|email_verification_instructions"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_email_verification"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const existingEmailElements = this.props.emails.map((e) => {
|
|
||||||
return (
|
|
||||||
<ExistingEmailAddress
|
|
||||||
email={e}
|
|
||||||
onRemoved={this.onRemoved}
|
|
||||||
key={e.address}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let addButton = (
|
|
||||||
<AccessibleButton onClick={this.onAddClick} kind="primary" disabled={this.props.disabled}>
|
|
||||||
{_t("action|add")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
if (this.state.verifying) {
|
|
||||||
addButton = (
|
|
||||||
<div>
|
|
||||||
<div>{_t("settings|general|add_email_instructions")}</div>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onContinueClick}
|
|
||||||
kind="primary"
|
|
||||||
disabled={this.state.continueDisabled}
|
|
||||||
>
|
|
||||||
{_t("action|continue")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{existingEmailElements}
|
|
||||||
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true}>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
label={_t("settings|general|email_address_label")}
|
|
||||||
autoComplete="email"
|
|
||||||
disabled={this.props.disabled || this.state.verifying}
|
|
||||||
value={this.state.newEmailAddress}
|
|
||||||
onChange={this.onChangeNewEmailAddress}
|
|
||||||
/>
|
|
||||||
{addButton}
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,342 +0,0 @@
|
||||||
/*
|
|
||||||
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 { IAuthData, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
|
|
||||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
|
||||||
import Field from "../../elements/Field";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
|
||||||
import AddThreepid, { ThirdPartyIdentifier } from "../../../../AddThreepid";
|
|
||||||
import CountryDropdown from "../../auth/CountryDropdown";
|
|
||||||
import Modal from "../../../../Modal";
|
|
||||||
import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog";
|
|
||||||
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
|
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
|
|
||||||
interface IExistingPhoneNumberProps {
|
|
||||||
msisdn: ThirdPartyIdentifier;
|
|
||||||
onRemoved: (phoneNumber: ThirdPartyIdentifier) => void;
|
|
||||||
/**
|
|
||||||
* Disable removing phone number
|
|
||||||
*/
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IExistingPhoneNumberState {
|
|
||||||
verifyRemove: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
|
|
||||||
public constructor(props: IExistingPhoneNumberProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifyRemove: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDontRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onActuallyRemove = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
MatrixClientPeg.safeGet()
|
|
||||||
.deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address)
|
|
||||||
.then(() => {
|
|
||||||
return this.props.onRemoved(this.props.msisdn);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to remove contact information: " + err);
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_remove_3pid"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
if (this.state.verifyRemove) {
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_promptText">
|
|
||||||
{_t("settings|general|remove_msisdn_prompt", { phone: this.props.msisdn.address })}
|
|
||||||
</span>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onActuallyRemove}
|
|
||||||
kind="danger_sm"
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
>
|
|
||||||
{_t("action|remove")}
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onDontRemove}
|
|
||||||
kind="link_sm"
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
>
|
|
||||||
{_t("action|cancel")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_address">
|
|
||||||
+{this.props.msisdn.address}
|
|
||||||
</span>
|
|
||||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm" disabled={this.props.disabled}>
|
|
||||||
{_t("action|remove")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
msisdns: ThirdPartyIdentifier[];
|
|
||||||
onMsisdnsChange: (phoneNumbers: ThirdPartyIdentifier[]) => void;
|
|
||||||
/**
|
|
||||||
* Adding or removing phone numbers is disabled when truthy
|
|
||||||
*/
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
verifying: boolean;
|
|
||||||
verifyError: string | null;
|
|
||||||
verifyMsisdn: string;
|
|
||||||
addTask: AddThreepid | null;
|
|
||||||
continueDisabled: boolean;
|
|
||||||
phoneCountry: string;
|
|
||||||
newPhoneNumber: string;
|
|
||||||
newPhoneNumberCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PhoneNumbers extends React.Component<IProps, IState> {
|
|
||||||
public constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifying: false,
|
|
||||||
verifyError: null,
|
|
||||||
verifyMsisdn: "",
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
phoneCountry: "",
|
|
||||||
newPhoneNumber: "",
|
|
||||||
newPhoneNumberCode: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRemoved = (address: ThirdPartyIdentifier): void => {
|
|
||||||
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
|
||||||
this.props.onMsisdnsChange(msisdns);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
this.setState({
|
|
||||||
newPhoneNumber: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
this.setState({
|
|
||||||
newPhoneNumberCode: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAddClick = (e: ButtonEvent | React.FormEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!this.state.newPhoneNumber) return;
|
|
||||||
|
|
||||||
const phoneNumber = this.state.newPhoneNumber;
|
|
||||||
const phoneCountry = this.state.phoneCountry;
|
|
||||||
|
|
||||||
const task = new AddThreepid(MatrixClientPeg.safeGet());
|
|
||||||
this.setState({ verifying: true, continueDisabled: true, addTask: task });
|
|
||||||
|
|
||||||
task.addMsisdn(phoneCountry, phoneNumber)
|
|
||||||
.then((response) => {
|
|
||||||
this.setState({ continueDisabled: false, verifyMsisdn: response.msisdn });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to add phone number " + phoneNumber + " " + err);
|
|
||||||
this.setState({ verifying: false, continueDisabled: false, addTask: null });
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("common|error"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onContinueClick = (e: ButtonEvent | React.FormEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.setState({ continueDisabled: true });
|
|
||||||
const token = this.state.newPhoneNumberCode;
|
|
||||||
const address = this.state.verifyMsisdn;
|
|
||||||
this.state.addTask
|
|
||||||
?.haveMsisdnToken(token)
|
|
||||||
.then(([finished]: [success?: boolean, result?: IAuthData | Error | null] = []) => {
|
|
||||||
let newPhoneNumber = this.state.newPhoneNumber;
|
|
||||||
if (finished !== false) {
|
|
||||||
const msisdns = [...this.props.msisdns, { address, medium: ThreepidMedium.Phone }];
|
|
||||||
this.props.onMsisdnsChange(msisdns);
|
|
||||||
newPhoneNumber = "";
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
verifying: false,
|
|
||||||
verifyMsisdn: "",
|
|
||||||
verifyError: null,
|
|
||||||
newPhoneNumber,
|
|
||||||
newPhoneNumberCode: "",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Unable to verify phone number: " + err);
|
|
||||||
this.setState({ continueDisabled: false });
|
|
||||||
|
|
||||||
let underlyingError = err;
|
|
||||||
if (err instanceof UserFriendlyError) {
|
|
||||||
underlyingError = err.cause;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_msisdn_verification"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
|
||||||
this.setState({ phoneCountry: country.iso2 });
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const existingPhoneElements = this.props.msisdns.map((p) => {
|
|
||||||
return (
|
|
||||||
<ExistingPhoneNumber
|
|
||||||
msisdn={p}
|
|
||||||
onRemoved={this.onRemoved}
|
|
||||||
key={p.address}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let addVerifySection = (
|
|
||||||
<AccessibleButton onClick={this.onAddClick} kind="primary" disabled={this.props.disabled}>
|
|
||||||
{_t("action|add")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
if (this.state.verifying) {
|
|
||||||
const msisdn = this.state.verifyMsisdn;
|
|
||||||
addVerifySection = (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{_t("settings|general|add_msisdn_instructions", { msisdn: msisdn })}
|
|
||||||
<br />
|
|
||||||
{this.state.verifyError}
|
|
||||||
</div>
|
|
||||||
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
label={_t("settings|general|msisdn_verification_field_label")}
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={this.props.disabled || this.state.continueDisabled}
|
|
||||||
value={this.state.newPhoneNumberCode}
|
|
||||||
onChange={this.onChangeNewPhoneNumberCode}
|
|
||||||
/>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this.onContinueClick}
|
|
||||||
kind="primary"
|
|
||||||
disabled={
|
|
||||||
this.props.disabled ||
|
|
||||||
this.state.continueDisabled ||
|
|
||||||
this.state.newPhoneNumberCode.length === 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{_t("action|continue")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const phoneCountry = (
|
|
||||||
<CountryDropdown
|
|
||||||
onOptionChange={this.onCountryChanged}
|
|
||||||
className="mx_PhoneNumbers_country"
|
|
||||||
value={this.state.phoneCountry}
|
|
||||||
disabled={this.state.verifying}
|
|
||||||
isSmall={true}
|
|
||||||
showPrefix={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{existingPhoneElements}
|
|
||||||
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
|
||||||
<div className="mx_PhoneNumbers_input">
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
label={_t("settings|general|msisdn_label")}
|
|
||||||
autoComplete="tel-national"
|
|
||||||
disabled={this.props.disabled || this.state.verifying}
|
|
||||||
prefixComponent={phoneCountry}
|
|
||||||
value={this.state.newPhoneNumber}
|
|
||||||
onChange={this.onChangeNewPhoneNumber}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{addVerifySection}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,8 +19,6 @@ import { SERVICE_TYPES, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { Alert } from "@vector-im/compound-web";
|
import { Alert } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import DiscoveryEmailAddresses from "../discovery/EmailAddresses";
|
|
||||||
import DiscoveryPhoneNumbers from "../discovery/PhoneNumbers";
|
|
||||||
import { getThreepidsWithBindStatus } from "../../../../boundThreepids";
|
import { getThreepidsWithBindStatus } from "../../../../boundThreepids";
|
||||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
import { ThirdPartyIdentifier } from "../../../../AddThreepid";
|
import { ThirdPartyIdentifier } from "../../../../AddThreepid";
|
||||||
|
@ -36,6 +34,7 @@ import { abbreviateUrl } from "../../../../utils/UrlUtils";
|
||||||
import { useDispatcher } from "../../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../../hooks/useDispatcher";
|
||||||
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../../dispatcher/payloads";
|
||||||
|
import { AddRemoveThreepids } from "../AddRemoveThreepids";
|
||||||
|
|
||||||
type RequiredPolicyInfo =
|
type RequiredPolicyInfo =
|
||||||
| {
|
| {
|
||||||
|
@ -56,9 +55,9 @@ type RequiredPolicyInfo =
|
||||||
export const DiscoverySettings: React.FC = () => {
|
export const DiscoverySettings: React.FC = () => {
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
|
const [isLoadingThreepids, setIsLoadingThreepids] = useState<boolean>(true);
|
||||||
const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]);
|
const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]);
|
||||||
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]);
|
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]);
|
||||||
const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading");
|
|
||||||
const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl()));
|
const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl()));
|
||||||
const [canMake3pidChanges, setCanMake3pidChanges] = useState<boolean>(false);
|
const [canMake3pidChanges, setCanMake3pidChanges] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -71,9 +70,11 @@ export const DiscoverySettings: React.FC = () => {
|
||||||
const [hasTerms, setHasTerms] = useState<boolean>(false);
|
const [hasTerms, setHasTerms] = useState<boolean>(false);
|
||||||
|
|
||||||
const getThreepidState = useCallback(async () => {
|
const getThreepidState = useCallback(async () => {
|
||||||
|
setIsLoadingThreepids(true);
|
||||||
const threepids = await getThreepidsWithBindStatus(client);
|
const threepids = await getThreepidsWithBindStatus(client);
|
||||||
setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email));
|
setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email));
|
||||||
setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone));
|
setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone));
|
||||||
|
setIsLoadingThreepids(false);
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
useDispatcher(
|
useDispatcher(
|
||||||
|
@ -133,11 +134,7 @@ export const DiscoverySettings: React.FC = () => {
|
||||||
);
|
);
|
||||||
logger.warn(e);
|
logger.warn(e);
|
||||||
}
|
}
|
||||||
|
} catch (e) {}
|
||||||
setLoadingState("loaded");
|
|
||||||
} catch (e) {
|
|
||||||
setLoadingState("error");
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}, [client, getThreepidState]);
|
}, [client, getThreepidState]);
|
||||||
|
|
||||||
|
@ -163,23 +160,44 @@ export const DiscoverySettings: React.FC = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const threepidSection = idServerName ? (
|
let threepidSection;
|
||||||
<>
|
if (idServerName) {
|
||||||
<DiscoveryEmailAddresses
|
threepidSection = (
|
||||||
emails={emails}
|
<>
|
||||||
isLoading={loadingState === "loading"}
|
<SettingsSubsection
|
||||||
disabled={!canMake3pidChanges}
|
heading={_t("settings|general|emails_heading")}
|
||||||
/>
|
description={emails.length === 0 ? _t("settings|general|discovery_email_empty") : undefined}
|
||||||
<DiscoveryPhoneNumbers
|
stretchContent
|
||||||
msisdns={phoneNumbers}
|
>
|
||||||
isLoading={loadingState === "loading"}
|
<AddRemoveThreepids
|
||||||
disabled={!canMake3pidChanges}
|
mode="is"
|
||||||
/>
|
medium={ThreepidMedium.Email}
|
||||||
</>
|
threepids={emails}
|
||||||
) : null;
|
onChange={getThreepidState}
|
||||||
|
disabled={!canMake3pidChanges}
|
||||||
|
isLoading={isLoadingThreepids}
|
||||||
|
/>
|
||||||
|
</SettingsSubsection>
|
||||||
|
<SettingsSubsection
|
||||||
|
heading={_t("settings|general|msisdns_heading")}
|
||||||
|
description={phoneNumbers.length === 0 ? _t("settings|general|discovery_msisdn_empty") : undefined}
|
||||||
|
stretchContent
|
||||||
|
>
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="is"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={phoneNumbers}
|
||||||
|
onChange={getThreepidState}
|
||||||
|
disabled={!canMake3pidChanges}
|
||||||
|
isLoading={isLoadingThreepids}
|
||||||
|
/>
|
||||||
|
</SettingsSubsection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSubsection heading={_t("settings|discovery|title")} data-testid="discoverySection">
|
<SettingsSubsection heading={_t("settings|discovery|title")} data-testid="discoverySection" stretchContent>
|
||||||
{threepidSection}
|
{threepidSection}
|
||||||
{/* has its own heading as it includes the current identity server */}
|
{/* has its own heading as it includes the current identity server */}
|
||||||
<SetIdServer missingTerms={false} />
|
<SetIdServer missingTerms={false} />
|
||||||
|
|
|
@ -1,251 +0,0 @@
|
||||||
/*
|
|
||||||
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 { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
|
||||||
import Modal from "../../../../Modal";
|
|
||||||
import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../../AddThreepid";
|
|
||||||
import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog";
|
|
||||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
|
||||||
import InlineSpinner from "../../elements/InlineSpinner";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
|
||||||
|
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface IEmailAddressProps {
|
|
||||||
email: ThirdPartyIdentifier;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IEmailAddressState {
|
|
||||||
verifying: boolean;
|
|
||||||
addTask: AddThreepid | null;
|
|
||||||
continueDisabled: boolean;
|
|
||||||
bound?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddressState> {
|
|
||||||
public constructor(props: IEmailAddressProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { bound } = props.email;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifying: false,
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
bound,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IEmailAddressProps>): void {
|
|
||||||
if (this.props.email !== prevProps.email) {
|
|
||||||
const { bound } = this.props.email;
|
|
||||||
this.setState({ bound });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async changeBinding({ bind, label, errorTitle }: Binding): Promise<void> {
|
|
||||||
const { medium, address } = this.props.email;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (bind) {
|
|
||||||
const task = new AddThreepid(MatrixClientPeg.safeGet());
|
|
||||||
this.setState({
|
|
||||||
verifying: true,
|
|
||||||
continueDisabled: true,
|
|
||||||
addTask: task,
|
|
||||||
});
|
|
||||||
await task.bindEmailAddress(address);
|
|
||||||
this.setState({
|
|
||||||
continueDisabled: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await MatrixClientPeg.safeGet().unbindThreePid(medium, address);
|
|
||||||
}
|
|
||||||
this.setState({ bound: bind });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`changeBinding: Unable to ${label} email address ${address}`, err);
|
|
||||||
this.setState({
|
|
||||||
verifying: false,
|
|
||||||
continueDisabled: false,
|
|
||||||
addTask: null,
|
|
||||||
});
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: errorTitle,
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRevokeClick = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.changeBinding({
|
|
||||||
bind: false,
|
|
||||||
label: "revoke",
|
|
||||||
errorTitle: _t("settings|general|error_revoke_email_discovery"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onShareClick = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.changeBinding({
|
|
||||||
bind: true,
|
|
||||||
label: "share",
|
|
||||||
errorTitle: _t("settings|general|error_share_email_discovery"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onContinueClick = async (e: ButtonEvent): Promise<void> => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Prevent the continue button from being pressed multiple times while we're working
|
|
||||||
this.setState({ continueDisabled: true });
|
|
||||||
try {
|
|
||||||
await this.state.addTask?.checkEmailLinkClicked();
|
|
||||||
this.setState({
|
|
||||||
addTask: null,
|
|
||||||
verifying: false,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Unable to verify email address:`, err);
|
|
||||||
|
|
||||||
let underlyingError = err;
|
|
||||||
if (err instanceof UserFriendlyError) {
|
|
||||||
underlyingError = err.cause;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|email_not_verified"),
|
|
||||||
description: _t("settings|general|email_verification_instructions"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Unable to verify email address: " + err);
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_email_verification"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// Re-enable the continue button so the user can retry
|
|
||||||
this.setState({ continueDisabled: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const { address } = this.props.email;
|
|
||||||
const { verifying, bound } = this.state;
|
|
||||||
|
|
||||||
let status;
|
|
||||||
if (verifying) {
|
|
||||||
status = (
|
|
||||||
<span>
|
|
||||||
{_t("settings|general|discovery_email_verification_instructions")}
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
kind="primary_sm"
|
|
||||||
onClick={this.onContinueClick}
|
|
||||||
disabled={this.state.continueDisabled}
|
|
||||||
>
|
|
||||||
{_t("action|complete")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (bound) {
|
|
||||||
status = (
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
kind="danger_sm"
|
|
||||||
onClick={this.onRevokeClick}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
>
|
|
||||||
{_t("action|revoke")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
status = (
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
kind="primary_sm"
|
|
||||||
onClick={this.onShareClick}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
>
|
|
||||||
{_t("action|share")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_address">{address}</span>
|
|
||||||
{status}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interface IProps {
|
|
||||||
emails: ThirdPartyIdentifier[];
|
|
||||||
isLoading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EmailAddresses extends React.Component<IProps> {
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
let content;
|
|
||||||
if (this.props.isLoading) {
|
|
||||||
content = <InlineSpinner />;
|
|
||||||
} else if (this.props.emails.length > 0) {
|
|
||||||
content = this.props.emails.map((e) => {
|
|
||||||
return <EmailAddress email={e} key={e.address} disabled={this.props.disabled} />;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasEmails = !!this.props.emails.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsSubsection
|
|
||||||
heading={_t("settings|general|emails_heading")}
|
|
||||||
description={(!hasEmails && _t("settings|general|discovery_email_empty")) || undefined}
|
|
||||||
stretchContent
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</SettingsSubsection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,263 +0,0 @@
|
||||||
/*
|
|
||||||
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 { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
|
||||||
import Modal from "../../../../Modal";
|
|
||||||
import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../../AddThreepid";
|
|
||||||
import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog";
|
|
||||||
import Field from "../../elements/Field";
|
|
||||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
|
||||||
import InlineSpinner from "../../elements/InlineSpinner";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
|
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
|
|
||||||
interface IPhoneNumberProps {
|
|
||||||
msisdn: ThirdPartyIdentifier;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPhoneNumberState {
|
|
||||||
verifying: boolean;
|
|
||||||
verificationCode: string;
|
|
||||||
addTask: AddThreepid | null;
|
|
||||||
continueDisabled: boolean;
|
|
||||||
bound?: boolean;
|
|
||||||
verifyError: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumberState> {
|
|
||||||
public constructor(props: IPhoneNumberProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { bound } = props.msisdn;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifying: false,
|
|
||||||
verificationCode: "",
|
|
||||||
addTask: null,
|
|
||||||
continueDisabled: false,
|
|
||||||
bound,
|
|
||||||
verifyError: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IPhoneNumberProps>): void {
|
|
||||||
if (this.props.msisdn !== prevProps.msisdn) {
|
|
||||||
const { bound } = this.props.msisdn;
|
|
||||||
this.setState({ bound });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async changeBinding({ bind, label, errorTitle }: Binding): Promise<void> {
|
|
||||||
const { medium, address } = this.props.msisdn;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (bind) {
|
|
||||||
const task = new AddThreepid(MatrixClientPeg.safeGet());
|
|
||||||
this.setState({
|
|
||||||
verifying: true,
|
|
||||||
continueDisabled: true,
|
|
||||||
addTask: task,
|
|
||||||
});
|
|
||||||
// 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
|
|
||||||
// @ts-ignore
|
|
||||||
await task.bindMsisdn(null, `+${address}`);
|
|
||||||
this.setState({
|
|
||||||
continueDisabled: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await MatrixClientPeg.safeGet().unbindThreePid(medium, address);
|
|
||||||
}
|
|
||||||
this.setState({ bound: bind });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`changeBinding: Unable to ${label} phone number ${address}`, err);
|
|
||||||
this.setState({
|
|
||||||
verifying: false,
|
|
||||||
continueDisabled: false,
|
|
||||||
addTask: null,
|
|
||||||
});
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: errorTitle,
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRevokeClick = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.changeBinding({
|
|
||||||
bind: false,
|
|
||||||
label: "revoke",
|
|
||||||
errorTitle: _t("settings|general|error_revoke_msisdn_discovery"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onShareClick = (e: ButtonEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.changeBinding({
|
|
||||||
bind: true,
|
|
||||||
label: "share",
|
|
||||||
errorTitle: _t("settings|general|error_share_msisdn_discovery"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
this.setState({
|
|
||||||
verificationCode: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onContinueClick = async (e: ButtonEvent | React.FormEvent): Promise<void> => {
|
|
||||||
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) {
|
|
||||||
logger.error("Unable to verify phone number:", err);
|
|
||||||
|
|
||||||
let underlyingError = err;
|
|
||||||
if (err instanceof UserFriendlyError) {
|
|
||||||
underlyingError = err.cause;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ continueDisabled: false });
|
|
||||||
if (underlyingError instanceof MatrixError && underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") {
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: _t("settings|general|error_msisdn_verification"),
|
|
||||||
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const { address } = this.props.msisdn;
|
|
||||||
const { verifying, bound } = this.state;
|
|
||||||
|
|
||||||
let status;
|
|
||||||
if (verifying) {
|
|
||||||
status = (
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_verification">
|
|
||||||
<span>
|
|
||||||
{_t("settings|general|msisdn_verification_instructions")}
|
|
||||||
<br />
|
|
||||||
{this.state.verifyError}
|
|
||||||
</span>
|
|
||||||
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
label={_t("settings|general|msisdn_verification_field_label")}
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={this.state.continueDisabled}
|
|
||||||
value={this.state.verificationCode}
|
|
||||||
onChange={this.onVerificationCodeChange}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (bound) {
|
|
||||||
status = (
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
kind="danger_sm"
|
|
||||||
onClick={this.onRevokeClick}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
>
|
|
||||||
{_t("action|revoke")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
status = (
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_EmailAddressesPhoneNumbers_discovery_existing_button"
|
|
||||||
kind="primary_sm"
|
|
||||||
onClick={this.onShareClick}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
>
|
|
||||||
{_t("action|share")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_EmailAddressesPhoneNumbers_discovery_existing">
|
|
||||||
<span className="mx_EmailAddressesPhoneNumbers_discovery_existing_address">+{address}</span>
|
|
||||||
{status}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
msisdns: ThirdPartyIdentifier[];
|
|
||||||
isLoading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PhoneNumbers extends React.Component<IProps> {
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
let content;
|
|
||||||
if (this.props.isLoading) {
|
|
||||||
content = <InlineSpinner />;
|
|
||||||
} else if (this.props.msisdns.length > 0) {
|
|
||||||
content = this.props.msisdns.map((e) => {
|
|
||||||
return <PhoneNumber msisdn={e} key={e.address} disabled={this.props.disabled} />;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = (!content && _t("settings|general|discovery_msisdn_empty")) || undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsSubsection
|
|
||||||
data-testid="mx_DiscoveryPhoneNumbers"
|
|
||||||
heading={_t("settings|general|msisdns_heading")}
|
|
||||||
description={description}
|
|
||||||
stretchContent
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</SettingsSubsection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2532,7 +2532,6 @@
|
||||||
"error_share_msisdn_discovery": "Unable to share phone number",
|
"error_share_msisdn_discovery": "Unable to share phone number",
|
||||||
"identity_server_no_token": "No identity access token found",
|
"identity_server_no_token": "No identity access token found",
|
||||||
"identity_server_not_set": "Identity server not set",
|
"identity_server_not_set": "Identity server not set",
|
||||||
"incorrect_msisdn_verification": "Incorrect verification code",
|
|
||||||
"language_section": "Language",
|
"language_section": "Language",
|
||||||
"msisdn_in_use": "This phone number is already in use",
|
"msisdn_in_use": "This phone number is already in use",
|
||||||
"msisdn_label": "Phone Number",
|
"msisdn_label": "Phone Number",
|
||||||
|
|
534
test/components/views/settings/AddRemoveThreepids-test.tsx
Normal file
534
test/components/views/settings/AddRemoveThreepids-test.tsx
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 { render, screen } from "@testing-library/react";
|
||||||
|
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||||
|
import React from "react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { AddRemoveThreepids } from "../../../../src/components/views/settings/AddRemoveThreepids";
|
||||||
|
import { clearAllModals, stubClient } from "../../../test-utils";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
|
||||||
|
const MOCK_IDENTITY_ACCESS_TOKEN = "mock_identity_access_token";
|
||||||
|
const mockGetAccessToken = jest.fn().mockResolvedValue(MOCK_IDENTITY_ACCESS_TOKEN);
|
||||||
|
jest.mock("../../../../src/IdentityAuthClient", () =>
|
||||||
|
jest.fn().mockImplementation(() => ({
|
||||||
|
getAccessToken: mockGetAccessToken,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const EMAIL1 = {
|
||||||
|
medium: ThreepidMedium.Email,
|
||||||
|
address: "alice@nowhere.dummy",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PHONE1 = {
|
||||||
|
medium: ThreepidMedium.Phone,
|
||||||
|
address: "447700900000",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PHONE1_LOCALNUM = "07700900000";
|
||||||
|
|
||||||
|
describe("AddRemoveThreepids", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = stubClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
clearAllModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientProviderWrapper: React.FC = ({ children }) => (
|
||||||
|
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should render a loader while loading", async () => {
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={true}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render email addresses", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[EMAIL1]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render phone numbers", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[PHONE1]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle no email addresses", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add an email address", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: "Email Address" });
|
||||||
|
await userEvent.type(input, EMAIL1.address);
|
||||||
|
const addButton = screen.getByRole("button", { name: "Add" });
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
expect(client.requestAdd3pidEmailToken).toHaveBeenCalledWith(EMAIL1.address, client.generateClientSecret(), 1);
|
||||||
|
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||||
|
|
||||||
|
expect(continueButton).toBeEnabled();
|
||||||
|
|
||||||
|
await userEvent.click(continueButton);
|
||||||
|
|
||||||
|
expect(client.addThreePidOnly).toHaveBeenCalledWith({
|
||||||
|
client_secret: client.generateClientSecret(),
|
||||||
|
sid: "1",
|
||||||
|
auth: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display an error if the link has not been clicked", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
const createDialogFn = jest.spyOn(Modal, "createDialog");
|
||||||
|
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: "Email Address" });
|
||||||
|
await userEvent.type(input, EMAIL1.address);
|
||||||
|
const addButton = screen.getByRole("button", { name: "Add" });
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||||
|
|
||||||
|
expect(continueButton).toBeEnabled();
|
||||||
|
|
||||||
|
mocked(client).addThreePidOnly.mockRejectedValueOnce(new Error("Unauthorized"));
|
||||||
|
|
||||||
|
await userEvent.click(continueButton);
|
||||||
|
|
||||||
|
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
||||||
|
description: "Unauthorized",
|
||||||
|
title: "Unable to verify email address.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a phone number", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
|
||||||
|
sid: "1",
|
||||||
|
msisdn: PHONE1.address,
|
||||||
|
intl_fmt: "+" + PHONE1.address,
|
||||||
|
success: true,
|
||||||
|
submit_url: "https://example.dummy",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const countryDropdown = screen.getByRole("button", { name: "Country Dropdown" });
|
||||||
|
await userEvent.click(countryDropdown);
|
||||||
|
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
|
||||||
|
await userEvent.click(gbOption);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: "Phone Number" });
|
||||||
|
await userEvent.type(input, PHONE1_LOCALNUM);
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: "Add" });
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
|
||||||
|
"GB",
|
||||||
|
PHONE1_LOCALNUM,
|
||||||
|
client.generateClientSecret(),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||||
|
|
||||||
|
expect(continueButton).toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
|
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||||
|
await userEvent.type(verificationInput, "123456");
|
||||||
|
|
||||||
|
expect(continueButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
await userEvent.click(continueButton);
|
||||||
|
|
||||||
|
expect(client.addThreePidOnly).toHaveBeenCalledWith({
|
||||||
|
client_secret: client.generateClientSecret(),
|
||||||
|
sid: "1",
|
||||||
|
auth: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display an error if the code is incorrect", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
const createDialogFn = jest.spyOn(Modal, "createDialog");
|
||||||
|
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
|
||||||
|
sid: "1",
|
||||||
|
msisdn: PHONE1.address,
|
||||||
|
intl_fmt: "+" + PHONE1.address,
|
||||||
|
success: true,
|
||||||
|
submit_url: "https://example.dummy",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: "Phone Number" });
|
||||||
|
await userEvent.type(input, PHONE1_LOCALNUM);
|
||||||
|
|
||||||
|
const countryDropdown = screen.getByRole("button", { name: "Country Dropdown" });
|
||||||
|
await userEvent.click(countryDropdown);
|
||||||
|
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
|
||||||
|
await userEvent.click(gbOption);
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: "Add" });
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
mocked(client).addThreePidOnly.mockRejectedValueOnce(new Error("Unauthorized"));
|
||||||
|
|
||||||
|
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||||
|
await userEvent.type(verificationInput, "123457");
|
||||||
|
|
||||||
|
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||||
|
await userEvent.click(continueButton);
|
||||||
|
|
||||||
|
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
||||||
|
description: "Unauthorized",
|
||||||
|
title: "Unable to verify phone number.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove an email address", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[EMAIL1]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeButton = screen.getByRole("button", { name: "Remove" });
|
||||||
|
await userEvent.click(removeButton);
|
||||||
|
|
||||||
|
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
|
||||||
|
|
||||||
|
const confirmRemoveButton = screen.getByRole("button", { name: "Remove" });
|
||||||
|
await userEvent.click(confirmRemoveButton);
|
||||||
|
|
||||||
|
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return to default view if adding is cancelled", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[EMAIL1]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeButton = screen.getByRole("button", { name: "Remove" });
|
||||||
|
await userEvent.click(removeButton);
|
||||||
|
|
||||||
|
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
|
||||||
|
|
||||||
|
const confirmRemoveButton = screen.getByRole("button", { name: "Cancel" });
|
||||||
|
await userEvent.click(confirmRemoveButton);
|
||||||
|
|
||||||
|
expect(screen.queryByText(`Remove ${EMAIL1.address}?`)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(client.deleteThreePid).not.toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||||
|
expect(onChangeFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a phone number", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="hs"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[PHONE1]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeButton = screen.getByRole("button", { name: "Remove" });
|
||||||
|
await userEvent.click(removeButton);
|
||||||
|
|
||||||
|
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible();
|
||||||
|
|
||||||
|
const confirmRemoveButton = screen.getByRole("button", { name: "Remove" });
|
||||||
|
await userEvent.click(confirmRemoveButton);
|
||||||
|
|
||||||
|
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind an email address", async () => {
|
||||||
|
mocked(client).requestEmailToken.mockResolvedValue({ sid: "1" });
|
||||||
|
|
||||||
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
|
||||||
|
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="is"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[Object.assign({}, EMAIL1, { bound: false })]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(EMAIL1.address)).toBeVisible();
|
||||||
|
const shareButton = screen.getByRole("button", { name: "Share" });
|
||||||
|
await userEvent.click(shareButton);
|
||||||
|
|
||||||
|
expect(screen.getByText("Verify the link in your inbox")).toBeVisible();
|
||||||
|
|
||||||
|
expect(client.requestEmailToken).toHaveBeenCalledWith(
|
||||||
|
EMAIL1.address,
|
||||||
|
client.generateClientSecret(),
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
MOCK_IDENTITY_ACCESS_TOKEN,
|
||||||
|
);
|
||||||
|
|
||||||
|
const completeButton = screen.getByRole("button", { name: "Complete" });
|
||||||
|
await userEvent.click(completeButton);
|
||||||
|
|
||||||
|
expect(client.bindThreePid).toHaveBeenCalledWith({
|
||||||
|
sid: "1",
|
||||||
|
client_secret: client.generateClientSecret(),
|
||||||
|
id_server: "https://the_best_id_server.dummy",
|
||||||
|
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind a phone number", async () => {
|
||||||
|
mocked(client).requestMsisdnToken.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
sid: "1",
|
||||||
|
msisdn: PHONE1.address,
|
||||||
|
intl_fmt: "+" + PHONE1.address,
|
||||||
|
});
|
||||||
|
|
||||||
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
|
||||||
|
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="is"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[Object.assign({}, PHONE1, { bound: false })]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(PHONE1.address)).toBeVisible();
|
||||||
|
const shareButton = screen.getByRole("button", { name: "Share" });
|
||||||
|
await userEvent.click(shareButton);
|
||||||
|
|
||||||
|
expect(screen.getByText("Please enter verification code sent via text.")).toBeVisible();
|
||||||
|
|
||||||
|
expect(client.requestMsisdnToken).toHaveBeenCalledWith(
|
||||||
|
null,
|
||||||
|
"+" + PHONE1.address,
|
||||||
|
client.generateClientSecret(),
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
MOCK_IDENTITY_ACCESS_TOKEN,
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||||
|
await userEvent.type(codeInput, "123456");
|
||||||
|
await userEvent.keyboard("{Enter}");
|
||||||
|
|
||||||
|
expect(client.bindThreePid).toHaveBeenCalledWith({
|
||||||
|
sid: "1",
|
||||||
|
client_secret: client.generateClientSecret(),
|
||||||
|
id_server: "https://the_best_id_server.dummy",
|
||||||
|
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should revoke a bound email address", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="is"
|
||||||
|
medium={ThreepidMedium.Email}
|
||||||
|
threepids={[Object.assign({}, EMAIL1, { bound: true })]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(EMAIL1.address)).toBeVisible();
|
||||||
|
const revokeButton = screen.getByRole("button", { name: "Revoke" });
|
||||||
|
await userEvent.click(revokeButton);
|
||||||
|
|
||||||
|
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should revoke a bound phone number", async () => {
|
||||||
|
const onChangeFn = jest.fn();
|
||||||
|
render(
|
||||||
|
<AddRemoveThreepids
|
||||||
|
mode="is"
|
||||||
|
medium={ThreepidMedium.Phone}
|
||||||
|
threepids={[Object.assign({}, PHONE1, { bound: true })]}
|
||||||
|
isLoading={false}
|
||||||
|
onChange={onChangeFn}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: clientProviderWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(PHONE1.address)).toBeVisible();
|
||||||
|
const revokeButton = screen.getByRole("button", { name: "Revoke" });
|
||||||
|
await userEvent.click(revokeButton);
|
||||||
|
|
||||||
|
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
|
||||||
|
expect(onChangeFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,172 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AddRemoveThreepids should handle no email addresses 1`] = `
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
autocomplete="off"
|
||||||
|
novalidate=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autocomplete="email"
|
||||||
|
id="mx_Field_3"
|
||||||
|
label="Email Address"
|
||||||
|
placeholder="Email Address"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_3"
|
||||||
|
>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AddRemoveThreepids should render email addresses 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_AddRemoveThreepids_existing"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_AddRemoveThreepids_existing_address"
|
||||||
|
>
|
||||||
|
alice@nowhere.dummy
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
autocomplete="off"
|
||||||
|
novalidate=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autocomplete="email"
|
||||||
|
id="mx_Field_1"
|
||||||
|
label="Email Address"
|
||||||
|
placeholder="Email Address"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_1"
|
||||||
|
>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AddRemoveThreepids should render phone numbers 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_AddRemoveThreepids_existing"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_AddRemoveThreepids_existing_address"
|
||||||
|
>
|
||||||
|
447700900000
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
autocomplete="off"
|
||||||
|
novalidate=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Field_prefix"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown mx_PhoneNumbers_country mx_CountryDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-describedby="mx_CountryDropdown_value"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="Country Dropdown"
|
||||||
|
aria-owns="mx_CountryDropdown_input"
|
||||||
|
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown_option"
|
||||||
|
id="mx_CountryDropdown_value"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_CountryDropdown_shortOption"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown_option_emoji"
|
||||||
|
>
|
||||||
|
🇺🇸
|
||||||
|
</div>
|
||||||
|
+1
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="mx_Dropdown_arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
autocomplete="tel-national"
|
||||||
|
id="mx_Field_2"
|
||||||
|
label="Phone Number"
|
||||||
|
placeholder="Phone Number"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_2"
|
||||||
|
>
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 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 { render } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
|
|
||||||
import PhoneNumbers from "../../../../../src/components/views/settings/account/PhoneNumbers";
|
|
||||||
import { stubClient } from "../../../../test-utils";
|
|
||||||
import SdkConfig from "../../../../../src/SdkConfig";
|
|
||||||
|
|
||||||
describe("<PhoneNumbers />", () => {
|
|
||||||
it("should allow a phone number to be added", async () => {
|
|
||||||
SdkConfig.add({
|
|
||||||
default_country_code: "GB",
|
|
||||||
});
|
|
||||||
|
|
||||||
const cli = stubClient();
|
|
||||||
const onMsisdnsChange = jest.fn();
|
|
||||||
const { asFragment, getByLabelText, getByText } = render(
|
|
||||||
<PhoneNumbers msisdns={[]} onMsisdnsChange={onMsisdnsChange} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
mocked(cli.requestAdd3pidMsisdnToken).mockResolvedValue({
|
|
||||||
sid: "SID",
|
|
||||||
msisdn: "447900111222",
|
|
||||||
submit_url: "https://server.url",
|
|
||||||
success: true,
|
|
||||||
intl_fmt: "no-clue",
|
|
||||||
});
|
|
||||||
mocked(cli.submitMsisdnTokenOtherUrl).mockResolvedValue({ success: true });
|
|
||||||
mocked(cli.addThreePidOnly).mockResolvedValue({});
|
|
||||||
|
|
||||||
const phoneNumberField = getByLabelText("Phone Number");
|
|
||||||
await userEvent.type(phoneNumberField, "7900111222");
|
|
||||||
await userEvent.click(getByText("Add"));
|
|
||||||
|
|
||||||
expect(cli.requestAdd3pidMsisdnToken).toHaveBeenCalledWith("GB", "7900111222", "t35tcl1Ent5ECr3T", 1);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
const verificationCodeField = getByLabelText("Verification code");
|
|
||||||
await userEvent.type(verificationCodeField, "123666");
|
|
||||||
await userEvent.click(getByText("Continue"));
|
|
||||||
|
|
||||||
expect(cli.submitMsisdnTokenOtherUrl).toHaveBeenCalledWith(
|
|
||||||
"https://server.url",
|
|
||||||
"SID",
|
|
||||||
"t35tcl1Ent5ECr3T",
|
|
||||||
"123666",
|
|
||||||
);
|
|
||||||
expect(onMsisdnsChange).toHaveBeenCalledWith([{ address: "447900111222", medium: "msisdn" }]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,110 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<PhoneNumbers /> should allow a phone number to be added 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<form
|
|
||||||
autocomplete="off"
|
|
||||||
class="mx_PhoneNumbers_new"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_PhoneNumbers_input"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_Field_prefix"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dropdown mx_PhoneNumbers_country mx_CountryDropdown mx_Dropdown_disabled"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-describedby="mx_CountryDropdown_value"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-label="Country Dropdown"
|
|
||||||
aria-owns="mx_CountryDropdown_input"
|
|
||||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled"
|
|
||||||
disabled=""
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dropdown_option"
|
|
||||||
id="mx_CountryDropdown_value"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_CountryDropdown_shortOption"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dropdown_option_emoji"
|
|
||||||
>
|
|
||||||
🇬🇧
|
|
||||||
</div>
|
|
||||||
+44
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="mx_Dropdown_arrow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
autocomplete="tel-national"
|
|
||||||
disabled=""
|
|
||||||
id="mx_Field_1"
|
|
||||||
label="Phone Number"
|
|
||||||
placeholder="Phone Number"
|
|
||||||
type="text"
|
|
||||||
value="7900111222"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_Field_1"
|
|
||||||
>
|
|
||||||
Phone Number
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
A text message has been sent to +447900111222. Please enter the verification code it contains.
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
autocomplete="off"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
autocomplete="off"
|
|
||||||
id="mx_Field_2"
|
|
||||||
label="Verification code"
|
|
||||||
placeholder="Verification code"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_Field_2"
|
|
||||||
>
|
|
||||||
Verification code
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
aria-disabled="true"
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
|
||||||
disabled=""
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
|
@ -1,167 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 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 { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import { IThreepid, ThreepidMedium, IRequestTokenResponse, MatrixError } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { TranslationKey, UserFriendlyError } from "../../../../../src/languageHandler";
|
|
||||||
import EmailAddresses, { EmailAddress } from "../../../../../src/components/views/settings/discovery/EmailAddresses";
|
|
||||||
import { clearAllModals, getMockClientWithEventEmitter } from "../../../../test-utils";
|
|
||||||
|
|
||||||
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
|
|
||||||
jest.mock("../../../../../src/IdentityAuthClient", () =>
|
|
||||||
jest.fn().mockImplementation(() => ({
|
|
||||||
getAccessToken: mockGetAccessToken,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailThreepidFixture: IThreepid = {
|
|
||||||
medium: ThreepidMedium.Email,
|
|
||||||
address: "foo@bar.com",
|
|
||||||
validated_at: 12345,
|
|
||||||
added_at: 12342,
|
|
||||||
bound: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("<EmailAddress/>", () => {
|
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
|
||||||
getIdentityServerUrl: jest.fn().mockReturnValue("https://fake-identity-server"),
|
|
||||||
generateClientSecret: jest.fn(),
|
|
||||||
requestEmailToken: jest.fn(),
|
|
||||||
bindThreePid: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
await clearAllModals();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should track props.email.bound changes", async () => {
|
|
||||||
const { rerender } = render(<EmailAddress email={emailThreepidFixture} />);
|
|
||||||
await screen.findByText("Share");
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<EmailAddress
|
|
||||||
email={{
|
|
||||||
...emailThreepidFixture,
|
|
||||||
bound: true,
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await screen.findByText("Revoke");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Email verification share phase", () => {
|
|
||||||
it("shows translated error message", async () => {
|
|
||||||
render(<EmailAddress email={emailThreepidFixture} />);
|
|
||||||
mockClient.requestEmailToken.mockRejectedValue(
|
|
||||||
new MatrixError(
|
|
||||||
{ errcode: "M_THREEPID_IN_USE", error: "Some fake MatrixError occured" },
|
|
||||||
400,
|
|
||||||
"https://fake-url/",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
fireEvent.click(screen.getByText("Share"));
|
|
||||||
|
|
||||||
// Expect error dialog/modal to be shown. We have to wait for the UI to transition.
|
|
||||||
expect(await screen.findByText("This email address is already in use")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Email verification complete phase", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
// Start these tests out at the "Complete" phase
|
|
||||||
render(<EmailAddress email={emailThreepidFixture} />);
|
|
||||||
mockClient.requestEmailToken.mockResolvedValue({ sid: "123-fake-sid" } satisfies IRequestTokenResponse);
|
|
||||||
fireEvent.click(screen.getByText("Share"));
|
|
||||||
// Then wait for the completion screen to come up
|
|
||||||
await screen.findByText("Complete");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Shows error dialog when share completion fails (email not verified yet)", async () => {
|
|
||||||
mockClient.bindThreePid.mockRejectedValue(
|
|
||||||
new MatrixError(
|
|
||||||
{ errcode: "M_THREEPID_AUTH_FAILED", error: "Some fake MatrixError occured" },
|
|
||||||
403,
|
|
||||||
"https://fake-url/",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await expect(screen.findByText("Complete")).resolves.not.toHaveAttribute("aria-disabled", "true");
|
|
||||||
fireEvent.click(screen.getByText("Complete"));
|
|
||||||
|
|
||||||
// Expect error dialog/modal to be shown. We have to wait for the UI to transition.
|
|
||||||
// Check the title
|
|
||||||
expect(await screen.findByText("Your email address hasn't been verified yet")).toBeInTheDocument();
|
|
||||||
// Check the description
|
|
||||||
expect(
|
|
||||||
await screen.findByText(
|
|
||||||
"Click the link in the email you received to verify and then click continue again.",
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Shows error dialog when share completion fails (UserFriendlyError)", async () => {
|
|
||||||
const fakeErrorText = "Fake UserFriendlyError error in test" as TranslationKey;
|
|
||||||
mockClient.bindThreePid.mockRejectedValue(new UserFriendlyError(fakeErrorText));
|
|
||||||
await expect(screen.findByText("Complete")).resolves.not.toHaveAttribute("aria-disabled", "true");
|
|
||||||
fireEvent.click(screen.getByText("Complete"));
|
|
||||||
|
|
||||||
// Expect error dialog/modal to be shown. We have to wait for the UI to transition.
|
|
||||||
// Check the title
|
|
||||||
expect(await screen.findByText("Unable to verify email address.")).toBeInTheDocument();
|
|
||||||
// Check the description
|
|
||||||
expect(await screen.findByText(fakeErrorText)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Shows error dialog when share completion fails (generic error)", async () => {
|
|
||||||
const fakeErrorText = "Fake plain error in test";
|
|
||||||
mockClient.bindThreePid.mockRejectedValue(new Error(fakeErrorText));
|
|
||||||
await expect(screen.findByText("Complete")).resolves.not.toHaveAttribute("aria-disabled", "true");
|
|
||||||
fireEvent.click(screen.getByText("Complete"));
|
|
||||||
|
|
||||||
// Expect error dialog/modal to be shown. We have to wait for the UI to transition.
|
|
||||||
// Check the title
|
|
||||||
expect(await screen.findByText("Unable to verify email address.")).toBeInTheDocument();
|
|
||||||
// Check the description
|
|
||||||
expect(await screen.findByText(fakeErrorText)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("<EmailAddresses />", () => {
|
|
||||||
it("should render a loader while loading", async () => {
|
|
||||||
const { container } = render(<EmailAddresses emails={[emailThreepidFixture]} isLoading={true} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render email addresses", async () => {
|
|
||||||
const { container } = render(<EmailAddresses emails={[emailThreepidFixture]} isLoading={false} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle no email addresses", async () => {
|
|
||||||
const { container } = render(<EmailAddresses emails={[]} isLoading={false} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 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 { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
||||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
|
|
||||||
import PhoneNumbers, { PhoneNumber } from "../../../../../src/components/views/settings/discovery/PhoneNumbers";
|
|
||||||
import { stubClient } from "../../../../test-utils";
|
|
||||||
|
|
||||||
const msisdn: IThreepid = {
|
|
||||||
medium: ThreepidMedium.Phone,
|
|
||||||
address: "441111111111",
|
|
||||||
validated_at: 12345,
|
|
||||||
added_at: 12342,
|
|
||||||
bound: false,
|
|
||||||
};
|
|
||||||
describe("<PhoneNumber/>", () => {
|
|
||||||
it("should track props.msisdn.bound changes", async () => {
|
|
||||||
const { rerender } = render(<PhoneNumber msisdn={{ ...msisdn }} />);
|
|
||||||
await screen.findByText("Share");
|
|
||||||
|
|
||||||
rerender(<PhoneNumber msisdn={{ ...msisdn, bound: true }} />);
|
|
||||||
await screen.findByText("Revoke");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockGetAccessToken = jest.fn().mockResolvedValue("$$getAccessToken");
|
|
||||||
jest.mock("../../../../../src/IdentityAuthClient", () =>
|
|
||||||
jest.fn().mockImplementation(() => ({
|
|
||||||
getAccessToken: mockGetAccessToken,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("<PhoneNumbers />", () => {
|
|
||||||
it("should render a loader while loading", async () => {
|
|
||||||
const { container } = render(<PhoneNumbers msisdns={[{ ...msisdn }]} isLoading={true} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render phone numbers", async () => {
|
|
||||||
const { container } = render(<PhoneNumbers msisdns={[{ ...msisdn }]} isLoading={false} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle no numbers", async () => {
|
|
||||||
const { container } = render(<PhoneNumbers msisdns={[]} isLoading={false} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow binding msisdn", async () => {
|
|
||||||
const cli = stubClient();
|
|
||||||
const { getByText, getByLabelText, asFragment } = render(
|
|
||||||
<PhoneNumbers msisdns={[{ ...msisdn }]} isLoading={false} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
mocked(cli.requestMsisdnToken).mockResolvedValue({
|
|
||||||
sid: "SID",
|
|
||||||
msisdn: "+447900111222",
|
|
||||||
submit_url: "https://server.url",
|
|
||||||
success: true,
|
|
||||||
intl_fmt: "no-clue",
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(getByText("Share"));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(cli.requestMsisdnToken).toHaveBeenCalledWith(
|
|
||||||
null,
|
|
||||||
"+441111111111",
|
|
||||||
"t35tcl1Ent5ECr3T",
|
|
||||||
1,
|
|
||||||
undefined,
|
|
||||||
"$$getAccessToken",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
const verificationCodeField = getByLabelText("Verification code");
|
|
||||||
await userEvent.type(verificationCodeField, "123666{Enter}");
|
|
||||||
|
|
||||||
expect(cli.submitMsisdnToken).toHaveBeenCalledWith("SID", "t35tcl1Ent5ECr3T", "123666", "$$getAccessToken");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,97 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<EmailAddresses /> should handle no email addresses 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Email addresses
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_description"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_text"
|
|
||||||
>
|
|
||||||
Discovery options will appear once you have added an email.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<EmailAddresses /> should render a loader while loading 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Email addresses
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_InlineSpinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading…"
|
|
||||||
class="mx_InlineSpinner_icon mx_Spinner_icon"
|
|
||||||
style="width: 16px; height: 16px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<EmailAddresses /> should render email addresses 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Email addresses
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_address"
|
|
||||||
>
|
|
||||||
foo@bar.com
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_EmailAddressesPhoneNumbers_discovery_existing_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_sm"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
|
@ -1,163 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<PhoneNumbers /> should allow binding msisdn 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
data-testid="mx_DiscoveryPhoneNumbers"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Phone numbers
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_address"
|
|
||||||
>
|
|
||||||
+441111111111
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_verification"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Please enter verification code sent via text.
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
<form
|
|
||||||
autocomplete="off"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
autocomplete="off"
|
|
||||||
id="mx_Field_1"
|
|
||||||
label="Verification code"
|
|
||||||
placeholder="Verification code"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_Field_1"
|
|
||||||
>
|
|
||||||
Verification code
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<PhoneNumbers /> should handle no numbers 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
data-testid="mx_DiscoveryPhoneNumbers"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Phone numbers
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_description"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_text"
|
|
||||||
>
|
|
||||||
Discovery options will appear once you have added a phone number.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<PhoneNumbers /> should render a loader while loading 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
data-testid="mx_DiscoveryPhoneNumbers"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Phone numbers
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_InlineSpinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading…"
|
|
||||||
class="mx_InlineSpinner_icon mx_Spinner_icon"
|
|
||||||
style="width: 16px; height: 16px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<PhoneNumbers /> should render phone numbers 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection"
|
|
||||||
data-testid="mx_DiscoveryPhoneNumbers"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsectionHeading"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
|
||||||
>
|
|
||||||
Phone numbers
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_address"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
441111111111
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_EmailAddressesPhoneNumbers_discovery_existing_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_sm"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
|
@ -18,10 +18,10 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing"
|
class="mx_AddRemoveThreepids_existing"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_address"
|
class="mx_AddRemoveThreepids_existing_address"
|
||||||
>
|
>
|
||||||
test@test.io
|
test@test.io
|
||||||
</span>
|
</span>
|
||||||
|
@ -84,12 +84,11 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
||||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing"
|
class="mx_AddRemoveThreepids_existing"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_EmailAddressesPhoneNumbers_discovery_existing_address"
|
class="mx_AddRemoveThreepids_existing_address"
|
||||||
>
|
>
|
||||||
+
|
|
||||||
123456789
|
123456789
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
@ -102,75 +101,70 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="mx_PhoneNumbers_new"
|
|
||||||
novalidate=""
|
novalidate=""
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_PhoneNumbers_input"
|
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft"
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft"
|
class="mx_Field_prefix"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="mx_Field_prefix"
|
class="mx_Dropdown mx_PhoneNumbers_country mx_CountryDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_Dropdown mx_PhoneNumbers_country mx_CountryDropdown"
|
aria-describedby="mx_CountryDropdown_value"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="Country Dropdown"
|
||||||
|
aria-owns="mx_CountryDropdown_input"
|
||||||
|
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_CountryDropdown_value"
|
class="mx_Dropdown_option"
|
||||||
aria-expanded="false"
|
id="mx_CountryDropdown_value"
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-label="Country Dropdown"
|
|
||||||
aria-owns="mx_CountryDropdown_input"
|
|
||||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="mx_Dropdown_option"
|
|
||||||
id="mx_CountryDropdown_value"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_CountryDropdown_shortOption"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dropdown_option_emoji"
|
|
||||||
>
|
|
||||||
🇺🇸
|
|
||||||
</div>
|
|
||||||
+1
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
<span
|
||||||
class="mx_Dropdown_arrow"
|
class="mx_CountryDropdown_shortOption"
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown_option_emoji"
|
||||||
|
>
|
||||||
|
🇺🇸
|
||||||
|
</div>
|
||||||
|
+1
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
class="mx_Dropdown_arrow"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<input
|
</span>
|
||||||
autocomplete="tel-national"
|
<input
|
||||||
id="mx_Field_10"
|
autocomplete="tel-national"
|
||||||
label="Phone Number"
|
id="mx_Field_10"
|
||||||
placeholder="Phone Number"
|
label="Phone Number"
|
||||||
type="text"
|
placeholder="Phone Number"
|
||||||
value=""
|
type="text"
|
||||||
/>
|
value=""
|
||||||
<label
|
/>
|
||||||
for="mx_Field_10"
|
<label
|
||||||
>
|
for="mx_Field_10"
|
||||||
Phone Number
|
>
|
||||||
</label>
|
Phone Number
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -265,8 +265,13 @@ export function createTestClient(): MatrixClient {
|
||||||
knockRoom: jest.fn(),
|
knockRoom: jest.fn(),
|
||||||
leave: jest.fn(),
|
leave: jest.fn(),
|
||||||
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
|
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
|
||||||
|
requestAdd3pidEmailToken: jest.fn(),
|
||||||
requestAdd3pidMsisdnToken: jest.fn(),
|
requestAdd3pidMsisdnToken: jest.fn(),
|
||||||
submitMsisdnTokenOtherUrl: jest.fn(),
|
submitMsisdnTokenOtherUrl: jest.fn(),
|
||||||
|
deleteThreePid: jest.fn().mockResolvedValue({}),
|
||||||
|
bindThreePid: jest.fn().mockResolvedValue({}),
|
||||||
|
unbindThreePid: jest.fn().mockResolvedValue({}),
|
||||||
|
requestEmailToken: jest.fn(),
|
||||||
addThreePidOnly: jest.fn(),
|
addThreePidOnly: jest.fn(),
|
||||||
requestMsisdnToken: jest.fn(),
|
requestMsisdnToken: jest.fn(),
|
||||||
submitMsisdnToken: jest.fn(),
|
submitMsisdnToken: jest.fn(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue