You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-08 03:42:14 +03:00
Prompt the user when key storage is unexpectedly off (#29912)
* Assert that we set backup_disabled when turning off key storage * Prompt the user when key storage is unexpectedly off * Playwright tests for the Turn on key storage toast
This commit is contained in:
@@ -67,8 +67,9 @@ test.describe("Cryptography", function () {
|
||||
// Bob has a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
|
||||
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Not now" }).click();
|
||||
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
|
||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
|
@@ -8,7 +8,8 @@
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
@@ -53,3 +54,114 @@ test.describe("Key storage out of sync toast", () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("'Turn on key storage' toast", () => {
|
||||
let botClient: Bot | undefined;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials, toasts }) => {
|
||||
// Set up all crypto stuff. Key storage defaults to on.
|
||||
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
const recoveryKey = res.recoveryKey;
|
||||
botClient = res.botClient;
|
||||
|
||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await toasts.rejectToast("Notifications");
|
||||
});
|
||||
|
||||
test("should not show toast if key storage is on", async ({ page, toasts }) => {
|
||||
// Given the default situation after signing in
|
||||
// Then no toast is shown (because key storage is on)
|
||||
await toasts.assertNoToasts();
|
||||
|
||||
// When we reload
|
||||
await page.reload();
|
||||
|
||||
// Give the toasts time to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Then still no toast is shown
|
||||
await toasts.assertNoToasts();
|
||||
});
|
||||
|
||||
test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => {
|
||||
// Given the backup is disabled because we disabled it
|
||||
await disableKeyBackup(app);
|
||||
|
||||
// Then no toast is shown
|
||||
await toasts.assertNoToasts();
|
||||
|
||||
// When we reload
|
||||
await page.reload();
|
||||
|
||||
// Give the toasts time to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Then still no toast is shown
|
||||
await toasts.assertNoToasts();
|
||||
});
|
||||
|
||||
test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => {
|
||||
// Given the backup is disabled but we didn't set account data saying that is expected
|
||||
await disableKeyBackup(app);
|
||||
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
|
||||
|
||||
// Wait for the account data setting to stick
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// When we enter the app
|
||||
await page.reload();
|
||||
|
||||
// Then the toast is displayed
|
||||
let toast = await toasts.getToast("Turn on key storage");
|
||||
|
||||
// And when we click "Continue"
|
||||
await toast.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Then we see the Encryption settings dialog with an option to turn on key storage
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
|
||||
// And when we close that
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
// Then we see the toast again
|
||||
toast = await toasts.getToast("Turn on key storage");
|
||||
|
||||
// And when we click "Dismiss"
|
||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||
|
||||
// Then we see the "are you sure?" dialog
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to keep key storage turned off?" }),
|
||||
).toBeVisible();
|
||||
|
||||
// And when we close it by clicking away
|
||||
await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } });
|
||||
|
||||
// Then we see the toast again
|
||||
toast = await toasts.getToast("Turn on key storage");
|
||||
|
||||
// And when we click Dismiss and then "Go to Settings"
|
||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||
await page.getByRole("button", { name: "Go to Settings" }).click();
|
||||
|
||||
// Then we see Encryption settings again
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
|
||||
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
toast = await toasts.getToast("Turn on key storage");
|
||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
|
||||
// Then the toast is gone
|
||||
await toasts.assertNoToasts();
|
||||
});
|
||||
});
|
||||
|
@@ -316,6 +316,25 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the encryption settings and disable key storage (and recovery)
|
||||
* Assumes that the current device has been verified
|
||||
*/
|
||||
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||
if (await keyStorageToggle.isChecked()) {
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
||||
|
||||
// Wait for the update to account data to stick
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
await app.settings.closeDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||
*
|
||||
|
@@ -593,6 +593,7 @@ legend {
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_EncryptionUserSettingsTab button,
|
||||
.mx_EncryptionCard button,
|
||||
.mx_UserProfileSettings button,
|
||||
.mx_ShareDialog button,
|
||||
.mx_UnpinAllDialog button,
|
||||
|
@@ -131,6 +131,7 @@
|
||||
@import "./views/dialogs/_BugReportDialog.pcss";
|
||||
@import "./views/dialogs/_ChangelogDialog.pcss";
|
||||
@import "./views/dialogs/_CompoundDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmKeyStorageOffDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.pcss";
|
||||
|
@@ -79,6 +79,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
background-color: $primary-content;
|
||||
}
|
||||
|
||||
&.mx_Toast_icon_key_storage::after {
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/settings-solid.svg");
|
||||
background-color: $primary-content;
|
||||
}
|
||||
|
||||
&.mx_Toast_icon_labs::after {
|
||||
mask-image: url("$(res)/img/element-icons/flask.svg");
|
||||
background-color: $secondary-content;
|
||||
|
16
res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss
Normal file
16
res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_ConfirmKeyStorageOffDialog {
|
||||
.mx_Dialog_border {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.mx_EncryptionCard {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@@ -97,6 +97,7 @@ export default class DeviceListener {
|
||||
this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
this.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged);
|
||||
this.client.on(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.on(ClientEvent.Sync, this.onSync);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
@@ -132,7 +133,7 @@ export default class DeviceListener {
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.cachedKeyBackupStatus = undefined;
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
this.ourDeviceIdsAtStart = null;
|
||||
this.displayingToastsForDeviceIds = new Set();
|
||||
this.client = undefined;
|
||||
@@ -157,6 +158,13 @@ export default class DeviceListener {
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account data "m.org.matrix.custom.backup_disabled" to { "disabled": true }.
|
||||
*/
|
||||
public async recordKeyBackupDisabled(): Promise<void> {
|
||||
await this.client?.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
|
||||
}
|
||||
|
||||
private async ensureDeviceIdsAtStartPopulated(): Promise<void> {
|
||||
if (this.ourDeviceIdsAtStart === null) {
|
||||
this.ourDeviceIdsAtStart = await this.getDeviceIds();
|
||||
@@ -192,6 +200,11 @@ export default class DeviceListener {
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onKeyBackupStatusChanged = (): void => {
|
||||
this.cachedKeyBackupUploadActive = undefined;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onCrossSingingKeysChanged = (): void => {
|
||||
this.recheck();
|
||||
};
|
||||
@@ -201,11 +214,13 @@ export default class DeviceListener {
|
||||
// * migrated SSSS to symmetric
|
||||
// * uploaded keys to secret storage
|
||||
// * completed secret storage creation
|
||||
// * disabled key backup
|
||||
// which result in account data changes affecting checks below.
|
||||
if (
|
||||
ev.getType().startsWith("m.secret_storage.") ||
|
||||
ev.getType().startsWith("m.cross_signing.") ||
|
||||
ev.getType() === "m.megolm_backup.v1"
|
||||
ev.getType() === "m.megolm_backup.v1" ||
|
||||
ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
) {
|
||||
this.recheck();
|
||||
}
|
||||
@@ -324,7 +339,16 @@ export default class DeviceListener {
|
||||
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
|
||||
);
|
||||
|
||||
const allSystemsReady = crossSigningReady && secretStorageReady && allCrossSigningSecretsCached;
|
||||
const keyBackupUploadActive = await this.isKeyBackupUploadActive();
|
||||
const backupDisabled = await this.recheckBackupDisabled(cli);
|
||||
|
||||
// We warn if key backup upload is turned off and we have not explicitly
|
||||
// said we are OK with that.
|
||||
const keyBackupIsOk = keyBackupUploadActive || backupDisabled;
|
||||
|
||||
const allSystemsReady =
|
||||
crossSigningReady && keyBackupIsOk && secretStorageReady && allCrossSigningSecretsCached;
|
||||
|
||||
await this.reportCryptoSessionStateToAnalytics(cli);
|
||||
|
||||
if (this.dismissedThisDeviceToast || allSystemsReady) {
|
||||
@@ -353,14 +377,19 @@ export default class DeviceListener {
|
||||
crossSigningStatus.privateKeysCachedLocally,
|
||||
);
|
||||
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
} else if (!keyBackupIsOk) {
|
||||
logSpan.info("Key backup upload is unexpectedly turned off: showing TURN_ON_KEY_STORAGE toast");
|
||||
showSetupEncryptionToast(SetupKind.TURN_ON_KEY_STORAGE);
|
||||
} else if (defaultKeyId === null) {
|
||||
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage)
|
||||
const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
|
||||
if (!disabledEvent?.getContent().disabled) {
|
||||
// The user just hasn't set up 4S yet: if they have key
|
||||
// backup, prompt them to turn on recovery too. (If not, they
|
||||
// have explicitly opted out, so don't hassle them.)
|
||||
if (keyBackupUploadActive) {
|
||||
logSpan.info("No default 4S key: showing SET_UP_RECOVERY toast");
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
|
||||
} else {
|
||||
logSpan.info("No default 4S key but backup disabled: no toast needed");
|
||||
hideSetupEncryptionToast();
|
||||
}
|
||||
} else {
|
||||
// some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
|
||||
@@ -443,6 +472,16 @@ export default class DeviceListener {
|
||||
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account data for `backup_disabled`. If this is the first time,
|
||||
* fetch it from the server (in case the initial sync has not finished).
|
||||
* Otherwise, fetch it from the store as normal.
|
||||
*/
|
||||
private async recheckBackupDisabled(cli: MatrixClient): Promise<boolean> {
|
||||
const backupDisabled = await cli.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
|
||||
return !!backupDisabled?.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports current recovery state to analytics.
|
||||
* Checks if the session is verified and if the recovery is correctly set up (i.e all secrets known locally and in 4S).
|
||||
@@ -512,7 +551,7 @@ export default class DeviceListener {
|
||||
* trigger an auto-rageshake).
|
||||
*/
|
||||
private checkKeyBackupStatus = async (): Promise<void> => {
|
||||
if (!(await this.getKeyBackupStatus())) {
|
||||
if (!(await this.isKeyBackupUploadActive())) {
|
||||
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
|
||||
}
|
||||
};
|
||||
@@ -520,28 +559,34 @@ export default class DeviceListener {
|
||||
/**
|
||||
* Is key backup enabled? Use a cached answer if we have one.
|
||||
*/
|
||||
private getKeyBackupStatus = async (): Promise<boolean> => {
|
||||
private isKeyBackupUploadActive = async (): Promise<boolean> => {
|
||||
if (!this.client) {
|
||||
// To preserve existing behaviour, if there is no client, we
|
||||
// pretend key storage is on.
|
||||
// pretend key backup upload is on.
|
||||
//
|
||||
// Someone looking to improve this code could try throwing an error
|
||||
// here since we don't expect client to be undefined.
|
||||
return true;
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
// If there is no crypto, there is no key backup
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we've already cached the answer, return it.
|
||||
if (this.cachedKeyBackupStatus !== undefined) {
|
||||
return this.cachedKeyBackupStatus;
|
||||
if (this.cachedKeyBackupUploadActive !== undefined) {
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
}
|
||||
|
||||
// Fetch the answer and cache it
|
||||
const activeKeyBackupVersion = await this.client.getCrypto()?.getActiveSessionBackupVersion();
|
||||
this.cachedKeyBackupStatus = !!activeKeyBackupVersion;
|
||||
const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
|
||||
|
||||
return this.cachedKeyBackupStatus;
|
||||
return this.cachedKeyBackupUploadActive;
|
||||
};
|
||||
private cachedKeyBackupStatus: boolean | undefined = undefined;
|
||||
private cachedKeyBackupUploadActive: boolean | undefined = undefined;
|
||||
|
||||
private onRecordClientInformationSettingChange: CallbackFn = (
|
||||
_originalSettingName,
|
||||
|
80
src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
Normal file
80
src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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 ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { EncryptionCard } from "../settings/encryption/EncryptionCard";
|
||||
import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButtons";
|
||||
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserTab";
|
||||
|
||||
interface Props {
|
||||
onFinished: (dismissed: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user whether they really want to dismiss the toast about key storage.
|
||||
*
|
||||
* Launched from the {@link SetupEncryptionToast} in mode `TURN_ON_KEY_STORAGE`,
|
||||
* when the user clicks "Dismiss". The caller handles any action via the
|
||||
* `onFinished` prop which takes a boolean that is true if the user clicked
|
||||
* "Yes, dismiss".
|
||||
*/
|
||||
export default class ConfirmKeyStorageOffDialog extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
private onGoToSettingsClick = (): void => {
|
||||
// Open Settings at the Encryption tab
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onDismissClick = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<EncryptionCard
|
||||
Icon={ErrorIcon}
|
||||
destructive={true}
|
||||
title={_t("settings|encryption|confirm_key_storage_off")}
|
||||
>
|
||||
{_t("settings|encryption|confirm_key_storage_off_description", undefined, {
|
||||
a: (sub) => (
|
||||
<>
|
||||
<br />
|
||||
<a href="https://element.io/help#encryption5" target="_blank" rel="noreferrer noopener">
|
||||
{sub} <PopOutIcon />
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
})}
|
||||
<EncryptionCardButtons>
|
||||
<Button onClick={this.onGoToSettingsClick} autoFocus kind="primary" className="">
|
||||
{_t("common|go_to_settings")}
|
||||
</Button>
|
||||
<Button onClick={this.onDismissClick} kind="secondary">
|
||||
{_t("action|yes_dismiss")}
|
||||
</Button>
|
||||
</EncryptionCardButtons>
|
||||
</EncryptionCard>
|
||||
);
|
||||
}
|
||||
}
|
@@ -157,6 +157,7 @@
|
||||
"view_message": "View message",
|
||||
"view_source": "View Source",
|
||||
"yes": "Yes",
|
||||
"yes_dismiss": "Yes, dismiss",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out"
|
||||
},
|
||||
@@ -981,6 +982,8 @@
|
||||
"setup_secure_backup": {
|
||||
"explainer": "Back up your keys before signing out to avoid losing them."
|
||||
},
|
||||
"turn_on_key_storage": "Turn on key storage",
|
||||
"turn_on_key_storage_description": "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices.",
|
||||
"udd": {
|
||||
"interactive_verification_button": "Interactively verify by emoji",
|
||||
"other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.",
|
||||
@@ -2559,6 +2562,8 @@
|
||||
"session_key": "Session key:",
|
||||
"title": "Advanced"
|
||||
},
|
||||
"confirm_key_storage_off": "Are you sure you want to keep key storage turned off?",
|
||||
"confirm_key_storage_off_description": "If you sign out of all your devices you will lose your message history and will need to verify all your existing contacts again. <a>Learn more</a>",
|
||||
"delete_key_storage": {
|
||||
"breadcrumb_page": "Delete key storage",
|
||||
"confirm": "Delete key storage",
|
||||
|
@@ -24,6 +24,7 @@ import { type OpenToTabPayload } from "../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { UserTab } from "../components/views/dialogs/UserTab";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import ConfirmKeyStorageOffDialog from "../components/views/dialogs/ConfirmKeyStorageOffDialog";
|
||||
|
||||
const TOAST_KEY = "setupencryption";
|
||||
|
||||
@@ -37,6 +38,8 @@ const getTitle = (kind: Kind): string => {
|
||||
return _t("encryption|verify_toast_title");
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
return _t("encryption|key_storage_out_of_sync");
|
||||
case Kind.TURN_ON_KEY_STORAGE:
|
||||
return _t("encryption|turn_on_key_storage");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,6 +52,8 @@ const getIcon = (kind: Kind): string | undefined => {
|
||||
case Kind.VERIFY_THIS_SESSION:
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
return "verification_warning";
|
||||
case Kind.TURN_ON_KEY_STORAGE:
|
||||
return "key_storage";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,6 +67,8 @@ const getSetupCaption = (kind: Kind): string => {
|
||||
return _t("action|verify");
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
return _t("encryption|enter_recovery_key");
|
||||
case Kind.TURN_ON_KEY_STORAGE:
|
||||
return _t("action|continue");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,6 +94,8 @@ const getSecondaryButtonLabel = (kind: Kind): string => {
|
||||
return _t("encryption|verification|unverified_sessions_toast_reject");
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
return _t("encryption|forgot_recovery_key");
|
||||
case Kind.TURN_ON_KEY_STORAGE:
|
||||
return _t("action|dismiss");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,6 +109,8 @@ const getDescription = (kind: Kind): string => {
|
||||
return _t("encryption|verify_toast_description");
|
||||
case Kind.KEY_STORAGE_OUT_OF_SYNC:
|
||||
return _t("encryption|key_storage_out_of_sync_description");
|
||||
case Kind.TURN_ON_KEY_STORAGE:
|
||||
return _t("encryption|turn_on_key_storage_description");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,6 +134,10 @@ export enum Kind {
|
||||
* Prompt the user to enter their recovery key
|
||||
*/
|
||||
KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync",
|
||||
/**
|
||||
* Prompt the user to turn on key storage
|
||||
*/
|
||||
TURN_ON_KEY_STORAGE = "turn_on_key_storage",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +158,13 @@ export const showToast = (kind: Kind): void => {
|
||||
const onPrimaryClick = async (): Promise<void> => {
|
||||
if (kind === Kind.VERIFY_THIS_SESSION) {
|
||||
Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true);
|
||||
} else if (kind == Kind.TURN_ON_KEY_STORAGE) {
|
||||
// Open the user settings dialog to the encryption tab
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
} else {
|
||||
const modal = Modal.createDialog(
|
||||
Spinner,
|
||||
@@ -161,7 +183,7 @@ export const showToast = (kind: Kind): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSecondaryClick = (): void => {
|
||||
const onSecondaryClick = async (): Promise<void> => {
|
||||
if (kind === Kind.KEY_STORAGE_OUT_OF_SYNC) {
|
||||
// Open the user settings dialog to the encryption tab and start the flow to reset encryption
|
||||
const payload: OpenToTabPayload = {
|
||||
@@ -170,6 +192,15 @@ export const showToast = (kind: Kind): void => {
|
||||
props: { initialEncryptionState: "reset_identity_forgot" },
|
||||
};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
} else if (kind === Kind.TURN_ON_KEY_STORAGE) {
|
||||
// The user clicked "Dismiss": offer them "Are you sure?"
|
||||
const modal = Modal.createDialog(ConfirmKeyStorageOffDialog, undefined, "mx_ConfirmKeyStorageOffDialog");
|
||||
const [dismissed] = await modal.finished;
|
||||
if (dismissed) {
|
||||
const deviceListener = DeviceListener.sharedInstance();
|
||||
await deviceListener.recordKeyBackupDisabled();
|
||||
deviceListener.dismissEncryptionSetup();
|
||||
}
|
||||
} else {
|
||||
DeviceListener.sharedInstance().dismissEncryptionSetup();
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ import {
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||
|
||||
import DeviceListener from "../../src/DeviceListener";
|
||||
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/DeviceListener";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast";
|
||||
import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast";
|
||||
@@ -118,6 +118,7 @@ describe("DeviceListener", () => {
|
||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
||||
setAccountData: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
getAccountDataFromServer: jest.fn(),
|
||||
deleteAccountData: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
secretStorage: {
|
||||
@@ -309,6 +310,8 @@ describe("DeviceListener", () => {
|
||||
it("hides setup encryption toast when cross signing and secret storage are ready", async () => {
|
||||
mockCrypto!.isCrossSigningReady.mockResolvedValue(true);
|
||||
mockCrypto!.isSecretStorageReady.mockResolvedValue(true);
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
await createAndStart();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
});
|
||||
@@ -377,6 +380,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
it("hides the out-of-sync toast when one of the secrets is missing", async () => {
|
||||
mockCrypto!.isSecretStorageReady.mockResolvedValue(true);
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
// First show the toast
|
||||
mockCrypto!.getCrossSigningStatus.mockResolvedValue({
|
||||
@@ -414,6 +418,7 @@ describe("DeviceListener", () => {
|
||||
it("shows set up recovery toast when user has a key backup available", async () => {
|
||||
// non falsy response
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null);
|
||||
|
||||
await createAndStart();
|
||||
@@ -444,6 +449,9 @@ describe("DeviceListener", () => {
|
||||
|
||||
it("dispatches keybackup event when key backup is not enabled", async () => {
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null);
|
||||
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
|
||||
);
|
||||
await createAndStart();
|
||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ReportKeyBackupNotEnabled,
|
||||
@@ -463,6 +471,137 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sets backup_disabled account data when we call recordKeyBackupDisabled", async () => {
|
||||
const instance = await createAndStart();
|
||||
await instance.recordKeyBackupDisabled();
|
||||
|
||||
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when crypto is in use and set up", () => {
|
||||
beforeEach(() => {
|
||||
// Encryption is in use
|
||||
mockClient.getRooms.mockReturnValue([{ roomId: "!room1" }, { roomId: "!room2" }] as unknown as Room[]);
|
||||
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
|
||||
// The device is verified
|
||||
mockCrypto.getDeviceVerificationStatus.mockResolvedValue(
|
||||
new DeviceVerificationStatus({ crossSigningVerified: true }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("but key storage is off", () => {
|
||||
beforeEach(() => {
|
||||
// There is no active key backup/storage
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("shows the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
|
||||
// Given key backup is off but the account data saying we turned it off is not set
|
||||
// (m.org.matrix.custom.backup_disabled)
|
||||
mockClient.getAccountData.mockReturnValue(undefined);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
// Given key backup is off but the account data says we turned it on (this should not happen - the
|
||||
// account data should only be updated if we turn on key storage)
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: false } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
// Given key backup is off but the account data saying we turned it off is set
|
||||
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and key storage is on", () => {
|
||||
beforeEach(() => {
|
||||
// There is an active key backup/storage
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
|
||||
// Given key backup is on and the account data saying we turned it off is not set
|
||||
mockClient.getAccountData.mockReturnValue(undefined);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
// Given key backup is on and the account data says we turned it on
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: false } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
// Given key backup is on but the account data saying we turned it off is set (this should never
|
||||
// happen - it should only be set when we turn off key storage or dismiss the toast)
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: true } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unverified sessions toasts", () => {
|
||||
const currentDevice = new Device({ deviceId, userId: userId, algorithms: [], keys: new Map() });
|
||||
const device2 = new Device({ deviceId: "d2", userId: userId, algorithms: [], keys: new Map() });
|
||||
@@ -997,6 +1136,8 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
|
||||
it("shows the 'set up recovery' toast if user has not set up 4S", async () => {
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(SetupEncryptionToast.Kind.SET_UP_RECOVERY);
|
||||
|
@@ -87,5 +87,8 @@ describe("KeyStoragePanelViewModel", () => {
|
||||
await result.current.setEnabled(false);
|
||||
|
||||
expect(mocked(matrixClient.getCrypto()!.disableKeyStorage)).toHaveBeenCalled();
|
||||
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 { render } from "jest-matrix-react";
|
||||
|
||||
import ConfirmKeyStorageOffDialog from "../../../../../src/components/views/dialogs/ConfirmKeyStorageOffDialog";
|
||||
|
||||
describe("ConfirmKeyStorageOffDialog", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={jest.fn()} />);
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onFinished with dismissed=true if we dismiss", () => {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={onFinished} />);
|
||||
|
||||
dialog.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("calls onFinished with dismissed=true if we continue", () => {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={onFinished} />);
|
||||
|
||||
dialog.getByRole("button", { name: "Go to Settings" }).click();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
@@ -0,0 +1,82 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmKeyStorageOffDialog renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_o77nw_8 _destructive_o77nw_34"
|
||||
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 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 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Are you sure you want to keep key storage turned off?
|
||||
</h2>
|
||||
</div>
|
||||
<span>
|
||||
If you sign out of all your devices you will lose your message history and will need to verify all your existing contacts again.
|
||||
<br />
|
||||
<a
|
||||
href="https://element.io/help#encryption5"
|
||||
rel="noreferrer noopener"
|
||||
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>
|
||||
</span>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Go to Settings
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Yes, dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
@@ -14,6 +14,8 @@ import ToastContainer from "../../../src/components/structures/ToastContainer";
|
||||
import { Kind, showToast } from "../../../src/toasts/SetupEncryptionToast";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import DeviceListener from "../../../src/DeviceListener";
|
||||
import Modal from "../../../src/Modal";
|
||||
import ConfirmKeyStorageOffDialog from "../../../src/components/views/dialogs/ConfirmKeyStorageOffDialog";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
@@ -83,4 +85,55 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Turn on key storage", () => {
|
||||
it("should render the toast", async () => {
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
await expect(screen.findByText("Turn on key storage")).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Dismiss" })).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Continue" })).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open settings to the Encryption tab when 'Continue' clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordKeyBackupDisabled");
|
||||
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Continue" }));
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_user_settings",
|
||||
initialTabId: "USER_ENCRYPTION_TAB",
|
||||
});
|
||||
|
||||
expect(DeviceListener.sharedInstance().recordKeyBackupDisabled).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should open the confirm key storage off dialog when 'Dismiss' clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordKeyBackupDisabled");
|
||||
|
||||
// Given that as soon as the dialog opens, it closes and says "yes they clicked dismiss"
|
||||
jest.spyOn(Modal, "createDialog").mockImplementation(() => {
|
||||
return { finished: Promise.resolve([true]) } as any;
|
||||
});
|
||||
|
||||
// When we show the toast, and click Dismiss
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Dismiss" }));
|
||||
|
||||
// Then the dialog was opened
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(
|
||||
ConfirmKeyStorageOffDialog,
|
||||
undefined,
|
||||
"mx_ConfirmKeyStorageOffDialog",
|
||||
);
|
||||
|
||||
// And the backup was disabled when the dialog's onFinished was called
|
||||
expect(DeviceListener.sharedInstance().recordKeyBackupDisabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user