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`] = `
flow to setup a recovery key should ask the user
`;
-exports[` flow to setup a recovery key should ask the user to enter the recovery key 2`] = `
+exports[` flow to set up a recovery key should ask the user to enter the recovery key 2`] = `
flow to setup a recovery key should ask the user
`;
-exports[` flow to setup a recovery key should display information about the recovery key 1`] = `
+exports[` flow to set up a recovery key should display information about the recovery key 1`] = `
flow to setup a recovery key should display infor
`;
-exports[` flow to setup a recovery key should display the recovery key 1`] = `
+exports[` flow to set up a recovery key should display the recovery key 1`] = `