Prompt for terms of service on integration manager changes

Part of https://github.com/vector-im/riot-web/issues/10539
This commit is contained in:
Travis Ralston 2019-08-15 13:28:23 -06:00
parent ded2297523
commit 27504e1578
5 changed files with 122 additions and 37 deletions

View file

@ -46,6 +46,8 @@ export default class Field extends React.PureComponent {
// and a `feedback` react component field to provide feedback // and a `feedback` react component field to provide feedback
// to the user. // to the user.
onValidate: PropTypes.func, onValidate: PropTypes.func,
// If specified, overrides the value returned by onValidate.
flagInvalid: PropTypes.bool,
// If specified, contents will appear as a tooltip on the element and // If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed. // validation feedback tooltips will be suppressed.
tooltipContent: PropTypes.node, tooltipContent: PropTypes.node,
@ -137,7 +139,10 @@ export default class Field extends React.PureComponent {
}, VALIDATION_THROTTLE_MS); }, VALIDATION_THROTTLE_MS);
render() { render() {
const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props; const {
element, prefix, onValidate, children, tooltipContent,
flagInvalid, ...inputProps,
} = this.props;
const inputElement = element || "input"; const inputElement = element || "input";
@ -157,13 +162,16 @@ export default class Field extends React.PureComponent {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>; prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
} }
const hasValidationFlag = flagInvalid != null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, { const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
// If we have a prefix element, leave the label always at the top left and // If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do // don't animate it, as it looks a bit clunky and would add complexity to do
// properly. // properly.
mx_Field_labelAlwaysTopLeft: prefix, mx_Field_labelAlwaysTopLeft: prefix,
mx_Field_valid: onValidate && this.state.valid === true, mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: onValidate && this.state.valid === false, mx_Field_invalid: hasValidationFlag
? flagInvalid
: onValidate && this.state.valid === false,
}); });
// Handle displaying feedback on validity // Handle displaying feedback on validity

View file

