From 23597e959b3690838410e7eeeff76d4ff523b462 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 30 Apr 2025 11:08:38 +0100 Subject: [PATCH] Delegate to new ResetIdentityDialog from SetupEncryptionBody (#29701) --- playwright/e2e/crypto/dehydration.spec.ts | 40 +++- playwright/e2e/login/login-consent.spec.ts | 37 +++ .../encryption-tab.spec.ts | 222 ++++++++++-------- .../structures/auth/CompleteSecurity.tsx | 5 +- .../structures/auth/SetupEncryptionBody.tsx | 43 +--- .../views/dialogs/ResetIdentityDialog.tsx | 49 ++++ .../settings/encryption/ResetIdentityBody.tsx | 26 +- .../encryption/ResetIdentityPanel.tsx | 9 +- .../tabs/user/EncryptionUserSettingsTab.tsx | 2 +- src/i18n/strings/en_EN.json | 3 - src/stores/SetupEncryptionStore.ts | 33 --- .../security/ResetIdentityDialog-test.tsx | 63 +++++ .../security/SetupEncryptionDialog-test.tsx | 88 +++++++ .../structures/auth/CompleteSecurity-test.tsx | 18 +- .../encryption/ResetIdentityPanel-test.tsx | 14 +- .../stores/SetupEncryptionStore-test.ts | 19 +- 16 files changed, 453 insertions(+), 218 deletions(-) create mode 100644 src/components/views/dialogs/ResetIdentityDialog.tsx create mode 100644 test/components/views/dialogs/security/ResetIdentityDialog-test.tsx create mode 100644 test/components/views/dialogs/security/SetupEncryptionDialog-test.tsx diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 2f545b8d14..379fc36cf9 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { isDendrite } from "../../plugins/homeserver/dendrite"; -import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts"; +import { createBot, logIntoElement } from "./utils.ts"; import { type Client } from "../../pages/client.ts"; import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; @@ -28,21 +28,27 @@ test.describe("Dehydration", () => { test.skip(isDendrite, "does not yet support dehydration v2"); test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => { - // Verify the device by resetting the key (which will create SSSS, and dehydrated device) + // Verify the device by resetting the identity key, and then set up recovery (which will create SSSS, and dehydrated device) const securityTab = await app.settings.openUserSettings("Security & Privacy"); await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible(); await app.closeDialog(); - // Verify the device by resetting the key + // Reset the identity key const settings = await app.settings.openUserSettings("Encryption"); await settings.getByRole("button", { name: "Verify this device" }).click(); await page.getByRole("button", { name: "Proceed with reset" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Copy" }).click(); + + // Set up recovery + await page.getByRole("button", { name: "Set up recovery" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Done" }).click(); + const recoveryKey = await page.getByTestId("recoveryKey").innerText(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("textbox").fill(recoveryKey); + await page.getByRole("button", { name: "Finish set up" }).click(); + await page.getByRole("button", { name: "Close" }).click(); await expectDehydratedDeviceEnabled(app); @@ -80,7 +86,7 @@ test.describe("Dehydration", () => { await expectDehydratedDeviceEnabled(app); }); - test("Reset recovery key during login re-creates dehydrated device", async ({ + test("Reset identity during login and set up recovery re-creates dehydrated device", async ({ page, homeserver, app, @@ -99,16 +105,26 @@ test.describe("Dehydration", () => { // Log in our client await logIntoElement(page, credentials); - // Oh no, we forgot our recovery key + // Oh no, we forgot our recovery key - reset our identity await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click(); - await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click(); + await expect( + page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + await page.getByPlaceholder("Password").fill(credentials.password); + await page.getByRole("button", { name: "Continue" }).click(); - await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password }); + // And set up recovery + const settings = await app.settings.openUserSettings("Encryption"); + await settings.getByRole("button", { name: "Set up recovery" }).click(); + await settings.getByRole("button", { name: "Continue" }).click(); + const recoveryKey = await settings.getByTestId("recoveryKey").innerText(); + await settings.getByRole("button", { name: "Continue" }).click(); + await settings.getByRole("textbox").fill(recoveryKey); + await settings.getByRole("button", { name: "Finish set up" }).click(); // There should be a brand new dehydrated device - const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client); - expect(dehydratedDeviceIds.length).toBe(1); - expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]); + await expectDehydratedDeviceEnabled(app); }); test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => { diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index c4a4b1409f..23baf023fa 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -288,6 +288,43 @@ test.describe("Login", () => { await expect(h1).toBeVisible(); }); }); + + test("Can reset identity to become verified", async ({ page, homeserver, request, credentials }) => { + // Log in + const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }); + if (!res.ok()) { + console.log(`Uploading dummy keys failed with HTTP status ${res.status}`, await res.json()); + throw new Error("Uploading dummy keys failed"); + } + + await page.goto("/"); + await login(page, homeserver, credentials); + + await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible(); + + // Start the reset process + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // First try cancelling and restarting + await page.getByRole("button", { name: "Cancel" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // Then click outside the dialog and restart + await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true }); + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // Finally we actually continue + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByPlaceholder("Password").fill(credentials.password); + await page.getByRole("button", { name: "Continue" }).click(); + + // We end up at the Home screen + await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Welcome Dave", exact: true })).toBeVisible(); + }); }); }); 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 427c801ef4..ed1daecf35 100644 --- a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts +++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts @@ -19,126 +19,162 @@ import { test.describe("Encryption tab", () => { test.use({ displayName: "Alice" }); - let recoveryKey: GeneratedSecretStorageKey; - let expectedBackupVersion: string; + test.describe("when encryption is set up", () => { + let recoveryKey: GeneratedSecretStorageKey; + let expectedBackupVersion: string; - test.beforeEach(async ({ page, homeserver, credentials }) => { - // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key - const res = await createBot(page, homeserver, credentials); - recoveryKey = res.recoveryKey; - expectedBackupVersion = res.expectedBackupVersion; - }); + test.beforeEach(async ({ page, homeserver, credentials }) => { + // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key + const res = await createBot(page, homeserver, credentials); + recoveryKey = res.recoveryKey; + expectedBackupVersion = res.expectedBackupVersion; + }); - test( - "should show a 'Verify this device' button if the device is unverified", - { tag: "@screenshot" }, - async ({ page, app, util }) => { - const dialog = await util.openEncryptionTab(); - const content = util.getEncryptionTabContent(); + test( + "should show a 'Verify this device' button if the device is unverified", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + const dialog = await util.openEncryptionTab(); + const content = util.getEncryptionTabContent(); - // The user's device is in an unverified state, therefore the only option available to them here is to verify it - const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); - await expect(verifyButton).toBeVisible(); - await expect(content).toMatchScreenshot("verify-device-encryption-tab.png"); - await verifyButton.click(); + // The user's device is in an unverified state, therefore the only option available to them here is to verify it + const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); + await expect(verifyButton).toBeVisible(); + await expect(content).toMatchScreenshot("verify-device-encryption-tab.png"); + await verifyButton.click(); - await util.verifyDevice(recoveryKey); + await util.verifyDevice(recoveryKey); - await expect(content).toMatchScreenshot("default-tab.png", { - mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")], - }); + await expect(content).toMatchScreenshot("default-tab.png", { + mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")], + }); - // Check that our device is now cross-signed - await checkDeviceIsCrossSigned(app); + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); - // Check that the current device is connected to key backup - // The backup decryption key should be in cache also, as we got it directly from the 4S - await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); - }, - ); + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); + }, + ); - // Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB. - // - // This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. - // We simulate this case by deleting the cached secrets in the indexedDB. - test( - "should prompt to enter the recovery key when the secrets are not cached locally", - { tag: "@screenshot" }, - async ({ page, app, util }) => { + // Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB. + // + // This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. + // We simulate this case by deleting the cached secrets in the indexedDB. + test( + "should prompt to enter the recovery key when the secrets are not cached locally", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await verifySession(app, recoveryKey.encodedPrivateKey); + // We need to delete the cached secrets + await deleteCachedSecrets(page); + + await util.openEncryptionTab(); + // We ask the user to enter the recovery key + const dialog = util.getEncryptionTabContent(); + const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); + await expect(enterKeyButton).toBeVisible(); + await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); + await enterKeyButton.click(); + + // Fill the recovery key + await util.enterRecoveryKey(recoveryKey); + await expect(dialog).toMatchScreenshot("default-tab.png", { + mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")], + }); + + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); + + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + 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, recoveryKey.encodedPrivateKey); // 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(); - // We ask the user to enter the recovery key const dialog = util.getEncryptionTabContent(); - const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); - await expect(enterKeyButton).toBeVisible(); - await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); - await enterKeyButton.click(); + await dialog.getByRole("button", { name: "Forgot recovery key?" }).click(); - // Fill the recovery key - await util.enterRecoveryKey(recoveryKey); - await expect(dialog).toMatchScreenshot("default-tab.png", { - mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")], - }); + // The user is prompted to reset their identity + await expect( + dialog.getByText("Forgot your recovery key? You’ll need to reset your identity."), + ).toBeVisible(); + }); - // Check that our device is now cross-signed - await checkDeviceIsCrossSigned(app); + test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => { + await verifySession(app, recoveryKey.encodedPrivateKey); + await util.openEncryptionTab(); - // Check that the current device is connected to key backup - // The backup decryption key should be in cache also, as we got it directly from the 4S - await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); - }, - ); + await page.getByRole("checkbox", { name: "Allow key storage" }).click(); - test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({ - page, - app, - util, - }) => { - await verifySession(app, recoveryKey.encodedPrivateKey); - // We need to delete the cached secrets - await deleteCachedSecrets(page); + await expect( + page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }), + ).toBeVisible(); - // 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(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png"); - // The user is prompted to reset their identity - await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible(); + const deleteRequestPromises = [ + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")), + page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")), + ]; + + await page.getByRole("button", { name: "Delete key storage" }).click(); + + await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked(); + + for (const prom of deleteRequestPromises) { + const request = await prom; + expect(request.method()).toBe("PUT"); + expect(request.postData()).toBe(JSON.stringify({})); + } + }); }); - test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => { - await verifySession(app, recoveryKey.encodedPrivateKey); - await util.openEncryptionTab(); + test.describe("when encryption is not set up", () => { + test("'Verify this device' allows us to become verified", async ({ + page, + user, + credentials, + app, + }, workerInfo) => { + const settings = await app.settings.openUserSettings("Encryption"); - await page.getByRole("checkbox", { name: "Allow key storage" }).click(); + // Initially, our device is not verified + await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible(); - await expect( - page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }), - ).toBeVisible(); + // We will reset our identity + await settings.getByRole("button", { name: "Verify this device" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png"); + // First try cancelling and restarting + await page.getByRole("button", { name: "Cancel" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - const deleteRequestPromises = [ - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")), - page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")), - ]; + // Then click outside the dialog and restart + await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true }); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - await page.getByRole("button", { name: "Delete key storage" }).click(); + // Finally we actually continue + await page.getByRole("button", { name: "Continue" }).click(); - await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked(); - - for (const prom of deleteRequestPromises) { - const request = await prom; - expect(request.method()).toBe("PUT"); - expect(request.postData()).toBe(JSON.stringify({})); - } + // Now we are verified, so we see the Key storage toggle + await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible(); + }); }); }); diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index a0f2be8836..5bade7b24a 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -78,9 +78,6 @@ export default class CompleteSecurity extends React.Component { } else if (phase === Phase.Busy) { icon = ; title = _t("encryption|verification|after_new_login|verify_this_device"); - } else if (phase === Phase.ConfirmReset) { - icon = ; - title = _t("encryption|verification|after_new_login|reset_confirmation"); } else if (phase === Phase.Finished) { // SetupEncryptionBody will take care of calling onFinished, we don't need to do anything } else { @@ -90,7 +87,7 @@ export default class CompleteSecurity extends React.Component { const forceVerification = SdkConfig.get("force_verification"); let skipButton; - if (!forceVerification && (phase === Phase.Intro || phase === Phase.ConfirmReset)) { + if (!forceVerification && phase === Phase.Intro) { skipButton = ( private onResetClick = (ev: ButtonEvent): void => { ev.preventDefault(); - const store = SetupEncryptionStore.sharedInstance(); - store.reset(); - }; - - private onResetConfirmClick = (): void => { - this.props.onFinished(); - const store = SetupEncryptionStore.sharedInstance(); - store.resetConfirm(); - }; - - private onResetBackClick = (): void => { - const store = SetupEncryptionStore.sharedInstance(); - store.returnAfterReset(); + Modal.createDialog(ResetIdentityDialog, { + onReset: () => { + // The user completed the reset process - close this dialog + this.props.onFinished(); + const store = SetupEncryptionStore.sharedInstance(); + store.done(); + }, + variant: "confirm", + }); }; private onDoneClick = (): void => { @@ -157,7 +154,7 @@ export default class SetupEncryptionBody extends React.Component

{_t("encryption|verification|no_key_or_device")}

- + {_t("encryption|verification|reset_proceed_prompt")}
@@ -246,22 +243,6 @@ export default class SetupEncryptionBody extends React.Component ); - } else if (phase === Phase.ConfirmReset) { - return ( -
-

{_t("encryption|verification|verify_reset_warning_1")}

-

{_t("encryption|verification|verify_reset_warning_2")}

- -
- - {_t("encryption|verification|reset_proceed_prompt")} - - - {_t("action|go_back")} - -
-
- ); } else if (phase === Phase.Busy || phase === Phase.Loading) { return ; } else { diff --git a/src/components/views/dialogs/ResetIdentityDialog.tsx b/src/components/views/dialogs/ResetIdentityDialog.tsx new file mode 100644 index 0000000000..b946ab1d79 --- /dev/null +++ b/src/components/views/dialogs/ResetIdentityDialog.tsx @@ -0,0 +1,49 @@ +/* + * 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, { type JSX } from "react"; + +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ResetIdentityBody, type ResetIdentityBodyVariant } from "../settings/encryption/ResetIdentityBody"; + +interface ResetIdentityDialogProps { + /** + * Called when the dialog is complete. + * + * `ResetIdentityDialog` expects this to be provided by `Modal.createDialog`, and that it will close the dialog. + */ + onFinished: () => void; + + /** + * Called when the identity is reset (before onFinished is called). + */ + onReset: () => void; + + /** + * Which variant of this dialog to show. + */ + variant: ResetIdentityBodyVariant; +} + +/** + * The dialog for resetting the identity of the current user. + */ +export function ResetIdentityDialog({ onFinished, onReset, variant }: ResetIdentityDialogProps): JSX.Element { + const matrixClient = MatrixClientPeg.safeGet(); + + const onResetWrapper: () => void = () => { + onReset(); + // Close the dialog + onFinished(); + }; + return ( + + + + ); +} diff --git a/src/components/views/settings/encryption/ResetIdentityBody.tsx b/src/components/views/settings/encryption/ResetIdentityBody.tsx index f2c339ca4d..a6b0b2c12e 100644 --- a/src/components/views/settings/encryption/ResetIdentityBody.tsx +++ b/src/components/views/settings/encryption/ResetIdentityBody.tsx @@ -9,7 +9,7 @@ import { Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/co import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; -import React, { type JSX, useState, type MouseEventHandler } from "react"; +import React, { type JSX, useState } from "react"; import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "./EncryptionCard"; @@ -22,7 +22,8 @@ interface ResetIdentityBodyProps { /** * Called when the identity is reset. */ - onFinish: MouseEventHandler; + onReset: () => void; + /** * Called when the cancel button is clicked. */ @@ -36,22 +37,26 @@ interface ResetIdentityBodyProps { } /** - * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their - * identity has been compromised. + * The variant of the panel to show. This affects the message displayed to the user. + * + * "compromised" is shown when the user chose 'Reset cryptographic identity' explicitly in settings, usually because + * they believe their identity has been compromised. * * "sync_failed" is shown when the user tried to recover their identity but the process failed, probably because * the required information is missing from recovery. * - * "forgot" is shown when the user has just forgotten their passphrase. + * "forgot" is shown when the user chose 'Forgot recovery key?' during `SetupEncryptionToast`. + * + * "confirm" is shown when the user chose 'Reset all' during `SetupEncryptionBody`. */ -export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed"; +export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm"; /** * User interface component allowing the user to reset their cryptographic identity. * * Used by {@link ResetIdentityPanel}. */ -export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIdentityBodyProps): JSX.Element { +export function ResetIdentityBody({ onCancelClick, onReset, variant }: ResetIdentityBodyProps): JSX.Element { const matrixClient = useMatrixClientContext(); // After the user clicks "Continue", we disable the button so it can't be @@ -78,12 +83,12 @@ export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIde