Disable 3pid fields in settings when m.3pid_changes
capability is disabled (#11430)
* check m.3pid_changes capability in user settings * comments * assume 3pid_changes is true for older evrsions
This commit is contained in:
parent
dfded8d4d3
commit
5c1b62cf99
7 changed files with 440 additions and 18 deletions
|
@ -44,6 +44,10 @@ that is available.
|
||||||
interface IExistingEmailAddressProps {
|
interface IExistingEmailAddressProps {
|
||||||
email: ThirdPartyIdentifier;
|
email: ThirdPartyIdentifier;
|
||||||
onRemoved: (emails: ThirdPartyIdentifier) => void;
|
onRemoved: (emails: ThirdPartyIdentifier) => void;
|
||||||
|
/**
|
||||||
|
* Disallow removal of this email address when truthy
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IExistingEmailAddressState {
|
interface IExistingEmailAddressState {
|
||||||
|
@ -121,7 +125,7 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
|
||||||
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_address">
|
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_address">
|
||||||
{this.props.email.address}
|
{this.props.email.address}
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm" disabled={this.props.disabled}>
|
||||||
{_t("Remove")}
|
{_t("Remove")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,6 +136,10 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
|
||||||
interface IProps {
|
interface IProps {
|
||||||
emails: ThirdPartyIdentifier[];
|
emails: ThirdPartyIdentifier[];
|
||||||
onEmailsChange: (emails: ThirdPartyIdentifier[]) => void;
|
onEmailsChange: (emails: ThirdPartyIdentifier[]) => void;
|
||||||
|
/**
|
||||||
|
* Adding or removing emails is disabled when truthy
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -248,11 +256,18 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const existingEmailElements = this.props.emails.map((e) => {
|
const existingEmailElements = this.props.emails.map((e) => {
|
||||||
return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
|
return (
|
||||||
|
<ExistingEmailAddress
|
||||||
|
email={e}
|
||||||
|
onRemoved={this.onRemoved}
|
||||||
|
key={e.address}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let addButton = (
|
let addButton = (
|
||||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary" disabled={this.props.disabled}>
|
||||||
{_t("Add")}
|
{_t("Add")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -283,7 +298,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
|
||||||
type="text"
|
type="text"
|
||||||
label={_t("Email Address")}
|
label={_t("Email Address")}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled={this.state.verifying}
|
disabled={this.props.disabled || this.state.verifying}
|
||||||
value={this.state.newEmailAddress}
|
value={this.state.newEmailAddress}
|
||||||
onChange={this.onChangeNewEmailAddress}
|
onChange={this.onChangeNewEmailAddress}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,6 +39,10 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
interface IExistingPhoneNumberProps {
|
interface IExistingPhoneNumberProps {
|
||||||
msisdn: ThirdPartyIdentifier;
|
msisdn: ThirdPartyIdentifier;
|
||||||
onRemoved: (phoneNumber: ThirdPartyIdentifier) => void;
|
onRemoved: (phoneNumber: ThirdPartyIdentifier) => void;
|
||||||
|
/**
|
||||||
|
* Disable removing phone number
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IExistingPhoneNumberState {
|
interface IExistingPhoneNumberState {
|
||||||
|
@ -116,7 +120,7 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
|
||||||
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_address">
|
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_address">
|
||||||
+{this.props.msisdn.address}
|
+{this.props.msisdn.address}
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm" disabled={this.props.disabled}>
|
||||||
{_t("Remove")}
|
{_t("Remove")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,6 +131,10 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
|
||||||
interface IProps {
|
interface IProps {
|
||||||
msisdns: ThirdPartyIdentifier[];
|
msisdns: ThirdPartyIdentifier[];
|
||||||
onMsisdnsChange: (phoneNumbers: ThirdPartyIdentifier[]) => void;
|
onMsisdnsChange: (phoneNumbers: ThirdPartyIdentifier[]) => void;
|
||||||
|
/**
|
||||||
|
* Adding or removing phone numbers is disabled when truthy
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -251,11 +259,18 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const existingPhoneElements = this.props.msisdns.map((p) => {
|
const existingPhoneElements = this.props.msisdns.map((p) => {
|
||||||
return <ExistingPhoneNumber msisdn={p} onRemoved={this.onRemoved} key={p.address} />;
|
return (
|
||||||
|
<ExistingPhoneNumber
|
||||||
|
msisdn={p}
|
||||||
|
onRemoved={this.onRemoved}
|
||||||
|
key={p.address}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let addVerifySection = (
|
let addVerifySection = (
|
||||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary" disabled={this.props.disabled}>
|
||||||
{_t("Add")}
|
{_t("Add")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -277,14 +292,18 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
type="text"
|
type="text"
|
||||||
label={_t("Verification code")}
|
label={_t("Verification code")}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.props.disabled || this.state.continueDisabled}
|
||||||
value={this.state.newPhoneNumberCode}
|
value={this.state.newPhoneNumberCode}
|
||||||
onChange={this.onChangeNewPhoneNumberCode}
|
onChange={this.onChangeNewPhoneNumberCode}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled || this.state.newPhoneNumberCode.length === 0}
|
disabled={
|
||||||
|
this.props.disabled ||
|
||||||
|
this.state.continueDisabled ||
|
||||||
|
this.state.newPhoneNumberCode.length === 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{_t("Continue")}
|
{_t("Continue")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -313,7 +332,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
type="text"
|
type="text"
|
||||||
label={_t("Phone Number")}
|
label={_t("Phone Number")}
|
||||||
autoComplete="tel-national"
|
autoComplete="tel-national"
|
||||||
disabled={this.state.verifying}
|
disabled={this.props.disabled || this.state.verifying}
|
||||||
prefixComponent={phoneCountry}
|
prefixComponent={phoneCountry}
|
||||||
value={this.state.newPhoneNumber}
|
value={this.state.newPhoneNumber}
|
||||||
onChange={this.onChangeNewPhoneNumber}
|
onChange={this.onChangeNewPhoneNumber}
|
||||||
|
|
|
@ -46,6 +46,7 @@ TODO: Reduce all the copying between account vs. discovery components.
|
||||||
|
|
||||||
interface IEmailAddressProps {
|
interface IEmailAddressProps {
|
||||||
email: ThirdPartyIdentifier;
|
email: ThirdPartyIdentifier;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEmailAddressState {
|
interface IEmailAddressState {
|
||||||
|
@ -194,6 +195,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
|
||||||
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
onClick={this.onRevokeClick}
|
onClick={this.onRevokeClick}
|
||||||
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{_t("Revoke")}
|
{_t("Revoke")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -204,6 +206,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
|
||||||
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
||||||
kind="primary_sm"
|
kind="primary_sm"
|
||||||
onClick={this.onShareClick}
|
onClick={this.onShareClick}
|
||||||
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{_t("Share")}
|
{_t("Share")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -221,6 +224,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
|
||||||
interface IProps {
|
interface IProps {
|
||||||
emails: ThirdPartyIdentifier[];
|
emails: ThirdPartyIdentifier[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EmailAddresses extends React.Component<IProps> {
|
export default class EmailAddresses extends React.Component<IProps> {
|
||||||
|
@ -230,7 +234,7 @@ export default class EmailAddresses extends React.Component<IProps> {
|
||||||
content = <InlineSpinner />;
|
content = <InlineSpinner />;
|
||||||
} else if (this.props.emails.length > 0) {
|
} else if (this.props.emails.length > 0) {
|
||||||
content = this.props.emails.map((e) => {
|
content = this.props.emails.map((e) => {
|
||||||
return <EmailAddress email={e} key={e.address} />;
|
return <EmailAddress email={e} key={e.address} disabled={this.props.disabled} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
|
|
||||||
interface IPhoneNumberProps {
|
interface IPhoneNumberProps {
|
||||||
msisdn: ThirdPartyIdentifier;
|
msisdn: ThirdPartyIdentifier;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPhoneNumberState {
|
interface IPhoneNumberState {
|
||||||
|
@ -202,6 +203,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
|
||||||
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
onClick={this.onRevokeClick}
|
onClick={this.onRevokeClick}
|
||||||
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{_t("Revoke")}
|
{_t("Revoke")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -212,6 +214,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
|
||||||
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
|
||||||
kind="primary_sm"
|
kind="primary_sm"
|
||||||
onClick={this.onShareClick}
|
onClick={this.onShareClick}
|
||||||
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{_t("Share")}
|
{_t("Share")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -230,6 +233,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
|
||||||
interface IProps {
|
interface IProps {
|
||||||
msisdns: ThirdPartyIdentifier[];
|
msisdns: ThirdPartyIdentifier[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PhoneNumbers extends React.Component<IProps> {
|
export default class PhoneNumbers extends React.Component<IProps> {
|
||||||
|
@ -239,7 +243,7 @@ export default class PhoneNumbers extends React.Component<IProps> {
|
||||||
content = <InlineSpinner />;
|
content = <InlineSpinner />;
|
||||||
} else if (this.props.msisdns.length > 0) {
|
} else if (this.props.msisdns.length > 0) {
|
||||||
content = this.props.msisdns.map((e) => {
|
content = this.props.msisdns.map((e) => {
|
||||||
return <PhoneNumber msisdn={e} key={e.address} />;
|
return <PhoneNumber msisdn={e} key={e.address} disabled={this.props.disabled} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,7 @@ interface IState {
|
||||||
canChangePassword: boolean;
|
canChangePassword: boolean;
|
||||||
idServerName?: string;
|
idServerName?: string;
|
||||||
externalAccountManagementUrl?: string;
|
externalAccountManagementUrl?: string;
|
||||||
|
canMake3pidChanges: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
|
@ -120,6 +121,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
msisdns: [],
|
msisdns: [],
|
||||||
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
||||||
canChangePassword: false,
|
canChangePassword: false,
|
||||||
|
canMake3pidChanges: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -174,8 +176,12 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
||||||
|
|
||||||
const externalAccountManagementUrl = getDelegatedAuthAccountUrl(cli.getClientWellKnown());
|
const externalAccountManagementUrl = getDelegatedAuthAccountUrl(cli.getClientWellKnown());
|
||||||
|
// https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability
|
||||||
|
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
||||||
|
// so the behaviour for when it is missing has to be assume true
|
||||||
|
const canMake3pidChanges = !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true;
|
||||||
|
|
||||||
this.setState({ canChangePassword, externalAccountManagementUrl });
|
this.setState({ canChangePassword, externalAccountManagementUrl, canMake3pidChanges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getThreepidState(): Promise<void> {
|
private async getThreepidState(): Promise<void> {
|
||||||
|
@ -323,12 +329,20 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
const emails = this.state.loading3pids ? (
|
const emails = this.state.loading3pids ? (
|
||||||
<InlineSpinner />
|
<InlineSpinner />
|
||||||
) : (
|
) : (
|
||||||
<AccountEmailAddresses emails={this.state.emails} onEmailsChange={this.onEmailsChange} />
|
<AccountEmailAddresses
|
||||||
|
emails={this.state.emails}
|
||||||
|
onEmailsChange={this.onEmailsChange}
|
||||||
|
disabled={!this.state.canMake3pidChanges}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
const msisdns = this.state.loading3pids ? (
|
const msisdns = this.state.loading3pids ? (
|
||||||
<InlineSpinner />
|
<InlineSpinner />
|
||||||
) : (
|
) : (
|
||||||
<AccountPhoneNumbers msisdns={this.state.msisdns} onMsisdnsChange={this.onMsisdnsChange} />
|
<AccountPhoneNumbers
|
||||||
|
msisdns={this.state.msisdns}
|
||||||
|
onMsisdnsChange={this.onMsisdnsChange}
|
||||||
|
disabled={!this.state.canMake3pidChanges}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
threepidSection = (
|
threepidSection = (
|
||||||
<>
|
<>
|
||||||
|
@ -463,8 +477,16 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
|
|
||||||
const threepidSection = this.state.haveIdServer ? (
|
const threepidSection = this.state.haveIdServer ? (
|
||||||
<>
|
<>
|
||||||
<DiscoveryEmailAddresses emails={this.state.emails} isLoading={this.state.loading3pids} />
|
<DiscoveryEmailAddresses
|
||||||
<DiscoveryPhoneNumbers msisdns={this.state.msisdns} isLoading={this.state.loading3pids} />
|
emails={this.state.emails}
|
||||||
|
isLoading={this.state.loading3pids}
|
||||||
|
disabled={!this.state.canMake3pidChanges}
|
||||||
|
/>
|
||||||
|
<DiscoveryPhoneNumbers
|
||||||
|
msisdns={this.state.msisdns}
|
||||||
|
isLoading={this.state.loading3pids}
|
||||||
|
disabled={!this.state.canMake3pidChanges}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
|
||||||
import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
|
import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
|
@ -38,6 +39,10 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(userId),
|
...mockClientMethodsUser(userId),
|
||||||
...mockClientMethodsServer(),
|
...mockClientMethodsServer(),
|
||||||
|
getCapabilities: jest.fn(),
|
||||||
|
getThreePids: jest.fn(),
|
||||||
|
getIdentityServerUrl: jest.fn(),
|
||||||
|
deleteThreePid: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getComponent = () => (
|
const getComponent = () => (
|
||||||
|
@ -46,15 +51,23 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
|
||||||
const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown");
|
const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
clientWellKnownSpy.mockReturnValue({});
|
clientWellKnownSpy.mockReturnValue({});
|
||||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||||
jest.spyOn(logger, "error").mockRestore();
|
jest.spyOn(logger, "error").mockRestore();
|
||||||
|
|
||||||
|
mockClient.getCapabilities.mockResolvedValue({});
|
||||||
|
mockClient.getThreePids.mockResolvedValue({
|
||||||
|
threepids: [],
|
||||||
|
});
|
||||||
|
mockClient.deleteThreePid.mockResolvedValue({
|
||||||
|
id_server_unbind_result: "success",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show account management link when not available", () => {
|
it("does not show account management link when not available", () => {
|
||||||
|
@ -159,4 +172,174 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
expect(screen.getByText("Deactivate account").parentElement!).toMatchSnapshot();
|
expect(screen.getByText("Deactivate account").parentElement!).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("3pids", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.getCapabilities.mockResolvedValue({
|
||||||
|
"m.3pid_changes": {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockClient.getThreePids.mockResolvedValue({
|
||||||
|
threepids: [
|
||||||
|
{
|
||||||
|
medium: ThreepidMedium.Email,
|
||||||
|
address: "test@test.io",
|
||||||
|
validated_at: 1685067124552,
|
||||||
|
added_at: 1685067124552,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
medium: ThreepidMedium.Phone,
|
||||||
|
address: "123456789",
|
||||||
|
validated_at: 1685067124552,
|
||||||
|
added_at: 1685067124552,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
mockClient.getIdentityServerUrl.mockReturnValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show loaders while 3pids load", () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(screen.getByTestId("mx_AccountEmailAddresses")).getByLabelText("Loading…"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(within(screen.getByTestId("mx_AccountPhoneNumbers")).getByLabelText("Loading…")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display 3pid email addresses and phone numbers", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByTestId("mx_AccountEmailAddresses")).toMatchSnapshot();
|
||||||
|
expect(screen.getByTestId("mx_AccountPhoneNumbers")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow removing an existing email addresses", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
||||||
|
|
||||||
|
fireEvent.click(within(section).getByText("Remove"));
|
||||||
|
|
||||||
|
// confirm removal
|
||||||
|
expect(screen.getByText("Remove test@test.io?")).toBeInTheDocument();
|
||||||
|
fireEvent.click(within(section).getByText("Remove"));
|
||||||
|
|
||||||
|
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, "test@test.io");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding a new email address", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
||||||
|
|
||||||
|
// just check the fields are enabled
|
||||||
|
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
|
||||||
|
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow removing an existing phone number", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
||||||
|
|
||||||
|
fireEvent.click(within(section).getByText("Remove"));
|
||||||
|
|
||||||
|
// confirm removal
|
||||||
|
expect(screen.getByText("Remove 123456789?")).toBeInTheDocument();
|
||||||
|
fireEvent.click(within(section).getByText("Remove"));
|
||||||
|
|
||||||
|
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, "123456789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding a new phone number", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
||||||
|
|
||||||
|
// just check the fields are enabled
|
||||||
|
expect(within(section).getByLabelText("Phone Number")).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow 3pid changes when capabilities does not have 3pid_changes", async () => {
|
||||||
|
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
||||||
|
// so the behaviour for when it is missing has to be assume true
|
||||||
|
mockClient.getCapabilities.mockResolvedValue({});
|
||||||
|
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
||||||
|
|
||||||
|
// just check the fields are enabled
|
||||||
|
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
|
||||||
|
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when 3pid changes capability is disabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.getCapabilities.mockResolvedValue({
|
||||||
|
"m.3pid_changes": {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow removing email addresses", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
||||||
|
|
||||||
|
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow adding a new email addresses", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
||||||
|
|
||||||
|
// fields are not enabled
|
||||||
|
expect(within(section).getByLabelText("Email Address")).toBeDisabled();
|
||||||
|
expect(within(section).getByText("Add")).toHaveAttribute("aria-disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow removing phone numbers", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
||||||
|
|
||||||
|
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow adding a new phone number", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
||||||
|
|
||||||
|
expect(within(section).getByLabelText("Phone Number")).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,180 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses and phone numbers 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection"
|
||||||
|
data-testid="mx_AccountEmailAddresses"
|
||||||
|
>
|
||||||
|
<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_GeneralUserSettingsTab_section--discovery_existing"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_GeneralUserSettingsTab_section--discovery_existing_address"
|
||||||
|
>
|
||||||
|
test@test.io
|
||||||
|
</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_57"
|
||||||
|
label="Email Address"
|
||||||
|
placeholder="Email Address"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_57"
|
||||||
|
>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses and phone numbers 2`] = `
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection"
|
||||||
|
data-testid="mx_AccountPhoneNumbers"
|
||||||
|
>
|
||||||
|
<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_GeneralUserSettingsTab_section--discovery_existing"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_GeneralUserSettingsTab_section--discovery_existing_address"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
123456789
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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_58"
|
||||||
|
label="Phone Number"
|
||||||
|
placeholder="Phone Number"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_58"
|
||||||
|
>
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<GeneralUserSettingsTab /> Manage integrations should render manage integrations sections 1`] = `
|
exports[`<GeneralUserSettingsTab /> Manage integrations should render manage integrations sections 1`] = `
|
||||||
<label
|
<label
|
||||||
class="mx_SetIntegrationManager"
|
class="mx_SetIntegrationManager"
|
||||||
|
|
Loading…
Reference in a new issue