diff --git a/res/css/_components.scss b/res/css/_components.scss
index 579369a5098..b8811c742f2 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -180,6 +180,7 @@
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
+@import "./views/terms/_InlineTermsAgreement.scss";
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss";
diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
index 16467165cf5..ae55733192c 100644
--- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
@@ -28,3 +28,7 @@ limitations under the License.
.mx_GeneralUserSettingsTab_languageInput {
@mixin mx_Settings_fullWidthField;
}
+
+.mx_GeneralUserSettingsTab_warningIcon {
+ vertical-align: middle;
+}
diff --git a/res/css/views/terms/_InlineTermsAgreement.scss b/res/css/views/terms/_InlineTermsAgreement.scss
new file mode 100644
index 00000000000..e00dcf31d15
--- /dev/null
+++ b/res/css/views/terms/_InlineTermsAgreement.scss
@@ -0,0 +1,45 @@
+/*
+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.
+*/
+
+.mx_InlineTermsAgreement_cbContainer {
+ margin-bottom: 10px;
+ font-size: 14px;
+
+ a {
+ color: $accent-color;
+ text-decoration: none;
+ }
+
+ .mx_InlineTermsAgreement_checkbox {
+ margin-top: 10px;
+
+ input {
+ vertical-align: text-bottom;
+ }
+ }
+}
+
+.mx_InlineTermsAgreement_link {
+ display: inline-block;
+ mask-image: url('$(res)/img/external-link.svg');
+ background-color: $accent-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ width: 12px;
+ height: 12px;
+ margin-left: 3px;
+ vertical-align: middle;
+}
diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js
index d3b4d8a6de7..075ae93709a 100644
--- a/src/IdentityAuthClient.js
+++ b/src/IdentityAuthClient.js
@@ -65,7 +65,7 @@ export default class IdentityAuthClient {
}
// Returns a promise that resolves to the access_token string from the IS
- async getAccessToken() {
+ async getAccessToken(check=true) {
if (!this.authEnabled) {
// The current IS doesn't support authentication
return null;
@@ -77,7 +77,7 @@ export default class IdentityAuthClient {
}
if (!token) {
- token = await this.registerForToken();
+ token = await this.registerForToken(check);
if (token) {
this.accessToken = token;
this._writeToken();
@@ -85,18 +85,20 @@ export default class IdentityAuthClient {
return token;
}
- try {
- await this._checkToken(token);
- } catch (e) {
- if (e instanceof TermsNotSignedError) {
- // Retrying won't help this
- throw e;
- }
- // Retry in case token expired
- token = await this.registerForToken();
- if (token) {
- this.accessToken = token;
- this._writeToken();
+ if (check) {
+ try {
+ await this._checkToken(token);
+ } catch (e) {
+ if (e instanceof TermsNotSignedError) {
+ // Retrying won't help this
+ throw e;
+ }
+ // Retry in case token expired
+ token = await this.registerForToken();
+ if (token) {
+ this.accessToken = token;
+ this._writeToken();
+ }
}
}
@@ -126,12 +128,12 @@ export default class IdentityAuthClient {
// See also https://github.com/vector-im/riot-web/issues/10455.
}
- async registerForToken() {
+ async registerForToken(check=true) {
try {
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
const { access_token: identityAccessToken } =
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
- await this._checkToken(identityAccessToken);
+ if (check) await this._checkToken(identityAccessToken);
return identityAccessToken;
} catch (e) {
if (e.cors === "rejected" || e.httpStatus === 404) {
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js
index c5e979da04f..9c2a59bc824 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.js
@@ -25,38 +25,7 @@ import dis from "../../../dispatcher";
import { getThreepidBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {SERVICE_TYPES} from "matrix-js-sdk";
-
-/**
- * If a url has no path component, etc. abbreviate it to just the hostname
- *
- * @param {string} u The url to be abbreviated
- * @returns {string} The abbreviated url
- */
-function abbreviateUrl(u) {
- if (!u) return '';
-
- const parsedUrl = url.parse(u);
- // if it's something we can't parse as a url then just return it
- if (!parsedUrl) return u;
-
- if (parsedUrl.path == '/') {
- // we ignore query / hash parts: these aren't relevant for IS server URLs
- return parsedUrl.host;
- }
-
- return u;
-}
-
-function unabbreviateUrl(u) {
- if (!u) return '';
-
- let longUrl = u;
- if (!u.startsWith('https://')) longUrl = 'https://' + u;
- const parsed = url.parse(longUrl);
- if (parsed.hostname === null) return u;
-
- return longUrl;
-}
+import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
/**
* Check an IS URL is valid, including liveness check
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 7a8d123fcd2..e37fa003f72 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -33,6 +33,10 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../..";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher";
+import {Service, startTermsFlow} from "../../../../../Terms";
+import {SERVICE_TYPES} from "matrix-js-sdk";
+import IdentityAuthClient from "../../../../../IdentityAuthClient";
+import {abbreviateUrl} from "../../../../../utils/UrlUtils";
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {
@@ -47,6 +51,13 @@ export default class GeneralUserSettingsTab extends React.Component {
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverRequiresIdServer: null,
+ idServerHasUnsignedTerms: false,
+ requiredPolicyInfo: { // This object is passed along to a component for handling
+ hasTerms: false,
+ // policiesAndServices, // From the startTermsFlow callback
+ // agreedUrls, // From the startTermsFlow callback
+ // resolve, // Promise resolve function for startTermsFlow callback
+ },
};
this.dispatcherRef = dis.register(this._onAction);
@@ -55,6 +66,9 @@ export default class GeneralUserSettingsTab extends React.Component {
async componentWillMount() {
const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam();
this.setState({serverRequiresIdServer});
+
+ // Check to see if terms need accepting
+ this._checkTerms();
}
componentWillUnmount() {
@@ -64,9 +78,48 @@ export default class GeneralUserSettingsTab extends React.Component {
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
+ this._checkTerms();
}
};
+ async _checkTerms() {
+ if (!this.state.haveIdServer) {
+ this.setState({idServerHasUnsignedTerms: false});
+ return;
+ }
+
+ // By starting the terms flow we get the logic for checking which terms the user has signed
+ // for free. So we might as well use that for our own purposes.
+ const authClient = new IdentityAuthClient();
+ console.log("Getting access token...");
+ const idAccessToken = await authClient.getAccessToken(/*check=*/false);
+ console.log("Got access token: " + idAccessToken);
+ startTermsFlow([new Service(
+ SERVICE_TYPES.IS,
+ MatrixClientPeg.get().getIdentityServerUrl(),
+ idAccessToken,
+ )], (policiesAndServices, agreedUrls, extraClassNames) => {
+ return new Promise((resolve, reject) => {
+ this.setState({
+ idServerName: abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()),
+ requiredPolicyInfo: {
+ hasTerms: true,
+ policiesAndServices,
+ agreedUrls,
+ resolve,
+ },
+ });
+ });
+ }).then(() => {
+ // User accepted all terms
+ this.setState({
+ requiredPolicyInfo: {
+ hasTerms: false,
+ },
+ });
+ });
+ }
+
_onLanguageChange = (newLanguage) => {
if (this.state.language === newLanguage) return;
@@ -198,6 +251,23 @@ export default class GeneralUserSettingsTab extends React.Component {
}
_renderDiscoverySection() {
+ if (this.state.requiredPolicyInfo.hasTerms) {
+ const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
+ const intro =
+ {_t(
+ "Agree to the identity server (%(serverName)s) Terms of Service to " +
+ "allow yourself to be discoverable by email address or phone number.",
+ {serverName: this.state.idServerName},
+ )}
+ ;
+ return