Prompt users to set up recovery (#30075)

* Show indicator in settings dialog when user doesn't have recovery set up

* Update settings headers to use red dot for recommended settings

* update recovery setup toast and remember if the user dismisses it

* update playwright snapshots

* use typed event emitters

* reverse logic for the account data flag

* fix comment and type
This commit is contained in:
Hubert Chathi
2025-06-18 16:20:17 +00:00
committed by GitHub
parent 2034f8b6bb
commit af984c0e80
26 changed files with 269 additions and 33 deletions
+3 -1
View File
@@ -29,6 +29,7 @@ export class Tab<T extends string> {
* @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask.
* @param {JSX.Element} body The JSX for the tab container.
* @param {string} screenName The screen name to report to Posthog.
* @param {string} labelClassName Additional class to add to the tab label.
*/
public constructor(
public readonly id: T,
@@ -36,6 +37,7 @@ export class Tab<T extends string> {
public readonly icon: string | JSX.Element | null,
public readonly body: JSX.Element,
public readonly screenName?: ScreenName,
public readonly labelClassName?: string,
) {}
}
@@ -85,7 +87,7 @@ interface ITabLabelProps<T extends string> {
}
function TabLabel<T extends string>({ tab, isActive, showToolip, onClick }: ITabLabelProps<T>): JSX.Element {
const classes = classNames("mx_TabbedView_tabLabel", {
const classes = classNames("mx_TabbedView_tabLabel", tab.labelClassName, {
mx_TabbedView_tabLabel_active: isActive,
});
@@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { ClientEvent, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { Toast } from "@vector-im/compound-web";
import React, { type JSX, useState } from "react";
import UserProfileIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
@@ -44,6 +45,7 @@ import { UserTab } from "./UserTab";
import { type NonEmptyArray } from "../../../@types/common";
import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext";
import { useSettingValue } from "../../../hooks/useSettings";
import { NoChange, useEventEmitterAsyncState, type AsyncStateCallbackResult } from "../../../hooks/useEventEmitter";
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab";
@@ -100,6 +102,26 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
const [initialEncryptionState, setInitialEncryptionState] = useState(props.initialEncryptionState);
// If the user doesn't have Recovery set up (no default Secret Storage key),
// we show an indicator on the Encryption tab.
const showSetupRecoveryIndicator = useEventEmitterAsyncState(
props.sdkContext.client,
ClientEvent.AccountData,
async (event?: MatrixEvent): AsyncStateCallbackResult<boolean> => {
if (event === undefined || event.getType() === "m.secret_storage.default_key") {
const client = props.sdkContext.client;
if (!client) {
return false;
}
return !(await client.secretStorage.getDefaultKeyId());
}
return new NoChange();
},
[],
false,
);
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
const tabs: Tab<UserTab>[] = [];
@@ -196,6 +218,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
<KeyIcon />,
<EncryptionUserSettingsTab initialState={initialEncryptionState} />,
"UserSettingsEncryption",
showSetupRecoveryIndicator ? "mx_SettingsDialog_tabLabelsAlert" : undefined,
),
);
@@ -6,10 +6,9 @@
*/
import React, { type JSX } from "react";
import classNames from "classnames";
import { Heading } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
/**
* The heading for a settings section.
*/
@@ -25,9 +24,12 @@ interface SettingsHeaderProps {
}
export function SettingsHeader({ hasRecommendedTag = false, label }: SettingsHeaderProps): JSX.Element {
const classes = classNames("mx_SettingsHeader", {
mx_SettingsHeader_recommended: hasRecommendedTag,
});
return (
<Heading className="mx_SettingsHeader" as="h2" size="sm" weight="semibold">
{label} {hasRecommendedTag && <span>{_t("common|recommended")}</span>}
<Heading className={classes} as="h2" size="sm" weight="semibold">
{label}
</Heading>
);
}
@@ -29,6 +29,7 @@ import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydra
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
import { EncryptionCardButtons } from "./EncryptionCardButtons";
import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx";
import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
/**
* The possible states of the component.
@@ -131,6 +132,10 @@ export function ChangeRecoveryKey({
});
await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true });
});
// Record the fact that the user explicitly enabled recovery.
await matrixClient.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: true });
onFinish();
} catch (e) {
logErrorAndShowErrorDialog("Failed to set up secret storage", e);