Prompt users to set up recovery (#30075)
* Show indicator in settings dialog when user doesn't have recovery set up * Update settings headers to use red dot for recommended settings * update recovery setup toast and remember if the user dismisses it * update playwright snapshots * use typed event emitters * reverse logic for the account data flag * fix comment and type
This commit is contained in:
@@ -153,7 +153,11 @@ export const mockClientMethodsCrypto = (): Partial<
|
||||
> => ({
|
||||
isKeyBackupKeyStored: jest.fn(),
|
||||
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
||||
secretStorage: { hasKey: jest.fn(), isStored: jest.fn().mockResolvedValue(null) },
|
||||
secretStorage: {
|
||||
hasKey: jest.fn(),
|
||||
isStored: jest.fn().mockResolvedValue(null),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
getCrypto: jest.fn().mockReturnValue({
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
getCrossSigningStatus: jest.fn().mockResolvedValue({
|
||||
|
||||
@@ -480,6 +480,15 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the recovery account data when we call recordRecoveryDisabled", async () => {
|
||||
const instance = await createAndStart();
|
||||
await instance.recordRecoveryDisabled();
|
||||
|
||||
expect(mockClient.setAccountData).toHaveBeenCalledWith("io.element.recovery", {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when crypto is in use and set up", () => {
|
||||
beforeEach(() => {
|
||||
// Encryption is in use
|
||||
|
||||
@@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactElement } from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import { mocked, type MockedObject } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent, MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SettingsStore, { type CallbackFn } from "../../../../../src/settings/SettingsStore";
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
@@ -250,4 +250,28 @@ describe("<UserSettingsDialog />", () => {
|
||||
// unwatches settings on unmount
|
||||
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir");
|
||||
});
|
||||
|
||||
it("displays an indicator when user needs to set up recovery", async () => {
|
||||
// Initially, the user doesn't have secret storage, so it should display
|
||||
// an indicator.
|
||||
mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null);
|
||||
|
||||
const { container } = render(getComponent());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".mx_SettingsDialog_tabLabelsAlert")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Test that the handler ignores unknown account data
|
||||
mockClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: "bar" }));
|
||||
|
||||
// The user now has secret storage. Trigger an update and check that
|
||||
// the indicator disappears.
|
||||
mockClient.secretStorage.getDefaultKeyId.mockResolvedValue("foo");
|
||||
mockClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: "m.secret_storage.default_key" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".mx_SettingsDialog_tabLabelsAlert")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`<SettingsHeader /> should render the component 1`] = `
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Settings Header
|
||||
Settings Header
|
||||
</h2>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -13,12 +13,9 @@ exports[`<SettingsHeader /> should render the component 1`] = `
|
||||
exports[`<SettingsHeader /> should render the component with the recommended tag 1`] = `
|
||||
<DocumentFragment>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader mx_SettingsHeader_recommended"
|
||||
>
|
||||
Settings Header
|
||||
<span>
|
||||
Recommended
|
||||
</span>
|
||||
Settings Header
|
||||
</h2>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -104,12 +104,14 @@ describe("<ChangeRecoveryKey />", () => {
|
||||
expect(screen.getByText("The recovery key you entered is not correct.")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
const setAccountDataSpy = jest.spyOn(matrixClient, "setAccountData");
|
||||
await userEvent.clear(input);
|
||||
// If the user enters the correct recovery key, the finish button should be enabled
|
||||
await userEvent.type(input, "encoded private key");
|
||||
await waitFor(() => expect(finishButton).not.toHaveAttribute("aria-disabled", "true"));
|
||||
|
||||
await user.click(finishButton);
|
||||
expect(setAccountDataSpy).toHaveBeenCalledWith("io.element.recovery", { enabled: true });
|
||||
expect(onFinish).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ exports[`<RecoveryPanel /> should allow to change the recovery key when everythi
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
@@ -51,12 +51,9 @@ exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader mx_SettingsHeader_recommended"
|
||||
>
|
||||
Recovery
|
||||
<span>
|
||||
Recommended
|
||||
</span>
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
@@ -97,7 +94,7 @@ exports[`<RecoveryPanel /> should be in loading state when checking the recovery
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ exports[`<RecoveyPanelOutOfSync /> should render 1`] = `
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
Recovery
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
|
||||
@@ -18,7 +18,7 @@ exports[`<EncryptionUserSettingsTab /> should display a verify button when the e
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Device not verified
|
||||
Device not verified
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
@@ -100,7 +100,7 @@ exports[`<EncryptionUserSettingsTab /> should display the recovery out of sync p
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
Recovery
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
|
||||
@@ -36,14 +36,16 @@ describe("SetupEncryptionToast", () => {
|
||||
expect(await screen.findByRole("heading", { name: "Set up recovery" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should dismiss the toast when 'not now' button clicked", async () => {
|
||||
it("should dismiss the toast when 'Dismiss' button clicked, and remember it", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordRecoveryDisabled");
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup");
|
||||
|
||||
showToast(Kind.SET_UP_RECOVERY);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Not now" }));
|
||||
await user.click(await screen.findByRole("button", { name: "Dismiss" }));
|
||||
|
||||
expect(DeviceListener.sharedInstance().recordRecoveryDisabled).toHaveBeenCalled();
|
||||
expect(DeviceListener.sharedInstance().dismissEncryptionSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user