This repository was archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 823
/
Copy pathTerms.ts
206 lines (181 loc) · 7.77 KB
/
Terms.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*
Copyright 2019, 2021 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 classNames from 'classnames';
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { MatrixClientPeg } from './MatrixClientPeg';
import * as sdk from '.';
import Modal from './Modal';
import { logger } from "matrix-js-sdk/src/logger";
export class TermsNotSignedError extends Error {}
/**
* Class representing a service that may have terms & conditions that
* require agreement from the user before the user can use that service.
*/
export class Service {
/**
* @param {MatrixClient.SERVICE_TYPES} serviceType The type of service
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service
*/
constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {
}
}
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 TermsInteractionCallback = (
policiesAndServicePairs: {
service: Service;
policies: Policies;
}[],
agreedUrls: string[],
extraClassNames?: string,
) => Promise<string[]>;
/**
* Start a flow where the user is presented with terms & conditions for some services
*
* @param {Service[]} services Object with keys 'serviceType', 'baseUrl', 'accessToken'
* @param {function} interactionCallback Function called with:
* * an array of { service: {Service}, policies: {terms response from API} }
* * an array of URLs the user has already agreed to
* 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(
services: Service[],
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
) {
const termsPromises = services.map(
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
);
/*
* 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"
* }
* }
* }
* }
*/
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; });
// fetch the set of agreed policy URLs from account data
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
let agreedUrlSet: Set<string>;
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.
const unagreedPoliciesAndServicePairs = [];
for (const { service, policies } of policiesAndServicePairs) {
const unagreedPolicies = {};
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) {
unagreedPoliciesAndServicePairs.push({ service, policies: unagreedPolicies });
}
}
// if there's anything left to agree to, prompt the user
const numAcceptedBeforeAgreement = agreedUrlSet.size;
if (unagreedPoliciesAndServicePairs.length > 0) {
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));
} else {
logger.log("User has already agreed to all required policies");
}
// 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) {
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
}
const agreePromises = policiesAndServicePairs.map((policiesAndService) => {
// 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
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
for (const policy of Object.values(policiesAndService.policies)) {
for (const lang of Object.keys(policy)) {
if (lang === 'version') continue;
if (policy[lang].url === url) return true;
}
}
return false;
});
if (urlsForService.length === 0) return Promise.resolve();
return MatrixClientPeg.get().agreeToTerms(
policiesAndService.service.serviceType,
policiesAndService.service.baseUrl,
policiesAndService.service.accessToken,
urlsForService,
);
});
return Promise.all(agreePromises);
}
export async function dialogTermsInteractionCallback(
policiesAndServicePairs: {
service: Service;
policies: { [policy: string]: Policy };
}[],
agreedUrls: string[],
extraClassNames?: string,
): Promise<string[]> {
logger.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
const { finished } = Modal.createTrackedDialog<[boolean, string[]]>('Terms of Service', '', TermsDialog, {
policiesAndServicePairs,
agreedUrls,
}, classNames("mx_TermsDialog", extraClassNames));
const [done, _agreedUrls] = await finished;
if (!done) {
throw new TermsNotSignedError();
}
return _agreedUrls;
}