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
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>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue