Skip to content

Commit b330de5

Browse files
authored
Factor out crypto setup process into a store (#28675)
* Factor out crypto setup process into a store To make components pure and avoid react 18 dev mode problems due to components making requests when mounted. * fix test * test for the store * Add comment
1 parent b86bb5c commit b330de5

File tree

8 files changed

+274
-153
lines changed

8 files changed

+274
-153
lines changed

src/@types/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { IConfigOptions } from "../IConfigOptions";
4444
import { MatrixDispatcher } from "../dispatcher/dispatcher";
4545
import { DeepReadonly } from "./common";
4646
import MatrixChat from "../components/structures/MatrixChat";
47+
import { InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
4748

4849
/* eslint-disable @typescript-eslint/naming-convention */
4950

@@ -117,6 +118,7 @@ declare global {
117118
mxPerformanceEntryNames: any;
118119
mxUIStore: UIStore;
119120
mxSetupEncryptionStore?: SetupEncryptionStore;
121+
mxInitialCryptoStore?: InitialCryptoSetupStore;
120122
mxRoomScrollStateStore?: RoomScrollStateStore;
121123
mxActiveWidgetStore?: ActiveWidgetStore;
122124
mxOnRecaptchaLoaded?: () => void;

src/components/structures/MatrixChat.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ import { SessionLockStolenView } from "./auth/SessionLockStolenView";
132132
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
133133
import { LoginSplashView } from "./auth/LoginSplashView";
134134
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
135+
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
135136

136137
// legacy export
137138
export { default as Views } from "../../Views";
@@ -428,6 +429,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
428429
!(await shouldSkipSetupEncryption(cli))
429430
) {
430431
// if cross-signing is not yet set up, do so now if possible.
432+
InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup(
433+
cli,
434+
Boolean(this.tokenLogin),
435+
this.stores,
436+
this.onCompleteSecurityE2eSetupFinished,
437+
);
431438
this.setStateForNewView({ view: Views.E2E_SETUP });
432439
} else {
433440
this.onLoggedIn();
@@ -2073,14 +2080,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
20732080
} else if (this.state.view === Views.COMPLETE_SECURITY) {
20742081
view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
20752082
} else if (this.state.view === Views.E2E_SETUP) {
2076-
view = (
2077-
<E2eSetup
2078-
matrixClient={MatrixClientPeg.safeGet()}
2079-
onFinished={this.onCompleteSecurityE2eSetupFinished}
2080-
accountPassword={this.stores.accountPasswordStore.getPassword()}
2081-
tokenLogin={!!this.tokenLogin}
2082-
/>
2083-
);
2083+
view = <E2eSetup onFinished={this.onCompleteSecurityE2eSetupFinished} />;
20842084
} else if (this.state.view === Views.LOGGED_IN) {
20852085
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
20862086
// latter is set via the dispatcher). If we don't yet have a `page_type`,

src/components/structures/auth/E2eSetup.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,21 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

99
import React from "react";
10-
import { MatrixClient } from "matrix-js-sdk/src/matrix";
1110

1211
import AuthPage from "../../views/auth/AuthPage";
1312
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
1413
import { InitialCryptoSetupDialog } from "../../views/dialogs/security/InitialCryptoSetupDialog";
1514

1615
interface IProps {
17-
matrixClient: MatrixClient;
1816
onFinished: () => void;
19-
accountPassword?: string;
20-
tokenLogin: boolean;
2117
}
2218

2319
export default class E2eSetup extends React.Component<IProps> {
2420
public render(): React.ReactNode {
2521
return (
2622
<AuthPage>
2723
<CompleteSecurityBody>
28-
<InitialCryptoSetupDialog
29-
matrixClient={this.props.matrixClient}
30-
onFinished={this.props.onFinished}
31-
accountPassword={this.props.accountPassword}
32-
tokenLogin={this.props.tokenLogin}
33-
/>
24+
<InitialCryptoSetupDialog onFinished={this.props.onFinished} />
3425
</CompleteSecurityBody>
3526
</AuthPage>
3627
);

src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
77
Please see LICENSE files in the repository root for full details.
88
*/
99

10-
import React, { useCallback, useEffect, useState } from "react";
11-
import { logger } from "matrix-js-sdk/src/logger";
12-
import { MatrixClient } from "matrix-js-sdk/src/matrix";
10+
import React, { useCallback } from "react";
1311

1412
import { _t } from "../../../../languageHandler";
1513
import DialogButtons from "../../elements/DialogButtons";
1614
import BaseDialog from "../BaseDialog";
1715
import Spinner from "../../elements/Spinner";
18-
import { createCrossSigning } from "../../../../CreateCrossSigning";
16+
import { InitialCryptoSetupStore, useInitialCryptoSetupStatus } from "../../../../stores/InitialCryptoSetupStore";
1917

2018
interface Props {
21-
matrixClient: MatrixClient;
22-
accountPassword?: string;
23-
tokenLogin: boolean;
2419
onFinished: (success?: boolean) => void;
2520
}
2621

@@ -29,54 +24,27 @@ interface Props {
2924
* In most cases, only a spinner is shown, but for more
3025
* complex auth like SSO, the user may need to complete some steps to proceed.
3126
*/
32-
export const InitialCryptoSetupDialog: React.FC<Props> = ({
33-
matrixClient,
34-
accountPassword,
35-
tokenLogin,
36-
onFinished,
37-
}) => {
38-
const [error, setError] = useState(false);
27+
export const InitialCryptoSetupDialog: React.FC<Props> = ({ onFinished }) => {
28+
const onRetryClick = useCallback(() => {
29+
InitialCryptoSetupStore.sharedInstance().retry();
30+
}, []);
3931

40-
const doSetup = useCallback(async () => {
41-
const cryptoApi = matrixClient.getCrypto();
42-
if (!cryptoApi) return;
43-
44-
setError(false);
45-
46-
try {
47-
await createCrossSigning(matrixClient, tokenLogin, accountPassword);
48-
49-
onFinished(true);
50-
} catch (e) {
51-
if (tokenLogin) {
52-
// ignore any failures, we are relying on grace period here
53-
onFinished(false);
54-
return;
55-
}
56-
57-
setError(true);
58-
logger.error("Error bootstrapping cross-signing", e);
59-
}
60-
}, [matrixClient, tokenLogin, accountPassword, onFinished]);
61-
62-
const onCancel = useCallback(() => {
32+
const onCancelClick = useCallback(() => {
6333
onFinished(false);
6434
}, [onFinished]);
6535

66-
useEffect(() => {
67-
doSetup();
68-
}, [doSetup]);
36+
const status = useInitialCryptoSetupStatus(InitialCryptoSetupStore.sharedInstance());
6937

7038
let content;
71-
if (error) {
39+
if (status === "error") {
7240
content = (
7341
<div>
7442
<p>{_t("encryption|unable_to_setup_keys_error")}</p>
7543
<div className="mx_Dialog_buttons">
7644
<DialogButtons
7745
primaryButton={_t("action|retry")}
78-
onPrimaryButtonClick={doSetup}
79-
onCancel={onCancel}
46+
onPrimaryButtonClick={onRetryClick}
47+
onCancel={onCancelClick}
8048
/>
8149
</div>
8250
</div>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import EventEmitter from "events";
9+
import { MatrixClient } from "matrix-js-sdk/src/matrix";
10+
import { logger } from "matrix-js-sdk/src/logger";
11+
import { useEffect, useState } from "react";
12+
13+
import { createCrossSigning } from "../CreateCrossSigning";
14+
import { SdkContextClass } from "../contexts/SDKContext";
15+
16+
type Status = "in_progress" | "complete" | "error" | undefined;
17+
18+
export const useInitialCryptoSetupStatus = (store: InitialCryptoSetupStore): Status => {
19+
const [status, setStatus] = useState<Status>(store.getStatus());
20+
21+
useEffect(() => {
22+
const update = (): void => {
23+
setStatus(store.getStatus());
24+
};
25+
26+
store.on("update", update);
27+
28+
return () => {
29+
store.off("update", update);
30+
};
31+
}, [store]);
32+
33+
return status;
34+
};
35+
36+
/**
37+
* Logic for setting up crypto state that's done immediately after
38+
* a user registers. Should be transparent to the user, not requiring
39+
* interaction in most cases.
40+
* As distinct from SetupEncryptionStore which is for setting up
41+
* 4S or verifying the device, will always require interaction
42+
* from the user in some form.
43+
*/
44+
export class InitialCryptoSetupStore extends EventEmitter {
45+
private status: Status = undefined;
46+
47+
private client?: MatrixClient;
48+
private isTokenLogin?: boolean;
49+
private stores?: SdkContextClass;
50+
private onFinished?: (success: boolean) => void;
51+
52+
public static sharedInstance(): InitialCryptoSetupStore {
53+
if (!window.mxInitialCryptoStore) window.mxInitialCryptoStore = new InitialCryptoSetupStore();
54+
return window.mxInitialCryptoStore;
55+
}
56+
57+
public getStatus(): Status {
58+
return this.status;
59+
}
60+
61+
/**
62+
* Start the initial crypto setup process.
63+
*
64+
* @param {MatrixClient} client The client to use for the setup
65+
* @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false
66+
* @param {SdkContextClass} stores The stores to use for the setup
67+
*/
68+
public startInitialCryptoSetup(
69+
client: MatrixClient,
70+
isTokenLogin: boolean,
71+
stores: SdkContextClass,
72+
onFinished: (success: boolean) => void,
73+
): void {
74+
this.client = client;
75+
this.isTokenLogin = isTokenLogin;
76+
this.stores = stores;
77+
this.onFinished = onFinished;
78+
79+
// We just start this process: it's progress is tracked by the events rather
80+
// than returning a promise, so we don't bother.
81+
this.doSetup().catch(() => logger.error("Initial crypto setup failed"));
82+
}
83+
84+
/**
85+
* Retry the initial crypto setup process.
86+
*
87+
* If no crypto setup is currently in process, this will return false.
88+
*
89+
* @returns {boolean} True if a retry was initiated, otherwise false
90+
*/
91+
public retry(): boolean {
92+
if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false;
93+
94+
this.doSetup().catch(() => logger.error("Initial crypto setup failed"));
95+
96+
return true;
97+
}
98+
99+
private reset(): void {
100+
this.client = undefined;
101+
this.isTokenLogin = undefined;
102+
this.stores = undefined;
103+
}
104+
105+
private async doSetup(): Promise<void> {
106+
if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) {
107+
throw new Error("No setup is in progress");
108+
}
109+
110+
const cryptoApi = this.client.getCrypto();
111+
if (!cryptoApi) throw new Error("No crypto module found!");
112+
113+
this.status = "in_progress";
114+
this.emit("update");
115+
116+
try {
117+
await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword());
118+
119+
this.reset();
120+
121+
this.status = "complete";
122+
this.emit("update");
123+
this.onFinished?.(true);
124+
} catch (e) {
125+
if (this.isTokenLogin) {
126+
// ignore any failures, we are relying on grace period here
127+
this.reset();
128+
129+
this.status = "complete";
130+
this.emit("update");
131+
this.onFinished?.(true);
132+
133+
return;
134+
}
135+
logger.error("Error bootstrapping cross-signing", e);
136+
this.status = "error";
137+
this.emit("update");
138+
}
139+
}
140+
}

src/stores/SetupEncryptionStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export enum Phase {
3333
ConfirmReset = 6,
3434
}
3535

36+
/**
37+
* Logic for setting up 4S and/or verifying the user's device: a process requiring
38+
* ongoing interaction with the user, as distinct from InitialCryptoSetupStore which
39+
* a (usually) non-interactive process that happens immediately after registration.
40+
*/
3641
export class SetupEncryptionStore extends EventEmitter {
3742
private started?: boolean;
3843
public phase?: Phase;

0 commit comments

Comments
 (0)