You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-06 16:22:46 +03:00
Fix share button in discovery settings being disabled incorrectly (#29151)
* Fix share button in discovery settings being disabled incorrectly Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve types & add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add missing snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
aa01b17f9e
commit
4f1eac67a8
33
src/Terms.ts
33
src/Terms.ts
@@ -7,11 +7,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { SERVICE_TYPES, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { SERVICE_TYPES, MatrixClient, Terms, Policy, InternationalisedPolicy } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import TermsDialog from "./components/views/dialogs/TermsDialog";
|
import TermsDialog from "./components/views/dialogs/TermsDialog";
|
||||||
|
import { pickBestLanguage } from "./languageHandler.tsx";
|
||||||
|
|
||||||
export class TermsNotSignedError extends Error {}
|
export class TermsNotSignedError extends Error {}
|
||||||
|
|
||||||
@@ -32,23 +33,8 @@ export class Service {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalisedPolicy {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Policy {
|
|
||||||
// @ts-ignore: No great way to express indexed types together with other keys
|
|
||||||
version: string;
|
|
||||||
[lang: string]: LocalisedPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Policies = {
|
|
||||||
[policy: string]: Policy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ServicePolicyPair = {
|
export type ServicePolicyPair = {
|
||||||
policies: Policies;
|
policies: Terms["policies"];
|
||||||
service: Service;
|
service: Service;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,6 +44,11 @@ export type TermsInteractionCallback = (
|
|||||||
extraClassNames?: string,
|
extraClassNames?: string,
|
||||||
) => Promise<string[]>;
|
) => Promise<string[]>;
|
||||||
|
|
||||||
|
export function pickBestPolicyLanguage(policy: Policy): InternationalisedPolicy | undefined {
|
||||||
|
const termsLang = pickBestLanguage(Object.keys(policy).filter((k) => k !== "version"));
|
||||||
|
return <InternationalisedPolicy>policy[termsLang];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a flow where the user is presented with terms & conditions for some services
|
* Start a flow where the user is presented with terms & conditions for some services
|
||||||
*
|
*
|
||||||
@@ -96,7 +87,7 @@ export async function startTermsFlow(
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
|
const terms: Terms[] = await Promise.all(termsPromises);
|
||||||
const policiesAndServicePairs = terms.map((t, i) => {
|
const policiesAndServicePairs = terms.map((t, i) => {
|
||||||
return { service: services[i], policies: t.policies };
|
return { service: services[i], policies: t.policies };
|
||||||
});
|
});
|
||||||
@@ -113,11 +104,11 @@ export async function startTermsFlow(
|
|||||||
// things they've not agreed to yet.
|
// things they've not agreed to yet.
|
||||||
const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = [];
|
const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = [];
|
||||||
for (const { service, policies } of policiesAndServicePairs) {
|
for (const { service, policies } of policiesAndServicePairs) {
|
||||||
const unagreedPolicies: Policies = {};
|
const unagreedPolicies: Terms["policies"] = {};
|
||||||
for (const [policyName, policy] of Object.entries(policies)) {
|
for (const [policyName, policy] of Object.entries(policies)) {
|
||||||
let policyAgreed = false;
|
let policyAgreed = false;
|
||||||
for (const lang of Object.keys(policy)) {
|
for (const lang of Object.keys(policy)) {
|
||||||
if (lang === "version") continue;
|
if (lang === "version" || typeof policy[lang] === "string") continue;
|
||||||
if (agreedUrlSet.has(policy[lang].url)) {
|
if (agreedUrlSet.has(policy[lang].url)) {
|
||||||
policyAgreed = true;
|
policyAgreed = true;
|
||||||
break;
|
break;
|
||||||
@@ -154,7 +145,7 @@ export async function startTermsFlow(
|
|||||||
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
|
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
|
||||||
for (const policy of Object.values(policiesAndService.policies)) {
|
for (const policy of Object.values(policiesAndService.policies)) {
|
||||||
for (const lang of Object.keys(policy)) {
|
for (const lang of Object.keys(policy)) {
|
||||||
if (lang === "version") continue;
|
if (lang === "version" || typeof policy[lang] === "string") continue;
|
||||||
if (policy[lang].url === url) return true;
|
if (policy[lang].url === url) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { InternationalisedPolicy, Terms, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth";
|
import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react";
|
import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react";
|
||||||
@@ -16,14 +16,13 @@ import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-o
|
|||||||
|
|
||||||
import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg";
|
import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import { LocalisedPolicy, Policies } from "../../../Terms";
|
|
||||||
import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier";
|
import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier";
|
||||||
import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton";
|
import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import CaptchaForm from "./CaptchaForm";
|
import CaptchaForm from "./CaptchaForm";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
|
import { pickBestPolicyLanguage } from "../../../Terms.ts";
|
||||||
|
|
||||||
/* This file contains a collection of components which are used by the
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* InteractiveAuth to prompt the user to enter the information needed
|
||||||
@@ -235,12 +234,10 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||||
stageParams?: {
|
stageParams?: Partial<Terms>;
|
||||||
policies?: Policies;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
interface LocalisedPolicyWithId extends InternationalisedPolicy {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +275,6 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
const allPolicies = this.props.stageParams?.policies || {};
|
const allPolicies = this.props.stageParams?.policies || {};
|
||||||
const prefLang = SettingsStore.getValue("language");
|
|
||||||
const initToggles: Record<string, boolean> = {};
|
const initToggles: Record<string, boolean> = {};
|
||||||
const pickedPolicies: {
|
const pickedPolicies: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -287,17 +283,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
|||||||
}[] = [];
|
}[] = [];
|
||||||
for (const policyId of Object.keys(allPolicies)) {
|
for (const policyId of Object.keys(allPolicies)) {
|
||||||
const policy = allPolicies[policyId];
|
const policy = allPolicies[policyId];
|
||||||
|
const langPolicy = pickBestPolicyLanguage(policy);
|
||||||
// Pick a language based on the user's language, falling back to english,
|
|
||||||
// and finally to the first language available. If there's still no policy
|
|
||||||
// available then the homeserver isn't respecting the spec.
|
|
||||||
let langPolicy: LocalisedPolicy | undefined = policy[prefLang];
|
|
||||||
if (!langPolicy) langPolicy = policy["en"];
|
|
||||||
if (!langPolicy) {
|
|
||||||
// last resort
|
|
||||||
const firstLang = Object.keys(policy).find((e) => e !== "version");
|
|
||||||
langPolicy = firstLang ? policy[firstLang] : undefined;
|
|
||||||
}
|
|
||||||
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
|
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
|
||||||
|
|
||||||
initToggles[policyId] = false;
|
initToggles[policyId] = false;
|
||||||
|
@@ -9,10 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/matrix";
|
import { SERVICE_TYPES } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t, pickBestLanguage } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import { ServicePolicyPair } from "../../../Terms";
|
import { pickBestPolicyLanguage, ServicePolicyPair } from "../../../Terms";
|
||||||
import ExternalLink from "../elements/ExternalLink";
|
import ExternalLink from "../elements/ExternalLink";
|
||||||
import { parseUrl } from "../../../utils/UrlUtils";
|
import { parseUrl } from "../../../utils/UrlUtils";
|
||||||
|
|
||||||
@@ -126,8 +126,8 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||||||
|
|
||||||
const policyValues = Object.values(policiesAndService.policies);
|
const policyValues = Object.values(policiesAndService.policies);
|
||||||
for (let i = 0; i < policyValues.length; ++i) {
|
for (let i = 0; i < policyValues.length; ++i) {
|
||||||
const termDoc = policyValues[i];
|
const internationalisedPolicy = pickBestPolicyLanguage(policyValues[i]);
|
||||||
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== "version"));
|
if (!internationalisedPolicy) continue;
|
||||||
let serviceName: JSX.Element | undefined;
|
let serviceName: JSX.Element | undefined;
|
||||||
let summary: JSX.Element | undefined;
|
let summary: JSX.Element | undefined;
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
@@ -136,19 +136,19 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
<tr key={termDoc[termsLang].url}>
|
<tr key={internationalisedPolicy.url}>
|
||||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||||
<td>
|
<td>
|
||||||
<ExternalLink rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
<ExternalLink rel="noreferrer noopener" target="_blank" href={internationalisedPolicy.url}>
|
||||||
{termDoc[termsLang].name}
|
{internationalisedPolicy.name}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TermsCheckbox
|
<TermsCheckbox
|
||||||
url={termDoc[termsLang].url}
|
url={internationalisedPolicy.url}
|
||||||
onChange={this.onTermsCheckboxChange}
|
onChange={this.onTermsCheckboxChange}
|
||||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
checked={Boolean(this.state.agreedUrls[internationalisedPolicy.url])}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>,
|
</tr>,
|
||||||
@@ -164,7 +164,7 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||||||
for (const terms of Object.values(policiesAndService.policies)) {
|
for (const terms of Object.values(policiesAndService.policies)) {
|
||||||
let docAgreed = false;
|
let docAgreed = false;
|
||||||
for (const lang of Object.keys(terms)) {
|
for (const lang of Object.keys(terms)) {
|
||||||
if (lang === "version") continue;
|
if (lang === "version" || typeof terms[lang] === "string") continue;
|
||||||
if (this.state.agreedUrls[terms[lang].url]) {
|
if (this.state.agreedUrls[terms[lang].url]) {
|
||||||
docAgreed = true;
|
docAgreed = true;
|
||||||
break;
|
break;
|
||||||
|
@@ -58,7 +58,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
agreedUrls: null, // From the startTermsFlow callback
|
agreedUrls: null, // From the startTermsFlow callback
|
||||||
resolve: null, // Promise resolve function for startTermsFlow callback
|
resolve: null, // Promise resolve function for startTermsFlow callback
|
||||||
});
|
});
|
||||||
const [hasTerms, setHasTerms] = useState<boolean>(false);
|
const [mustAgreeToTerms, setMustAgreeToTerms] = useState<boolean>(false);
|
||||||
|
|
||||||
const getThreepidState = useCallback(async () => {
|
const getThreepidState = useCallback(async () => {
|
||||||
setIsLoadingThreepids(true);
|
setIsLoadingThreepids(true);
|
||||||
@@ -103,7 +103,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
(policiesAndServices, agreedUrls, extraClassNames) => {
|
(policiesAndServices, agreedUrls, extraClassNames) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setIdServerName(abbreviateUrl(idServerUrl));
|
setIdServerName(abbreviateUrl(idServerUrl));
|
||||||
setHasTerms(true);
|
setMustAgreeToTerms(true);
|
||||||
setRequiredPolicyInfo({
|
setRequiredPolicyInfo({
|
||||||
policiesAndServices,
|
policiesAndServices,
|
||||||
agreedUrls,
|
agreedUrls,
|
||||||
@@ -113,7 +113,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
// User accepted all terms
|
// User accepted all terms
|
||||||
setHasTerms(false);
|
setMustAgreeToTerms(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`,
|
`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`,
|
||||||
@@ -126,7 +126,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
|
|
||||||
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
|
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
|
||||||
|
|
||||||
if (hasTerms && requiredPolicyInfo.policiesAndServices) {
|
if (mustAgreeToTerms && requiredPolicyInfo.policiesAndServices) {
|
||||||
const intro = (
|
const intro = (
|
||||||
<Alert type="info" title={_t("settings|general|discovery_needs_terms_title")}>
|
<Alert type="info" title={_t("settings|general|discovery_needs_terms_title")}>
|
||||||
{_t("settings|general|discovery_needs_terms", { serverName: idServerName })}
|
{_t("settings|general|discovery_needs_terms", { serverName: idServerName })}
|
||||||
@@ -160,7 +160,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
medium={ThreepidMedium.Email}
|
medium={ThreepidMedium.Email}
|
||||||
threepids={emails}
|
threepids={emails}
|
||||||
onChange={getThreepidState}
|
onChange={getThreepidState}
|
||||||
disabled={!hasTerms}
|
disabled={mustAgreeToTerms}
|
||||||
isLoading={isLoadingThreepids}
|
isLoading={isLoadingThreepids}
|
||||||
/>
|
/>
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
@@ -174,7 +174,7 @@ export const DiscoverySettings: React.FC = () => {
|
|||||||
medium={ThreepidMedium.Phone}
|
medium={ThreepidMedium.Phone}
|
||||||
threepids={phoneNumbers}
|
threepids={phoneNumbers}
|
||||||
onChange={getThreepidState}
|
onChange={getThreepidState}
|
||||||
disabled={!hasTerms}
|
disabled={mustAgreeToTerms}
|
||||||
isLoading={isLoadingThreepids}
|
isLoading={isLoadingThreepids}
|
||||||
/>
|
/>
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
@@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { _t, pickBestLanguage } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { objectClone } from "../../../utils/objects";
|
import { objectClone } from "../../../utils/objects";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { ServicePolicyPair } from "../../../Terms";
|
import { pickBestPolicyLanguage, ServicePolicyPair } from "../../../Terms";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
policiesAndServicePairs: ServicePolicyPair[];
|
policiesAndServicePairs: ServicePolicyPair[];
|
||||||
@@ -47,11 +47,12 @@ export default class InlineTermsAgreement extends React.Component<IProps, IState
|
|||||||
for (const servicePolicies of this.props.policiesAndServicePairs) {
|
for (const servicePolicies of this.props.policiesAndServicePairs) {
|
||||||
const availablePolicies = Object.values(servicePolicies.policies);
|
const availablePolicies = Object.values(servicePolicies.policies);
|
||||||
for (const policy of availablePolicies) {
|
for (const policy of availablePolicies) {
|
||||||
const language = pickBestLanguage(Object.keys(policy).filter((p) => p !== "version"));
|
const internationalisedPolicy = pickBestPolicyLanguage(policy);
|
||||||
|
if (!internationalisedPolicy) continue;
|
||||||
const renderablePolicy: Policy = {
|
const renderablePolicy: Policy = {
|
||||||
checked: false,
|
checked: false,
|
||||||
url: policy[language].url,
|
url: internationalisedPolicy.url,
|
||||||
name: policy[language].name,
|
name: internationalisedPolicy.name,
|
||||||
};
|
};
|
||||||
policies.push(renderablePolicy);
|
policies.push(renderablePolicy);
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SERVICE_TYPES, HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { SERVICE_TYPES, HTTPError, MatrixClient, Terms } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
import { Policies } from "../Terms";
|
|
||||||
|
|
||||||
export function getDefaultIdentityServerUrl(): string | undefined {
|
export function getDefaultIdentityServerUrl(): string | undefined {
|
||||||
return SdkConfig.get("validated_server_config")?.isUrl;
|
return SdkConfig.get("validated_server_config")?.isUrl;
|
||||||
@@ -25,7 +24,7 @@ export function setToDefaultIdentityServer(matrixClient: MatrixClient): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function doesIdentityServerHaveTerms(matrixClient: MatrixClient, fullUrl: string): Promise<boolean> {
|
export async function doesIdentityServerHaveTerms(matrixClient: MatrixClient, fullUrl: string): Promise<boolean> {
|
||||||
let terms: { policies?: Policies } | null;
|
let terms: Partial<Terms> | null;
|
||||||
try {
|
try {
|
||||||
terms = await matrixClient.getTerms(SERVICE_TYPES.IS, fullUrl);
|
terms = await matrixClient.getTerms(SERVICE_TYPES.IS, fullUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -217,6 +217,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
|
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
|
||||||
getIdentityAccount: jest.fn().mockResolvedValue({}),
|
getIdentityAccount: jest.fn().mockResolvedValue({}),
|
||||||
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
||||||
|
agreeToTerms: jest.fn(),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
|
||||||
isVersionSupported: jest.fn().mockResolvedValue(undefined),
|
isVersionSupported: jest.fn().mockResolvedValue(undefined),
|
||||||
getPushRules: jest.fn().mockResolvedValue(undefined),
|
getPushRules: jest.fn().mockResolvedValue(undefined),
|
||||||
|
@@ -97,7 +97,7 @@ describe("ScalarAuthClient", function () {
|
|||||||
body: { errcode: "M_TERMS_NOT_SIGNED" },
|
body: { errcode: "M_TERMS_NOT_SIGNED" },
|
||||||
});
|
});
|
||||||
sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken1"));
|
sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken1"));
|
||||||
mocked(client.getTerms).mockResolvedValue({ policies: [] });
|
mocked(client.getTerms).mockResolvedValue({ policies: {} });
|
||||||
|
|
||||||
await expect(sac.registerForToken()).resolves.toBe("testtoken1");
|
await expect(sac.registerForToken()).resolves.toBe("testtoken1");
|
||||||
});
|
});
|
||||||
|
@@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent, EventType, SERVICE_TYPES } from "matrix-js-sdk/src/matrix";
|
import { EventType, MatrixEvent, Policy, SERVICE_TYPES, Terms } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { screen, within } from "jest-matrix-react";
|
||||||
|
|
||||||
import { startTermsFlow, Service } from "../../src/Terms";
|
import { dialogTermsInteractionCallback, Service, startTermsFlow } from "../../src/Terms";
|
||||||
import { getMockClientWithEventEmitter } from "../test-utils";
|
import { getMockClientWithEventEmitter } from "../test-utils";
|
||||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ const POLICY_ONE = {
|
|||||||
name: "The first policy",
|
name: "The first policy",
|
||||||
url: "http://example.com/one",
|
url: "http://example.com/one",
|
||||||
},
|
},
|
||||||
};
|
} satisfies Policy;
|
||||||
|
|
||||||
const POLICY_TWO = {
|
const POLICY_TWO = {
|
||||||
version: "IX",
|
version: "IX",
|
||||||
@@ -26,7 +27,7 @@ const POLICY_TWO = {
|
|||||||
name: "The second policy",
|
name: "The second policy",
|
||||||
url: "http://example.com/two",
|
url: "http://example.com/two",
|
||||||
},
|
},
|
||||||
};
|
} satisfies Policy;
|
||||||
|
|
||||||
const IM_SERVICE_ONE = new Service(SERVICE_TYPES.IM, "https://imone.test", "a token token");
|
const IM_SERVICE_ONE = new Service(SERVICE_TYPES.IM, "https://imone.test", "a token token");
|
||||||
const IM_SERVICE_TWO = new Service(SERVICE_TYPES.IM, "https://imtwo.test", "a token token");
|
const IM_SERVICE_TWO = new Service(SERVICE_TYPES.IM, "https://imtwo.test", "a token token");
|
||||||
@@ -42,7 +43,7 @@ describe("Terms", function () {
|
|||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockClient.getAccountData.mockReturnValue(undefined);
|
mockClient.getAccountData.mockReturnValue(undefined);
|
||||||
mockClient.getTerms.mockResolvedValue(null);
|
mockClient.getTerms.mockResolvedValue({ policies: {} });
|
||||||
mockClient.setAccountData.mockResolvedValue({});
|
mockClient.setAccountData.mockResolvedValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,22 +142,25 @@ describe("Terms", function () {
|
|||||||
});
|
});
|
||||||
mockClient.getAccountData.mockReturnValue(directEvent);
|
mockClient.getAccountData.mockReturnValue(directEvent);
|
||||||
|
|
||||||
mockClient.getTerms.mockImplementation(async (_serviceTypes: SERVICE_TYPES, baseUrl: string) => {
|
mockClient.getTerms.mockImplementation(
|
||||||
switch (baseUrl) {
|
async (_serviceTypes: SERVICE_TYPES, baseUrl: string): Promise<Terms> => {
|
||||||
case "https://imone.test":
|
switch (baseUrl) {
|
||||||
return {
|
case "https://imone.test":
|
||||||
policies: {
|
return {
|
||||||
policy_the_first: POLICY_ONE,
|
policies: {
|
||||||
},
|
policy_the_first: POLICY_ONE,
|
||||||
};
|
},
|
||||||
case "https://imtwo.test":
|
};
|
||||||
return {
|
case "https://imtwo.test":
|
||||||
policies: {
|
return {
|
||||||
policy_the_second: POLICY_TWO,
|
policies: {
|
||||||
},
|
policy_the_second: POLICY_TWO,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
});
|
}
|
||||||
|
return { policies: {} };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const interactionCallback = jest.fn().mockResolvedValue(["http://example.com/one", "http://example.com/two"]);
|
const interactionCallback = jest.fn().mockResolvedValue(["http://example.com/one", "http://example.com/two"]);
|
||||||
await startTermsFlow(mockClient, [IM_SERVICE_ONE, IM_SERVICE_TWO], interactionCallback);
|
await startTermsFlow(mockClient, [IM_SERVICE_ONE, IM_SERVICE_TWO], interactionCallback);
|
||||||
@@ -180,3 +184,29 @@ describe("Terms", function () {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dialogTermsInteractionCallback", () => {
|
||||||
|
it("should render a dialog with the expected terms", async () => {
|
||||||
|
dialogTermsInteractionCallback(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
service: new Service(SERVICE_TYPES.IS, "http://base_url", "access_token"),
|
||||||
|
policies: {
|
||||||
|
sample: {
|
||||||
|
version: "VERSION",
|
||||||
|
en: {
|
||||||
|
name: "Terms",
|
||||||
|
url: "http://base_url/terms",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
expect(within(dialog).getByRole("link")).toHaveAttribute("href", "http://base_url/terms");
|
||||||
|
expect(dialog).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
113
test/unit-tests/__snapshots__/Terms-test.tsx.snap
Normal file
113
test/unit-tests/__snapshots__/Terms-test.tsx.snap
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`dialogTermsInteractionCallback should render a dialog with the expected terms 1`] = `
|
||||||
|
<div
|
||||||
|
aria-describedby="mx_Dialog_content"
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class=""
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Terms of Service
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="mx_Dialog_content"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
To continue you need to accept the terms of this service.
|
||||||
|
</p>
|
||||||
|
<table
|
||||||
|
class="mx_TermsDialog_termsTable"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
class="mx_TermsDialog_termsTableHeader"
|
||||||
|
>
|
||||||
|
<th>
|
||||||
|
Service
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Summary
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Document
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Accept
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="mx_TermsDialog_service"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Identity server
|
||||||
|
<br />
|
||||||
|
(
|
||||||
|
base_url
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="mx_TermsDialog_summary"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Find others by phone or email
|
||||||
|
<br />
|
||||||
|
Be found by phone or email
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
class="mx_ExternalLink"
|
||||||
|
href="http://base_url/terms"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Terms
|
||||||
|
<i
|
||||||
|
class="mx_ExternalLink_icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@@ -10,10 +10,12 @@ import React from "react";
|
|||||||
import { render, screen, waitFor, act, fireEvent } from "jest-matrix-react";
|
import { render, screen, waitFor, act, fireEvent } from "jest-matrix-react";
|
||||||
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { Policy } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmailIdentityAuthEntry,
|
EmailIdentityAuthEntry,
|
||||||
MasUnlockCrossSigningAuthEntry,
|
MasUnlockCrossSigningAuthEntry,
|
||||||
|
TermsAuthEntry,
|
||||||
} from "../../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
} from "../../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
||||||
import { createTestClient } from "../../../../test-utils";
|
import { createTestClient } from "../../../../test-utils";
|
||||||
|
|
||||||
@@ -99,3 +101,38 @@ describe("<MasUnlockCrossSigningAuthEntry/>", () => {
|
|||||||
expect(submitAuthDict).toHaveBeenCalledWith({});
|
expect(submitAuthDict).toHaveBeenCalledWith({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("<TermsAuthEntry/>", () => {
|
||||||
|
const renderAuth = (policy: Policy, props = {}) => {
|
||||||
|
const matrixClient = createTestClient();
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<TermsAuthEntry
|
||||||
|
matrixClient={matrixClient}
|
||||||
|
loginType={AuthType.Email}
|
||||||
|
onPhaseChange={jest.fn()}
|
||||||
|
submitAuthDict={jest.fn()}
|
||||||
|
fail={jest.fn()}
|
||||||
|
clientSecret="my secret"
|
||||||
|
showContinue={true}
|
||||||
|
stageParams={{
|
||||||
|
policies: {
|
||||||
|
test_policy: policy,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("should render", () => {
|
||||||
|
const { container } = renderAuth({
|
||||||
|
version: "alpha",
|
||||||
|
en: {
|
||||||
|
name: "Test Policy",
|
||||||
|
url: "https://example.com/en",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -82,3 +82,38 @@ exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<TermsAuthEntry/> should render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_InteractiveAuthEntryComponents"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Please review and accept the policies of this homeserver:
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
class="mx_InteractiveAuthEntryComponents_termsPolicy"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="https://example.com/en"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Test Policy
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
aria-disabled="true"
|
||||||
|
class="mx_AccessibleButton mx_InteractiveAuthEntryComponents_termsSubmit mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
||||||
|
disabled=""
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { act, render, screen } from "jest-matrix-react";
|
import { act, render, screen } from "jest-matrix-react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent, Terms, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
@@ -26,6 +26,18 @@ jest.mock("../../../../../../src/IdentityAuthClient", () =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sampleTerms = {
|
||||||
|
policies: {
|
||||||
|
terms: { version: "alpha", en: { name: "No ball games", url: "https://foobar" } },
|
||||||
|
},
|
||||||
|
} satisfies Terms;
|
||||||
|
|
||||||
|
const invalidTerms = {
|
||||||
|
policies: {
|
||||||
|
terms: { version: "invalid" },
|
||||||
|
},
|
||||||
|
} satisfies Terms;
|
||||||
|
|
||||||
describe("DiscoverySettings", () => {
|
describe("DiscoverySettings", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
|
||||||
@@ -51,20 +63,17 @@ describe("DiscoverySettings", () => {
|
|||||||
|
|
||||||
it("displays alert if an identity server needs terms accepting", async () => {
|
it("displays alert if an identity server needs terms accepting", async () => {
|
||||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||||
mocked(client).getTerms.mockResolvedValue({
|
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||||
["policies"]: { en: "No ball games" },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||||
|
|
||||||
await expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("link")).toHaveAttribute("href", "https://foobar");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("button to accept terms is disabled if checkbox not checked", async () => {
|
it("button to accept terms is disabled if checkbox not checked", async () => {
|
||||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||||
mocked(client).getTerms.mockResolvedValue({
|
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||||
["policies"]: { en: "No ball games" },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||||
|
|
||||||
@@ -93,4 +102,40 @@ describe("DiscoverySettings", () => {
|
|||||||
|
|
||||||
expect(client.getThreePids).toHaveBeenCalled();
|
expect(client.getThreePids).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not disable share button if terms accepted", async () => {
|
||||||
|
mocked(client).getThreePids.mockResolvedValue({
|
||||||
|
threepids: [
|
||||||
|
{
|
||||||
|
medium: ThreepidMedium.Email,
|
||||||
|
address: "test@email.com",
|
||||||
|
bound: false,
|
||||||
|
added_at: 123,
|
||||||
|
validated_at: 234,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||||
|
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||||
|
mocked(client).getAccountData.mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
content: { accepted: [sampleTerms.policies["terms"]["en"].url] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||||
|
|
||||||
|
const shareButton = await screen.findByRole("button", { name: "Share" });
|
||||||
|
expect(shareButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show invalid terms", async () => {
|
||||||
|
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||||
|
mocked(client).getTerms.mockResolvedValue(invalidTerms);
|
||||||
|
|
||||||
|
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||||
|
|
||||||
|
expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user