Merge pull request #3316 from matrix-org/travis/terms/inline-im-is
Prompt for terms of service on integration manager changes
This commit is contained in:
commit
218bee8071
5 changed files with 121 additions and 37 deletions
|
@ -46,6 +46,8 @@ export default class Field extends React.PureComponent {
|
|||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
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
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent: PropTypes.node,
|
||||
|
@ -137,7 +139,9 @@ export default class Field extends React.PureComponent {
|
|||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
render() {
|
||||
const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props;
|
||||
const {
|
||||
element, prefix, onValidate, children, tooltipContent, flagInvalid,
|
||||
...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
|
@ -157,13 +161,16 @@ export default class Field extends React.PureComponent {
|
|||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
}
|
||||
|
||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
|
||||
// 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
|
||||
// properly.
|
||||
mx_Field_labelAlwaysTopLeft: prefix,
|
||||
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
|
||||
|
|
|
@ -19,6 +19,10 @@ import {_t} from "../../../languageHandler";
|
|||
import sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
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 {
|
||||
constructor() {
|
||||
|
@ -31,6 +35,7 @@ export default class SetIntegrationManager extends React.Component {
|
|||
url: "", // user-entered text
|
||||
error: null,
|
||||
busy: false,
|
||||
checking: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -40,14 +45,14 @@ export default class SetIntegrationManager extends React.Component {
|
|||
};
|
||||
|
||||
_getTooltip = () => {
|
||||
if (this.state.busy) {
|
||||
if (this.state.checking) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
return <div>
|
||||
<InlineSpinner />
|
||||
{ _t("Checking server") }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
return this.state.error;
|
||||
return <span className="warning">{this.state.error}</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -57,22 +62,7 @@ export default class SetIntegrationManager extends React.Component {
|
|||
return !!this.state.url && !this.state.busy;
|
||||
};
|
||||
|
||||
_setManager = async (ev) => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
_continueTerms = async (manager) => {
|
||||
try {
|
||||
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
|
||||
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() {
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
|
@ -120,11 +189,15 @@ export default class SetIntegrationManager extends React.Component {
|
|||
<span className="mx_SettingsTab_subsectionText">
|
||||
{bodyText}
|
||||
</span>
|
||||
<Field label={_t("Enter a new integration manager")}
|
||||
<Field
|
||||
label={_t("Enter a new integration manager")}
|
||||
id="mx_SetIntegrationManager_newUrl"
|
||||
type="text" value={this.state.url} autoComplete="off"
|
||||
type="text" value={this.state.url}
|
||||
autoComplete="off"
|
||||
onChange={this._onUrlChanged}
|
||||
tooltipContent={this._getTooltip()}
|
||||
disabled={this.state.busy}
|
||||
flagInvalid={!!this.state.error}
|
||||
/>
|
||||
<AccessibleButton
|
||||
kind="primary_sm"
|
||||
|
|
|
@ -558,8 +558,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.",
|
||||
"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",
|
||||
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
|
||||
"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.",
|
||||
"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",
|
||||
|
|
|
@ -40,7 +40,14 @@ export class IntegrationManagerInstance {
|
|||
|
||||
get name(): string {
|
||||
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 {
|
||||
|
|
|
@ -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.
|
||||
* @returns {Promise<IntegrationManagerInstance>} Resolves to an integration manager instance,
|
||||
* or null if none was found.
|
||||
|
@ -153,20 +154,12 @@ export class IntegrationManagers {
|
|||
|
||||
// All discovered managers are per-user managers
|
||||
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
|
||||
const client = manager.getScalarClient();
|
||||
try {
|
||||
// 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;
|
||||
}
|
||||
// We don't test the manager because the caller may need to do extra
|
||||
// checks or similar with it. For instance, they may need to deal with
|
||||
// terms of service or want to call something particular.
|
||||
|
||||
console.log("Integration manager is alive and functioning");
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue