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 73666d61c0..4fa467dd35 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png differ 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