diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx index 979b4bee047..3d9a0679699 100644 --- a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -19,7 +19,6 @@ import { } from "@vector-im/compound-web"; import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy"; import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid"; -import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "./EncryptionCard"; @@ -27,6 +26,7 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; import { copyPlaintext } from "../../../../utils/strings"; import { withSecretStorageKeyCache } from "../../../../SecurityManager"; +import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx"; /** * The possible states of the component. @@ -130,7 +130,7 @@ export function ChangeRecoveryKey({ ); onFinish(); } catch (e) { - logger.error("Failed to bootstrap secret storage", e); + logErrorAndShowErrorDialog("Failed to set up secret storage", e); } }} submitButtonLabel={ diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index 38ba489c7f1..2772350a0c1 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -8,11 +8,14 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t, _td, lookupString, type Tags, type TranslatedString, type TranslationKey } from "../languageHandler"; import SdkConfig from "../SdkConfig"; import { type ValidatedServerConfig } from "./ValidatedServerConfig"; import ExternalLink from "../components/views/elements/ExternalLink"; +import Modal from "../Modal.tsx"; +import ErrorDialog from "../components/views/dialogs/ErrorDialog.tsx"; export const resourceLimitStrings = { "monthly_active_user": _td("error|mau"), @@ -191,3 +194,27 @@ export function messageForConnectionError( return errorText; } + +/** + * Utility for handling unexpected errors: pops up the error dialog. + * + * Example usage: + * ``` + * try { + * /// complicated operation + * } catch (e) { + * logErrorAndShowErrorDialog("Failed complicated operation", e); + * } + * ``` + * + * This isn't particularly intended to be pretty; rather it lets the user know that *something* has gone wrong so that + * they can report a bug. The general idea is that it's better to let the user know of a failure, even if they + * can't do anything about it, than it is to fail silently with the appearance of success. + * + * @param title - Title for the error dialog. + * @param error - The thrown error. Becomes the content of the error dialog. + */ +export function logErrorAndShowErrorDialog(title: string, error: any): void { + logger.error(`${title}:`, error); + Modal.createDialog(ErrorDialog, { title, description: `${error}` }); +} diff --git a/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx b/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx index 783aaf701c3..706efb2b90c 100644 --- a/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx +++ b/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx @@ -9,6 +9,7 @@ import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; import { ChangeRecoveryKey } from "../../../../../../src/components/views/settings/encryption/ChangeRecoveryKey"; import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils"; @@ -18,6 +19,10 @@ jest.mock("../../../../../../src/utils/strings", () => ({ copyPlaintext: jest.fn(), })); +afterEach(() => { + jest.restoreAllMocks(); +}); + describe("", () => { let matrixClient: MatrixClient; @@ -36,7 +41,7 @@ describe("", () => { ); } - describe("flow to setup a recovery key", () => { + describe("flow to set up a recovery key", () => { it("should display information about the recovery key", async () => { const user = userEvent.setup(); @@ -107,6 +112,33 @@ describe("", () => { await user.click(finishButton); expect(onFinish).toHaveBeenCalledWith(); }); + + it("should display errors from bootstrapSecretStorage", async () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockReturnValue(undefined); + mocked(matrixClient.getCrypto()!).bootstrapSecretStorage.mockRejectedValue(new Error("can't bootstrap")); + + const user = userEvent.setup(); + renderComponent(false); + + // Display the recovery key to save + await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" }))); + // Display the form to confirm the recovery key + await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" }))); + + await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument()); + + const finishButton = screen.getByRole("button", { name: "Finish set up" }); + const input = screen.getByRole("textbox"); + await userEvent.type(input, "encoded private key"); + await user.click(finishButton); + + await screen.findByText("Failed to set up secret storage"); + await screen.findByText("Error: can't bootstrap"); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to set up secret storage:", + new Error("can't bootstrap"), + ); + }); }); describe("flow to change the recovery key", () => { diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap index 0719c6cc531..882476080e9 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap @@ -160,7 +160,7 @@ exports[` flow to change the recovery key should display th `; -exports[` flow to setup a recovery key should ask the user to enter the recovery key 1`] = ` +exports[` flow to set up a recovery key should ask the user to enter the recovery key 1`] = `