From ea0baee1014e6cbf73b39c2502bc2f803d354983 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Jun 2024 14:04:19 +0100 Subject: [PATCH] Split out email & phone number settings to separate components & move discovery to privacy tab (#12670) * WIP update of threepid settings section * Remove email / phone number section from original place and don't show the new one if 3pids are disabled * Update snapshots * Pull identity server / 3pid binding settings out to separate component and put it in the security & privacy section which is its new home * Update snapshot * Move relevant part of test & update screenshots / snapshots * Remove unnecessary dependency * Add test for discovery settings * Add spacing in terms agreement --- .../general-user-settings-tab.spec.ts | 5 - .../security-user-settings-tab.spec.ts | 9 + .../general-linux.png | Bin 50155 -> 48582 bytes .../views/terms/_InlineTermsAgreement.pcss | 1 + .../settings/UserPersonalInfoSettings.tsx | 130 +++++++++ .../settings/discovery/DiscoverySettings.tsx | 190 +++++++++++++ .../tabs/user/GeneralUserSettingsTab.tsx | 258 +----------------- .../tabs/user/SecurityUserSettingsTab.tsx | 2 + src/i18n/strings/en_EN.json | 12 +- .../discovery/DiscoverySettings-test.tsx | 104 +++++++ .../EmailAddresses-test.tsx.snap | 2 +- .../__snapshots__/PhoneNumbers-test.tsx.snap | 2 +- .../GeneralUserSettingsTab-test.tsx.snap | 8 +- 13 files changed, 454 insertions(+), 269 deletions(-) create mode 100644 src/components/views/settings/UserPersonalInfoSettings.tsx create mode 100644 src/components/views/settings/discovery/DiscoverySettings.tsx create mode 100644 test/components/views/settings/discovery/DiscoverySettings-test.tsx diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/general-user-settings-tab.spec.ts index 0244962914..32946053d8 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -91,11 +91,6 @@ test.describe("General user settings tab", () => { // Assert that the default value is rendered again await expect(languageInput.getByText("English")).toBeVisible(); - const setIdServer = uut.locator(".mx_SetIdServer"); - await setIdServer.scrollIntoViewIfNeeded(); - // Assert that an input area for identity server exists - await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); - const setIntegrationManager = uut.locator(".mx_SetIntegrationManager"); await setIntegrationManager.scrollIntoViewIfNeeded(); await expect( diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 08640f603b..5cd2a92c16 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -47,5 +47,14 @@ test.describe("Security user settings tab", () => { await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(); }); }); + + test("should contain section to set ID server", async ({ app }) => { + const tab = await app.settings.openUserSettings("Security"); + + const setIdServer = tab.locator(".mx_SetIdServer"); + await setIdServer.scrollIntoViewIfNeeded(); + // Assert that an input area for identity server exists + await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); + }); }); }); diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index 73666d61c0ee380bef6ca1bfa42c2aaec999cb8d..4fa467dd351714e8d469e3f4b59347d5b17e80a4 100644 GIT binary patch delta 11831 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 WJ3G^OvVv~f@>Wz@q(E5v)Bghe0#ZBx diff --git a/res/css/views/terms/_InlineTermsAgreement.pcss b/res/css/views/terms/_InlineTermsAgreement.pcss index d7732b2a0d..162d1341e4 100644 --- a/res/css/views/terms/_InlineTermsAgreement.pcss +++ b/res/css/views/terms/_InlineTermsAgreement.pcss @@ -15,6 +15,7 @@ limitations under the License. */ .mx_InlineTermsAgreement_cbContainer { + margin-top: var(--cpd-space-4x); margin-bottom: 10px; font: var(--cpd-font-body-md-regular); diff --git a/src/components/views/settings/UserPersonalInfoSettings.tsx b/src/components/views/settings/UserPersonalInfoSettings.tsx new file mode 100644 index 0000000000..8e5880a517 --- /dev/null +++ b/src/components/views/settings/UserPersonalInfoSettings.tsx @@ -0,0 +1,130 @@ +/* +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, useEffect, useState } from "react"; +import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { Alert } from "@vector-im/compound-web"; + +import AccountEmailAddresses from "./account/EmailAddresses"; +import AccountPhoneNumbers from "./account/PhoneNumbers"; +import { _t } from "../../../languageHandler"; +import InlineSpinner from "../elements/InlineSpinner"; +import SettingsSubsection from "./shared/SettingsSubsection"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { ThirdPartyIdentifier } from "../../../AddThreepid"; +import SettingsStore from "../../../settings/SettingsStore"; +import { UIFeature } from "../../../settings/UIFeature"; + +type LoadingState = "loading" | "loaded" | "error"; + +interface ThreepidSectionWrapperProps { + error: string; + loadingState: LoadingState; + children: React.ReactNode; +} + +const ThreepidSectionWrapper: React.FC = ({ error, loadingState, children }) => { + if (loadingState === "loading") { + return ; + } else if (loadingState === "error") { + return ( + + {error} + + ); + } else { + return <>{children}; + } +}; + +interface UserPersonalInfoSettingsProps { + canMake3pidChanges: boolean; +} + +/** + * Settings controls allowing the user to set personal information like email addresses. + */ +export const UserPersonalInfoSettings: React.FC = ({ canMake3pidChanges }) => { + const [emails, setEmails] = useState(); + const [phoneNumbers, setPhoneNumbers] = useState(); + const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading"); + + const client = useMatrixClientContext(); + + useEffect(() => { + (async () => { + try { + const threepids = await client.getThreePids(); + setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email)); + setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + setLoadingState("loaded"); + } catch (e) { + setLoadingState("error"); + } + })(); + }, [client]); + + const onEmailsChange = useCallback((emails: ThirdPartyIdentifier[]) => { + setEmails(emails); + }, []); + + const onMsisdnsChange = useCallback((msisdns: ThirdPartyIdentifier[]) => { + setPhoneNumbers(msisdns); + }, []); + + if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; + + return ( +
+

{_t("settings|general|personal_info")}

+ + + + + + + + + + + +
+ ); +}; + +export default UserPersonalInfoSettings; diff --git a/src/components/views/settings/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx new file mode 100644 index 0000000000..8b1a20ac2e --- /dev/null +++ b/src/components/views/settings/discovery/DiscoverySettings.tsx @@ -0,0 +1,190 @@ +/* +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, useEffect, useState } from "react"; +import { SERVICE_TYPES, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Alert } from "@vector-im/compound-web"; + +import DiscoveryEmailAddresses from "../discovery/EmailAddresses"; +import DiscoveryPhoneNumbers from "../discovery/PhoneNumbers"; +import { getThreepidsWithBindStatus } from "../../../../boundThreepids"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { ThirdPartyIdentifier } from "../../../../AddThreepid"; +import SettingsStore from "../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../settings/UIFeature"; +import { _t } from "../../../../languageHandler"; +import SetIdServer from "../SetIdServer"; +import SettingsSubsection from "../shared/SettingsSubsection"; +import InlineTermsAgreement from "../../terms/InlineTermsAgreement"; +import { Service, ServicePolicyPair, startTermsFlow } from "../../../../Terms"; +import IdentityAuthClient from "../../../../IdentityAuthClient"; +import { abbreviateUrl } from "../../../../utils/UrlUtils"; +import { useDispatcher } from "../../../../hooks/useDispatcher"; +import defaultDispatcher from "../../../../dispatcher/dispatcher"; +import { ActionPayload } from "../../../../dispatcher/payloads"; + +type RequiredPolicyInfo = + | { + // This object is passed along to a component for handling + policiesAndServices: null; // From the startTermsFlow callback + agreedUrls: null; // From the startTermsFlow callback + resolve: null; // Promise resolve function for startTermsFlow callback + } + | { + policiesAndServices: ServicePolicyPair[]; + agreedUrls: string[]; + resolve: (values: string[]) => void; + }; + +/** + * Settings controlling how a user's email addreses and phone numbers can be used to discover them + */ +export const DiscoverySettings: React.FC = () => { + const client = useMatrixClientContext(); + + const [emails, setEmails] = useState([]); + const [phoneNumbers, setPhoneNumbers] = useState([]); + const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading"); + const [idServerName, setIdServerName] = useState(abbreviateUrl(client.getIdentityServerUrl())); + const [canMake3pidChanges, setCanMake3pidChanges] = useState(false); + + const [requiredPolicyInfo, setRequiredPolicyInfo] = useState({ + // This object is passed along to a component for handling + policiesAndServices: null, // From the startTermsFlow callback + agreedUrls: null, // From the startTermsFlow callback + resolve: null, // Promise resolve function for startTermsFlow callback + }); + const [hasTerms, setHasTerms] = useState(false); + + const getThreepidState = useCallback(async () => { + const threepids = await getThreepidsWithBindStatus(client); + setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email)); + setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + }, [client]); + + useDispatcher( + defaultDispatcher, + useCallback( + (payload: ActionPayload) => { + if (payload.action === "id_server_changed") { + setIdServerName(abbreviateUrl(client.getIdentityServerUrl())); + + getThreepidState().then(); + } + }, + [client, getThreepidState], + ), + ); + + useEffect(() => { + (async () => { + try { + await getThreepidState(); + + const capabilities = await client.getCapabilities(); + setCanMake3pidChanges( + !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true, + ); + + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const idServerUrl = client.getIdentityServerUrl(); + if (!idServerUrl) { + return; + } + + const authClient = new IdentityAuthClient(); + try { + const idAccessToken = await authClient.getAccessToken({ check: false }); + await startTermsFlow( + client, + [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)], + (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve) => { + setIdServerName(abbreviateUrl(idServerUrl)); + setHasTerms(true); + setRequiredPolicyInfo({ + policiesAndServices, + agreedUrls, + resolve, + }); + }); + }, + ); + // User accepted all terms + setHasTerms(false); + } catch (e) { + logger.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`, + ); + logger.warn(e); + } + + setLoadingState("loaded"); + } catch (e) { + setLoadingState("error"); + } + })(); + }, [client, getThreepidState]); + + if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; + + if (hasTerms && requiredPolicyInfo.policiesAndServices) { + const intro = ( + + {_t("settings|general|discovery_needs_terms", { serverName: idServerName })} + + ); + return ( + <> + + {/* has its own heading as it includes the current identity server */} + + + ); + } + + const threepidSection = idServerName ? ( + <> + + + + ) : null; + + return ( + + {threepidSection} + {/* has its own heading as it includes the current identity server */} + + + ); +}; + +export default DiscoverySettings; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 0e32b9126c..3b53e5f2f1 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -17,10 +17,9 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { SERVICE_TYPES, HTTPError, IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { HTTPError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { Icon as WarningIcon } from "../../../../../../res/img/feather-customised/warning-triangle.svg"; import { UserFriendlyError, _t } from "../../../../../languageHandler"; import UserProfileSettings from "../../UserProfileSettings"; import * as languageHandler from "../../../../../languageHandler"; @@ -31,22 +30,10 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PlatformPeg from "../../../../../PlatformPeg"; import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher/dispatcher"; -import { Service, ServicePolicyPair, startTermsFlow } from "../../../../../Terms"; -import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import { abbreviateUrl } from "../../../../../utils/UrlUtils"; -import { getThreepidsWithBindStatus } from "../../../../../boundThreepids"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { UIFeature } from "../../../../../settings/UIFeature"; -import { ActionPayload } from "../../../../../dispatcher/payloads"; import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog"; -import AccountPhoneNumbers from "../../account/PhoneNumbers"; -import AccountEmailAddresses from "../../account/EmailAddresses"; -import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; -import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; import ChangePassword from "../../ChangePassword"; -import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; -import SetIdServer from "../../SetIdServer"; import SetIntegrationManager from "../../SetIntegrationManager"; import ToggleSwitch from "../../../elements/ToggleSwitch"; import { IS_MAC } from "../../../../../Keyboard"; @@ -54,10 +41,8 @@ import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading"; -import Heading from "../../../typography/Heading"; -import InlineSpinner from "../../../elements/InlineSpinner"; -import { ThirdPartyIdentifier } from "../../../../../AddThreepid"; import { SDKContext } from "../../../../../contexts/SDKContext"; +import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; interface IProps { closeSettingsFn: () => void; @@ -67,25 +52,6 @@ interface IState { language: string; spellCheckEnabled?: boolean; spellCheckLanguages: string[]; - haveIdServer: boolean; - idServerHasUnsignedTerms: boolean; - requiredPolicyInfo: - | { - // This object is passed along to a component for handling - hasTerms: false; - policiesAndServices: null; // From the startTermsFlow callback - agreedUrls: null; // From the startTermsFlow callback - resolve: null; // Promise resolve function for startTermsFlow callback - } - | { - hasTerms: boolean; - policiesAndServices: ServicePolicyPair[]; - agreedUrls: string[]; - resolve: (values: string[]) => void; - }; - emails: ThirdPartyIdentifier[]; - msisdns: ThirdPartyIdentifier[]; - loading3pids: boolean; // whether or not the emails and msisdns have been loaded canChangePassword: boolean; idServerName?: string; externalAccountManagementUrl?: string; @@ -96,38 +62,19 @@ export default class GeneralUserSettingsTab extends React.Component; - private readonly dispatcherRef: string; - public constructor(props: IProps, context: React.ContextType) { super(props); this.context = context; - const cli = this.context.client!; - this.state = { language: languageHandler.getCurrentLanguage(), spellCheckEnabled: false, spellCheckLanguages: [], - haveIdServer: Boolean(cli.getIdentityServerUrl()), - idServerHasUnsignedTerms: false, - requiredPolicyInfo: { - // This object is passed along to a component for handling - hasTerms: false, - policiesAndServices: null, // From the startTermsFlow callback - agreedUrls: null, // From the startTermsFlow callback - resolve: null, // Promise resolve function for startTermsFlow callback - }, - emails: [], - msisdns: [], - loading3pids: true, // whether or not the emails and msisdns have been loaded canChangePassword: false, canMake3pidChanges: false, }; - this.dispatcherRef = dis.register(this.onAction); - this.getCapabilities(); - this.getThreepidState(); } public async componentDidMount(): Promise { @@ -145,25 +92,6 @@ export default class GeneralUserSettingsTab extends React.Component { - if (payload.action === "id_server_changed") { - this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) }); - this.getThreepidState(); - } - }; - - private onEmailsChange = (emails: ThirdPartyIdentifier[]): void => { - this.setState({ emails }); - }; - - private onMsisdnsChange = (msisdns: ThirdPartyIdentifier[]): void => { - this.setState({ msisdns }); - }; - private async getCapabilities(): Promise { const cli = this.context.client!; @@ -185,73 +113,6 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context.client!; - - // Check to see if terms need accepting - this.checkTerms(); - - // Need to get 3PIDs generally for Account section and possibly also for - // Discovery (assuming we have an IS and terms are agreed). - let threepids: IThreepid[] = []; - try { - threepids = await getThreepidsWithBindStatus(cli); - } catch (e) { - const idServerUrl = cli.getIdentityServerUrl(); - logger.warn( - `Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`, - ); - logger.warn(e); - } - this.setState({ - emails: threepids.filter((a) => a.medium === ThreepidMedium.Email), - msisdns: threepids.filter((a) => a.medium === ThreepidMedium.Phone), - loading3pids: false, - }); - } - - private async checkTerms(): Promise { - // By starting the terms flow we get the logic for checking which terms the user has signed - // for free. So we might as well use that for our own purposes. - const idServerUrl = this.context.client!.getIdentityServerUrl(); - if (!this.state.haveIdServer || !idServerUrl) { - this.setState({ idServerHasUnsignedTerms: false }); - return; - } - - const authClient = new IdentityAuthClient(); - try { - const idAccessToken = await authClient.getAccessToken({ check: false }); - await startTermsFlow( - this.context.client!, - [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)], - (policiesAndServices, agreedUrls, extraClassNames) => { - return new Promise((resolve, reject) => { - this.setState({ - idServerName: abbreviateUrl(idServerUrl), - requiredPolicyInfo: { - hasTerms: true, - policiesAndServices, - agreedUrls, - resolve, - }, - }); - }); - }, - ); - // User accepted all terms - this.setState({ - requiredPolicyInfo: { - ...this.state.requiredPolicyInfo, // set first so we can override - hasTerms: false, - }, - }); - } catch (e) { - logger.warn(`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`); - logger.warn(e); - } - } - private onLanguageChange = (newLanguage: string): void => { if (this.state.language === newLanguage) return; @@ -324,48 +185,6 @@ export default class GeneralUserSettingsTab extends React.Component - ) : ( - - ); - const msisdns = this.state.loading3pids ? ( - - ) : ( - - ); - threepidSection = ( - <> - - {emails} - - - - {msisdns} - - - ); - } - let passwordChangeSection: ReactNode = null; if (this.state.canChangePassword) { passwordChangeSection = ( @@ -419,7 +238,6 @@ export default class GeneralUserSettingsTab extends React.Component - {threepidSection} ); } @@ -455,51 +273,6 @@ export default class GeneralUserSettingsTab extends React.Component - {_t("settings|general|discovery_needs_terms", { serverName: this.state.idServerName })} - - ); - return ( - <> - - {/* has its own heading as it includes the current identity server */} - - - ); - } - - const threepidSection = this.state.haveIdServer ? ( - <> - - - - ) : null; - - return ( - <> - {threepidSection} - {/* has its own heading as it includes the current identity server */} - - - ); - } - private renderManagementSection(): JSX.Element { // TODO: Improve warning text for account deactivation return ( @@ -533,40 +306,15 @@ export default class GeneralUserSettingsTab extends React.Component - ) : null; - const heading = ( - - {discoWarning} - {_t("settings|general|discovery_section")} - - ); - discoverySection = ( - - {this.renderDiscoverySection()} - - ); - } - return ( + {this.renderAccountSection()} {this.renderLanguageSection()} {supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null} - {discoverySection} {this.renderIntegrationManagerSection()} {accountManagementSection} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 80c02bb546..c636721201 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -43,6 +43,7 @@ import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { useOwnDevices } from "../../devices/useOwnDevices"; +import DiscoverySettings from "../../discovery/DiscoverySettings"; interface IIgnoredUserProps { userId: string; @@ -336,6 +337,7 @@ export default class SecurityUserSettingsTab extends React.Component + /plain to send without markdown.", @@ -2479,11 +2482,11 @@ "deactivate_section": "Deactivate Account", "deactivate_warning": "Deactivating your account is a permanent action — be careful!", "dialog_title": "Settings: General", - "discovery_email_empty": "Discovery options will appear once you have added an email above.", + "discovery_email_empty": "Discovery options will appear once you have added an email.", "discovery_email_verification_instructions": "Verify the link in your inbox", - "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number above.", + "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number.", "discovery_needs_terms": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", - "discovery_section": "Discovery", + "discovery_needs_terms_title": "Let people find you", "display_name": "Display Name", "display_name_error": "Unable to set display name", "email_address_in_use": "This email address is already in use", @@ -2522,11 +2525,14 @@ "oidc_manage_button": "Manage account", "password_change_section": "Set a new account password…", "password_change_success": "Your password was successfully changed.", + "personal_info": "Personal info", "profile_subtitle": "This is how you appear to others on the app.", "remove_email_prompt": "Remove %(email)s?", "remove_msisdn_prompt": "Remove %(phone)s?", "spell_check_locale_placeholder": "Choose a locale", "spell_check_section": "Spell check", + "unable_to_load_emails": "Unable to load email addresses", + "unable_to_load_msisdns": "Unable to load phone numbers", "username": "Username" }, "image_thumbnails": "Show previews/thumbnails for images", diff --git a/test/components/views/settings/discovery/DiscoverySettings-test.tsx b/test/components/views/settings/discovery/DiscoverySettings-test.tsx new file mode 100644 index 0000000000..51a976bbc0 --- /dev/null +++ b/test/components/views/settings/discovery/DiscoverySettings-test.tsx @@ -0,0 +1,104 @@ +/* +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 from "react"; +import { act, render, screen } from "@testing-library/react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; +import userEvent from "@testing-library/user-event"; + +import DiscoverySettings from "../../../../../src/components/views/settings/discovery/DiscoverySettings"; +import { stubClient } from "../../../../test-utils"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import { UIFeature } from "../../../../../src/settings/UIFeature"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; +import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; + +const mockGetAccessToken = jest.fn().mockResolvedValue("$$getAccessToken"); +jest.mock("../../../../../src/IdentityAuthClient", () => + jest.fn().mockImplementation(() => ({ + getAccessToken: mockGetAccessToken, + })), +); + +describe("DiscoverySettings", () => { + let client: MatrixClient; + + beforeEach(() => { + client = stubClient(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const DiscoveryWrapper = (props = {}) => ; + + it("is empty if 3pid features are disabled", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => { + if (key === UIFeature.ThirdPartyID) return false; + }); + + const { container } = render(, { wrapper: DiscoveryWrapper }); + + expect(container).toBeEmptyDOMElement(); + }); + + it("displays alert if an identity server needs terms accepting", async () => { + mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com"); + mocked(client).getTerms.mockResolvedValue({ + ["policies"]: { en: "No ball games" }, + }); + + render(, { wrapper: DiscoveryWrapper }); + + await expect(await screen.findByText("Let people find you")).toBeInTheDocument(); + }); + + it("button to accept terms is disabled if checkbox not checked", async () => { + mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com"); + mocked(client).getTerms.mockResolvedValue({ + ["policies"]: { en: "No ball games" }, + }); + + render(, { wrapper: DiscoveryWrapper }); + + const acceptCheckbox = await screen.findByRole("checkbox", { name: "Accept" }); + const continueButton = screen.getByRole("button", { name: "Continue" }); + expect(acceptCheckbox).toBeInTheDocument(); + expect(continueButton).toHaveAttribute("aria-disabled", "true"); + + await userEvent.click(acceptCheckbox); + expect(continueButton).not.toHaveAttribute("aria-disabled", "true"); + }); + + it("updates if ID server is changed", async () => { + render(, { wrapper: DiscoveryWrapper }); + + mocked(client).getThreePids.mockClear(); + + act(() => { + defaultDispatcher.dispatch( + { + action: "id_server_changed", + }, + true, + ); + }); + + expect(client.getThreePids).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap b/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap index 275f864dfc..536c72e8eb 100644 --- a/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap +++ b/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap @@ -20,7 +20,7 @@ exports[` should handle no email addresses 1`] = `
- Discovery options will appear once you have added an email above. + Discovery options will appear once you have added an email.
diff --git a/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap b/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap index 00a136ca84..948ee105b0 100644 --- a/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap +++ b/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap @@ -83,7 +83,7 @@ exports[` should handle no numbers 1`] = `
- Discovery options will appear once you have added a phone number above. + Discovery options will appear once you have added a phone number.
diff --git a/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap index 04e786dc7d..8d15bf8e7e 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap @@ -42,14 +42,14 @@ exports[` 3pids should display 3pid email addresses an > @@ -150,14 +150,14 @@ exports[` 3pids should display 3pid email addresses an