diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index 3154564cd3..06cece0af2 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -18,13 +18,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
-import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
-
-const PHASE_INTRO = 0;
-const PHASE_BUSY = 1;
-const PHASE_DONE = 2;
-const PHASE_CONFIRM_SKIP = 3;
+import {
+ SetupEncryptionStore,
+ PHASE_INTRO,
+ PHASE_BUSY,
+ PHASE_DONE,
+ PHASE_CONFIRM_SKIP,
+} from '../../../stores/SetupEncryptionStore';
+import SetupEncryptionBody from "./SetupEncryptionBody";
export default class CompleteSecurity extends React.Component {
static propTypes = {
@@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component {
constructor() {
super();
-
- this.state = {
- phase: PHASE_INTRO,
- // this serves dual purpose as the object for the request logic and
- // the presence of it insidicating that we're in 'verify mode'.
- // Because of the latter, it lives in the state.
- verificationRequest: null,
- backupInfo: null,
- };
- MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
+ const store = SetupEncryptionStore.sharedInstance();
+ store.on("update", this._onStoreUpdate);
+ store.start();
+ this.state = {phase: store.phase};
}
+ _onStoreUpdate = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ this.setState({phase: store.phase});
+ };
+
componentWillUnmount() {
- if (this.state.verificationRequest) {
- this.state.verificationRequest.off("change", this.onVerificationRequestChange);
- }
- if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
- }
- }
-
- _onUsePassphraseClick = async () => {
- this.setState({
- phase: PHASE_BUSY,
- });
- const cli = MatrixClientPeg.get();
- try {
- const backupInfo = await cli.getKeyBackupVersion();
- this.setState({backupInfo});
-
- // The control flow is fairly twisted here...
- // For the purposes of completing security, we only wait on getting
- // as far as the trust check and then show a green shield.
- // We also begin the key backup restore as well, which we're
- // awaiting inside `accessSecretStorage` only so that it keeps your
- // passphase cached for that work. This dialog itself will only wait
- // on the first trust check, and the key backup restore will happen
- // in the background.
- await new Promise((resolve, reject) => {
- try {
- accessSecretStorage(async () => {
- await cli.checkOwnCrossSigningTrust();
- resolve();
- if (backupInfo) {
- // A complete restore can take many minutes for large
- // accounts / slow servers, so we allow the dialog
- // to advance before this.
- await cli.restoreKeyBackupWithSecretStorage(backupInfo);
- }
- });
- } catch (e) {
- console.error(e);
- reject(e);
- }
- });
-
- if (cli.getCrossSigningId()) {
- this.setState({
- phase: PHASE_DONE,
- });
- }
- } catch (e) {
- if (!(e instanceof AccessCancelledError)) {
- console.log(e);
- }
- // this will throw if the user hits cancel, so ignore
- this.setState({
- phase: PHASE_INTRO,
- });
- }
- }
-
- onVerificationRequest = async (request) => {
- if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
-
- if (this.state.verificationRequest) {
- this.state.verificationRequest.off("change", this.onVerificationRequestChange);
- }
- await request.accept();
- request.on("change", this.onVerificationRequestChange);
- this.setState({
- verificationRequest: request,
- });
- }
-
- onVerificationRequestChange = () => {
- if (this.state.verificationRequest.cancelled) {
- this.state.verificationRequest.off("change", this.onVerificationRequestChange);
- this.setState({
- verificationRequest: null,
- });
- }
- }
-
- onSkipClick = () => {
- this.setState({
- phase: PHASE_CONFIRM_SKIP,
- });
- }
-
- onSkipConfirmClick = () => {
- this.props.onFinished();
- }
-
- onSkipBackClick = () => {
- this.setState({
- phase: PHASE_INTRO,
- });
- }
-
- onDoneClick = () => {
- this.props.onFinished();
+ const store = SetupEncryptionStore.sharedInstance();
+ store.off("update", this._onStoreUpdate);
+ store.stop();
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
- const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
-
- const {
- phase,
- } = this.state;
-
+ const {phase} = this.state;
let icon;
let title;
- let body;
-
- if (this.state.verificationRequest) {
- const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
- body = ;
- } else if (phase === PHASE_INTRO) {
- const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
+ if (phase === PHASE_INTRO) {
icon = ;
title = _t("Complete security");
- body = (
-
-
{_t(
- "Open an existing session & use it to verify this one, " +
- "granting it access to encrypted messages.",
- )}
-
{_t("Waiting…")}
-
{_t(
- "If you can’t access one, ",
- {}, {
- button: sub =>
- {sub}
- ,
- })}
-
-
- );
} else if (phase === PHASE_DONE) {
icon = ;
title = _t("Session verified");
- let message;
- if (this.state.backupInfo) {
- message = {_t(
- "Your new session is now verified. It has access to your " +
- "encrypted messages, and other users will see it as trusted.",
- )}
;
- } else {
- message = {_t(
- "Your new session is now verified. Other users will see it as trusted.",
- )}
;
- }
- body = (
-
- );
} else if (phase === PHASE_CONFIRM_SKIP) {
icon = ;
title = _t("Are you sure?");
- body = (
-
-
{_t(
- "Without completing security on this session, it won’t have " +
- "access to encrypted messages.",
- )}
-
-
- {_t("Skip")}
-
-
- {_t("Go Back")}
-
-
-
- );
} else if (phase === PHASE_BUSY) {
- const Spinner = sdk.getComponent('views.elements.Spinner');
icon = ;
title = _t("Complete security");
- body = ;
} else {
throw new Error(`Unknown phase ${phase}`);
}
@@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component {
{title}
- {body}
+
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
new file mode 100644
index 0000000000..a59fa08b32
--- /dev/null
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -0,0 +1,196 @@
+/*
+Copyright 2020 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 } from '../../../languageHandler';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import * as sdk from '../../../index';
+import {
+ SetupEncryptionStore,
+ PHASE_INTRO,
+ PHASE_BUSY,
+ PHASE_DONE,
+ PHASE_CONFIRM_SKIP,
+ PHASE_FINISHED,
+} from '../../../stores/SetupEncryptionStore';
+
+export default class SetupEncryptionBody extends React.Component {
+ static propTypes = {
+ onFinished: PropTypes.func.isRequired,
+ };
+
+ constructor() {
+ super();
+ const store = SetupEncryptionStore.sharedInstance();
+ store.on("update", this._onStoreUpdate);
+ store.start();
+ this.state = {
+ phase: store.phase,
+ // this serves dual purpose as the object for the request logic and
+ // the presence of it insidicating that we're in 'verify mode'.
+ // Because of the latter, it lives in the state.
+ verificationRequest: store.verificationRequest,
+ backupInfo: store.backupInfo,
+ };
+ }
+
+ _onStoreUpdate = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ if (store.phase === PHASE_FINISHED) {
+ this.props.onFinished();
+ return;
+ }
+ this.setState({
+ phase: store.phase,
+ verificationRequest: store.verificationRequest,
+ backupInfo: store.backupInfo,
+ });
+ };
+
+ componentWillUnmount() {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.off("update", this._onStoreUpdate);
+ store.stop();
+ }
+
+ _onUsePassphraseClick = async () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.usePassPhrase();
+ }
+
+ onSkipClick = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.skip();
+ }
+
+ onSkipConfirmClick = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.skipConfirm();
+ }
+
+ onSkipBackClick = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.returnAfterSkip();
+ }
+
+ onDoneClick = () => {
+ const store = SetupEncryptionStore.sharedInstance();
+ store.done();
+ }
+
+ render() {
+ const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
+
+ const {
+ phase,
+ } = this.state;
+
+ if (this.state.verificationRequest) {
+ const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
+ return ;
+ } else if (phase === PHASE_INTRO) {
+ const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
+ return (
+
+
{_t(
+ "Open an existing session & use it to verify this one, " +
+ "granting it access to encrypted messages.",
+ )}
+
{_t("Waiting…")}
+
{_t(
+ "If you can’t access one, ",
+ {}, {
+ button: sub =>
+ {sub}
+ ,
+ })}
+
+
+ );
+ } else if (phase === PHASE_DONE) {
+ let message;
+ if (this.state.backupInfo) {
+ message = {_t(
+ "Your new session is now verified. It has access to your " +
+ "encrypted messages, and other users will see it as trusted.",
+ )}
;
+ } else {
+ message = {_t(
+ "Your new session is now verified. Other users will see it as trusted.",
+ )}
;
+ }
+ return (
+
+ );
+ } else if (phase === PHASE_CONFIRM_SKIP) {
+ return (
+
+
{_t(
+ "Without completing security on this session, it won’t have " +
+ "access to encrypted messages.",
+ )}
+
+
+ {_t("Skip")}
+
+
+ {_t("Go Back")}
+
+
+
+ );
+ } else if (phase === PHASE_BUSY) {
+ const Spinner = sdk.getComponent('views.elements.Spinner');
+ return ;
+ } else {
+ throw new Error(`Unknown phase ${phase}`);
+ }
+ }
+}
diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
new file mode 100644
index 0000000000..93c1770b1f
--- /dev/null
+++ b/src/stores/SetupEncryptionStore.js
@@ -0,0 +1,147 @@
+/*
+Copyright 2020 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 EventEmitter from 'events';
+import { MatrixClientPeg } from '../MatrixClientPeg';
+import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManager';
+
+export const PHASE_INTRO = 0;
+export const PHASE_BUSY = 1;
+export const PHASE_DONE = 2; //final done stage, but still showing UX
+export const PHASE_CONFIRM_SKIP = 3;
+export const PHASE_FINISHED = 4; //UX can be closed
+
+/**
+ * Holds the active "Complete Security" session
+ */
+export class SetupEncryptionStore extends EventEmitter {
+ static sharedInstance() {
+ if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
+ return global.mx_SetupEncryptionStore;
+ }
+
+ start() {
+ if (this._started) {
+ return;
+ }
+ this._started = true;
+ this.phase = PHASE_INTRO;
+ this.verificationRequest = null;
+ this.backupInfo = null;
+ MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
+ }
+
+ stop() {
+ if (!this._started) {
+ return;
+ }
+ this._started = false;
+ if (this.verificationRequest) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ }
+ if (MatrixClientPeg.get()) {
+ MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
+ }
+ }
+
+ async usePassPhrase() {
+ this.phase = PHASE_BUSY;
+ this.emit("update");
+ const cli = MatrixClientPeg.get();
+ try {
+ const backupInfo = await cli.getKeyBackupVersion();
+ this.backupInfo = backupInfo;
+ this.emit("update");
+ // The control flow is fairly twisted here...
+ // For the purposes of completing security, we only wait on getting
+ // as far as the trust check and then show a green shield.
+ // We also begin the key backup restore as well, which we're
+ // awaiting inside `accessSecretStorage` only so that it keeps your
+ // passphase cached for that work. This dialog itself will only wait
+ // on the first trust check, and the key backup restore will happen
+ // in the background.
+ await new Promise((resolve, reject) => {
+ try {
+ accessSecretStorage(async () => {
+ await cli.checkOwnCrossSigningTrust();
+ resolve();
+ if (backupInfo) {
+ // A complete restore can take many minutes for large
+ // accounts / slow servers, so we allow the dialog
+ // to advance before this.
+ await cli.restoreKeyBackupWithSecretStorage(backupInfo);
+ }
+ }).catch(reject);
+ } catch (e) {
+ console.error(e);
+ reject(e);
+ }
+ });
+
+ if (cli.getCrossSigningId()) {
+ this.phase = PHASE_DONE;
+ this.emit("update");
+ }
+ } catch (e) {
+ if (!(e instanceof AccessCancelledError)) {
+ console.log(e);
+ }
+ // this will throw if the user hits cancel, so ignore
+ this.phase = PHASE_INTRO;
+ this.emit("update");
+ }
+ }
+
+ onVerificationRequest = async (request) => {
+ if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
+
+ if (this.verificationRequest) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ }
+ this.verificationRequest = request;
+ await request.accept();
+ request.on("change", this.onVerificationRequestChange);
+ this.emit("update");
+ }
+
+ onVerificationRequestChange = () => {
+ if (this.verificationRequest.cancelled) {
+ this.verificationRequest.off("change", this.onVerificationRequestChange);
+ this.verificationRequest = null;
+ this.emit("update");
+ }
+ }
+
+ skip() {
+ this.phase = PHASE_CONFIRM_SKIP;
+ this.emit("update");
+ }
+
+ skipConfirm() {
+ this.phase = PHASE_FINISHED;
+ this.emit("update");
+ }
+
+ returnAfterSkip() {
+ this.phase = PHASE_INTRO;
+ this.emit("update");
+ }
+
+ done() {
+ this.phase = PHASE_FINISHED;
+ this.emit("update");
+ }
+}