2019-07-10 17:50:10 +08:00
|
|
|
/*
|
2021-04-06 19:26:50 +08:00
|
|
|
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
2019-07-10 17:50:10 +08:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2019-07-23 22:11:38 +08:00
|
|
|
import classNames from "classnames";
|
2021-07-02 04:05:57 +08:00
|
|
|
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
2021-10-23 06:23:32 +08:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2019-07-10 17:50:10 +08:00
|
|
|
|
2021-06-29 20:11:58 +08:00
|
|
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
2019-07-10 17:50:10 +08:00
|
|
|
import Modal from "./Modal";
|
2022-03-03 07:33:40 +08:00
|
|
|
import TermsDialog from "./components/views/dialogs/TermsDialog";
|
2019-07-10 17:50:10 +08:00
|
|
|
|
|
|
|
export class TermsNotSignedError extends Error {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class representing a service that may have terms & conditions that
|
2019-07-10 21:17:15 +08:00
|
|
|
* require agreement from the user before the user can use that service.
|
2019-07-10 17:50:10 +08:00
|
|
|
*/
|
|
|
|
export class Service {
|
|
|
|
/**
|
2019-07-10 19:08:26 +08:00
|
|
|
* @param {MatrixClient.SERVICE_TYPES} serviceType The type of service
|
2019-07-10 17:50:10 +08:00
|
|
|
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
|
|
|
* @param {string} accessToken The user's access token for the service
|
|
|
|
*/
|
2022-12-16 20:29:59 +08:00
|
|
|
public constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {}
|
2019-07-10 17:50:10 +08:00
|
|
|
}
|
|
|
|
|
2021-05-13 02:28:22 +08:00
|
|
|
export interface LocalisedPolicy {
|
|
|
|
name: string;
|
|
|
|
url: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Policy {
|
2021-04-06 19:26:50 +08:00
|
|
|
// @ts-ignore: No great way to express indexed types together with other keys
|
|
|
|
version: string;
|
2021-05-13 02:28:22 +08:00
|
|
|
[lang: string]: LocalisedPolicy;
|
2021-04-06 19:26:50 +08:00
|
|
|
}
|
2021-05-13 02:28:22 +08:00
|
|
|
|
|
|
|
export type Policies = {
|
2021-07-02 06:23:03 +08:00
|
|
|
[policy: string]: Policy;
|
2021-04-06 19:26:50 +08:00
|
|
|
};
|
|
|
|
|
2023-02-13 19:39:16 +08:00
|
|
|
export type ServicePolicyPair = {
|
|
|
|
policies: Policies;
|
|
|
|
service: Service;
|
|
|
|
};
|
|
|
|
|
2021-04-06 19:26:50 +08:00
|
|
|
export type TermsInteractionCallback = (
|
2023-02-13 19:39:16 +08:00
|
|
|
policiesAndServicePairs: ServicePolicyPair[],
|
2021-04-06 19:26:50 +08:00
|
|
|
agreedUrls: string[],
|
|
|
|
extraClassNames?: string,
|
|
|
|
) => Promise<string[]>;
|
|
|
|
|
2019-07-10 23:07:31 +08:00
|
|
|
/**
|
2019-07-10 21:22:50 +08:00
|
|
|
* Start a flow where the user is presented with terms & conditions for some services
|
|
|
|
*
|
2019-07-11 17:53:45 +08:00
|
|
|
* @param {Service[]} services Object with keys 'serviceType', 'baseUrl', 'accessToken'
|
|
|
|
* @param {function} interactionCallback Function called with:
|
2019-07-22 19:23:28 +08:00
|
|
|
* * an array of { service: {Service}, policies: {terms response from API} }
|
2019-07-11 17:53:45 +08:00
|
|
|
* * an array of URLs the user has already agreed to
|
2019-07-10 21:22:50 +08: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.
|
|
|
|
*/
|
2019-07-23 22:11:38 +08:00
|
|
|
export async function startTermsFlow(
|
2021-04-06 19:26:50 +08:00
|
|
|
services: Service[],
|
|
|
|
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
|
2023-01-12 21:25:14 +08:00
|
|
|
): Promise<void> {
|
2019-07-11 23:28:24 +08:00
|
|
|
const termsPromises = services.map((s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl));
|
2019-07-10 17:50:10 +08:00
|
|
|
|
2019-07-10 21:25:30 +08: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 19:26:50 +08:00
|
|
|
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
|
2019-07-10 21:27:29 +08:00
|
|
|
const policiesAndServicePairs = terms.map((t, i) => {
|
|
|
|
return { service: services[i], policies: t.policies };
|
|
|
|
});
|
2019-07-10 17:50:10 +08:00
|
|
|
|
2019-07-11 17:53:45 +08:00
|
|
|
// fetch the set of agreed policy URLs from account data
|
|
|
|
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData("m.accepted_terms");
|
2021-06-02 12:21:04 +08:00
|
|
|
let agreedUrlSet: Set<string>;
|
2019-07-11 17:53:45 +08: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 19:39:16 +08:00
|
|
|
const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = [];
|
2021-06-29 20:11:58 +08:00
|
|
|
for (const { service, policies } of policiesAndServicePairs) {
|
2023-02-13 19:39:16 +08:00
|
|
|
const unagreedPolicies: Policies = {};
|
2019-07-11 17:53:45 +08: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 20:11:58 +08:00
|
|
|
unagreedPoliciesAndServicePairs.push({ service, policies: unagreedPolicies });
|
2019-07-11 17:53:45 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if there's anything left to agree to, prompt the user
|
2019-09-11 01:16:45 +08:00
|
|
|
const numAcceptedBeforeAgreement = agreedUrlSet.size;
|
2019-07-11 17:53:45 +08:00
|
|
|
if (unagreedPoliciesAndServicePairs.length > 0) {
|
2019-07-22 19:23:55 +08:00
|
|
|
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
|
2021-09-21 23:48:09 +08:00
|
|
|
logger.log("User has agreed to URLs", newlyAgreedUrls);
|
2019-09-05 20:38:32 +08:00
|
|
|
// Merge with previously agreed URLs
|
|
|
|
newlyAgreedUrls.forEach((url) => agreedUrlSet.add(url));
|
2019-07-11 17:53:45 +08:00
|
|
|
} else {
|
2021-09-21 23:48:09 +08:00
|
|
|
logger.log("User has already agreed to all required policies");
|
2019-07-11 17:53:45 +08:00
|
|
|
}
|
|
|
|
|
2019-09-11 01:13:35 +08: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 20:11:58 +08:00
|
|
|
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
|
2019-09-11 01:13:35 +08:00
|
|
|
await MatrixClientPeg.get().setAccountData("m.accepted_terms", newAcceptedTerms);
|
|
|
|
}
|
2019-07-10 17:50:10 +08:00
|
|
|
|
2019-07-10 22:12:05 +08:00
|
|
|
const agreePromises = policiesAndServicePairs.map((policiesAndService) => {
|
2019-07-10 17:50:10 +08: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 17:53:45 +08:00
|
|
|
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
|
2019-07-10 22:12:05 +08:00
|
|
|
for (const policy of Object.values(policiesAndService.policies)) {
|
|
|
|
for (const lang of Object.keys(policy)) {
|
2019-07-10 17:50:10 +08:00
|
|
|
if (lang === "version") continue;
|
2019-07-10 22:12:05 +08:00
|
|
|
if (policy[lang].url === url) return true;
|
2019-07-10 17:50:10 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (urlsForService.length === 0) return Promise.resolve();
|
|
|
|
|
|
|
|
return MatrixClientPeg.get().agreeToTerms(
|
2019-07-10 22:12:05 +08:00
|
|
|
policiesAndService.service.serviceType,
|
|
|
|
policiesAndService.service.baseUrl,
|
|
|
|
policiesAndService.service.accessToken,
|
2019-07-10 17:50:10 +08:00
|
|
|
urlsForService,
|
|
|
|
);
|
|
|
|
});
|
2023-01-12 21:25:14 +08:00
|
|
|
await Promise.all(agreePromises);
|
2019-07-10 17:50:10 +08:00
|
|
|
}
|
|
|
|
|
2021-10-25 19:37:59 +08:00
|
|
|
export async function dialogTermsInteractionCallback(
|
2021-04-06 19:26:50 +08:00
|
|
|
policiesAndServicePairs: {
|
2021-07-02 06:23:03 +08:00
|
|
|
service: Service;
|
|
|
|
policies: { [policy: string]: Policy };
|
2021-04-06 19:26:50 +08:00
|
|
|
}[],
|
|
|
|
agreedUrls: string[],
|
|
|
|
extraClassNames?: string,
|
|
|
|
): Promise<string[]> {
|
2021-10-25 19:37:59 +08:00
|
|
|
logger.log("Terms that need agreement", policiesAndServicePairs);
|
|
|
|
|
2023-02-28 18:31:48 +08:00
|
|
|
const { finished } = Modal.createDialog(
|
2022-06-15 00:51:51 +08:00
|
|
|
TermsDialog,
|
|
|
|
{
|
2021-10-25 19:37:59 +08:00
|
|
|
policiesAndServicePairs,
|
|
|
|
agreedUrls,
|
|
|
|
},
|
|
|
|
classNames("mx_TermsDialog", extraClassNames),
|
|
|
|
);
|
|
|
|
|
|
|
|
const [done, _agreedUrls] = await finished;
|
2023-03-07 21:19:18 +08:00
|
|
|
if (!done || !_agreedUrls) {
|
2021-10-25 19:37:59 +08:00
|
|
|
throw new TermsNotSignedError();
|
|
|
|
}
|
|
|
|
return _agreedUrls;
|
2019-07-10 17:50:10 +08:00
|
|
|
}
|