Skip to content

Commit

Permalink
Prompt for terms of service on integration manager changes
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Aug 15, 2019
1 parent ded2297 commit 27504e1
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 31 deletions.
12 changes: 10 additions & 2 deletions src/components/views/elements/Field.js
Expand Up @@ -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,
Expand Down Expand Up @@ -137,7 +139,10 @@ 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";

Expand All @@ -157,13 +162,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
Expand Down
101 changes: 87 additions & 14 deletions src/components/views/settings/SetIntegrationManager.js
Expand Up @@ -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() {
Expand All @@ -31,6 +35,7 @@ export default class SetIntegrationManager extends React.Component {
url: "", // user-entered text
error: null,
busy: false,
checking: false,
};
}

Expand All @@ -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;
}
Expand All @@ -57,37 +62,101 @@ export default class SetIntegrationManager extends React.Component {
return !!this.state.url && !this.state.busy;
};

_continueTerms = async (manager) => {
try {
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
this.setState({
busy: false,
error: null,
currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
url: "", // clear input
});
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t("Failed to update integration manager"),
});
}
};

_setManager = async (ev) => {
// Don't reload the page when the user hits enter in the form.
ev.preventDefault();
ev.stopPropagation();

this.setState({busy: true});
this.setState({busy: true, checking: true, error: null});

const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
if (!manager) {
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 {
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
const client = manager.getScalarClient();
await client.connect();
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: null,
currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
url: "", // clear input
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);
this.setState({
busy: false,
error: _t("Failed to update integration manager"),
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() {
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/strings/en_EN.json
Expand Up @@ -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.",
"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",
Expand Down
9 changes: 8 additions & 1 deletion src/integrations/IntegrationManagerInstance.js
Expand Up @@ -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 {
Expand Down
19 changes: 6 additions & 13 deletions src/integrations/IntegrationManagers.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
Expand Down

0 comments on commit 27504e1

Please sign in to comment.