Files
ThreadNet-Web/src/Terms.ts
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

206 lines
7.4 KiB
TypeScript
Raw Normal View History

2019-07-10 10:50:10 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
2019-07-10 10:50:10 +01:00
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
2019-07-10 10:50:10 +01:00
*/
import classNames from "classnames";
import { SERVICE_TYPES, MatrixClient } from "matrix-js-sdk/src/matrix";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
2019-07-10 10:50:10 +01:00
import Modal from "./Modal";
2022-03-02 16:33:40 -07:00
import TermsDialog from "./components/views/dialogs/TermsDialog";
2019-07-10 10:50:10 +01:00
export class TermsNotSignedError extends Error {}
/**
* Class representing a service that may have terms & conditions that
2019-07-10 14:17:15 +01:00
* require agreement from the user before the user can use that service.
2019-07-10 10:50:10 +01:00
*/
export class Service {
/**
2019-07-10 12:08:26 +01:00
* @param {MatrixClient.SERVICE_TYPES} serviceType The type of service
2019-07-10 10:50:10 +01:00
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service
*/
2024-01-02 18:56:39 +00:00
public constructor(
public serviceType: SERVICE_TYPES,
public baseUrl: string,
public accessToken: string,
) {}
2019-07-10 10:50:10 +01:00
}
2021-05-12 19:28:22 +01:00
export interface LocalisedPolicy {
name: string;
url: string;
}
export interface Policy {
2021-04-06 12:26:50 +01:00
// @ts-ignore: No great way to express indexed types together with other keys
version: string;
2021-05-12 19:28:22 +01:00
[lang: string]: LocalisedPolicy;
2021-04-06 12:26:50 +01:00
}
2021-05-12 19:28:22 +01:00
export type Policies = {
2021-07-01 23:23:03 +01:00
[policy: string]: Policy;
2021-04-06 12:26:50 +01:00
};
2023-02-13 11:39:16 +00:00
export type ServicePolicyPair = {
policies: Policies;
service: Service;
};
2021-04-06 12:26:50 +01:00
export type TermsInteractionCallback = (
2023-02-13 11:39:16 +00:00
policiesAndServicePairs: ServicePolicyPair[],
2021-04-06 12:26:50 +01:00
agreedUrls: string[],
extraClassNames?: string,
) => Promise<string[]>;
2019-07-10 16:07:31 +01:00
/**
2019-07-10 14:22:50 +01:00
* Start a flow where the user is presented with terms & conditions for some services
*
* @param client The Matrix Client instance of the logged-in user
2019-07-11 10:53:45 +01:00
* @param {Service[]} services Object with keys 'serviceType', 'baseUrl', 'accessToken'
* @param {function} interactionCallback Function called with:
2019-07-22 12:23:28 +01:00
* * an array of { service: {Service}, policies: {terms response from API} }
2019-07-11 10:53:45 +01:00
* * an array of URLs the user has already agreed to
2019-07-10 14:22:50 +01:00
* Must return a Promise which resolves with a list of URLs of documents agreed to
* @returns {Promise} resolves when the user agreed to all necessary terms or rejects
* if they cancel.
*/
export async function startTermsFlow(
client: MatrixClient,
2021-04-06 12:26:50 +01:00
services: Service[],
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
): Promise<void> {
const termsPromises = services.map((s) => client.getTerms(s.serviceType, s.baseUrl));
2019-07-10 10:50:10 +01:00
2019-07-10 14:25:30 +01:00
/*
* a /terms response looks like:
* {
* "policies": {
* "terms_of_service": {
* "version": "2.0",
* "en": {
* "name": "Terms of Service",
* "url": "https://example.org/somewhere/terms-2.0-en.html"
* },
* "fr": {
* "name": "Conditions d'utilisation",
* "url": "https://example.org/somewhere/terms-2.0-fr.html"
* }
* }
* }
* }
*/
2021-04-06 12:26:50 +01:00
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
2019-07-10 14:27:29 +01:00
const policiesAndServicePairs = terms.map((t, i) => {
return { service: services[i], policies: t.policies };
});
2019-07-10 10:50:10 +01:00
2019-07-11 10:53:45 +01:00
// fetch the set of agreed policy URLs from account data
const currentAcceptedTerms = await client.getAccountData("m.accepted_terms");
2021-06-01 22:21:04 -06:00
let agreedUrlSet: Set<string>;
2019-07-11 10:53:45 +01:00
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
agreedUrlSet = new Set();
} else {
agreedUrlSet = new Set(currentAcceptedTerms.getContent().accepted);
}
// remove any policies the user has already agreed to and any services where
// they've already agreed to all the policies
// NB. it could be nicer to show the user stuff they've already agreed to,
// but then they'd assume they can un-check the boxes to un-agree to a policy,
// but that is not a thing the API supports, so probably best to just show
// things they've not agreed to yet.
2023-02-13 11:39:16 +00:00
const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = [];
2021-06-29 13:11:58 +01:00
for (const { service, policies } of policiesAndServicePairs) {
2023-02-13 11:39:16 +00:00
const unagreedPolicies: Policies = {};
2019-07-11 10:53:45 +01:00
for (const [policyName, policy] of Object.entries(policies)) {
let policyAgreed = false;
for (const lang of Object.keys(policy)) {
if (lang === "version") continue;
if (agreedUrlSet.has(policy[lang].url)) {
policyAgreed = true;
break;
}
}
if (!policyAgreed) unagreedPolicies[policyName] = policy;
}
if (Object.keys(unagreedPolicies).length > 0) {
2021-06-29 13:11:58 +01:00
unagreedPoliciesAndServicePairs.push({ service, policies: unagreedPolicies });
2019-07-11 10:53:45 +01:00
}
}
// if there's anything left to agree to, prompt the user
2019-09-10 11:16:45 -06:00
const numAcceptedBeforeAgreement = agreedUrlSet.size;
2019-07-11 10:53:45 +01:00
if (unagreedPoliciesAndServicePairs.length > 0) {
2019-07-22 12:23:55 +01:00
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
logger.log("User has agreed to URLs", newlyAgreedUrls);
// Merge with previously agreed URLs
newlyAgreedUrls.forEach((url) => agreedUrlSet.add(url));
2019-07-11 10:53:45 +01:00
} else {
logger.log("User has already agreed to all required policies");
2019-07-11 10:53:45 +01:00
}
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length
if (agreedUrlSet.size !== numAcceptedBeforeAgreement) {
2021-06-29 13:11:58 +01:00
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
await client.setAccountData("m.accepted_terms", newAcceptedTerms);
}
2019-07-10 10:50:10 +01:00
2019-07-10 15:12:05 +01:00
const agreePromises = policiesAndServicePairs.map((policiesAndService) => {
2019-07-10 10:50:10 +01:00
// filter the agreed URL list for ones that are actually for this service
// (one URL may be used for multiple services)
// Not a particularly efficient loop but probably fine given the numbers involved
2019-07-11 10:53:45 +01:00
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
2019-07-10 15:12:05 +01:00
for (const policy of Object.values(policiesAndService.policies)) {
for (const lang of Object.keys(policy)) {
2019-07-10 10:50:10 +01:00
if (lang === "version") continue;
2019-07-10 15:12:05 +01:00
if (policy[lang].url === url) return true;
2019-07-10 10:50:10 +01:00
}
}
return false;
});
if (urlsForService.length === 0) return Promise.resolve();
return client.agreeToTerms(
2019-07-10 15:12:05 +01:00
policiesAndService.service.serviceType,
policiesAndService.service.baseUrl,
policiesAndService.service.accessToken,
2019-07-10 10:50:10 +01:00
urlsForService,
);
});
await Promise.all(agreePromises);
2019-07-10 10:50:10 +01:00
}
export async function dialogTermsInteractionCallback(
2021-04-06 12:26:50 +01:00
policiesAndServicePairs: {
2021-07-01 23:23:03 +01:00
service: Service;
policies: { [policy: string]: Policy };
2021-04-06 12:26:50 +01:00
}[],
agreedUrls: string[],
extraClassNames?: string,
): Promise<string[]> {
logger.log("Terms that need agreement", policiesAndServicePairs);
2019-07-10 10:50:10 +01:00
const { finished } = Modal.createDialog(
2022-06-14 17:51:51 +01:00
TermsDialog,
{
policiesAndServicePairs,
agreedUrls,
},
classNames("mx_TermsDialog", extraClassNames),
);
const [done, _agreedUrls] = await finished;
if (!done || !_agreedUrls) {
throw new TermsNotSignedError();
}
return _agreedUrls;
2019-07-10 10:50:10 +01:00
}