1
0
mirror of https://github.com/element-hq/element-web.git synced 2025-08-08 03:42:14 +03:00

Add Advanced section to the user settings encryption tab (#28804)

* Make the encryption card more configurable:
- Change the icon
- Can set the destructive props

* Update compound

* Add advanced section

* Add the `Never send encrypted messages to unverified devices` settings

* - Add commercial license
- Remove generic type

* Rename EncryptionDetails css classes

* Use same uiAuthCallback

* Use h3 for title

* Add tests to `AdvancedPanel`

* Add tests to `EncryptionUserSettingsTab`

* Add tests to `ResetIdentityPanel`

* Get only the recovery section in recovery tests

* Add e2e test
This commit is contained in:
Florian Duros
2025-01-24 09:33:16 +01:00
committed by GitHub
parent a0044d6b5f
commit ac565dca80
31 changed files with 1296 additions and 71 deletions

View File

@@ -91,7 +91,7 @@
"@types/png-chunks-extract": "^1.0.2", "@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30", "@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^2.1.0", "@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.5.0", "@vector-im/compound-web": "^7.6.1",
"@vector-im/matrix-wysiwyg": "2.38.0", "@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",

View File

@@ -0,0 +1,73 @@
/*
* 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 { test, expect } from "./index";
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
import { bootstrapCrossSigningForClient } from "../../../pages/client";
test.describe("Advanced section in Encryption tab", () => {
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
const clientHandle = await app.client.prepareClient();
// Reset cross signing in order to have a verified session
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
});
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
await expect(section.getByText(deviceId)).toBeVisible();
await expect(section).toMatchScreenshot("encryption-details.png", {
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
});
});
test("should show the import room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Import keys" }).click();
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
});
test("should show the export room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Export keys" }).click();
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
});
test(
"should reset the cryptographic identity",
{ tag: "@screenshot" },
async ({ page, app, credentials, util }) => {
const tab = await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
await tab.getByRole("button", { name: "Continue" }).click();
// Fill password dialog and validate
const dialog = page.locator(".mx_InteractiveAuthDialog");
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
await dialog.getByRole("button", { name: "Continue" }).click();
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
// After resetting the identity, the user should set up a new recovery key
await expect(
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
).toBeVisible();
await checkDeviceIsCrossSigned(app);
},
);
});

View File

@@ -18,6 +18,8 @@ export { expect };
export const test = base.extend<{ export const test = base.extend<{
util: Helpers; util: Helpers;
}>({ }>({
displayName: "Alice",
util: async ({ page, app, bot }, use) => { util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app)); await use(new Helpers(page, app));
}, },
@@ -67,6 +69,20 @@ class Helpers {
return this.page.getByTestId("encryptionTab"); return this.page.getByTestId("encryptionTab");
} }
/**
* Get the recovery section
*/
getEncryptionRecoverySection() {
return this.page.getByTestId("recoveryPanel");
}
/**
* Get the encryption details section
*/
getEncryptionDetailsSection() {
return this.page.getByTestId("encryptionDetails");
}
/** /**
* Set the default key id of the secret storage to `null` * Set the default key id of the secret storage to `null`
*/ */
@@ -92,6 +108,6 @@ class Helpers {
const clipboardContent = await this.app.getClipboard(); const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent); await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: confirmButtonLabel }).click(); await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(dialog).toMatchScreenshot("default-recovery.png"); await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
} }
} }

View File

