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:
parent
ded2297523
commit
27504e1578
5 changed files with 122 additions and 37 deletions
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
{_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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue