Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Handle terms agreement in Discovery section of user settings #3327

Merged
merged 7 commits into from
Aug 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ limitations under the License.
.mx_GeneralUserSettingsTab_languageInput {
@mixin mx_Settings_fullWidthField;
}

.mx_GeneralUserSettingsTab_warningIcon {
vertical-align: middle;
}
45 changes: 45 additions & 0 deletions res/css/views/terms/_InlineTermsAgreement.scss
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 18 additions & 16 deletions src/IdentityAuthClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -77,26 +77,28 @@ export default class IdentityAuthClient {
}

if (!token) {
token = await this.registerForToken();
token = await this.registerForToken(check);
if (token) {
this.accessToken = token;
this._writeToken();
}
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();
}
}
}

Expand Down Expand Up @@ -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) {
Expand Down
33 changes: 1 addition & 32 deletions src/components/views/settings/SetIdServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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);
Expand All @@ -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() {
Expand All @@ -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;

Expand Down Expand Up @@ -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 = <span className="mx_SettingsTab_subsectionText">
{_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},
)}
</span>;
return <InlineTermsAgreement
policiesAndServicePairs={this.state.requiredPolicyInfo.policiesAndServices}
agreedUrls={this.state.requiredPolicyInfo.agreedUrls}
onFinished={this.state.requiredPolicyInfo.resolve}
introElement={intro}
/>;
}

const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
Expand Down Expand Up @@ -246,14 +316,20 @@ export default class GeneralUserSettingsTab extends React.Component {
}

render() {
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img className='mx_GeneralUserSettingsTab_warningIcon'
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
width="18" height="18" alt={_t("Warning")} />
: null;

return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{this._renderThemeSection()}
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
Expand Down
Loading