@@ -32,15 +32,19 @@ test.describe("Recovery section in Encryption tab", () => {
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => { test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it // The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible(); await expect(verifyButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png"); await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click(); await verifyButton.click();
await util.verifyDevice(recoveryKey); await util.verifyDevice(recoveryKey);
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});
// Check that our device is now cross-signed // Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
@@ -61,7 +65,7 @@ test.describe("Recovery section in Encryption tab", () => {
// The user can only change the recovery key // The user can only change the recovery key
const changeButton = dialog.getByRole("button", { name: "Change recovery key" }); const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
await expect(changeButton).toBeVisible(); await expect(changeButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png"); await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
await changeButton.click(); await changeButton.click();
// Display the new recovery key and click on the copy button // Display the new recovery key and click on the copy button
@@ -89,7 +93,7 @@ test.describe("Recovery section in Encryption tab", () => {
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
const setupButton = dialog.getByRole("button", { name: "Set up recovery" }); const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
await expect(setupButton).toBeVisible(); await expect(setupButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png"); await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
await setupButton.click(); await setupButton.click();
// Display an informative panel about the recovery key // Display an informative panel about the recovery key
@@ -137,12 +141,12 @@ test.describe("Recovery section in Encryption tab", () => {
const dialog = util.getEncryptionTabContent(); const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible(); await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click(); await enterKeyButton.click();
// Fill the recovery key // Fill the recovery key
await util.enterRecoveryKey(recoveryKey); await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-recovery.png"); await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed // Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -354,8 +354,10 @@
@import "./views/settings/_ThemeChoicePanel.pcss"; @import "./views/settings/_ThemeChoicePanel.pcss";
@import "./views/settings/_UpdateCheckButton.pcss"; @import "./views/settings/_UpdateCheckButton.pcss";
@import "./views/settings/_UserProfileSettings.pcss"; @import "./views/settings/_UserProfileSettings.pcss";
@import "./views/settings/encryption/_AdvancedPanel.pcss";
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss"; @import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
@import "./views/settings/encryption/_EncryptionCard.pcss"; @import "./views/settings/encryption/_EncryptionCard.pcss";
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
@import "./views/settings/tabs/_SettingsBanner.pcss"; @import "./views/settings/tabs/_SettingsBanner.pcss";
@import "./views/settings/tabs/_SettingsIndent.pcss"; @import "./views/settings/tabs/_SettingsIndent.pcss";
@import "./views/settings/tabs/_SettingsSection.pcss"; @import "./views/settings/tabs/_SettingsSection.pcss";

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2024 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.
*/
.mx_EncryptionDetails,
.mx_OtherSettings {
display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
width: 100%;
align-items: start;
.mx_EncryptionDetails_session_title,
.mx_OtherSettings_title {
font: var(--cpd-font-body-lg-semibold);
padding-bottom: var(--cpd-space-2x);
border-bottom: 1px solid var(--cpd-color-gray-400);
width: 100%;
margin: 0;
}
}
.mx_EncryptionDetails {
.mx_EncryptionDetails_session {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
width: 100%;
> div {
display: flex;
> span {
width: 50%;
word-wrap: break-word;
}
}
> div:nth-child(odd) {
background-color: var(--cpd-color-gray-200);
}
}
.mx_EncryptionDetails_buttons {
display: flex;
gap: var(--cpd-space-4x);
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2024 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.
*/
.mx_ResetIdentityPanel {
.mx_ResetIdentityPanel_content {
display: flex;
flex-direction: column;
gap: var(--cpd-space-3x);
> span {
font: var(--cpd-font-body-md-medium);
text-align: center;
}
}
.mx_ResetIdentityPanel_footer {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
justify-content: center;
}
}

View File

@@ -31,9 +31,15 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
throw new Error("No crypto API found!"); throw new Error("No crypto API found!");
} }
const doBootstrapUIAuth = async ( await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => uiAuthCallback(cli, makeRequest),
});
}
export async function uiAuthCallback(
matrixClient: MatrixClient,
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>, makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => { ): Promise<void> {
try { try {
await makeRequest({}); await makeRequest({});
} catch (error) { } catch (error) {
@@ -59,7 +65,7 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
const { finished } = Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"), title: _t("encryption|bootstrap_title"),
matrixClient: cli, matrixClient,
makeRequest, makeRequest,
aestheticsForStagePhases: { aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
@@ -71,9 +77,4 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
throw new Error("Cross-signing key upload auth canceled"); throw new Error("Cross-signing key upload auth canceled");
} }
} }
};
await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: doBootstrapUIAuth,
});
} }

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2024 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, { JSX, lazy, MouseEventHandler } from "react";
import { Button, HelpMessage, InlineField, InlineSpinner, Label, Root, ToggleControl } from "@vector-im/compound-web";
import DownloadIcon from "@vector-im/compound-design-tokens/assets/web/icons/download";
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
import { _t } from "../../../../languageHandler";
import { SettingsSection } from "../shared/SettingsSection";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import Modal from "../../../../Modal";
import { SettingLevel } from "../../../../settings/SettingLevel";
import { useSettingValueAt } from "../../../../hooks/useSettings";
import SettingsStore from "../../../../settings/SettingsStore";
interface AdvancedPanelProps {
/**
* Callback for when the user clicks the button to reset their identity.
*/
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
}
/**
* The advanced panel of the encryption settings.
*/
export function AdvancedPanel({ onResetIdentityClick }: AdvancedPanelProps): JSX.Element {
return (
<SettingsSection heading={_t("settings|encryption|advanced|title")} legacy={false}>
<EncryptionDetails onResetIdentityClick={onResetIdentityClick} />
<OtherSettings />
</SettingsSection>
);
}
interface EncryptionDetails {
/**
* Callback for when the user clicks the button to reset their identity.
*/
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
}
/**
* The encryption details section of the advanced panel.
*/
function EncryptionDetails({ onResetIdentityClick }: EncryptionDetails): JSX.Element {
const matrixClient = useMatrixClientContext();
// Null when the keys are not loaded yet
const keys = useAsyncMemo(() => matrixClient.getCrypto()!.getOwnDeviceKeys(), [matrixClient], null);
return (
<div className="mx_EncryptionDetails" data-testid="encryptionDetails">
<div className="mx_EncryptionDetails_session">
<h3 className="mx_EncryptionDetails_session_title">
{_t("settings|encryption|advanced|details_title")}
</h3>
<div>
<span>{_t("settings|encryption|advanced|session_id")}</span>
<span data-testid="deviceId">{matrixClient.deviceId}</span>
</div>
<div>
<span>{_t("settings|encryption|advanced|session_key")}</span>
<span data-testid="sessionKey">
{keys ? keys.ed25519 : <InlineSpinner aria-label={_t("common|loading")} />}
</span>
</div>
</div>
<div className="mx_EncryptionDetails_buttons">
<Button
size="sm"
kind="secondary"
Icon={ShareIcon}
onClick={() =>
Modal.createDialog(
lazy(
() => import("../../../../async-components/views/dialogs/security/ExportE2eKeysDialog"),
),
{ matrixClient },
)
}
>
{_t("settings|encryption|advanced|export_keys")}
</Button>
<Button
size="sm"
kind="secondary"
Icon={DownloadIcon}
onClick={() =>
Modal.createDialog(
lazy(
() => import("../../../../async-components/views/dialogs/security/ImportE2eKeysDialog"),
),
{ matrixClient },
)
}
>
{_t("settings|encryption|advanced|import_keys")}
</Button>
</div>
<Button size="sm" kind="tertiary" destructive={true} onClick={onResetIdentityClick}>
{_t("settings|encryption|advanced|reset_identity")}
</Button>
</div>
);
}
/**
* Display the never send encrypted message to unverified devices setting.
*/
function OtherSettings(): JSX.Element | null {
const blacklistUnverifiedDevices = useSettingValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
const canSetValue = SettingsStore.canSetValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE);
if (!canSetValue) return null;
return (
<Root
data-testid="otherSettings"
className="mx_OtherSettings"
onChange={async (evt) => {
const checked = new FormData(evt.currentTarget).get("neverSendEncrypted") === "on";
await SettingsStore.setValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE, checked);
}}
>
<h3 className="mx_OtherSettings_title">{_t("settings|encryption|advanced|other_people_device_title")}</h3>
<InlineField
name="neverSendEncrypted"
control={<ToggleControl name="neverSendEncrypted" defaultChecked={blacklistUnverifiedDevices} />}
>
<Label>{_t("settings|encryption|advanced|other_people_device_label")}</Label>
<HelpMessage>{_t("settings|encryption|advanced|other_people_device_description")}</HelpMessage>
</InlineField>
</Root>
);
}

View File

@@ -18,6 +18,7 @@ import {
TextControl, TextControl,
} from "@vector-im/compound-web"; } from "@vector-im/compound-web";
import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy"; 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 { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
@@ -157,7 +158,12 @@ export function ChangeRecoveryKey({
pages={pages} pages={pages}
onPageClick={onCancelClick} onPageClick={onCancelClick}
/> />
<EncryptionCard title={labels.title} description={labels.description} className="mx_ChangeRecoveryKey"> <EncryptionCard
Icon={KeyIcon}
title={labels.title}
description={labels.description}
className="mx_ChangeRecoveryKey"
>
{content} {content}
</EncryptionCard> </EncryptionCard>
</> </>

View File

@@ -5,9 +5,8 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import React, { JSX, PropsWithChildren } from "react"; import React, { JSX, PropsWithChildren, ComponentType, SVGAttributes } from "react";
import { BigIcon, Heading } from "@vector-im/compound-web"; import { BigIcon, Heading } from "@vector-im/compound-web";
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
import classNames from "classnames"; import classNames from "classnames";
interface EncryptionCardProps { interface EncryptionCardProps {
@@ -22,7 +21,15 @@ interface EncryptionCardProps {
/** /**
* The description of the card. * The description of the card.
*/ */
description: string; description?: string;
/**
* Whether this icon shows a destructive action.
*/
destructive?: boolean;
/**
* The icon to display.
*/
Icon: ComponentType<SVGAttributes<SVGElement>>;
} }
/** /**
@@ -32,18 +39,20 @@ export function EncryptionCard({
title, title,
description, description,
className, className,
destructive = false,
Icon,
children, children,
}: PropsWithChildren<EncryptionCardProps>): JSX.Element { }: PropsWithChildren<EncryptionCardProps>): JSX.Element {
return ( return (
<div className={classNames("mx_EncryptionCard", className)}> <div className={classNames("mx_EncryptionCard", className)}>
<div className="mx_EncryptionCard_header"> <div className="mx_EncryptionCard_header">
<BigIcon> <BigIcon destructive={destructive}>
<KeyIcon /> <Icon />
</BigIcon> </BigIcon>
<Heading as="h2" size="sm" weight="semibold"> <Heading as="h2" size="sm" weight="semibold">
{title} {title}
</Heading> </Heading>
<span>{description}</span> {description && <span>{description}</span>}
</div> </div>
{children} {children}
</div> </div>

View File

@@ -106,6 +106,7 @@ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps):
/> />
} }
subHeading={<Subheader state={state} />} subHeading={<Subheader state={state} />}
data-testid="recoveryPanel"
> >
{content} {content}
</SettingsSection> </SettingsSection>

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2024 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 { Breadcrumb, Button, 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";
import React, { MouseEventHandler } from "react";
import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "./EncryptionCard";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { uiAuthCallback } from "../../../../CreateCrossSigning";
interface ResetIdentityPanelProps {
/**
* Called when the identity is reset.
*/
onFinish: MouseEventHandler<HTMLButtonElement>;
/**
* Called when the cancel button is clicked or when we go back in the breadcrumbs.
*/
onCancelClick: () => void;
}
/**
* The panel for resetting the identity of the current user.
*/
export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPanelProps): JSX.Element {
const matrixClient = useMatrixClientContext();
return (
<>
<Breadcrumb
backLabel={_t("action|back")}
onBackClick={onCancelClick}
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
onPageClick={onCancelClick}
/>
<EncryptionCard
Icon={ErrorIcon}
destructive={true}
title={_t("settings|encryption|advanced|breadcrumb_title")}
className="mx_ResetIdentityPanel"
>
<div className="mx_ResetIdentityPanel_content">
<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>
<span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>
</div>
<div className="mx_ResetIdentityPanel_footer">
<Button
destructive={true}
onClick={async (evt) => {
await matrixClient
.getCrypto()
?.resetEncryption((makeRequest) => uiAuthCallback(matrixClient, makeRequest));
onFinish(evt);
}}
>
{_t("action|continue")}
</Button>
<Button kind="tertiary" onClick={onCancelClick}>
{_t("action|cancel")}
</Button>
</div>
</EncryptionCard>
</>
);
}

View File

@@ -6,7 +6,7 @@
*/ */
import React, { JSX, useCallback, useEffect, useState } from "react"; import React, { JSX, useCallback, useEffect, useState } from "react";
import { Button, InlineSpinner } from "@vector-im/compound-web"; import { Button, InlineSpinner, Separator } from "@vector-im/compound-web";
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer"; import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
import SettingsTab from "../SettingsTab"; import SettingsTab from "../SettingsTab";
@@ -18,6 +18,8 @@ import Modal from "../../../../../Modal";
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog"; import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubheader } from "../../SettingsSubheader"; import { SettingsSubheader } from "../../SettingsSubheader";
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
/** /**
* The state in the encryption settings tab. * The state in the encryption settings tab.
@@ -29,8 +31,9 @@ import { SettingsSubheader } from "../../SettingsSubheader";
* This happens when the user has a recovery key and the user clicks on "Change recovery key" button of the RecoveryPanel. * This happens when the user has a recovery key and the user clicks on "Change recovery key" button of the RecoveryPanel.
* - "set_recovery_key": The panel to show when the user is setting up their recovery key. * - "set_recovery_key": The panel to show when the user is setting up their recovery key.
* 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. * 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": The panel to show when the user is resetting their identity.
*/ */
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key"; type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key" | "reset_identity";
export function EncryptionUserSettingsTab(): JSX.Element { export function EncryptionUserSettingsTab(): JSX.Element {
const [state, setState] = useState<State>("loading"); const [state, setState] = useState<State>("loading");
@@ -46,11 +49,15 @@ export function EncryptionUserSettingsTab(): JSX.Element {
break; break;
case "main": case "main":
content = ( content = (
<>
<RecoveryPanel <RecoveryPanel
onChangeRecoveryKeyClick={(setupNewKey) => onChangeRecoveryKeyClick={(setupNewKey) =>
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key") setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
} }
/> />
<Separator kind="section" />
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity")} />
</>
); );
break; break;
case "change_recovery_key": case "change_recovery_key":
@@ -63,6 +70,9 @@ export function EncryptionUserSettingsTab(): JSX.Element {
/> />
); );
break; break;
case "reset_identity":
content = <ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />;
break;
} }
return ( return (

View File

@@ -2422,6 +2422,24 @@
"enable_markdown": "Enable Markdown", "enable_markdown": "Enable Markdown",
"enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.", "enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.",
"encryption": { "encryption": {
"advanced": {
"breadcrumb_first_description": "Your account details, contacts, preferences, and chat list will be kept",
"breadcrumb_page": "Reset encryption",
"breadcrumb_second_description": "You will lose any message history thats stored only on the server",
"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_warning": "Only do this if you believe your account has been compromised.",
"details_title": "Encryption details",
"export_keys": "Export keys",
"import_keys": "Import keys",
"other_people_device_description": "By default in encrypted rooms, do not send encrypted messages to anyone until youve verified them",
"other_people_device_label": "Never send encrypted messages to unverified devices",
"other_people_device_title": "Other peoples devices",
"reset_identity": "Reset cryptographic identity",
"session_id": "Session ID:",
"session_key": "Session key:",
"title": "Advanced"
},
"device_not_verified_button": "Verify this device", "device_not_verified_button": "Verify this device",
"device_not_verified_description": "You need to verify this device in order to view your encryption settings.", "device_not_verified_description": "You need to verify this device in order to view your encryption settings.",
"device_not_verified_title": "Device not verified", "device_not_verified_title": "Device not verified",

View File

@@ -116,7 +116,7 @@ export function createTestClient(): MatrixClient {
}, },
getCrypto: jest.fn().mockReturnValue({ getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(), getOwnDeviceKeys: jest.fn().mockResolvedValue({ ed25519: "ed25519", curve25519: "curve25519" }),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getUserVerificationStatus: jest.fn(), getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(),
@@ -151,6 +151,7 @@ export function createTestClient(): MatrixClient {
}, },
}), }),
isCrossSigningReady: jest.fn().mockResolvedValue(false), isCrossSigningReady: jest.fn().mockResolvedValue(false),
resetEncryption: jest.fn(),
}), }),
getPushActionsForEvent: jest.fn(), getPushActionsForEvent: jest.fn(),

View File

@@ -0,0 +1,99 @@
/*
* 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 from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
import { AdvancedPanel } from "../../../../../../src/components/views/settings/encryption/AdvancedPanel";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
describe("<AdvancedPanel />", () => {
let matrixClient: MatrixClient;
beforeEach(() => {
matrixClient = createTestClient();
});
async function renderAdvancedPanel(onResetIdentityClick = jest.fn()) {
const renderResult = render(
<AdvancedPanel onResetIdentityClick={onResetIdentityClick} />,
withClientContextRenderOptions(matrixClient),
);
// Wait for the device keys to be displayed
await waitFor(() => expect(screen.getByText("ed25519")).toBeInTheDocument());
return renderResult;
}
describe("<EncryptionDetails />", () => {
it("should display a spinner when loading the device keys", async () => {
jest.spyOn(matrixClient.getCrypto()!, "getOwnDeviceKeys").mockImplementation(() => new Promise(() => {}));
render(<AdvancedPanel onResetIdentityClick={jest.fn()} />, withClientContextRenderOptions(matrixClient));
expect(screen.getByTestId("encryptionDetails")).toMatchSnapshot();
});
it("should display the device keys", async () => {
await renderAdvancedPanel();
// session id
expect(screen.getByText("ABCDEFGHI")).toBeInTheDocument();
// session key
expect(screen.getByText("ed25519")).toBeInTheDocument();
expect(screen.getByTestId("encryptionDetails")).toMatchSnapshot();
});
it("should call the onResetIdentityClick callback when the reset cryptographic identity button is clicked", async () => {
const user = userEvent.setup();
const onResetIdentityClick = jest.fn();
await renderAdvancedPanel(onResetIdentityClick);
const resetIdentityButton = screen.getByRole("button", { name: "Reset cryptographic identity" });
await user.click(resetIdentityButton);
expect(onResetIdentityClick).toHaveBeenCalled();
});
});
describe("<OtherSettings />", () => {
it("should display the blacklist of unverified devices settings", async () => {
const user = userEvent.setup();
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(true);
jest.spyOn(SettingsStore, "setValue");
await renderAdvancedPanel();
expect(screen.getByTestId("otherSettings")).toMatchSnapshot();
const checkbox = screen.getByRole("checkbox", {
name: "Never send encrypted messages to unverified devices",
});
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"blacklistUnverifiedDevices",
null,
SettingLevel.DEVICE,
false,
);
});
it("should not display the section when the user can not set the value", async () => {
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(false);
jest.spyOn(SettingsStore, "setValue");
await renderAdvancedPanel();
expect(screen.queryByTestId("otherSettings")).toBeNull();
});
});
});

View File

@@ -7,13 +7,14 @@
import React from "react"; import React from "react";
import { render } from "jest-matrix-react"; import { render } from "jest-matrix-react";
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
import { EncryptionCard } from "../../../../../../src/components/views/settings/encryption/EncryptionCard"; import { EncryptionCard } from "../../../../../../src/components/views/settings/encryption/EncryptionCard";
describe("<EncryptionCard />", () => { describe("<EncryptionCard />", () => {
it("should render", () => { it("should render", () => {
const { asFragment } = render( const { asFragment } = render(
<EncryptionCard title="My title" description="My description"> <EncryptionCard Icon={KeyIcon} title="My title" description="My description">
Encryption card children Encryption card children
</EncryptionCard>, </EncryptionCard>,
); );

View File

@@ -0,0 +1,37 @@
/*
* 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 from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { ResetIdentityPanel } from "../../../../../../src/components/views/settings/encryption/ResetIdentityPanel";
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
describe("<ResetIdentityPanel />", () => {
let matrixClient: MatrixClient;
beforeEach(() => {
matrixClient = createTestClient();
});
it("should reset the encryption when the continue button is clicked", async () => {
const user = userEvent.setup();
const onFinish = jest.fn();
const { asFragment } = render(
<ResetIdentityPanel onFinish={onFinish} onCancelClick={jest.fn()} />,
withClientContextRenderOptions(matrixClient),
);
expect(asFragment()).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Continue" }));
expect(matrixClient.getCrypto()!.resetEncryption).toHaveBeenCalled();
expect(onFinish).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,253 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AdvancedPanel /> <EncryptionDetails /> should display a spinner when loading the device keys 1`] = `
<div
class="mx_EncryptionDetails"
data-testid="encryptionDetails"
>
<div
class="mx_EncryptionDetails_session"
>
<h3
class="mx_EncryptionDetails_session_title"
>
Encryption details
</h3>
<div>
<span>
Session ID:
</span>
<span
data-testid="deviceId"
>
ABCDEFGHI
</span>
</div>
<div>
<span>
Session key:
</span>
<span
data-testid="sessionKey"
>
<svg
aria-label="Loading…"
class="_icon_1ye7b_27"
fill="currentColor"
height="1em"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2Z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</div>
<div
class="mx_EncryptionDetails_buttons"
>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 15V7.85L9.125 9.725c-.2.2-.433.3-.7.3-.267 0-.508-.108-.725-.325a.93.93 0 0 1-.288-.712A.977.977 0 0 1 7.7 8.3l3.6-3.6c.1-.1.208-.17.325-.212.117-.042.242-.063.375-.063s.258.02.375.063a.877.877 0 0 1 .325.212l3.6 3.6c.2.2.296.438.287.713a.977.977 0 0 1-.287.687c-.2.2-.438.304-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15c0 .283-.096.52-.287.713A.968.968 0 0 1 12 16Zm-6 4c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.968.968 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
/>
</svg>
Export keys
</button>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15.575c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212l-3.6-3.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7c.183-.183.42-.28.712-.288.292-.008.53.08.713.263L11 12.15V5c0-.283.096-.52.287-.713A.968.968 0 0 1 12 4c.283 0 .52.096.713.287.191.192.287.43.287.713v7.15l1.875-1.875c.183-.183.42-.27.713-.263.291.009.529.105.712.288a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-3.6 3.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063ZM6 20c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.967.967 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
/>
</svg>
Import keys
</button>
</div>
<button
class="_button_i91xf_17 _destructive_i91xf_116"
data-kind="tertiary"
data-size="sm"
role="button"
tabindex="0"
>
Reset cryptographic identity
</button>
</div>
`;
exports[`<AdvancedPanel /> <EncryptionDetails /> should display the device keys 1`] = `
<div
class="mx_EncryptionDetails"
data-testid="encryptionDetails"
>
<div
class="mx_EncryptionDetails_session"
>
<h3
class="mx_EncryptionDetails_session_title"
>
Encryption details
</h3>
<div>
<span>
Session ID:
</span>
<span
data-testid="deviceId"
>
ABCDEFGHI
</span>
</div>
<div>
<span>
Session key:
</span>
<span
data-testid="sessionKey"
>
ed25519
</span>
</div>
</div>
<div
class="mx_EncryptionDetails_buttons"
>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 15V7.85L9.125 9.725c-.2.2-.433.3-.7.3-.267 0-.508-.108-.725-.325a.93.93 0 0 1-.288-.712A.977.977 0 0 1 7.7 8.3l3.6-3.6c.1-.1.208-.17.325-.212.117-.042.242-.063.375-.063s.258.02.375.063a.877.877 0 0 1 .325.212l3.6 3.6c.2.2.296.438.287.713a.977.977 0 0 1-.287.687c-.2.2-.438.304-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15c0 .283-.096.52-.287.713A.968.968 0 0 1 12 16Zm-6 4c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.968.968 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
/>
</svg>
Export keys
</button>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15.575c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212l-3.6-3.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7c.183-.183.42-.28.712-.288.292-.008.53.08.713.263L11 12.15V5c0-.283.096-.52.287-.713A.968.968 0 0 1 12 4c.283 0 .52.096.713.287.191.192.287.43.287.713v7.15l1.875-1.875c.183-.183.42-.27.713-.263.291.009.529.105.712.288a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-3.6 3.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063ZM6 20c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.967.967 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
/>
</svg>
Import keys
</button>
</div>
<button
class="_button_i91xf_17 _destructive_i91xf_116"
data-kind="tertiary"
data-size="sm"
role="button"
tabindex="0"
>
Reset cryptographic identity
</button>
</div>
`;
exports[`<AdvancedPanel /> <OtherSettings /> should display the blacklist of unverified devices settings 1`] = `
<form
class="_root_ssths_24 mx_OtherSettings"
data-testid="otherSettings"
>
<h3
class="mx_OtherSettings_title"
>
Other peoples devices
</h3>
<div
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
>
<input
aria-describedby="radix-:r7:"
checked=""
class="_input_qnvru_32"
id="radix-:r6:"
name="neverSendEncrypted"
title=""
type="checkbox"
/>
<div
class="_ui_qnvru_42"
/>
</div>
</div>
<div
class="_inline-field-body_ssths_46"
>
<label
class="_label_ssths_67"
for="radix-:r6:"
>
Never send encrypted messages to unverified devices
</label>
<span
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r7:"
>
By default in encrypted rooms, do not send encrypted messages to anyone until youve verified them
</span>
</div>
</div>
</form>
`;

View File

@@ -4,6 +4,7 @@ exports[`<RecoveryPanel /> should allow to change the recovery key when everythi
<DocumentFragment> <DocumentFragment>
<div <div
class="mx_SettingsSection mx_SettingsSection_newUi" class="mx_SettingsSection mx_SettingsSection_newUi"
data-testid="recoveryPanel"
> >
<div <div
class="mx_SettingsSection_header" class="mx_SettingsSection_header"
@@ -44,6 +45,7 @@ exports[`<RecoveryPanel /> should ask to enter the recovery key when secrets are
<DocumentFragment> <DocumentFragment>
<div <div
class="mx_SettingsSection mx_SettingsSection_newUi" class="mx_SettingsSection mx_SettingsSection_newUi"
data-testid="recoveryPanel"
> >
<div <div
class="mx_SettingsSection_header" class="mx_SettingsSection_header"
@@ -104,6 +106,7 @@ exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no
<DocumentFragment> <DocumentFragment>
<div <div
class="mx_SettingsSection mx_SettingsSection_newUi" class="mx_SettingsSection mx_SettingsSection_newUi"
data-testid="recoveryPanel"
> >
<div <div
class="mx_SettingsSection_header" class="mx_SettingsSection_header"
@@ -147,6 +150,7 @@ exports[`<RecoveryPanel /> should be in loading state when checking the recovery
<DocumentFragment> <DocumentFragment>
<div <div
class="mx_SettingsSection mx_SettingsSection_newUi" class="mx_SettingsSection mx_SettingsSection_newUi"
data-testid="recoveryPanel"
> >
<div <div
class="mx_SettingsSection_header" class="mx_SettingsSection_header"

View File

@@ -0,0 +1,184 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ResetIdentityPanel /> should reset the encryption when the continue button is clicked 1`] = `
<DocumentFragment>
<nav
class="_breadcrumb_ikpbb_17"
>
<button
aria-label="Back"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
role="button"
style="--cpd-icon-button-size: 28px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</div>
</button>
<ol
class="_pages_ikpbb_26"
>
<li>
<a
class="_link_ue21z_17"
data-kind="primary"
data-size="small"
rel="noreferrer noopener"
role="button"
tabindex="0"
>
Encryption
</a>
</li>
<li>
<span
aria-current="page"
class="_last-page_ikpbb_39"
>
Reset encryption
</span>
</li>
</ol>
</nav>
<div
class="mx_EncryptionCard mx_ResetIdentityPanel"
>
<div
class="mx_EncryptionCard_header"
>
<div
class="_content_md016_17 _destructive_md016_43"
data-size="large"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h2
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Are you sure you want to reset your identity?
</h2>
</div>
<div
class="mx_ResetIdentityPanel_content"
>
<ul
class="_visual-list_4dcf8_17"
>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26 _visual-list-item-icon-success_bqeu7_31"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
Your account details, contacts, preferences, and chat list will be kept
</li>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.287 7.287A.968.968 0 0 1 12 7c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 9a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 8c0-.283.096-.52.287-.713Zm0 4A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713v4a.97.97 0 0 1-.287.712A.968.968 0 0 1 12 17a.968.968 0 0 1-.713-.288A.968.968 0 0 1 11 16v-4c0-.283.096-.52.287-.713Z"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
fill-rule="evenodd"
/>
</svg>
You will lose any message history thats stored only on the server
</li>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.287 7.287A.968.968 0 0 1 12 7c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 9a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 8c0-.283.096-.52.287-.713Zm0 4A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713v4a.97.97 0 0 1-.287.712A.968.968 0 0 1 12 17a.968.968 0 0 1-.713-.288A.968.968 0 0 1 11 16v-4c0-.283.096-.52.287-.713Z"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
fill-rule="evenodd"
/>
</svg>
You will need to verify all your existing devices and contacts again
</li>
</ul>
<span>
Only do this if you believe your account has been compromised.
</span>
</div>
<div
class="mx_ResetIdentityPanel_footer"
>
<button
class="_button_i91xf_17 _destructive_i91xf_116"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
Continue
</button>
<button
class="_button_i91xf_17"
data-kind="tertiary"
data-size="lg"
role="button"
tabindex="0"
>
Cancel
</button>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -94,4 +94,19 @@ describe("<EncryptionUserSettingsTab />", () => {
await waitFor(() => expect(screen.getByText("Set up recovery")).toBeInTheDocument()); await waitFor(() => expect(screen.getByText("Set up recovery")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("should display the reset identity panel when the user clicks on the reset cryptographic identity panel", async () => {
const user = userEvent.setup();
const { asFragment } = renderComponent();
await waitFor(() => {
const button = screen.getByRole("button", { name: "Reset cryptographic identity" });
expect(button).toBeInTheDocument();
user.click(button);
});
await waitFor(() =>
expect(screen.getByText("Are you sure you want to reset your identity?")).toBeInTheDocument(),
);
expect(asFragment()).toMatchSnapshot();
});
}); });

View File

@@ -81,6 +81,198 @@ exports[`<EncryptionUserSettingsTab /> should display the change recovery key pa
</DocumentFragment> </DocumentFragment>
`; `;
exports[`<EncryptionUserSettingsTab /> should display the reset identity panel when the user clicks on the reset cryptographic identity panel 1`] = `
<DocumentFragment>
<div
class="mx_SettingsTab mx_EncryptionUserSettingsTab"
data-testid="encryptionTab"
>
<div
class="mx_SettingsTab_sections"
>
<nav
class="_breadcrumb_ikpbb_17"
>
<button
aria-label="Back"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
role="button"
style="--cpd-icon-button-size: 28px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</div>
</button>
<ol
class="_pages_ikpbb_26"
>
<li>
<a
class="_link_ue21z_17"
data-kind="primary"
data-size="small"
rel="noreferrer noopener"
role="button"
tabindex="0"
>
Encryption
</a>
</li>
<li>
<span
aria-current="page"
class="_last-page_ikpbb_39"
>
Reset encryption
</span>
</li>
</ol>
</nav>
<div
class="mx_EncryptionCard mx_ResetIdentityPanel"
>
<div
class="mx_EncryptionCard_header"
>
<div
class="_content_md016_17 _destructive_md016_43"
data-size="large"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h2
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Are you sure you want to reset your identity?
</h2>
</div>
<div
class="mx_ResetIdentityPanel_content"
>
<ul
class="_visual-list_4dcf8_17"
>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26 _visual-list-item-icon-success_bqeu7_31"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
Your account details, contacts, preferences, and chat list will be kept
</li>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.287 7.287A.968.968 0 0 1 12 7c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 9a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 8c0-.283.096-.52.287-.713Zm0 4A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713v4a.97.97 0 0 1-.287.712A.968.968 0 0 1 12 17a.968.968 0 0 1-.713-.288A.968.968 0 0 1 11 16v-4c0-.283.096-.52.287-.713Z"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
fill-rule="evenodd"
/>
</svg>
You will lose any message history thats stored only on the server
</li>
<li
class="_visual-list-item_bqeu7_17"
>
<svg
aria-hidden="true"
class="_visual-list-item-icon_bqeu7_26"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.287 7.287A.968.968 0 0 1 12 7c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 9a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 8c0-.283.096-.52.287-.713Zm0 4A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713v4a.97.97 0 0 1-.287.712A.968.968 0 0 1 12 17a.968.968 0 0 1-.713-.288A.968.968 0 0 1 11 16v-4c0-.283.096-.52.287-.713Z"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
fill-rule="evenodd"
/>
</svg>
You will need to verify all your existing devices and contacts again
</li>
</ul>
<span>
Only do this if you believe your account has been compromised.
</span>
</div>
<div
class="mx_ResetIdentityPanel_footer"
>
<button
class="_button_i91xf_17 _destructive_i91xf_116"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
Continue
</button>
<button
class="_button_i91xf_17"
data-kind="tertiary"
data-size="lg"
role="button"
tabindex="0"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`<EncryptionUserSettingsTab /> should display the set up recovery key when the user clicks on the set up recovery key button 1`] = ` exports[`<EncryptionUserSettingsTab /> should display the set up recovery key when the user clicks on the set up recovery key button 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div

View File

@@ -3467,10 +3467,10 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.3.tgz#8205ffb455a09d71a02d838f3dbb8503c4e6ec27" resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.3.tgz#8205ffb455a09d71a02d838f3dbb8503c4e6ec27"
integrity sha512-U4UF7MVguENf0lQnkU2a9p/3llTsLXzbzmFFOxi0h6ny2igNxZj/kROP/jXTxxV9xD4TNn3z098Bos4J/qJpBA== integrity sha512-U4UF7MVguENf0lQnkU2a9p/3llTsLXzbzmFFOxi0h6ny2igNxZj/kROP/jXTxxV9xD4TNn3z098Bos4J/qJpBA==
"@vector-im/compound-web@^7.5.0": "@vector-im/compound-web@^7.6.1":
version "7.5.0" version "7.6.1"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.5.0.tgz#1547af5f0ee27b94f79ab11eee006059f3d09707" resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.6.1.tgz#c41fc8b2e4c5938041e1f0ff9792f8fbadd9ab87"
integrity sha512-Xhef8H5WrRmPuanzRBs8rnl+hwbcQnC7nKSCupUczAQ5hjlieBx4vcQYQ/nMkrs4rMGjgfFtR3E18wT5LlML/A== integrity sha512-LdHGFslkyky2aNPZwIOY9GgWn1VOUa2EBKHln8HBvpxnYPcs3/A2nb1+6SsJ7+Y0TzKc2HA0rZ3qPDhQ3hjZYQ==
dependencies: dependencies:
"@floating-ui/react" "^0.27.0" "@floating-ui/react" "^0.27.0"
"@radix-ui/react-context-menu" "^2.2.1" "@radix-ui/react-context-menu" "^2.2.1"