diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 9822f0522e..71a4e381d1 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -146,6 +146,29 @@ test.describe("Cryptography", function () { }).toPass(); }); + // When the user resets their identity, key storage also gets enabled. + // Check that the toggle updates to show the correct state. + test("Key backup status updates after resetting identity", async ({ page, app, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + + const encryptionTab = await app.settings.openUserSettings("Encryption"); + const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" }); + // Check that key storage starts off as disabled + expect(await keyStorageToggle.isChecked()).toBe(false); + // Find "the Reset cryptographic identity" button + await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click(); + + // Confirm + await encryptionTab.getByRole("button", { name: "Continue" }).click(); + + // Enter the password + await page.getByPlaceholder("Password").fill(aliceCredentials.password); + await page.getByRole("button", { name: "Continue" }).click(); + + // Key storage should now be enabled + expect(await keyStorageToggle.isChecked()).toBe(true); + }); + test( "creating a DM should work, being e2e-encrypted / user verification", { tag: "@screenshot" }, diff --git a/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts index 60ad21236f..ca15b6a6e3 100644 --- a/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts +++ b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts @@ -5,11 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../DeviceListener"; +import { useEventEmitterAsyncState } from "../../../../hooks/useEventEmitter"; interface KeyStoragePanelState { /** @@ -37,31 +39,37 @@ interface KeyStoragePanelState { /** Returns a ViewModel for use in {@link KeyStoragePanel} and {@link DeleteKeyStoragePanel}. */ export function useKeyStoragePanelViewModel(): KeyStoragePanelState { - const [isEnabled, setIsEnabled] = useState(undefined); const [loading, setLoading] = useState(true); // Whilst the change is being made, the toggle will reflect the pending value rather than the actual state const [pendingValue, setPendingValue] = useState(undefined); const matrixClient = useMatrixClientContext(); - const checkStatus = useCallback(async () => { - const crypto = matrixClient.getCrypto(); - if (!crypto) { - logger.error("Can't check key backup status: no crypto module available"); - return; - } - // The toggle is enabled only if this device will upload megolm keys to the backup. - // This is consistent with EX. - const activeBackupVersion = await crypto.getActiveSessionBackupVersion(); - setIsEnabled(activeBackupVersion !== null); - }, [matrixClient]); + const isEnabled = useEventEmitterAsyncState( + matrixClient, + CryptoEvent.KeyBackupStatus, + async (enabled?: boolean) => { + // If we're called as a result of an event, rather than during + // initialisation, we can get the backup status from the event + // instead of having to query the backup version. + if (enabled !== undefined) { + return enabled; + } - useEffect(() => { - (async () => { - await checkStatus(); + const crypto = matrixClient.getCrypto(); + if (!crypto) { + logger.error("Can't check key backup status: no crypto module available"); + return; + } + // The toggle is enabled only if this device will upload megolm keys to the backup. + // This is consistent with EX. + const activeBackupVersion = await crypto.getActiveSessionBackupVersion(); setLoading(false); - })(); - }, [checkStatus]); + return activeBackupVersion !== null; + }, + [matrixClient], + undefined, + ); const setEnabled = useCallback( async (enable: boolean) => { @@ -121,14 +129,12 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState { // so this will stop EX turning it back on spontaneously. await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true }); } - - await checkStatus(); } finally { setPendingValue(undefined); DeviceListener.sharedInstance().start(matrixClient); } }, - [setPendingValue, checkStatus, matrixClient], + [setPendingValue, matrixClient], ); return { isEnabled: pendingValue ?? isEnabled, setEnabled, loading, busy: pendingValue !== undefined }; diff --git a/test/unit-tests/components/viewmodels/settings/encryption/KeyStoragePanelViewModel-test.ts b/test/unit-tests/components/viewmodels/settings/encryption/KeyStoragePanelViewModel-test.ts index fee28cf38f..eb9c81c4a7 100644 --- a/test/unit-tests/components/viewmodels/settings/encryption/KeyStoragePanelViewModel-test.ts +++ b/test/unit-tests/components/viewmodels/settings/encryption/KeyStoragePanelViewModel-test.ts @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { renderHook } from "jest-matrix-react"; +import { renderHook, waitFor } from "jest-matrix-react"; import { act } from "react"; import { mocked } from "jest-mock"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import type { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; @@ -37,6 +38,23 @@ describe("KeyStoragePanelViewModel", () => { expect(result.current.busy).toBe(true); }); + it("should update if a KeyBackupStatus event is received", async () => { + const { result } = renderHook( + () => useKeyStoragePanelViewModel(), + withClientContextRenderOptions(matrixClient), + ); + await waitFor(() => expect(result.current.isEnabled).toBe(false)); + + const mock = mocked(matrixClient.getCrypto()!.getActiveSessionBackupVersion); + mock.mockResolvedValue("1"); + matrixClient.emit(CryptoEvent.KeyBackupStatus, true); + await waitFor(() => expect(result.current.isEnabled).toBe(true)); + + mock.mockResolvedValue(null); + matrixClient.emit(CryptoEvent.KeyBackupStatus, false); + await waitFor(() => expect(result.current.isEnabled).toBe(false)); + }); + it("should call resetKeyBackup if there is no backup currently", async () => { mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);