Delegate to new ResetIdentityDialog from SetupEncryptionBody (#29701)

This commit is contained in:
Andy Balaam
2025-04-30 11:08:38 +01:00
committed by GitHub
parent 4f4f391959
commit 23597e959b
16 changed files with 453 additions and 218 deletions

View File

@@ -78,9 +78,6 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
} else if (phase === Phase.Busy) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("encryption|verification|after_new_login|verify_this_device");
} else if (phase === Phase.ConfirmReset) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
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<IProps, IState> {
const forceVerification = SdkConfig.get("force_verification");
let skipButton;
if (!forceVerification && (phase === Phase.Intro || phase === Phase.ConfirmReset)) {
if (!forceVerification && phase === Phase.Intro) {
skipButton = (
<AccessibleButton
onClick={this.onSkipClick}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -19,6 +19,7 @@ import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStor
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton";
import Spinner from "../../views/elements/Spinner";
import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog";
function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean {
return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations);
@@ -112,19 +113,15 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
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<IProps, IState>
<p>{_t("encryption|verification|no_key_or_device")}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
<AccessibleButton kind="primary" onClick={this.onResetClick}>
{_t("encryption|verification|reset_proceed_prompt")}
</AccessibleButton>
</div>
@@ -246,22 +243,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
</div>
</div>
);
} else if (phase === Phase.ConfirmReset) {
return (
<div>
<p>{_t("encryption|verification|verify_reset_warning_1")}</p>
<p>{_t("encryption|verification|verify_reset_warning_2")}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
{_t("encryption|verification|reset_proceed_prompt")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
{_t("action|go_back")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />;
} else {

View File

@@ -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 (
<MatrixClientContext.Provider value={matrixClient}>
<ResetIdentityBody onReset={onResetWrapper} onCancelClick={onFinished} variant={variant} />
</MatrixClientContext.Provider>
);
}

View File

@@ -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<HTMLButtonElement>;
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
<Button
destructive={true}
disabled={inProgress}
onClick={async (evt) => {
onClick={async () => {
setInProgress(true);
await matrixClient
.getCrypto()
?.resetEncryption((makeRequest) => uiAuthCallback(matrixClient, makeRequest));
onFinish(evt);
onReset();
}}
>
{inProgress ? (
@@ -113,11 +118,10 @@ export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIde
function titleForVariant(variant: ResetIdentityBodyVariant): string {
switch (variant) {
case "compromised":
case "confirm":
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");
}

View File

@@ -6,7 +6,7 @@
*/
import { Breadcrumb } from "@vector-im/compound-web";
import React, { type JSX, type MouseEventHandler } from "react";
import React, { type JSX } from "react";
import { _t } from "../../../../languageHandler";
import { ResetIdentityBody, type ResetIdentityBodyVariant } from "./ResetIdentityBody";
@@ -15,7 +15,8 @@ interface ResetIdentityPanelProps {
/**
* Called when the identity is reset.
*/
onFinish: MouseEventHandler<HTMLButtonElement>;
onReset: () => void;
/**
* Called when the cancel button is clicked or when we go back in the breadcrumbs.
*/
@@ -32,7 +33,7 @@ interface ResetIdentityPanelProps {
*
* A thin wrapper around {@link ResetIdentityBody}, just adding breadcrumbs.
*/
export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetIdentityPanelProps): JSX.Element {
export function ResetIdentityPanel({ onCancelClick, onReset, variant }: ResetIdentityPanelProps): JSX.Element {
return (
<>
<Breadcrumb
@@ -41,7 +42,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
onPageClick={onCancelClick}
/>
<ResetIdentityBody onFinish={onFinish} onCancelClick={onCancelClick} variant={variant} />
<ResetIdentityBody onReset={onReset} onCancelClick={onCancelClick} variant={variant} />
</>
);
}

View File

@@ -128,7 +128,7 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props):
<ResetIdentityPanel
variant={findResetVariant(state)}
onCancelClick={checkEncryptionState}
onFinish={checkEncryptionState}
onReset={checkEncryptionState}
/>
);
break;

View File

@@ -993,7 +993,6 @@
"accepting": "Accepting…",
"after_new_login": {
"device_verified": "Device verified",
"reset_confirmation": "Really reset verification keys?",
"skip_verification": "Skip verification for now",
"unable_to_verify": "Unable to verify this device",
"verify_this_device": "Verify this device"
@@ -1064,8 +1063,6 @@
"verify_emoji_prompt": "Verify by comparing unique emoji.",
"verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.",
"verify_later": "I'll verify later",
"verify_reset_warning_1": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.",
"verify_reset_warning_2": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Key.",
"verify_using_device": "Verify with another device",
"verify_using_key": "Verify with Recovery Key",
"verify_using_key_or_phrase": "Verify with Recovery Key or Phrase",

View File

@@ -29,7 +29,6 @@ export enum Phase {
Done = 3, // final done stage, but still showing UX
ConfirmSkip = 4,
Finished = 5, // UX can be closed
ConfirmReset = 6,
}
/**
@@ -220,38 +219,6 @@ export class SetupEncryptionStore extends EventEmitter {
this.emit("update");
}
public reset(): void {
this.phase = Phase.ConfirmReset;
this.emit("update");
}
public async resetConfirm(): Promise<void> {
try {
// If we've gotten here, the user presumably lost their
// secret storage key if they had one. Start by resetting
// secret storage and setting up a new recovery key, then
// create new cross-signing keys once that succeeds.
await accessSecretStorage(
async (): Promise<void> => {
this.phase = Phase.Finished;
},
{
forceReset: true,
resetCrossSigning: true,
},
);
} catch (e) {
logger.error("Error resetting cross-signing", e);
this.phase = Phase.Intro;
}
this.emit("update");
}
public returnAfterReset(): void {
this.phase = Phase.Intro;
this.emit("update");
}
public done(): void {
this.phase = Phase.Finished;
this.emit("update");