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 6bffd793f60..a2799618143 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 e6664a5f79b..f1a9152e89a 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 a114c998b8b..46b8fc932d9 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 00000000000..fc6ba7d9593
--- /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 7b5bea56234..a5d47100d6f 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 f164342e27d..0b4b27e9d07 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 3896d05e885..c9adce2cc76 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 00000000000..36e35dbe832
--- /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 00000000000..16cac376fa7
--- /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 fdf8f7106dc..9137140c79c 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 5856e6fda33..2e507dd67af 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,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[` should ask to enter the recovery key when secrets are not cached 1`] = `
+exports[` should display a verify button when the encryption is not set up 1`] = `
should ask to enter the recovery key when
>
should ask to enter the recovery key when
- Recovery
+ Device not verified
- Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
@@ -39,7 +37,7 @@ exports[` should ask to enter the recovery key when
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
- Your key storage is out of sync. Click the button below to fix the problem.
+ You need to verify this device in order to view your encryption settings.
@@ -59,10 +57,10 @@ exports[` should ask to enter the recovery key when
xmlns="http://www.w3.org/2000/svg"
>
- Enter recovery key
+ Verify this device
@@ -70,7 +68,20 @@ exports[` should ask to enter the recovery key when
`;
-exports[` should display a verify button when the encryption is not set up 1`] = `
+exports[` should display the change recovery key panel when the user clicks on the change recovery button 1`] = `
+
+
+
+
+
+`;
+
+exports[` should display the recovery out of sync panel when secrets are not cached 1`] = `
should display a verify button when the e
>
should display a verify button when the e
- Device not verified
+ Recovery
+ Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
@@ -107,50 +120,50 @@ exports[` should display a verify button when the e
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
- You need to verify this device in order to view your encryption settings.
+ Your key storage is out of sync. Click one of the buttons below to fix the problem.
-
+ Forgot recovery key?
+
+
+
`;
-exports[` should display the change recovery key panel when the user clicks on the change recovery button 1`] = `
-
-
-
-
-
-`;
-
exports[` should display the reset identity panel when the user clicks on the reset cryptographic identity panel 1`] = `