Apply new design and display logic to logout confirmation dialog (#33426)

* apply new design to logout dialog

* factor out check for other verified devices

* only show recovery warning when user has no other verified devices

* fix playwright tests

* tweak style to better match design

* another playwright test fix

* fix playwright

* Look for the remove button within the dialog

* Use testid to locate 'Remove this device' button

* move rendering to sub-components, rather than embedded functions

* use <Type> element

* use <Text> for the <a> element

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
Hubert Chathi
2026-06-01 23:08:18 -04:00
committed by GitHub
parent 178e909dea
commit 2bd5224dbe
17 changed files with 771 additions and 354 deletions
@@ -1192,13 +1192,18 @@ describe("<MatrixChat />", () => {
getVersion: jest.fn().mockReturnValue("Version 0"),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
getUserDeviceInfo: jest.fn().mockReturnValue({
get: jest
.fn()
.mockReturnValue(
new Map([
["devid", { dehydrated: false, getIdentityKey: jest.fn().mockReturnValue("k") }],
]),
),
get: jest.fn().mockReturnValue(
new Map([
[
"devid",
{
deviceId: "devid",
dehydrated: false,
getIdentityKey: jest.fn().mockReturnValue("k"),
},
],
]),
),
}),
getUserVerificationStatus: jest
.fn()
@@ -8,11 +8,17 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { mocked, type MockedObject } from "jest-mock";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type CryptoApi, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { Device, DeviceVerification, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type CryptoApi, DeviceVerificationStatus, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { fireEvent, render, type RenderResult, screen, waitFor } from "jest-matrix-react";
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils";
import {
filterConsole,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsDevice,
mockClientMethodsUser,
} from "../../../../test-utils";
import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog";
import dispatch from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
@@ -25,10 +31,13 @@ describe("LogoutDialog", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsCrypto(),
...mockClientMethodsUser(),
...mockClientMethodsDevice(),
});
mockCrypto = mocked(mockClient.getCrypto()!);
Object.assign(mockCrypto, {
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
});
});
@@ -52,37 +61,131 @@ describe("LogoutDialog", () => {
await rendered.findByText("Are you sure you want to remove this device?");
});
it("shows a regular dialog if the user has another verified device", async () => {
const userId = mockClient.getUserId()!;
mockCrypto.getUserDeviceInfo.mockResolvedValue(
new Map([
[
userId,
new Map([
[
"otherDevice",
new Device({
deviceId: "otherDevice",
userId: userId,
algorithms: [],
keys: new Map([["curve25519:otherDevice", "akey"]]),
verified: DeviceVerification.Verified,
dehydrated: false,
}),
],
]),
],
]),
);
mockCrypto.getDeviceVerificationStatus.mockResolvedValue(new DeviceVerificationStatus({ signedByOwner: true }));
const rendered = renderComponent();
await rendered.findByText("Are you sure you want to remove this device?");
});
it("prompts user to set up recovery if backups are enabled but recovery isn't", async () => {
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
mockCrypto.isSecretStorageReady.mockResolvedValue(false);
const rendered = renderComponent();
await rendered.findByText("You'll lose access to your encrypted messages");
await rendered.findByText("You're about to lose access to your encrypted chats");
});
it("Prompts user to go to settings if there is a backup on the server", async () => {
it("Prompts user to set up recovery if there is a backup on the server but no secret storage", async () => {
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
const rendered = renderComponent();
await rendered.findByText("Go to Settings");
await rendered.findByText("Get recovery key");
expect(rendered.container).toMatchSnapshot();
jest.spyOn(dispatch, "dispatch");
fireEvent.click(await screen.findByRole("button", { name: "Go to Settings" }));
fireEvent.click(await screen.findByRole("button", { name: "Get recovery key" }));
await waitFor(() =>
expect(dispatch.dispatch).toHaveBeenCalledWith({
action: Action.ViewUserSettings,
initialTabId: UserTab.Encryption,
props: {
initialEncryptionState: "set_recovery_key",
},
}),
);
});
it("Prompts user to go to settings if there is no backup on the server", async () => {
it("Prompts user to set up recovery if there is no backup on the server", async () => {
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
const rendered = renderComponent();
await rendered.findByText("Go to Settings");
await rendered.findByText("Get recovery key");
expect(rendered.container).toMatchSnapshot();
});
fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
it("Prompts user to set up recovery if there is no backup on the server, and the user has other unverified/dehydrated devices", async () => {
const userId = mockClient.getUserId()!;
const testDeviceId = mockClient.getDeviceId()!;
mockCrypto.getUserDeviceInfo.mockResolvedValue(
new Map([
[
userId,
new Map([
// the current device
[
testDeviceId,
new Device({
deviceId: testDeviceId,
userId: userId,
algorithms: [],
keys: new Map([["curve25519:test-device-id", "akey"]]),
verified: DeviceVerification.Verified,
dehydrated: false,
}),
],
// a dehydrated device
[
"dehydratedDevice",
new Device({
deviceId: "otherDevice",
userId: userId,
algorithms: [],
keys: new Map([["curve25519:dehydratedDevice", "akey"]]),
verified: DeviceVerification.Verified,
dehydrated: true,
}),
],
// an unverified device
[
"otherDevice",
new Device({
deviceId: "otherDevice",
userId: userId,
algorithms: [],
keys: new Map([["curve25519:otherDevice", "akey"]]),
verified: DeviceVerification.Unverified,
dehydrated: false,
}),
],
]),
],
]),
);
mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId: string, deviceId: string) => {
switch (deviceId) {
case testDeviceId:
case "dehydratedDevice":
return new DeviceVerificationStatus({ signedByOwner: true });
case "otherDevice":
return new DeviceVerificationStatus({ signedByOwner: false });
default:
throw new Error("Unknown device ID");
}
});
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
const rendered = renderComponent();
await rendered.findByText("Get recovery key");
expect(rendered.container).toMatchSnapshot();
});
describe("when there is an error fetching backups", () => {
@@ -92,7 +195,7 @@ describe("LogoutDialog", () => {
throw new Error("beep");
});
const rendered = renderComponent();
await rendered.findByText("Go to Settings");
await rendered.findByText("Get recovery key");
});
});
});
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`LogoutDialog Prompts user to go to settings if there is a backup on the server 1`] = `
exports[`LogoutDialog Prompts user to set up recovery if there is a backup on the server but no secret storage 1`] = `
<div>
<div
data-focus-guard="true"
@@ -10,71 +10,123 @@ exports[`LogoutDialog Prompts user to go to settings if there is a backup on the
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_Dialog_fixedWidth"
class="mx_LogoutDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
/>
<div
class="mx_EncryptionCard mx_EncryptionCard_noBorder"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
You'll lose access to your encrypted messages
</h1>
</div>
<div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
class="mx_EncryptionCard_header"
>
<div>
<p>
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
</p>
<p>
When you remove this device you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
</p>
<p>
Back up your keys before removing this device to avoid losing them.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
<div
class="_big-icon_1ssbv_8"
data-kind="critical"
data-size="lg"
>
<button>
I don't want my encrypted messages
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
Go to Settings
</button>
</span>
</div>
<details>
<summary
class="mx_LogoutDialog_ExportKeyAdvanced"
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<h2
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
Advanced
</summary>
<p>
<button>
Manually export keys
</button>
You're about to lose access to your encrypted chats
</h2>
</div>
<div
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<p
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
>
This is your only device. If you remove it you'll need a recovery key in order to confirm your digital identity and restore your encrypted chats the next time you sign in.
</p>
</details>
<a
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
href="https://element.io/en/help#encryption16"
target="_blank"
>
Learn more
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
</div>
<div
class="mx_EncryptionCard_buttons"
>
<button
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="primary"
data-size="lg"
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="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
/>
</svg>
Get recovery key
</button>
<button
class="_button_1nw83_8 _has-icon_1nw83_60 _destructive_1nw83_110"
data-kind="tertiary"
data-size="lg"
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="M9 12.031q0-.424.288-.712A.97.97 0 0 1 10 11.03h7.15l-1.875-1.875a.96.96 0 0 1-.3-.7q0-.4.325-.725a.93.93 0 0 1 .712-.287.98.98 0 0 1 .688.287l3.6 3.6q.15.15.212.325.063.175.063.375 0 .201-.062.375a.9.9 0 0 1-.213.325l-3.6 3.6q-.3.3-.712.288a.98.98 0 0 1-.688-.288 1.02 1.02 0 0 1-.312-.712.93.93 0 0 1 .287-.713l1.875-1.875H10a.97.97 0 0 1-.712-.287A.97.97 0 0 1 9 12.03m-6-7q0-.824.588-1.412A1.93 1.93 0 0 1 5 3.03h6q.424 0 .713.288.287.287.287.712t-.287.713A.97.97 0 0 1 11 5.03H5v14h6q.424 0 .713.288.287.287.287.712t-.287.713a.97.97 0 0 1-.713.287H5q-.824 0-1.412-.587A1.93 1.93 0 0 1 3 19.03z"
/>
</svg>
Remove this device anyway
</button>
</div>
</div>
<div
aria-describedby="react-use-id-1"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
@@ -101,7 +153,7 @@ exports[`LogoutDialog Prompts user to go to settings if there is a backup on the
</div>
`;
exports[`LogoutDialog Prompts user to go to settings if there is no backup on the server 1`] = `
exports[`LogoutDialog Prompts user to set up recovery if there is no backup on the server 1`] = `
<div>
<div
data-focus-guard="true"
@@ -111,71 +163,276 @@ exports[`LogoutDialog Prompts user to go to settings if there is no backup on th
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_Dialog_fixedWidth"
class="mx_LogoutDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
/>
<div
class="mx_EncryptionCard mx_EncryptionCard_noBorder"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
You'll lose access to your encrypted messages
</h1>
</div>
<div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
class="mx_EncryptionCard_header"
>
<div>
<p>
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
</p>
<p>
When you remove this device you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
</p>
<p>
Back up your keys before removing this device to avoid losing them.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
<div
class="_big-icon_1ssbv_8"
data-kind="critical"
data-size="lg"
>
<button>
I don't want my encrypted messages
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
Go to Settings
</button>
</span>
</div>
<details>
<summary
class="mx_LogoutDialog_ExportKeyAdvanced"
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<h2
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
Advanced
</summary>
<p>
<button>
Manually export keys
</button>
You're about to lose access to your encrypted chats
</h2>
</div>
<div
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<p
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
>
This is your only device. If you remove it you'll need a recovery key in order to confirm your digital identity and restore your encrypted chats the next time you sign in.
</p>
</details>
<a
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
href="https://element.io/en/help#encryption16"
target="_blank"
>
Learn more
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
</div>
<div
class="mx_EncryptionCard_buttons"
>
<button
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="primary"
data-size="lg"
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="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
/>
</svg>
Get recovery key
</button>
<button
class="_button_1nw83_8 _has-icon_1nw83_60 _destructive_1nw83_110"
data-kind="tertiary"
data-size="lg"
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="M9 12.031q0-.424.288-.712A.97.97 0 0 1 10 11.03h7.15l-1.875-1.875a.96.96 0 0 1-.3-.7q0-.4.325-.725a.93.93 0 0 1 .712-.287.98.98 0 0 1 .688.287l3.6 3.6q.15.15.212.325.063.175.063.375 0 .201-.062.375a.9.9 0 0 1-.213.325l-3.6 3.6q-.3.3-.712.288a.98.98 0 0 1-.688-.288 1.02 1.02 0 0 1-.312-.712.93.93 0 0 1 .287-.713l1.875-1.875H10a.97.97 0 0 1-.712-.287A.97.97 0 0 1 9 12.03m-6-7q0-.824.588-1.412A1.93 1.93 0 0 1 5 3.03h6q.424 0 .713.288.287.287.287.712t-.287.713A.97.97 0 0 1 11 5.03H5v14h6q.424 0 .713.288.287.287.287.712t-.287.713a.97.97 0 0 1-.713.287H5q-.824 0-1.412-.587A1.93 1.93 0 0 1 3 19.03z"
/>
</svg>
Remove this device anyway
</button>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`LogoutDialog Prompts user to set up recovery if there is no backup on the server, and the user has other unverified/dehydrated devices 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_LogoutDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
/>
<div
class="mx_EncryptionCard mx_EncryptionCard_noBorder"
>
<div
class="mx_EncryptionCard_header"
>
<div
class="_big-icon_1ssbv_8"
data-kind="critical"
data-size="lg"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<h2
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
You're about to lose access to your encrypted chats
</h2>
</div>
<div
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<p
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
>
This is your only device. If you remove it you'll need a recovery key in order to confirm your digital identity and restore your encrypted chats the next time you sign in.
</p>
<a
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
href="https://element.io/en/help#encryption16"
target="_blank"
>
Learn more
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
</div>
<div
class="mx_EncryptionCard_buttons"
>
<button
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="primary"
data-size="lg"
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="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
/>
</svg>
Get recovery key
</button>
<button
class="_button_1nw83_8 _has-icon_1nw83_60 _destructive_1nw83_110"
data-kind="tertiary"
data-size="lg"
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="M9 12.031q0-.424.288-.712A.97.97 0 0 1 10 11.03h7.15l-1.875-1.875a.96.96 0 0 1-.3-.7q0-.4.325-.725a.93.93 0 0 1 .712-.287.98.98 0 0 1 .688.287l3.6 3.6q.15.15.212.325.063.175.063.375 0 .201-.062.375a.9.9 0 0 1-.213.325l-3.6 3.6q-.3.3-.712.288a.98.98 0 0 1-.688-.288 1.02 1.02 0 0 1-.312-.712.93.93 0 0 1 .287-.713l1.875-1.875H10a.97.97 0 0 1-.712-.287A.97.97 0 0 1 9 12.03m-6-7q0-.824.588-1.412A1.93 1.93 0 0 1 5 3.03h6q.424 0 .713.288.287.287.287.712t-.287.713A.97.97 0 0 1 11 5.03H5v14h6q.424 0 .713.288.287.287.287.712t-.287.713a.97.97 0 0 1-.713.287H5q-.824 0-1.412-.587A1.93 1.93 0 0 1 3 19.03z"
/>
</svg>
Remove this device anyway
</button>
</div>
</div>
<div
aria-describedby="react-use-id-1"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
@@ -79,10 +79,12 @@ function mockClient() {
} as unknown as Mocked<CryptoApi>;
const userId = "@user:server";
const deviceId = "ADEVICE";
getMockClientWithEventEmitter({
getCrypto: jest.fn().mockReturnValue(mockCrypto),
getUserId: jest.fn().mockReturnValue(userId),
getDeviceId: jest.fn().mockReturnValue(deviceId),
secretStorage: { isStored: jest.fn().mockReturnValue({}) },
});
}
@@ -18,7 +18,6 @@ import { mkStubRoom, stubClient } from "../../../../test-utils";
import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext";
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import dis from "../../../../../src/dispatcher/dispatcher";
import Modal from "../../../../../src/Modal";
interface MockedAvatarSettingProps {
@@ -218,13 +217,15 @@ describe("ProfileSettings", () => {
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
});
it("signs out directly if no rooms are encrypted", async () => {
it("displays confirmation dialog if no rooms are encrypted", async () => {
jest.spyOn(Modal, "createDialog");
renderProfileSettings(toastRack, client);
const signOutButton = await screen.findByText("Remove this device");
await userEvent.click(signOutButton);
expect(dis.dispatch).toHaveBeenCalledWith({ action: "logout" });
expect(Modal.createDialog).toHaveBeenCalled();
});
it("displays confirmation dialog if rooms are encrypted", async () => {
@@ -234,6 +235,7 @@ describe("ProfileSettings", () => {
client.getRooms = jest.fn().mockReturnValue([mockRoom]);
client.getCrypto = jest.fn().mockReturnValue({
isEncryptionEnabledInRoom: jest.fn().mockReturnValue(true),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
});
renderProfileSettings(toastRack, client);