diff --git a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts index 6bffd793f6..a279961814 100644 --- a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts +++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts @@ -93,4 +93,22 @@ test.describe("Encryption tab", () => { await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); }, ); + + test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({ + page, + app, + util, + }) => { + await verifySession(app, "new passphrase"); + // We need to delete the cached secrets + await deleteCachedSecrets(page); + + // The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button + await util.openEncryptionTab(); + const dialog = util.getEncryptionTabContent(); + await dialog.getByRole("button", { name: "Forgot recovery key?" }).click(); + + // The user is prompted to reset their identity + await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible(); + }); }); diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png index e6664a5f79..f1a9152e89 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a114c998b8..46b8fc932d 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -358,6 +358,7 @@ @import "./views/settings/encryption/_AdvancedPanel.pcss"; @import "./views/settings/encryption/_ChangeRecoveryKey.pcss"; @import "./views/settings/encryption/_EncryptionCard.pcss"; +@import "./views/settings/encryption/_RecoveryPanelOutOfSync.pcss"; @import "./views/settings/encryption/_ResetIdentityPanel.pcss"; @import "./views/settings/tabs/_SettingsBanner.pcss"; @import "./views/settings/tabs/_SettingsIndent.pcss"; diff --git a/res/css/views/settings/encryption/_RecoveryPanelOutOfSync.pcss b/res/css/views/settings/encryption/_RecoveryPanelOutOfSync.pcss new file mode 100644 index 0000000000..fc6ba7d959 --- /dev/null +++ b/res/css/views/settings/encryption/_RecoveryPanelOutOfSync.pcss @@ -0,0 +1,11 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.mx_RecoveryPanelOutOfSync { + display: flex; + gap: var(--cpd-space-2x); +} diff --git a/src/components/views/settings/encryption/RecoveryPanelOutOfSync.tsx b/src/components/views/settings/encryption/RecoveryPanelOutOfSync.tsx index 7b5bea5623..a5d47100d6 100644 --- a/src/components/views/settings/encryption/RecoveryPanelOutOfSync.tsx +++ b/src/components/views/settings/encryption/RecoveryPanelOutOfSync.tsx @@ -19,6 +19,10 @@ interface RecoveryPanelOutOfSyncProps { * Callback for when the user has finished entering their recovery key. */ onFinish: () => void; + /** + * Callback for when the user clicks on the "Forgot recovery key?" button. + */ + onForgotRecoveryKey: () => void; } /** @@ -28,7 +32,7 @@ interface RecoveryPanelOutOfSyncProps { * It prompts the user to enter their recovery key so that the secrets can be loaded from 4S into * the client. */ -export function RecoveryPanelOutOfSync({ onFinish }: RecoveryPanelOutOfSyncProps): JSX.Element { +export function RecoveryPanelOutOfSync({ onForgotRecoveryKey, onFinish }: RecoveryPanelOutOfSyncProps): JSX.Element { return ( - +
+ + +
); } diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index f164342e27..0b4b27e9d0 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -71,7 +71,12 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti content = ; break; case "secrets_not_cached": - content = ; + content = ( + setState("reset_identity_forgot")} + /> + ); break; case "main": content = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3896d05e88..c9adce2cc7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2496,7 +2496,8 @@ "description": "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.", "enter_key_error": "The recovery key you entered is not correct.", "enter_recovery_key": "Enter recovery key", - "key_storage_warning": "Your key storage is out of sync. Click the button below to fix the problem.", + "forgot_recovery_key": "Forgot recovery key?", + "key_storage_warning": "Your key storage is out of sync. Click one of the buttons below to fix the problem.", "save_key_description": "Do not share this with anyone!", "save_key_title": "Recovery key", "set_up_recovery": "Set up recovery", diff --git a/test/unit-tests/components/views/settings/encryption/RecoveryPanelOutOfSync-test.tsx b/test/unit-tests/components/views/settings/encryption/RecoveryPanelOutOfSync-test.tsx new file mode 100644 index 0000000000..36e35dbe83 --- /dev/null +++ b/test/unit-tests/components/views/settings/encryption/RecoveryPanelOutOfSync-test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; + +import { RecoveryPanelOutOfSync } from "../../../../../../src/components/views/settings/encryption/RecoveryPanelOutOfSync"; +import { accessSecretStorage } from "../../../../../../src/SecurityManager"; + +jest.mock("../../../../../../src/SecurityManager", () => ({ + accessSecretStorage: jest.fn(), +})); + +describe("", () => { + function renderComponent(onFinish = jest.fn(), onForgotRecoveryKey = jest.fn()) { + return render(); + } + + it("should render", () => { + const { asFragment } = renderComponent(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should call onForgotRecoveryKey when the 'Forgot recovery key?' is clicked", async () => { + const user = userEvent.setup(); + + const onForgotRecoveryKey = jest.fn(); + renderComponent(jest.fn(), onForgotRecoveryKey); + + await user.click(screen.getByRole("button", { name: "Forgot recovery key?" })); + expect(onForgotRecoveryKey).toHaveBeenCalled(); + }); + + it("should access to 4S and call onFinish when 'Enter recovery key' is clicked", async () => { + const user = userEvent.setup(); + mocked(accessSecretStorage).mockClear().mockResolvedValue(); + + const onFinish = jest.fn(); + renderComponent(onFinish); + + await user.click(screen.getByRole("button", { name: "Enter recovery key" })); + expect(accessSecretStorage).toHaveBeenCalled(); + expect(onFinish).toHaveBeenCalled(); + }); +}); diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap new file mode 100644 index 0000000000..16cac376fa --- /dev/null +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` + +
+
+

+ Recovery +

+
+ Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. + + + + + Your key storage is out of sync. Click one of the buttons below to fix the problem. + +
+
+
+ + +
+
+
+`; diff --git a/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx index fdf8f7106d..9137140c79 100644 --- a/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx @@ -10,7 +10,6 @@ import { render, screen } from "jest-matrix-react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { waitFor } from "@testing-library/dom"; import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; import { EncryptionUserSettingsTab, @@ -18,11 +17,6 @@ import { } from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab"; import { createTestClient, withClientContextRenderOptions } from "../../../../../../test-utils"; import Modal from "../../../../../../../src/Modal"; -import { accessSecretStorage } from "../../../../../../../src/SecurityManager"; - -jest.mock("../../../../../../../src/SecurityManager", () => ({ - accessSecretStorage: jest.fn(), -})); describe("", () => { let matrixClient: MatrixClient; @@ -42,8 +36,6 @@ describe("", () => { userSigningKey: true, }, }); - - mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); function renderComponent(props: { initialState?: State } = {}) { @@ -79,7 +71,7 @@ describe("", () => { await waitFor(() => expect(screen.getByText("Recovery")).toBeInTheDocument()); }); - it("should ask to enter the recovery key when secrets are not cached", async () => { + it("should display the recovery out of sync panel when secrets are not cached", async () => { // Secrets are not cached jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({ privateKeysInSecretStorage: true, @@ -97,8 +89,10 @@ describe("", () => { await waitFor(() => screen.getByRole("button", { name: "Enter recovery key" })); expect(asFragment()).toMatchSnapshot(); - await user.click(screen.getByRole("button", { name: "Enter recovery key" })); - expect(accessSecretStorage).toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Forgot recovery key?" })); + expect( + screen.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }), + ).toBeVisible(); }); it("should display the change recovery key panel when the user clicks on the change recovery button", async () => { diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap index 5856e6fda3..2e507dd67a 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap @@ -1,75 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should ask to enter the recovery key when secrets are not cached 1`] = ` - -
-
-
-
-

- Recovery -

-
- Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. - - - - - Your key storage is out of sync. Click the button below to fix the problem. - -
-
- -
-
-
-
-`; - exports[` should display a verify button when the encryption is not set up 1`] = `
should display the change recovery key pa `; +exports[` should display the recovery out of sync panel when secrets are not cached 1`] = ` + +
+
+
+
+

+ Recovery +

+
+ Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. + + + + + Your key storage is out of sync. Click one of the buttons below to fix the problem. + +
+
+
+ + +
+
+
+
+
+`; + exports[` should display the reset identity panel when the user clicks on the reset cryptographic identity panel 1`] = `