diff --git a/res/css/_components.scss b/res/css/_components.scss
index 579369a509..b8811c742f 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/terms/_InlineTermsAgreement.scss b/res/css/views/terms/_InlineTermsAgreement.scss
new file mode 100644
index 0000000000..e00dcf31d1
--- /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/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js
new file mode 100644
index 0000000000..c88612dacb
--- /dev/null
+++ b/src/components/views/terms/InlineTermsAgreement.js
@@ -0,0 +1,119 @@
+/*
+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.
+*/
+
+import React from "react";
+import PropTypes from "prop-types";
+import {_t, pickBestLanguage} from "../../../languageHandler";
+import sdk from "../../../..";
+
+export default class InlineTermsAgreement extends React.Component {
+ static propTypes = {
+ policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs
+ agreedUrls: PropTypes.array.isRequired, // array of URLs the user has accepted
+ onFinished: PropTypes.func.isRequired, // takes an argument of accepted URLs
+ introElement: PropTypes.node,
+ };
+
+ constructor() {
+ super();
+
+ this.state = {
+ policies: [],
+ busy: false,
+ };
+ }
+
+ componentDidMount() {
+ // Build all the terms the user needs to accept
+ const policies = []; // { checked, url, name }
+ for (const servicePolicies of this.props.policiesAndServicePairs) {
+ const availablePolicies = Object.values(servicePolicies.policies);
+ for (const policy of availablePolicies) {
+ const language = pickBestLanguage(Object.keys(policy).filter(p => p !== 'version'));
+ const renderablePolicy = {
+ checked: false,
+ url: policy[language].url,
+ name: policy[language].name,
+ };
+ policies.push(renderablePolicy);
+ }
+ }
+
+ this.setState({policies});
+ }
+
+ _togglePolicy = (index) => {
+ const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone
+ policies[index].checked = !policies[index].checked;
+ this.setState({policies});
+ };
+
+ _onContinue = () => {
+ const hasUnchecked = !!this.state.policies.some(p => !p.checked);
+ if (hasUnchecked) return;
+
+ this.setState({busy: true});
+ this.props.onFinished(this.state.policies.map(p => p.url));
+ };
+
+ _renderCheckboxes() {
+ const rendered = [];
+ for (let i = 0; i < this.state.policies.length; i++) {
+ const policy = this.state.policies[i];
+ const introText = _t(
+ "Accept