From 1bb482f6f73fc441795b70e9ae2028a7e98ff4a0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 18 Oct 2024 11:45:45 +0200 Subject: [PATCH] Replace `Matrix.getKeyBackupEnabled` by `MatrixClient.CryptoApi.getActiveSessionBackupVersion` (#28225) * Migrating deprecated sync `MatrixClient.getKeyBackupEnabled` to async `MatrixClient.CryptoApi.getActiveSessionBackupVersion` in `NewRecoveryMethodDialog`. Rewrite `NewRecoveryMethodDialog` into a functional component to make it easier to handle the new async method. * Migrating deprecated sync `MatrixClient.getKeyBackupEnabled` to async `MatrixClient.CryptoApi.getActiveSessionBackupVersion` in `MatrixChat`. --- .../security/NewRecoveryMethodDialog.tsx | 128 +++++++-------- src/components/structures/MatrixChat.tsx | 7 +- test/test-utils/test-utils.ts | 1 + .../security/NewRecoveryMethodDialog-test.tsx | 81 ++++++++++ .../NewRecoveryMethodDialog-test.tsx.snap | 146 ++++++++++++++++++ .../components/structures/MatrixChat-test.tsx | 21 ++- 6 files changed, 312 insertions(+), 72 deletions(-) create mode 100644 test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx create mode 100644 test/unit-tests/async-components/dialogs/security/__snapshots__/NewRecoveryMethodDialog-test.tsx.snap diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index ba08bcad23..ac18039749 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -7,10 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; +import React, { JSX, useEffect, useState } from "react"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; @@ -18,81 +16,73 @@ import RestoreKeyBackupDialog from "../../../../components/views/dialogs/securit import { Action } from "../../../../dispatcher/actions"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx"; -interface IProps { - newVersionInfo: KeyBackupInfo; +/** + * Properties for {@link NewRecoveryMethodDialog}. + */ +interface NewRecoveryMethodDialogProps { + /** + * Callback when the dialog is dismissed. + */ onFinished(): void; } -export default class NewRecoveryMethodDialog extends React.PureComponent { - private onOkClick = (): void => { - this.props.onFinished(); - }; +// Export as default instead of a named export so that it can be dynamically imported with `Modal.createDialogAsync` - private onGoToSettingsClick = (): void => { - this.props.onFinished(); - dis.fire(Action.ViewUserSettings); - }; +/** + * Dialog to inform the user that a new recovery method has been detected. + */ +export default function NewRecoveryMethodDialog({ onFinished }: NewRecoveryMethodDialogProps): JSX.Element { + const matrixClient = useMatrixClientContext(); + const [isKeyBackupEnabled, setIsKeyBackupEnabled] = useState(false); + useEffect(() => { + const checkBackupEnabled = async (): Promise => { + const crypto = matrixClient.getCrypto(); + setIsKeyBackupEnabled(Boolean(crypto && (await crypto.getActiveSessionBackupVersion()) !== null)); + }; - private onSetupClick = async (): Promise => { - Modal.createDialog( - RestoreKeyBackupDialog, - { - onFinished: this.props.onFinished, - }, - undefined, - /* priority = */ false, - /* static = */ true, - ); - }; + checkBackupEnabled(); + }, [matrixClient]); - public render(): React.ReactNode { - const title = ( - - {_t("encryption|new_recovery_method_detected|title")} - - ); - - const newMethodDetected =

{_t("encryption|new_recovery_method_detected|description_1")}

; - - const hackWarning = ( - {_t("encryption|new_recovery_method_detected|warning")} - ); - - let content: JSX.Element | undefined; - if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) { - content = ( -
- {newMethodDetected} -

{_t("encryption|new_recovery_method_detected|description_2")}

- {hackWarning} - -
- ); + function onClick(): void { + if (isKeyBackupEnabled) { + onFinished(); } else { - content = ( -
- {newMethodDetected} - {hackWarning} - -
+ Modal.createDialog( + RestoreKeyBackupDialog, + { + onFinished, + }, + undefined, + false, + true, ); } - - return ( - - {content} - - ); } + + return ( + + {_t("encryption|new_recovery_method_detected|title")} + + } + > +

{_t("encryption|new_recovery_method_detected|description_1")}

+ {isKeyBackupEnabled &&

{_t("encryption|new_recovery_method_detected|description_2")}

} + {_t("encryption|new_recovery_method_detected|warning")} + { + onFinished(); + dis.fire(Action.ViewUserSettings); + }} + /> +
+ ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 8b23455967..0f122b2d5f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1631,8 +1631,12 @@ export default class MatrixChat extends React.PureComponent { cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => { let haveNewVersion: boolean | undefined; let newVersionInfo: KeyBackupInfo | null = null; + const keyBackupEnabled = Boolean( + cli.getCrypto() && (await cli.getCrypto()?.getActiveSessionBackupVersion()) !== null, + ); + // if key backup is still enabled, there must be a new backup in place - if (cli.getKeyBackupEnabled()) { + if (keyBackupEnabled) { haveNewVersion = true; } else { // otherwise check the server to see if there's a new one @@ -1650,7 +1654,6 @@ export default class MatrixChat extends React.PureComponent { import( "../../async-components/views/dialogs/security/NewRecoveryMethodDialog" ) as unknown as Promise, - { newVersionInfo: newVersionInfo! }, ); } else { Modal.createDialogAsync( diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index dd1f47ad6d..43807eb030 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -129,6 +129,7 @@ export function createTestClient(): MatrixClient { getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), setDeviceIsolationMode: jest.fn(), prepareToEncrypt: jest.fn(), + getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), }), getPushActionsForEvent: jest.fn(), diff --git a/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx new file mode 100644 index 0000000000..fc964e57bf --- /dev/null +++ b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { render, screen } from "jest-matrix-react"; +import { waitFor } from "@testing-library/dom"; +import userEvent from "@testing-library/user-event"; +import { act } from "@testing-library/react-hooks/dom"; + +import NewRecoveryMethodDialog from "../../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog"; +import { createTestClient } from "../../../../test-utils"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext.tsx"; +import dis from "../../../../../src/dispatcher/dispatcher.ts"; +import { Action } from "../../../../../src/dispatcher/actions.ts"; +import Modal from "../../../../../src/Modal.tsx"; + +describe("", () => { + let matrixClient: MatrixClient; + beforeEach(() => { + matrixClient = createTestClient(); + jest.spyOn(dis, "fire"); + jest.spyOn(Modal, "createDialog"); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function renderComponent(onFinished: () => void = jest.fn()) { + return render( + + + , + ); + } + + test("when cancel is clicked", async () => { + const onFinished = jest.fn(); + act(() => { + renderComponent(onFinished); + }); + + await userEvent.click(screen.getByRole("button", { name: "Go to Settings" })); + expect(onFinished).toHaveBeenCalled(); + expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings); + }); + + test("when key backup is enabled", async () => { + jest.spyOn(matrixClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version"); + + const onFinished = jest.fn(); + + await act(async () => { + const { asFragment } = renderComponent(onFinished); + await waitFor(() => + expect( + screen.getByText("This session is encrypting history using the new recovery method."), + ).toBeInTheDocument(), + ); + expect(asFragment()).toMatchSnapshot(); + }); + + await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); + expect(onFinished).toHaveBeenCalled(); + }); + + test("when key backup is disabled", async () => { + const onFinished = jest.fn(); + + const { asFragment } = renderComponent(onFinished); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); + await waitFor(() => expect(Modal.createDialog).toHaveBeenCalled()); + }); +}); diff --git a/test/unit-tests/async-components/dialogs/security/__snapshots__/NewRecoveryMethodDialog-test.tsx.snap b/test/unit-tests/async-components/dialogs/security/__snapshots__/NewRecoveryMethodDialog-test.tsx.snap new file mode 100644 index 0000000000..08b5ee18d3 --- /dev/null +++ b/test/unit-tests/async-components/dialogs/security/__snapshots__/NewRecoveryMethodDialog-test.tsx.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when key backup is disabled 1`] = ` + +
+