Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit ea0baee

Browse files
authored
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
1 parent 7247524 commit ea0baee

File tree

13 files changed

+454
-269
lines changed

13 files changed

+454
-269
lines changed

playwright/e2e/settings/general-user-settings-tab.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,6 @@ test.describe("General user settings tab", () => {
9191
// Assert that the default value is rendered again
9292
await expect(languageInput.getByText("English")).toBeVisible();
9393

94-
const setIdServer = uut.locator(".mx_SetIdServer");
95-
await setIdServer.scrollIntoViewIfNeeded();
96-
// Assert that an input area for identity server exists
97-
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
98-
9994
const setIntegrationManager = uut.locator(".mx_SetIntegrationManager");
10095
await setIntegrationManager.scrollIntoViewIfNeeded();
10196
await expect(

playwright/e2e/settings/security-user-settings-tab.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,14 @@ test.describe("Security user settings tab", () => {
4747
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot();
4848
});
4949
});
50+
51+
test("should contain section to set ID server", async ({ app }) => {
52+
const tab = await app.settings.openUserSettings("Security");
53+
54+
const setIdServer = tab.locator(".mx_SetIdServer");
55+
await setIdServer.scrollIntoViewIfNeeded();
56+
// Assert that an input area for identity server exists
57+
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
58+
});
5059
});
5160
});
Loading

res/css/views/terms/_InlineTermsAgreement.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
.mx_InlineTermsAgreement_cbContainer {
18+
margin-top: var(--cpd-space-4x);
1819
margin-bottom: 10px;
1920
font: var(--cpd-font-body-md-regular);
2021

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { useCallback, useEffect, useState } from "react";
18+
import { ThreepidMedium } from "matrix-js-sdk/src/matrix";
19+
import { Alert } from "@vector-im/compound-web";
20+
21+
import AccountEmailAddresses from "./account/EmailAddresses";
22+
import AccountPhoneNumbers from "./account/PhoneNumbers";
23+
import { _t } from "../../../languageHandler";
24+
import InlineSpinner from "../elements/InlineSpinner";
25+
import SettingsSubsection from "./shared/SettingsSubsection";
26+
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
27+
import { ThirdPartyIdentifier } from "../../../AddThreepid";
28+
import SettingsStore from "../../../settings/SettingsStore";
29+
import { UIFeature } from "../../../settings/UIFeature";
30+
31+
type LoadingState = "loading" | "loaded" | "error";
32+
33+
interface ThreepidSectionWrapperProps {
34+
error: string;
35+
loadingState: LoadingState;
36+
children: React.ReactNode;
37+
}
38+
39+
const ThreepidSectionWrapper: React.FC<ThreepidSectionWrapperProps> = ({ error, loadingState, children }) => {
40+
if (loadingState === "loading") {
41+
return <InlineSpinner />;
42+
} else if (loadingState === "error") {
43+
return (
44+
<Alert type="critical" title={_t("common|error")}>
45+
{error}
46+
</Alert>
47+
);
48+
} else {
49+
return <>{children}</>;
50+
}
51+
};
52+
53+
interface UserPersonalInfoSettingsProps {
54+
canMake3pidChanges: boolean;
55+
}
56+
57+
/**
58+
* Settings controls allowing the user to set personal information like email addresses.
59+
*/
60+
export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> = ({ canMake3pidChanges }) => {
61+
const [emails, setEmails] = useState<ThirdPartyIdentifier[] | undefined>();
62+
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[] | undefined>();
63+
const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading");
64+
65+
const client = useMatrixClientContext();
66+
67+
useEffect(() => {
68+
(async () => {
69+
try {
70+
const threepids = await client.getThreePids();
71+
setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email));
72+
setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone));
73+
setLoadingState("loaded");
74+
} catch (e) {
75+
setLoadingState("error");
76+
}
77+
})();
78+
}, [client]);
79+
80+
const onEmailsChange = useCallback((emails: ThirdPartyIdentifier[]) => {
81+
setEmails(emails);
82+
}, []);
83+
84+
const onMsisdnsChange = useCallback((msisdns: ThirdPartyIdentifier[]) => {
85+
setPhoneNumbers(msisdns);
86+
}, []);
87+
88+
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
89+
90+
return (
91+
<div>
92+
<h2>{_t("settings|general|personal_info")}</h2>
93+
<SettingsSubsection
94+
heading={_t("settings|general|emails_heading")}
95+
stretchContent
96+
data-testid="mx_AccountEmailAddresses"
97+
>
98+
<ThreepidSectionWrapper
99+
error={_t("settings|general|unable_to_load_emails")}
100+
loadingState={loadingState}
101+
>
102+
<AccountEmailAddresses
103+
emails={emails!}
104+
onEmailsChange={onEmailsChange}
105+
disabled={!canMake3pidChanges}
106+
/>
107+
</ThreepidSectionWrapper>
108+
</SettingsSubsection>
109+
110+
<SettingsSubsection
111+
heading={_t("settings|general|msisdns_heading")}
112+
stretchContent
113+
data-testid="mx_AccountPhoneNumbers"
114+
>
115+
<ThreepidSectionWrapper
116+
error={_t("settings|general|unable_to_load_msisdns")}
117+
loadingState={loadingState}
118+
>
119+
<AccountPhoneNumbers
120+
msisdns={phoneNumbers!}
121+
onMsisdnsChange={onMsisdnsChange}
122+
disabled={!canMake3pidChanges}
123+
/>
124+
</ThreepidSectionWrapper>
125+
</SettingsSubsection>
126+
</div>
127+
);
128+
};
129+
130+
export default UserPersonalInfoSettings;
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { useCallback, useEffect, useState } from "react";
18+
import { SERVICE_TYPES, ThreepidMedium } from "matrix-js-sdk/src/matrix";
19+
import { logger } from "matrix-js-sdk/src/logger";
20+
import { Alert } from "@vector-im/compound-web";
21+
22+
import DiscoveryEmailAddresses from "../discovery/EmailAddresses";
23+
import DiscoveryPhoneNumbers from "../discovery/PhoneNumbers";
24+
import { getThreepidsWithBindStatus } from "../../../../boundThreepids";
25+
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
26+
import { ThirdPartyIdentifier } from "../../../../AddThreepid";
27+
import SettingsStore from "../../../../settings/SettingsStore";
28+
import { UIFeature } from "../../../../settings/UIFeature";
29+
import { _t } from "../../../../languageHandler";
30+
import SetIdServer from "../SetIdServer";
31+
import SettingsSubsection from "../shared/SettingsSubsection";
32+
import InlineTermsAgreement from "../../terms/InlineTermsAgreement";
33+
import { Service, ServicePolicyPair, startTermsFlow } from "../../../../Terms";
34+
import IdentityAuthClient from "../../../../IdentityAuthClient";
35+
import { abbreviateUrl } from "../../../../utils/UrlUtils";
36+
import { useDispatcher } from "../../../../hooks/useDispatcher";
37+
import defaultDispatcher from "../../../../dispatcher/dispatcher";
38+
import { ActionPayload } from "../../../../dispatcher/payloads";
39+
40+
type RequiredPolicyInfo =
41+
| {
42+
// This object is passed along to a component for handling
43+
policiesAndServices: null; // From the startTermsFlow callback
44+
agreedUrls: null; // From the startTermsFlow callback
45+
resolve: null; // Promise resolve function for startTermsFlow callback
46+
}
47+
| {
48+
policiesAndServices: ServicePolicyPair[];
49+
agreedUrls: string[];
50+
resolve: (values: string[]) => void;
51+
};
52+
53+
/**
54+
* Settings controlling how a user's email addreses and phone numbers can be used to discover them
55+
*/
56+
export const DiscoverySettings: React.FC = () => {
57+
const client = useMatrixClientContext();
58+
59+
const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]);
60+
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]);
61+
const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading");
62+
const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl()));
63+
const [canMake3pidChanges, setCanMake3pidChanges] = useState<boolean>(false);
64+
65+
const [requiredPolicyInfo, setRequiredPolicyInfo] = useState<RequiredPolicyInfo>({
66+
// This object is passed along to a component for handling
67+
policiesAndServices: null, // From the startTermsFlow callback
68+
agreedUrls: null, // From the startTermsFlow callback
69+
resolve: null, // Promise resolve function for startTermsFlow callback
70+
});
71+
const [hasTerms, setHasTerms] = useState<boolean>(false);
72+
73+
const getThreepidState = useCallback(async () => {
74+
const threepids = await getThreepidsWithBindStatus(client);
75+
setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email));
76+
setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone));
77+
}, [client]);
78+
79+
useDispatcher(
80+
defaultDispatcher,
81+
useCallback(
82+
(payload: ActionPayload) => {
83+
if (payload.action === "id_server_changed") {
84+
setIdServerName(abbreviateUrl(client.getIdentityServerUrl()));
85+
86+
getThreepidState().then();
87+
}
88+
},
89+
[client, getThreepidState],
90+
),
91+
);
92+
93+
useEffect(() => {
94+
(async () => {
95+
try {
96+
await getThreepidState();
97+
98+
const capabilities = await client.getCapabilities();
99+
setCanMake3pidChanges(
100+
!capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true,
101+
);
102+
103+
// By starting the terms flow we get the logic for checking which terms the user has signed
104+
// for free. So we might as well use that for our own purposes.
105+
const idServerUrl = client.getIdentityServerUrl();
106+
if (!idServerUrl) {
107+
return;
108+
}
109+
110+
const authClient = new IdentityAuthClient();
111+
try {
112+
const idAccessToken = await authClient.getAccessToken({ check: false });
113+
await startTermsFlow(
114+
client,
115+
[new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)],
116+
(policiesAndServices, agreedUrls, extraClassNames) => {
117+
return new Promise((resolve) => {
118+
setIdServerName(abbreviateUrl(idServerUrl));
119+
setHasTerms(true);
120+
setRequiredPolicyInfo({
121+
policiesAndServices,
122+
agreedUrls,
123+
resolve,
124+
});
125+
});
126+
},
127+
);
128+
// User accepted all terms
129+
setHasTerms(false);
130+
} catch (e) {
131+
logger.warn(
132+
`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`,
133+
);
134+
logger.warn(e);
135+
}
136+
137+
setLoadingState("loaded");
138+
} catch (e) {
139+
setLoadingState("error");
140+
}
141+
})();
142+
}, [client, getThreepidState]);
143+
144+
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
145+
146+
if (hasTerms && requiredPolicyInfo.policiesAndServices) {
147+
const intro = (
148+
<Alert type="info" title={_t("settings|general|discovery_needs_terms_title")}>
149+
{_t("settings|general|discovery_needs_terms", { serverName: idServerName })}
150+
</Alert>
151+
);
152+
return (
153+
<>
154+
<InlineTermsAgreement
155+
policiesAndServicePairs={requiredPolicyInfo.policiesAndServices}
156+
agreedUrls={requiredPolicyInfo.agreedUrls}
157+
onFinished={requiredPolicyInfo.resolve}
158+
introElement={intro}
159+
/>
160+
{/* has its own heading as it includes the current identity server */}
161+
<SetIdServer missingTerms={true} />
162+
</>
163+
);
164+
}
165+
166+
const threepidSection = idServerName ? (
167+
<>
168+
<DiscoveryEmailAddresses
169+
emails={emails}
170+
isLoading={loadingState === "loading"}
171+
disabled={!canMake3pidChanges}
172+
/>
173+
<DiscoveryPhoneNumbers
174+
msisdns={phoneNumbers}
175+
isLoading={loadingState === "loading"}
176+
disabled={!canMake3pidChanges}
177+
/>
178+
</>
179+
) : null;
180+
181+
return (
182+
<SettingsSubsection heading={_t("settings|discovery|title")} data-testid="discoverySection">
183+
{threepidSection}
184+
{/* has its own heading as it includes the current identity server */}
185+
<SetIdServer missingTerms={false} />
186+
</SettingsSubsection>
187+
);
188+
};
189+
190+
export default DiscoverySettings;

0 commit comments

Comments
 (0)