diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 29a665f05e..bb68051dfc 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -45,17 +45,16 @@ import { type NonEmptyArray } from "../../../@types/common"; import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext"; import { useSettingValue } from "../../../hooks/useSettings"; import { ToastContext, useActiveToast } from "../../../contexts/ToastContext"; -import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserSettingsTab"; +import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab"; interface IProps { initialTabId?: UserTab; showMsc4108QrCode?: boolean; - /** - * If `true`, the flow for a user to reset their encryption will be shown. In this case, `initialTabId` must be `UserTab.Encryption`. - * - * If false or undefined, show the tab as normal. + /* + * The initial state of the Encryption tab. + * If undefined, the default state is used ("loading"). */ - showResetIdentity?: boolean; + initialEncryptionState?: State; sdkContext: SdkContextClass; onFinished(): void; } @@ -99,7 +98,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { const mjolnirEnabled = useSettingValue("feature_mjolnir"); // store these props in state as changing tabs back and forth should clear them const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode); - const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity); + const [initialEncryptionState, setInitialEncryptionState] = useState(props.initialEncryptionState); const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; @@ -195,7 +194,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { UserTab.Encryption, _td("settings|encryption|title"), , - , + , "UserSettingsEncryption", ), ); @@ -234,7 +233,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { _setActiveTabId(tabId); // Clear these so switching away from the tab and back to it will not show the QR code again setShowMsc4108QrCode(false); - setShowResetIdentity(false); + setInitialEncryptionState(undefined); }; const [activeToast, toastRack] = useActiveToast(); diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx index f7962b8aaa..f77b6d1138 100644 --- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx +++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx @@ -29,17 +29,23 @@ interface ResetIdentityPanelProps { onCancelClick: () => void; /** - * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user this - * warning if they have to reset because they no longer have their key) - * - * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their - * identity has been compromised. - * - * "forgot" is shown when the user has just forgotten their passphrase. + * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user + * this warning if they have to reset because they no longer have their key) */ - variant: "compromised" | "forgot"; + variant: ResetIdentityPanelVariant; } +/** + * "compromised" is shown when the user chooses 'reset' 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. + */ +export type ResetIdentityPanelVariant = "compromised" | "forgot" | "sync_failed"; + /** * The panel for resetting the identity of the current user. */ @@ -58,15 +64,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]} onPageClick={onCancelClick} /> - + @@ -117,3 +115,16 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId ); } + +function titleForVariant(variant: ResetIdentityPanelVariant): string { + switch (variant) { + case "compromised": + return _t("settings|encryption|advanced|breadcrumb_title"); + case "sync_failed": + return _t("settings|encryption|advanced|breadcrumb_title_sync_failed"); + + default: + case "forgot": + return _t("settings|encryption|advanced|breadcrumb_title_forgot"); + } +} diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 987b1d95b6..e2c9f443ca 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -20,7 +20,7 @@ import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDial import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSubheader } from "../../SettingsSubheader"; import { AdvancedPanel } from "../../encryption/AdvancedPanel"; -import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel"; +import { ResetIdentityPanel, type ResetIdentityPanelVariant } from "../../encryption/ResetIdentityPanel"; import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync"; import { useTypedEventEmitter } from "../../../../../hooks/useEventEmitter"; import { KeyStoragePanel } from "../../encryption/KeyStoragePanel"; @@ -39,6 +39,7 @@ import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel"; * This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel. * - "reset_identity_compromised": The panel to show when the user is resetting their identity, in the case where their key is compromised. * - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key. + * - "reset_identity_sync_failed": The panel to show when the user us resetting their identity, in the case where recovery failed. * - "secrets_not_cached": The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. * If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails. * - "key_storage_delete": The confirmation page asking if the user really wants to turn off key storage. @@ -52,6 +53,7 @@ export type State = | "set_recovery_key" | "reset_identity_compromised" | "reset_identity_forgot" + | "reset_identity_sync_failed" | "secrets_not_cached" | "key_storage_delete"; @@ -120,9 +122,10 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props): break; case "reset_identity_compromised": case "reset_identity_forgot": + case "reset_identity_sync_failed": content = ( @@ -140,6 +143,23 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props): ); } +/** + * Given what state we want the tab to be in, what variant of the + * ResetIdentityPanel do we need? + */ +function findResetVariant(state: State): ResetIdentityPanelVariant { + switch (state) { + case "reset_identity_compromised": + return "compromised"; + case "reset_identity_sync_failed": + return "sync_failed"; + + default: + case "reset_identity_forgot": + return "forgot"; + } +} + /** * Hook to check if the user needs: * - to go through the SetupEncryption flow. diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1847abb4ad..90d06d04c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2528,6 +2528,7 @@ "breadcrumb_third_description": "You will need to verify all your existing devices and contacts again", "breadcrumb_title": "Are you sure you want to reset your identity?", "breadcrumb_title_forgot": "Forgot your recovery key? You’ll need to reset your identity.", + "breadcrumb_title_sync_failed": "Failed to sync key storage. You need to reset your identity.", "breadcrumb_warning": "Only do this if you believe your account has been compromised.", "details_title": "Encryption details", "do_not_close_warning": "Do not close this window until the reset is finished", diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 1613c5b9c9..2b50ccd267 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -14,7 +14,7 @@ import Modal from "../Modal"; import { _t } from "../languageHandler"; import DeviceListener from "../DeviceListener"; import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEncryptionDialog"; -import { accessSecretStorage } from "../SecurityManager"; +import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { ModuleRunner } from "../modules/ModuleRunner"; @@ -153,6 +153,8 @@ export const showToast = (kind: Kind): void => { ); try { await accessSecretStorage(); + } catch (error) { + onAccessSecretStorageFailed(error as Error); } finally { modal.close(); } @@ -165,7 +167,7 @@ export const showToast = (kind: Kind): void => { const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: UserTab.Encryption, - props: { showResetIdentity: true }, + props: { initialEncryptionState: "reset_identity_forgot" }, }; defaultDispatcher.dispatch(payload); } else { @@ -173,6 +175,27 @@ export const showToast = (kind: Kind): void => { } }; + /** + * We tried to accessSecretStorage, which triggered us to ask for the + * recovery key, but this failed. If the user just gave up, that is fine, + * but if not, that means downloading encryption info from 4S did not fix + * the problem we identified. Presumably, something is wrong with what + * they have in 4S: we tell them to reset their identity. + */ + const onAccessSecretStorageFailed = (error: Error): void => { + if (error instanceof AccessCancelledError) { + // The user cancelled the dialog - just allow it to close + } else { + // A real error happened - jump to the reset identity tab + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + props: { initialEncryptionState: "reset_identity_sync_failed" }, + }; + defaultDispatcher.dispatch(payload); + } + }; + ToastStore.sharedInstance().addOrReplaceToast({ key: TOAST_KEY, title: getTitle(kind), diff --git a/test/unit-tests/components/views/settings/encryption/ResetIdentityPanel-test.tsx b/test/unit-tests/components/views/settings/encryption/ResetIdentityPanel-test.tsx index e331e59824..07c6c3d930 100644 --- a/test/unit-tests/components/views/settings/encryption/ResetIdentityPanel-test.tsx +++ b/test/unit-tests/components/views/settings/encryption/ResetIdentityPanel-test.tsx @@ -54,4 +54,13 @@ describe("", () => { ); expect(asFragment()).toMatchSnapshot(); }); + + it("should display the 'sync failed' variant correctly", async () => { + const onFinish = jest.fn(); + const { asFragment } = render( + , + withClientContextRenderOptions(matrixClient), + ); + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/ResetIdentityPanel-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/ResetIdentityPanel-test.tsx.snap index 1b77adb67b..894abf4842 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/ResetIdentityPanel-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/ResetIdentityPanel-test.tsx.snap @@ -182,6 +182,188 @@ exports[` should display the 'forgot recovery key' variant `; +exports[` should display the 'sync failed' variant correctly 1`] = ` + + +
+
+
+ + + +
+

+ Failed to sync key storage. You need to reset your identity. +

+
+
+
    +
  • + + Your account details, contacts, preferences, and chat list will be kept +
  • +
  • + + You will lose any message history that’s stored only on the server +
  • +
  • + + You will need to verify all your existing devices and contacts again +
  • +
+
+
+ + +
+
+
+`; + exports[` should reset the encryption when the continue button is clicked 1`] = `