Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 15 additions & 24 deletions src/components/structures/auth/SetupEncryptionBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/settings/encryption/ResetIdentityDialog";

function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean {
return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations);
Expand Down Expand Up @@ -114,12 +115,18 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
ev.preventDefault();
const store = SetupEncryptionStore.sharedInstance();
store.reset();
};

private onResetConfirmClick = (): void => {
this.props.onFinished();
const store = SetupEncryptionStore.sharedInstance();
store.resetConfirm();
Modal.createDialog(ResetIdentityDialog, {
onResetFinished: () => {
close();
this.onDoneClick();
},
onCancelClick: () => {
close();
this.onResetBackClick();
},
variant: "forgot",
title: _t("encryption|verification|reset_confirm_title"),
});
};

private onResetBackClick = (): void => {
Expand Down Expand Up @@ -157,7 +164,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>
Expand Down Expand Up @@ -246,23 +253,7 @@ 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) {
} else if (phase === Phase.ConfirmReset || phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />;
} else {
logger.log(`SetupEncryptionBody: Unknown phase ${phase}`);
Expand Down
116 changes: 116 additions & 0 deletions src/components/views/settings/encryption/ResetIdentityBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2024-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 { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/compound-web";
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 { _t } from "../../../../languageHandler";
import { EncryptionCard } from "./EncryptionCard";
import { uiAuthCallback } from "../../../../CreateCrossSigning";
import { EncryptionCardButtons } from "./EncryptionCardButtons";
import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent";

/**
* Reset the user's cryptographic identity. This component should not be used
* directly, but should be used via a wrapper such as ResetIdentityPanel or
* ...? .
*/

interface ResetIdentityBodyProps {
/**
* Called when the identity is reset.
*/
onFinish: MouseEventHandler<HTMLButtonElement>;
/**
* Called when the cancel button is clicked.
*/
onCancelClick: () => void;

/**
* The title to display for the panel.
*/
title: string;

/**
* 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.
*/
variant: "compromised" | "forgot";

client: MatrixClient;
}

/**
* The panel for resetting the identity of the current user.
*/
export function ResetIdentityBody({
onCancelClick,
onFinish,
variant,
client,
title,
}: ResetIdentityBodyProps): JSX.Element {
// After the user clicks "Continue", we disable the button so it can't be
// clicked again, and warn the user not to close the window.
const [inProgress, setInProgress] = useState(false);

return (
<EncryptionCard Icon={ErrorIcon} destructive={true} title={title}>
<EncryptionCardEmphasisedContent>
<VisualList>
<VisualListItem Icon={CheckIcon} success={true}>
{_t("settings|encryption|advanced|breadcrumb_first_description")}
</VisualListItem>
<VisualListItem Icon={InfoIcon}>
{_t("settings|encryption|advanced|breadcrumb_second_description")}
</VisualListItem>
<VisualListItem Icon={InfoIcon}>
{_t("settings|encryption|advanced|breadcrumb_third_description")}
</VisualListItem>
</VisualList>
{variant === "compromised" && <span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>}
</EncryptionCardEmphasisedContent>
<EncryptionCardButtons>
<Button
destructive={true}
disabled={inProgress}
onClick={async (evt) => {
setInProgress(true);
await client.getCrypto()?.resetEncryption((makeRequest) => uiAuthCallback(client, makeRequest));
onFinish(evt);
}}
>
{inProgress ? (
<>
<InlineSpinner /> {_t("settings|encryption|advanced|reset_in_progress")}
</>
) : (
_t("action|continue")
)}
</Button>
{inProgress ? (
<EncryptionCardEmphasisedContent>
<span className="mx_ResetIdentityPanel_warning">
{_t("settings|encryption|advanced|do_not_close_warning")}
</span>
</EncryptionCardEmphasisedContent>
) : (
<Button kind="tertiary" onClick={onCancelClick}>
{_t("action|cancel")}
</Button>
)}
</EncryptionCardButtons>
</EncryptionCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 MouseEventHandler } from "react";

import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { ResetIdentityBody } from "./ResetIdentityBody";

interface ResetIdentityDialogProps {
/**
* Called when the dialog closes.
*/
onFinished: () => void;
/**
* Called when the identity is reset.
*/
onResetFinished: MouseEventHandler<HTMLButtonElement>;
/**
* Called when the cancel button is clicked.
*/
onCancelClick: () => void;

/**
* The title to display for the panel.
*/
title: string;

/**
* 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.
*/
variant: "compromised" | "forgot";
}

/**
* The dialog for resetting the identity of the current user.
*/
export function ResetIdentityDialog({
onFinished,
onCancelClick,
onResetFinished,
title,
variant,
}: ResetIdentityDialogProps): JSX.Element {
const client = MatrixClientPeg.safeGet();

// wrappers for ResetIdentityBody's callbacks so that onFinish gets called
// whenever the reset is done, whether by completing successfully, or by
// being cancelled
const onResetWrapper: MouseEventHandler<HTMLButtonElement> = (...args) => {
onFinished();
onResetFinished(...args);
};
const onCancelWrapper: () => void = () => {
onFinished();
onCancelClick();
};
return (
<ResetIdentityBody
onFinish={onResetWrapper}
onCancelClick={onCancelWrapper}
variant={variant}
title={title}
client={client}
/>
);
}
80 changes: 14 additions & 66 deletions src/components/views/settings/encryption/ResetIdentityPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
* Please see LICENSE files in the repository root for full details.
*/

import { Breadcrumb, Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/compound-web";
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 { Breadcrumb } from "@vector-im/compound-web";
import React, { type JSX, type MouseEventHandler } from "react";

import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "./EncryptionCard";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { uiAuthCallback } from "../../../../CreateCrossSigning";
import { EncryptionCardButtons } from "./EncryptionCardButtons";
import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent";
import { _t } from "../../../../languageHandler";
import { ResetIdentityBody } from "./ResetIdentityBody";

/**
* Wraps ResetIdentityBody to work in the settings page by adding a breadcrumb.
*/

interface ResetIdentityPanelProps {
/**
Expand Down Expand Up @@ -45,11 +43,6 @@ interface ResetIdentityPanelProps {
*/
export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetIdentityPanelProps): JSX.Element {
const matrixClient = useMatrixClientContext();

// After the user clicks "Continue", we disable the button so it can't be
// clicked again, and warn the user not to close the window.
const [inProgress, setInProgress] = useState(false);

return (
<>
<Breadcrumb
Expand All @@ -58,62 +51,17 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
onPageClick={onCancelClick}
/>
<EncryptionCard
Icon={ErrorIcon}
destructive={true}
<ResetIdentityBody
onFinish={onFinish}
onCancelClick={onCancelClick}
variant={variant}
title={
variant === "forgot"
? _t("settings|encryption|advanced|breadcrumb_title_forgot")
: _t("settings|encryption|advanced|breadcrumb_title")
}
>
<EncryptionCardEmphasisedContent>
<VisualList>
<VisualListItem Icon={CheckIcon} success={true}>
{_t("settings|encryption|advanced|breadcrumb_first_description")}
</VisualListItem>
<VisualListItem Icon={InfoIcon}>
{_t("settings|encryption|advanced|breadcrumb_second_description")}
</VisualListItem>
<VisualListItem Icon={InfoIcon}>
{_t("settings|encryption|advanced|breadcrumb_third_description")}
</VisualListItem>
</VisualList>
{variant === "compromised" && <span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>}
</EncryptionCardEmphasisedContent>
<EncryptionCardButtons>
<Button
destructive={true}
disabled={inProgress}
onClick={async (evt) => {
setInProgress(true);
await matrixClient
.getCrypto()
?.resetEncryption((makeRequest) => uiAuthCallback(matrixClient, makeRequest));
onFinish(evt);
}}
>
{inProgress ? (
<>
<InlineSpinner /> {_t("settings|encryption|advanced|reset_in_progress")}
</>
) : (
_t("action|continue")
)}
</Button>
{inProgress ? (
<EncryptionCardEmphasisedContent>
<span className="mx_ResetIdentityPanel_warning">
{_t("settings|encryption|advanced|do_not_close_warning")}
</span>
</EncryptionCardEmphasisedContent>
) : (
<Button kind="tertiary" onClick={onCancelClick}>
{_t("action|cancel")}
</Button>
)}
</EncryptionCardButtons>
</EncryptionCard>
client={matrixClient}
/>
</>
);
}
3 changes: 1 addition & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@
"request_toast_accept_user": "Verify User",
"request_toast_decline_counter": "Ignore (%(counter)s)",
"request_toast_detail": "%(deviceId)s from %(ip)s",
"reset_confirm_title": "Are you sure you want to reset your identity?",
"reset_proceed_prompt": "Proceed with reset",
"sas_caption_self": "Verify this device by confirming the following number appears on its screen.",
"sas_caption_user": "Verify this user by confirming the following number appears on their screen.",
Expand Down Expand Up @@ -1068,8 +1069,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",
Expand Down
Loading