@ -19,6 +19,10 @@ import {_t} from "../../../languageHandler";
import sdk from '../../../index'; import sdk from '../../../index';
import Field from "../elements/Field"; import Field from "../elements/Field";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance";
import Modal from "../../../Modal";
export default class SetIntegrationManager extends React.Component { export default class SetIntegrationManager extends React.Component {
constructor() { constructor() {
@ -31,6 +35,7 @@ export default class SetIntegrationManager extends React.Component {
url: "", // user-entered text url: "", // user-entered text
error: null, error: null,
busy: false, busy: false,
checking: false,
}; };
} }
@ -40,14 +45,14 @@ export default class SetIntegrationManager extends React.Component {
}; };
_getTooltip = () => { _getTooltip = () => {
if (this.state.busy) { if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div> return <div>
<InlineSpinner /> <InlineSpinner />
{ _t("Checking server") } { _t("Checking server") }
</div>; </div>;
} else if (this.state.error) { } else if (this.state.error) {
return this.state.error; return <span className="warning">{this.state.error}</span>;
} else { } else {
return null; return null;
} }
@ -57,22 +62,7 @@ export default class SetIntegrationManager extends React.Component {
return !!this.state.url && !this.state.busy; return !!this.state.url && !this.state.busy;
}; };
_setManager = async (ev) => { _continueTerms = async (manager) => {
// Don't reload the page when the user hits enter in the form.
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
if (!manager) {
this.setState({
busy: false,
error: _t("Integration manager offline or not accessible."),
});
return;
}
try { try {
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
this.setState({ this.setState({
@ -90,6 +80,85 @@ export default class SetIntegrationManager extends React.Component {
} }
}; };
_setManager = async (ev) => {
// Don't reload the page when the user hits enter in the form.
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true, checking: true, error: null});
let offline = false;
let manager: IntegrationManagerInstance;
try {
manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
offline = !manager; // no manager implies offline
} catch (e) {
console.error(e);
offline = true; // probably a connection error
}
if (offline) {
this.setState({
busy: false,
checking: false,
error: _t("Integration manager offline or not accessible."),
});
return;
}
// Test the manager (causes terms of service prompt if agreement is needed)
// We also cancel the tooltip at this point so it doesn't collide with the dialog.
this.setState({checking: false});
try {
const client = manager.getScalarClient();
await client.connect();
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t("Terms of service not accepted or the integration manager is invalid."),
});
return;
}
// Specifically request the terms of service to see if there are any.
// The above won't trigger a terms of service check if there are no terms to
// sign, so when there's no terms at all we need to ensure we tell the user.
let hasTerms = true;
try {
const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl);
hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0;
} catch (e) {
// Assume errors mean there are no terms. This could be a 404, 500, etc
console.error(e);
hasTerms = false;
}
if (!hasTerms) {
this.setState({busy: false});
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Integration manager has no terms of service"),
description: (
<div>
<span className="warning">
{_t("The integration manager you have chosen does not have any terms of service.")}
</span>
<span>
&nbsp;{_t("Only continue if you trust the owner of the server.")}
</span>
</div>
),
button: _t("Continue"),
onFinished: async (confirmed) => {
if (!confirmed) return;
this._continueTerms(manager);
},
});
return;
}
this._continueTerms(manager);
};
render() { render() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
@ -120,11 +189,15 @@ export default class SetIntegrationManager extends React.Component {
<span className="mx_SettingsTab_subsectionText"> <span className="mx_SettingsTab_subsectionText">
{bodyText} {bodyText}
</span> </span>
<Field label={_t("Enter a new integration manager")} <Field
label={_t("Enter a new integration manager")}
id="mx_SetIntegrationManager_newUrl" id="mx_SetIntegrationManager_newUrl"
type="text" value={this.state.url} autoComplete="off" type="text" value={this.state.url}
autoComplete="off"
onChange={this._onUrlChanged} onChange={this._onUrlChanged}
tooltipContent={this._getTooltip()} tooltipContent={this._getTooltip()}
disabled={this.state.busy}
flagInvalid={!!this.state.error}
/> />
<AccessibleButton <AccessibleButton
kind="primary_sm" kind="primary_sm"

View file

@ -557,8 +557,12 @@
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.",
"Change": "Change", "Change": "Change",
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
"Failed to update integration manager": "Failed to update integration manager", "Failed to update integration manager": "Failed to update integration manager",
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
"Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.",
"Integration manager has no terms of service": "Integration manager has no terms of service",
"The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.": "You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.", "You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.": "You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.",
"Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.",
"Integration Manager": "Integration Manager", "Integration Manager": "Integration Manager",

View file

@ -40,7 +40,14 @@ export class IntegrationManagerInstance {
get name(): string { get name(): string {
const parsed = url.parse(this.uiUrl); const parsed = url.parse(this.uiUrl);
return parsed.hostname; return parsed.host;
}
get trimmedApiUrl(): string {
const parsed = url.parse(this.apiUrl);
parsed.pathname = '';
parsed.path = '';
return parsed.format();
} }
getScalarClient(): ScalarAuthClient { getScalarClient(): ScalarAuthClient {

View file

@ -117,7 +117,8 @@ export class IntegrationManagers {
} }
/** /**
* Attempts to discover an integration manager using only its name. * Attempts to discover an integration manager using only its name. This will not validate that
* the integration manager is functional - that is the caller's responsibility.
* @param {string} domainName The domain name to look up. * @param {string} domainName The domain name to look up.
* @returns {Promise<IntegrationManagerInstance>} Resolves to an integration manager instance, * @returns {Promise<IntegrationManagerInstance>} Resolves to an integration manager instance,
* or null if none was found. * or null if none was found.
@ -153,20 +154,12 @@ export class IntegrationManagers {
// All discovered managers are per-user managers // All discovered managers are per-user managers
const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]); const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]);
console.log("Got integration manager response, checking for responsiveness"); console.log("Got an integration manager (untested)");
// Test the manager // We don't test the manager because the caller may need to do extra
const client = manager.getScalarClient(); // checks or similar with it. For instance, they may need to deal with
try { // terms of service or want to call something particular.
// not throwing an error is a success here
await client.connect();
} catch (e) {
console.error(e);
console.warn("Integration manager failed liveliness check");
return null;
}
console.log("Integration manager is alive and functioning");
return manager; return manager;
} }
} }