mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 17:25:50 +03:00
Merge pull request #3199 from matrix-org/dbkr/terms
ToS for ISes/IMs: prompt on use screen
This commit is contained in:
commit
2eb8a8879b
16 changed files with 876 additions and 90 deletions
|
@ -70,6 +70,7 @@
|
|||
@import "./views/dialogs/_SetPasswordDialog.scss";
|
||||
@import "./views/dialogs/_SettingsDialog.scss";
|
||||
@import "./views/dialogs/_ShareDialog.scss";
|
||||
@import "./views/dialogs/_TermsDialog.scss";
|
||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
||||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
|
|
35
res/css/views/dialogs/_TermsDialog.scss
Normal file
35
res/css/views/dialogs/_TermsDialog.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2019 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.
|
||||
*/
|
||||
|
||||
.mx_TermsDialog_termsTableHeader {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mx_TermsDialog_termsTable {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mx_TermsDialog_service, .mx_TermsDialog_summary {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.mx_TermsDialog_link {
|
||||
mask-image: url('$(res)/img/external-link.svg');
|
||||
background-color: $accent-color;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
Copyright 2019 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.
|
||||
|
@ -19,10 +20,9 @@ import URL from 'url';
|
|||
import dis from './dispatcher';
|
||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import sdk from "./index";
|
||||
import Modal from "./Modal";
|
||||
import MatrixClientPeg from "./MatrixClientPeg";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { showIntegrationsManager } from './integrations/integrations';
|
||||
|
||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
|
@ -193,13 +193,11 @@ export default class FromWidgetPostMessageApi {
|
|||
const integType = (data && data.integType) ? data.integType : null;
|
||||
const integId = (data && data.integId) ? data.integId : null;
|
||||
|
||||
// The dialog will take care of scalar auth for us
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
showIntegrationsManager({
|
||||
room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||
screen: 'type_' + integType,
|
||||
integrationId: integId,
|
||||
}, "mx_IntegrationsManager");
|
||||
});
|
||||
} else if (action === 'set_always_on_screen') {
|
||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
||||
const data = event.data.data;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -14,13 +15,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import url from 'url';
|
||||
import Promise from 'bluebird';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { Service, presentTermsForServices, TermsNotSignedError } from './Terms';
|
||||
const request = require('browser-request');
|
||||
|
||||
const SdkConfig = require('./SdkConfig');
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
|
||||
// The version of the integration manager API we're intending to work with
|
||||
const imApiVersion = "1.1";
|
||||
|
||||
|
@ -47,7 +52,7 @@ class ScalarAuthClient {
|
|||
return this.scalarToken != null; // undef or null
|
||||
}
|
||||
|
||||
// Returns a scalar_token string
|
||||
// Returns a promise that resolves to a scalar_token string
|
||||
getScalarToken() {
|
||||
let token = this.scalarToken;
|
||||
if (!token) token = window.localStorage.getItem("mx_scalar_token");
|
||||
|
@ -55,23 +60,17 @@ class ScalarAuthClient {
|
|||
if (!token) {
|
||||
return this.registerForToken();
|
||||
} else {
|
||||
return this.validateToken(token).then(userId => {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
if (userId !== me) {
|
||||
throw new Error("Scalar token is owned by someone else: " + me);
|
||||
return this._checkToken(token).catch((e) => {
|
||||
if (e instanceof TermsNotSignedError) {
|
||||
// retrying won't help this
|
||||
throw e;
|
||||
}
|
||||
return token;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
|
||||
// Something went wrong - try to get a new token.
|
||||
console.warn("Registering for new scalar token");
|
||||
return this.registerForToken();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateToken(token) {
|
||||
_getAccountName(token) {
|
||||
const url = SdkConfig.get().integrations_rest_url + "/account";
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
@ -83,8 +82,10 @@ class ScalarAuthClient {
|
|||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') {
|
||||
reject(new TermsNotSignedError());
|
||||
} else if (response.statusCode / 100 !== 2) {
|
||||
reject({statusCode: response.statusCode});
|
||||
reject(body);
|
||||
} else if (!body || !body.user_id) {
|
||||
reject(new Error("Missing user_id in response"));
|
||||
} else {
|
||||
|
@ -94,11 +95,54 @@ class ScalarAuthClient {
|
|||
});
|
||||
}
|
||||
|
||||
_checkToken(token) {
|
||||
return this._getAccountName(token).then(userId => {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
if (userId !== me) {
|
||||
throw new Error("Scalar token is owned by someone else: " + me);
|
||||
}
|
||||
return token;
|
||||
}).catch((e) => {
|
||||
if (e instanceof TermsNotSignedError) {
|
||||
console.log("Integrations manager requires new terms to be agreed to");
|
||||
// The terms endpoints are new and so live on standard _matrix prefixes,
|
||||
// but IM rest urls are currently configured with paths, so remove the
|
||||
// path from the base URL before passing it to the js-sdk
|
||||
|
||||
// We continue to use the full URL for the calls done by
|
||||
// matrix-react-sdk, but the standard terms API called
|
||||
// by the js-sdk lives on the standard _matrix path. This means we
|
||||
// don't support running IMs on a non-root path, but it's the only
|
||||
// realistic way of transitioning to _matrix paths since configs in
|
||||
// the wild contain bits of the API path.
|
||||
|
||||
// Once we've fully transitioned to _matrix URLs, we can give people
|
||||
// a grace period to update their configs, then use the rest url as
|
||||
// a regular base url.
|
||||
const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url);
|
||||
parsedImRestUrl.path = '';
|
||||
parsedImRestUrl.pathname = '';
|
||||
return presentTermsForServices([new Service(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
parsedImRestUrl.format(),
|
||||
token,
|
||||
)]).then(() => {
|
||||
return token;
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerForToken() {
|
||||
// Get openid bearer token from the HS as the first part of our dance
|
||||
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
|
||||
// Now we can send that to scalar and exchange it for a scalar token
|
||||
return this.exchangeForScalarToken(tokenObject);
|
||||
}).then((tokenObject) => {
|
||||
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
|
||||
return this._checkToken(tokenObject);
|
||||
}).then((tokenObject) => {
|
||||
window.localStorage.setItem("mx_scalar_token", tokenObject);
|
||||
return tokenObject;
|
||||
|
|
180
src/Terms.js
Normal file
180
src/Terms.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Copyright 2019 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 Promise from 'bluebird';
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import sdk from './';
|
||||
import Modal from './Modal';
|
||||
|
||||
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(serviceType, baseUrl, accessToken) {
|
||||
this.serviceType = serviceType;
|
||||
this.baseUrl = baseUrl;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present a popup to the user prompting them to agree to terms and conditions
|
||||
*
|
||||
* @param {Service[]} services Object with keys 'serviceType', 'baseUrl', 'accessToken'
|
||||
* @returns {Promise} resolves when the user agreed to all necessary terms or rejects
|
||||
* if they cancel.
|
||||
*/
|
||||
export function presentTermsForServices(services) {
|
||||
return startTermsFlow(services, dialogTermsInteractionCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, interactionCallback) {
|
||||
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 = 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;
|
||||
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
|
||||
if (unagreedPoliciesAndServicePairs.length > 0) {
|
||||
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
|
||||
console.log("User has agreed to URLs", newlyAgreedUrls);
|
||||
agreedUrlSet = new Set(newlyAgreedUrls);
|
||||
} else {
|
||||
console.log("User has already agreed to all required policies");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function dialogTermsInteractionCallback(policiesAndServicePairs, agreedUrls) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("Terms that need agreement", policiesAndServicePairs);
|
||||
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
||||
|
||||
Modal.createTrackedDialog('Terms of Service', '', TermsDialog, {
|
||||
policiesAndServicePairs,
|
||||
agreedUrls,
|
||||
onFinished: (done, agreedUrls) => {
|
||||
if (!done) {
|
||||
reject(new TermsNotSignedError());
|
||||
return;
|
||||
}
|
||||
resolve(agreedUrls);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
209
src/components/views/dialogs/TermsDialog.js
Normal file
209
src/components/views/dialogs/TermsDialog.js
Normal file
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
Copyright 2019 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 url from 'url';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
class TermsCheckbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.props.onChange(this.props.url, ev.target.checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input type="checkbox"
|
||||
onChange={this.onChange}
|
||||
checked={this.props.checked}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TermsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: PropTypes.array.isRequired,
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state = {
|
||||
// url -> boolean
|
||||
agreedUrls: {},
|
||||
};
|
||||
for (const url of props.agreedUrls) {
|
||||
this.state.agreedUrls[url] = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onNextClick = () => {
|
||||
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
||||
}
|
||||
|
||||
_nameForServiceType(serviceType, host) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
return <div>{_t("Integrations Manager")}<br />({host})</div>;
|
||||
}
|
||||
}
|
||||
|
||||
_summaryForServiceType(serviceType, docName) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
{_t("Find others by phone or email")}
|
||||
<br />
|
||||
{_t("Be found by phone or email")}
|
||||
{docName !== null ? <br /> : ''}
|
||||
{docName !== null ? '('+docName+')' : ''}
|
||||
</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
return <div>
|
||||
{_t("Use bots, bridges, widgets and sticker packs")}
|
||||
{docName !== null ? <br /> : ''}
|
||||
{docName !== null ? '('+docName+')' : ''}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
_onTermsCheckboxChange = (url, checked) => {
|
||||
this.setState({
|
||||
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const rows = [];
|
||||
for (const policiesAndService of this.props.policiesAndServicePairs) {
|
||||
const parsedBaseUrl = url.parse(policiesAndService.service.baseUrl);
|
||||
|
||||
const policyValues = Object.values(policiesAndService.policies);
|
||||
for (let i = 0; i < policyValues.length; ++i) {
|
||||
const termDoc = policyValues[i];
|
||||
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== 'version'));
|
||||
let serviceName;
|
||||
if (i === 0) {
|
||||
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
}
|
||||
const summary = this._summaryForServiceType(
|
||||
policiesAndService.service.serviceType,
|
||||
policyValues.length > 1 ? termDoc[termsLang].name : null,
|
||||
);
|
||||
|
||||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td><a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<div className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td><TermsCheckbox
|
||||
url={termDoc[termsLang].url}
|
||||
onChange={this._onTermsCheckboxChange}
|
||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||
/></td>
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
|
||||
// if all the documents for at least one service have been checked, we can enable
|
||||
// the submit button
|
||||
let enableSubmit = false;
|
||||
for (const policiesAndService of this.props.policiesAndServicePairs) {
|
||||
let docsAgreedForService = 0;
|
||||
for (const terms of Object.values(policiesAndService.policies)) {
|
||||
let docAgreed = false;
|
||||
for (const lang of Object.keys(terms)) {
|
||||
if (lang === 'version') continue;
|
||||
if (this.state.agreedUrls[terms[lang].url]) {
|
||||
docAgreed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (docAgreed) {
|
||||
++docsAgreedForService;
|
||||
}
|
||||
}
|
||||
if (docsAgreedForService === Object.keys(policiesAndService.policies).length) {
|
||||
enableSubmit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_TermsDialog'
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
title={_t("Terms of Service")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{_t("To continue you need to accept the Terms of this service.")}</p>
|
||||
|
||||
<table className="mx_TermsDialog_termsTable"><tbody>
|
||||
<tr className="mx_TermsDialog_termsTableHeader">
|
||||
<th>{_t("Service")}</th>
|
||||
<th>{_t("Summary")}</th>
|
||||
<th>{_t("Terms")}</th>
|
||||
<th>{_t("Accept")}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this._onNextClick}
|
||||
primaryDisabled={!enableSubmit}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import dis from '../../../dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
import { showIntegrationsManager } from '../../../integrations/integrations';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -250,13 +251,11 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
// The dialog handles scalar auth for us
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
showIntegrationsManager({
|
||||
room: this.props.room,
|
||||
screen: 'type_' + this.props.type,
|
||||
integrationId: this.props.id,
|
||||
}, "mx_IntegrationsManager");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { showIntegrationsManager } from '../../../integrations/integrations';
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -30,10 +30,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
onManageIntegrations = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
room: this.props.room,
|
||||
}, "mx_IntegrationsManager");
|
||||
showIntegrationsManager({ room: this.props.room });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import { _t } from '../../../languageHandler';
|
|||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { showIntegrationsManager } from '../../../integrations/integrations';
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -127,11 +128,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
showIntegrationsManager({
|
||||
room: this.props.room,
|
||||
screen: 'add_integ',
|
||||
}, 'mx_IntegrationsManager');
|
||||
});
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
|
|
@ -17,7 +17,6 @@ import React from 'react';
|
|||
import {_t, _td} from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -25,6 +24,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import { showIntegrationsManager } from '../../../integrations/integrations';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
|
@ -348,14 +348,11 @@ export default class Stickerpicker extends React.Component {
|
|||
* Launch the integrations manager on the stickers integration page
|
||||
*/
|
||||
_launchManageIntegrations() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
|
||||
// The integrations manager will handle scalar auth for us.
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
showIntegrationsManager({
|
||||
room: this.props.room,
|
||||
screen: `type_${widgetType}`,
|
||||
integrationId: this.state.widgetId,
|
||||
}, "mx_IntegrationsManager");
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -20,64 +20,29 @@ import PropTypes from 'prop-types';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
|
||||
export default class IntegrationsManager extends React.Component {
|
||||
static propTypes = {
|
||||
// the room object where the integrations manager should be opened in
|
||||
room: PropTypes.object.isRequired,
|
||||
// false to display an error saying that there is no integrations manager configured
|
||||
configured: PropTypes.bool.isRequired,
|
||||
|
||||
// the screen name to open
|
||||
screen: PropTypes.string,
|
||||
// false to display an error saying that we couldn't connect to the integrations manager
|
||||
connected: PropTypes.bool.isRequired,
|
||||
|
||||
// the integration ID to open
|
||||
integrationId: PropTypes.string,
|
||||
// true to display a loading spinner
|
||||
loading: PropTypes.bool.isRequired,
|
||||
|
||||
// The source URL to load
|
||||
url: PropTypes.string,
|
||||
|
||||
// callback when the manager is dismissed
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
configured: ScalarAuthClient.isPossible(),
|
||||
connected: false, // true if a `src` is set and able to be connected to
|
||||
src: null, // string for where to connect to
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.state.configured) return;
|
||||
|
||||
const scalarClient = new ScalarAuthClient();
|
||||
scalarClient.connect().then(() => {
|
||||
const hasCredentials = scalarClient.hasCredentials();
|
||||
if (!hasCredentials) {
|
||||
this.setState({
|
||||
connected: false,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
const src = scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
this.props.screen,
|
||||
this.props.integrationId,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
connected: true,
|
||||
src: src,
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
loading: false,
|
||||
connected: false,
|
||||
});
|
||||
});
|
||||
static defaultProps = {
|
||||
configured: true,
|
||||
connected: true,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -105,7 +70,7 @@ export default class IntegrationsManager extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.configured) {
|
||||
if (!this.props.configured) {
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_error'>
|
||||
<h3>{_t("No integrations server configured")}</h3>
|
||||
|
@ -114,7 +79,7 @@ export default class IntegrationsManager extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
if (this.props.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_loading'>
|
||||
|
@ -124,7 +89,7 @@ export default class IntegrationsManager extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (!this.state.connected) {
|
||||
if (!this.props.connected) {
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_error'>
|
||||
<h3>{_t("Cannot connect to integrations server")}</h3>
|
||||
|
@ -133,6 +98,6 @@ export default class IntegrationsManager extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
return <iframe src={this.state.src}></iframe>;
|
||||
return <iframe src={this.props.url}></iframe>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1265,6 +1265,17 @@
|
|||
"Missing session data": "Missing session data",
|
||||
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
||||
"Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
|
||||
"Identity Server": "Identity Server",
|
||||
"Integrations Manager": "Integrations Manager",
|
||||
"Find others by phone or email": "Find others by phone or email",
|
||||
"Be found by phone or email": "Be found by phone or email",
|
||||
"Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs",
|
||||
"Terms of Service": "Terms of Service",
|
||||
"To continue you need to accept the Terms of this service.": "To continue you need to accept the Terms of this service.",
|
||||
"Service": "Service",
|
||||
"Summary": "Summary",
|
||||
"Terms": "Terms",
|
||||
"Next": "Next",
|
||||
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.",
|
||||
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.",
|
||||
"Room contains unknown devices": "Room contains unknown devices",
|
||||
|
@ -1298,7 +1309,6 @@
|
|||
"Enter Recovery Passphrase": "Enter Recovery Passphrase",
|
||||
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.",
|
||||
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
|
||||
"Next": "Next",
|
||||
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
|
||||
"Enter Recovery Key": "Enter Recovery Key",
|
||||
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
|
||||
|
|
65
src/integrations/integrations.js
Normal file
65
src/integrations/integrations.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from "../index";
|
||||
import ScalarAuthClient from '../ScalarAuthClient';
|
||||
import Modal from '../Modal';
|
||||
import { TermsNotSignedError } from '../Terms';
|
||||
|
||||
export async function showIntegrationsManager(opts) {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
|
||||
let props = {};
|
||||
if (ScalarAuthClient.isPossible()) {
|
||||
props.loading = true;
|
||||
} else {
|
||||
props.configured = false;
|
||||
}
|
||||
|
||||
const close = Modal.createTrackedDialog(
|
||||
'Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager",
|
||||
).close;
|
||||
|
||||
if (!ScalarAuthClient.isPossible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scalarClient = new ScalarAuthClient();
|
||||
try {
|
||||
await scalarClient.connect();
|
||||
if (!scalarClient.hasCredentials()) {
|
||||
props = { connected: false };
|
||||
} else {
|
||||
props = {
|
||||
url: scalarClient.getScalarInterfaceUrlForRoom(
|
||||
opts.room,
|
||||
opts.screen,
|
||||
opts.integrationId,
|
||||
),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof TermsNotSignedError) {
|
||||
// user canceled terms dialog, so just cancel the action
|
||||
close();
|
||||
return;
|
||||
}
|
||||
console.error(err);
|
||||
props = { connected: false };
|
||||
}
|
||||
close();
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager");
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 MTRNord and Cooperative EITA
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2019 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.
|
||||
|
@ -353,6 +354,40 @@ export function getCurrentLanguage() {
|
|||
return counterpart.getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of language codes, pick the most appropriate one
|
||||
* given the current language (ie. getCurrentLanguage())
|
||||
* English is assumed to be a reasonable default.
|
||||
*
|
||||
* @param {string[]} langs List of language codes to pick from
|
||||
* @returns {string} The most appropriate language code from langs
|
||||
*/
|
||||
export function pickBestLanguage(langs) {
|
||||
const currentLang = getCurrentLanguage();
|
||||
const normalisedLangs = langs.map(normalizeLanguageKey);
|
||||
|
||||
{
|
||||
// Best is an exact match
|
||||
const currentLangIndex = normalisedLangs.indexOf(currentLang);
|
||||
if (currentLangIndex > -1) return langs[currentLangIndex];
|
||||
}
|
||||
|
||||
{
|
||||
// Failing that, a different dialect of the same lnguage
|
||||
const closeLangIndex = normalisedLangs.find((l) => l.substr(0,2) === currentLang.substr(0,2));
|
||||
if (closeLangIndex > -1) return langs[closeLangIndex];
|
||||
}
|
||||
|
||||
{
|
||||
// Neither of those? Try an english variant.
|
||||
const enIndex = normalisedLangs.find((l) => l.startsWith('en'));
|
||||
if (enIndex > -1) return langs[enIndex];
|
||||
}
|
||||
|
||||
// if nothing else, use the first
|
||||
return langs[0];
|
||||
}
|
||||
|
||||
function getLangsJson() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let url;
|
||||
|
|
56
test/ScalarAuthClient-test.js
Normal file
56
test/ScalarAuthClient-test.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2019 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 expect from 'expect';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import ScalarAuthClient from '../src/ScalarAuthClient';
|
||||
import MatrixClientPeg from '../src/MatrixClientPeg';
|
||||
import { stubClient } from './test-utils';
|
||||
|
||||
describe('ScalarAuthClient', function() {
|
||||
let clientSandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(window.localStorage, 'getItem').withArgs('mx_scalar_token').returns('brokentoken');
|
||||
clientSandbox = stubClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clientSandbox.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should request a new token if the old one fails', async function() {
|
||||
const sac = new ScalarAuthClient();
|
||||
|
||||
sac._getAccountName = sinon.stub();
|
||||
sac._getAccountName.withArgs('brokentoken').rejects({
|
||||
message: "Invalid token",
|
||||
});
|
||||
sac._getAccountName.withArgs('wokentoken').resolves(MatrixClientPeg.get().getUserId());
|
||||
|
||||
MatrixClientPeg.get().getOpenIdToken = sinon.stub().resolves('this is your openid token');
|
||||
|
||||
sac.exchangeForScalarToken = sinon.stub().withArgs('this is your openid token').resolves('wokentoken');
|
||||
|
||||
await sac.connect();
|
||||
|
||||
expect(sac.exchangeForScalarToken.calledWith('this is your openid token')).toBeTruthy();
|
||||
expect(sac.scalarToken).toEqual('wokentoken');
|
||||
});
|
||||
});
|
195
test/Terms-test.js
Normal file
195
test/Terms-test.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright 2019 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 expect from 'expect';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
|
||||
import { startTermsFlow, Service } from '../src/Terms';
|
||||
import { stubClient } from './test-utils';
|
||||
import MatrixClientPeg from '../src/MatrixClientPeg';
|
||||
|
||||
const POLICY_ONE = {
|
||||
version: "six",
|
||||
en: {
|
||||
name: "The first policy",
|
||||
url: "http://example.com/one",
|
||||
},
|
||||
};
|
||||
|
||||
const POLICY_TWO = {
|
||||
version: "IX",
|
||||
en: {
|
||||
name: "The second policy",
|
||||
url: "http://example.com/two",
|
||||
},
|
||||
};
|
||||
|
||||
const IM_SERVICE_ONE = new Service(Matrix.SERVICE_TYPES.IM, 'https://imone.test', 'a token token');
|
||||
const IM_SERVICE_TWO = new Service(Matrix.SERVICE_TYPES.IM, 'https://imtwo.test', 'a token token');
|
||||
|
||||
describe('Terms', function() {
|
||||
let sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = stubClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should prompt for all terms & services if no account data', async function() {
|
||||
MatrixClientPeg.get().getAccountData = sinon.stub().returns(null);
|
||||
MatrixClientPeg.get().getTerms = sinon.stub().returns({
|
||||
policies: {
|
||||
"policy_the_first": POLICY_ONE,
|
||||
},
|
||||
});
|
||||
const interactionCallback = sinon.stub().resolves([]);
|
||||
await startTermsFlow([IM_SERVICE_ONE], interactionCallback);
|
||||
console.log("interaction callback calls", interactionCallback.getCall(0));
|
||||
|
||||
expect(interactionCallback.calledWith([
|
||||
{
|
||||
service: IM_SERVICE_ONE,
|
||||
policies: {
|
||||
policy_the_first: POLICY_ONE,
|
||||
},
|
||||
},
|
||||
])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not prompt if all policies are signed in account data', async function() {
|
||||
MatrixClientPeg.get().getAccountData = sinon.stub().returns({
|
||||
getContent: sinon.stub().returns({
|
||||
accepted: ["http://example.com/one"],
|
||||
}),
|
||||
});
|
||||
MatrixClientPeg.get().getTerms = sinon.stub().returns({
|
||||
policies: {
|
||||
"policy_the_first": POLICY_ONE,
|
||||
},
|
||||
});
|
||||
MatrixClientPeg.get().agreeToTerms = sinon.stub();
|
||||
|
||||
const interactionCallback = sinon.spy();
|
||||
await startTermsFlow([IM_SERVICE_ONE], interactionCallback);
|
||||
console.log("agreeToTerms call", MatrixClientPeg.get().agreeToTerms.getCall(0).args);
|
||||
|
||||
expect(interactionCallback.called).toBeFalsy();
|
||||
expect(MatrixClientPeg.get().agreeToTerms.calledWith(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
'https://imone.test',
|
||||
'a token token',
|
||||
["http://example.com/one"],
|
||||
)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should prompt for only terms that aren't already signed", async function() {
|
||||
MatrixClientPeg.get().getAccountData = sinon.stub().returns({
|
||||
getContent: sinon.stub().returns({
|
||||
accepted: ["http://example.com/one"],
|
||||
}),
|
||||
});
|
||||
MatrixClientPeg.get().getTerms = sinon.stub().returns({
|
||||
policies: {
|
||||
"policy_the_first": POLICY_ONE,
|
||||
"policy_the_second": POLICY_TWO,
|
||||
},
|
||||
});
|
||||
MatrixClientPeg.get().agreeToTerms = sinon.stub();
|
||||
|
||||
const interactionCallback = sinon.stub().resolves(["http://example.com/one", "http://example.com/two"]);
|
||||
await startTermsFlow([IM_SERVICE_ONE], interactionCallback);
|
||||
console.log("interactionCallback call", interactionCallback.getCall(0).args);
|
||||
console.log("agreeToTerms call", MatrixClientPeg.get().agreeToTerms.getCall(0).args);
|
||||
|
||||
expect(interactionCallback.calledWith([
|
||||
{
|
||||
service: IM_SERVICE_ONE,
|
||||
policies: {
|
||||
policy_the_second: POLICY_TWO,
|
||||
},
|
||||
},
|
||||
])).toBeTruthy();
|
||||
expect(MatrixClientPeg.get().agreeToTerms.calledWith(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
'https://imone.test',
|
||||
'a token token',
|
||||
["http://example.com/one", "http://example.com/two"],
|
||||
)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should prompt for only services with un-agreed policies", async function() {
|
||||
MatrixClientPeg.get().getAccountData = sinon.stub().returns({
|
||||
getContent: sinon.stub().returns({
|
||||
accepted: ["http://example.com/one"],
|
||||
}),
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().getTerms = sinon.stub();
|
||||
MatrixClientPeg.get().getTerms.callsFake((serviceType, baseUrl, accessToken) => {
|
||||
switch (baseUrl) {
|
||||
case 'https://imone.test':
|
||||
return {
|
||||
policies: {
|
||||
"policy_the_first": POLICY_ONE,
|
||||
},
|
||||
};
|
||||
case 'https://imtwo.test':
|
||||
return {
|
||||
policies: {
|
||||
"policy_the_second": POLICY_TWO,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().agreeToTerms = sinon.stub();
|
||||
|
||||
const interactionCallback = sinon.stub().resolves(["http://example.com/one", "http://example.com/two"]);
|
||||
await startTermsFlow([IM_SERVICE_ONE, IM_SERVICE_TWO], interactionCallback);
|
||||
console.log("getTerms call 0", MatrixClientPeg.get().getTerms.getCall(0).args);
|
||||
console.log("getTerms call 1", MatrixClientPeg.get().getTerms.getCall(1).args);
|
||||
console.log("interactionCallback call", interactionCallback.getCall(0).args);
|
||||
console.log("agreeToTerms call", MatrixClientPeg.get().agreeToTerms.getCall(0).args);
|
||||
|
||||
expect(interactionCallback.calledWith([
|
||||
{
|
||||
service: IM_SERVICE_TWO,
|
||||
policies: {
|
||||
policy_the_second: POLICY_TWO,
|
||||
},
|
||||
},
|
||||
])).toBeTruthy();
|
||||
expect(MatrixClientPeg.get().agreeToTerms.calledWith(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
'https://imone.test',
|
||||
'a token token',
|
||||
["http://example.com/one"],
|
||||
)).toBeTruthy();
|
||||
expect(MatrixClientPeg.get().agreeToTerms.calledWith(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
'https://imtwo.test',
|
||||
'a token token',
|
||||
["http://example.com/two"],
|
||||
)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in a new issue