mirror of
https://github.com/vector-im/element-web.git
synced 2025-08-07 06:47:06 +02:00
Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast (#29138)
* Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast * Unused import & fix test * Test 'forgot' variant * Fix dependencies * Add more toast tests * Unused import * Test initialState in Encryption Tab * Let's see if github has any more luck running this test than me * Working playwright test with screenshot * year * Convert playwright test to use the bot client * Disambiguate Co-authored-by: Florian Duros <florianduros@element.io> * Add doc & do other part of rename * Split out into custom hook * Fix tests --------- Co-authored-by: Florian Duros <florianduros@element.io>
This commit is contained in:
parent
1c4e35606c
commit
9657d39cd6
54
playwright/e2e/crypto/toasts.spec.ts
Normal file
54
playwright/e2e/crypto/toasts.spec.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
|
||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
@ -214,6 +214,11 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
// if a securityKey was given, verify the new device
|
||||
if (securityKey !== undefined) {
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" });
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
// Fill in the security key
|
||||
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -50,6 +50,7 @@ import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserS
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
showMsc4108QrCode?: boolean;
|
||||
showResetIdentity?: boolean;
|
||||
sdkContext: SdkContextClass;
|
||||
onFinished(): void;
|
||||
}
|
||||
@ -91,8 +92,9 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
|
||||
export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const voipEnabled = useSettingValue(UIFeature.Voip);
|
||||
const mjolnirEnabled = useSettingValue("feature_mjolnir");
|
||||
// store this prop in state as changing tabs back and forth should clear it
|
||||
// store these props in state as changing tabs back and forth should clear it
|
||||
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
|
||||
const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity);
|
||||
|
||||
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
@ -184,7 +186,12 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
);
|
||||
|
||||
tabs.push(
|
||||
new Tab(UserTab.Encryption, _td("settings|encryption|title"), <KeyIcon />, <EncryptionUserSettingsTab />),
|
||||
new Tab(
|
||||
UserTab.Encryption,
|
||||
_td("settings|encryption|title"),
|
||||
<KeyIcon />,
|
||||
<EncryptionUserSettingsTab initialState={showResetIdentity ? "reset_identity_forgot" : undefined} />,
|
||||
),
|
||||
);
|
||||
|
||||
if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) {
|
||||
@ -219,8 +226,9 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.Account, props.initialTabId);
|
||||
const setActiveTabId = (tabId: UserTab): void => {
|
||||
_setActiveTabId(tabId);
|
||||
// Clear this so switching away from the tab and back to it will not show the QR code again
|
||||
// Clear these so switching away from the tab and back to it will not show the QR code again
|
||||
setShowMsc4108QrCode(false);
|
||||
setShowResetIdentity(false);
|
||||
};
|
||||
|
||||
const [activeToast, toastRack] = useActiveToast();
|
||||
|
@ -25,12 +25,21 @@ interface ResetIdentityPanelProps {
|
||||
* Called when the cancel button is clicked or when we go back in the breadcrumbs.
|
||||
*/
|
||||
onCancelClick: () => void;
|
||||
|
||||
/**
|
||||
* The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user this
|
||||
* warning if they have to reset because they no longer have their key)
|
||||
* "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their
|
||||
* identity has been compromised.
|
||||
* "forgot" is shown when the user has just forgotten their passphrase.
|
||||
*/
|
||||
variant: "compromised" | "forgot";
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel for resetting the identity of the current user.
|
||||
*/
|
||||
export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPanelProps): JSX.Element {
|
||||
export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetIdentityPanelProps): JSX.Element {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
|
||||
return (
|
||||
@ -44,7 +53,11 @@ export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPan
|
||||
<EncryptionCard
|
||||
Icon={ErrorIcon}
|
||||
destructive={true}
|
||||
title={_t("settings|encryption|advanced|breadcrumb_title")}
|
||||
title={
|
||||
variant === "forgot"
|
||||
? _t("settings|encryption|advanced|breadcrumb_title_forgot")
|
||||
: _t("settings|encryption|advanced|breadcrumb_title")
|
||||
}
|
||||
className="mx_ResetIdentityPanel"
|
||||
>
|
||||
<div className="mx_ResetIdentityPanel_content">
|
||||
@ -59,7 +72,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPan
|
||||
{_t("settings|encryption|advanced|breadcrumb_third_description")}
|
||||
</VisualListItem>
|
||||
</VisualList>
|
||||
<span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>
|
||||
{variant === "compromised" && <span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>}
|
||||
</div>
|
||||
<div className="mx_ResetIdentityPanel_footer">
|
||||
<Button
|
||||
|
@ -32,23 +32,35 @@ import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync"
|
||||
* 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.
|
||||
* 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.
|
||||
* - `secrets_not_cached`: The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
* - "reset_identity_compromised": The panel to show when the user is resetting their identity, in te case where their key is compromised.
|
||||
* - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key.
|
||||
* - `secrets_not_cached`: The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
* If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails.
|
||||
*
|
||||
*/
|
||||
type State =
|
||||
export type State =
|
||||
| "loading"
|
||||
| "main"
|
||||
| "set_up_encryption"
|
||||
| "change_recovery_key"
|
||||
| "set_recovery_key"
|
||||
| "reset_identity"
|
||||
| "reset_identity_compromised"
|
||||
| "reset_identity_forgot"
|
||||
| "secrets_not_cached";
|
||||
|
||||
export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
const [state, setState] = useState<State>("loading");
|
||||
const checkEncryptionState = useCheckEncryptionState(setState);
|
||||
interface EncryptionUserSettingsTabProps {
|
||||
/**
|
||||
* If the tab should start in a state other than the deasult
|
||||
*/
|
||||
initialState?: State;
|
||||
}
|
||||
|
||||
/**
|
||||
* The encryption settings tab.
|
||||
*/
|
||||
export function EncryptionUserSettingsTab({ initialState = "loading" }: EncryptionUserSettingsTabProps): JSX.Element {
|
||||
const [state, setState] = useState<State>(initialState);
|
||||
|
||||
const checkEncryptionState = useCheckEncryptionState(state, setState);
|
||||
|
||||
let content: JSX.Element;
|
||||
switch (state) {
|
||||
@ -70,7 +82,7 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
<Separator kind="section" />
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity")} />
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity_compromised")} />
|
||||
</>
|
||||
);
|
||||
break;
|
||||
@ -84,8 +96,23 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "reset_identity":
|
||||
content = <ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />;
|
||||
case "reset_identity_compromised":
|
||||
content = (
|
||||
<ResetIdentityPanel
|
||||
variant="compromised"
|
||||
onCancelClick={() => setState("main")}
|
||||
onFinish={() => setState("main")}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "reset_identity_forgot":
|
||||
content = (
|
||||
<ResetIdentityPanel
|
||||
variant="forgot"
|
||||
onCancelClick={() => setState("main")}
|
||||
onFinish={() => setState("main")}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -111,7 +138,7 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
* @param setState - callback passed from the EncryptionUserSettingsTab to set the current `State`.
|
||||
* @returns a callback function, which will re-run the logic and update the state.
|
||||
*/
|
||||
function useCheckEncryptionState(setState: (state: State) => void): () => Promise<void> {
|
||||
function useCheckEncryptionState(state: State, setState: (state: State) => void): () => Promise<void> {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
|
||||
const checkEncryptionState = useCallback(async () => {
|
||||
@ -129,8 +156,8 @@ function useCheckEncryptionState(setState: (state: State) => void): () => Promis
|
||||
|
||||
// Initialise the state when the component is mounted
|
||||
useEffect(() => {
|
||||
checkEncryptionState();
|
||||
}, [checkEncryptionState]);
|
||||
if (state === "loading") checkEncryptionState();
|
||||
}, [checkEncryptionState, state]);
|
||||
|
||||
// Also return the callback so that the component can re-run the logic.
|
||||
return checkEncryptionState;
|
||||
|
@ -2469,6 +2469,7 @@
|
||||
"breadcrumb_second_description": "You will lose any message history that’s 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_title_forgot": "Forgot your recovery key? You’ll need 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",
|
||||
|
@ -16,6 +16,10 @@ import GenericToast from "../components/views/toasts/GenericToast";
|
||||
import { ModuleRunner } from "../modules/ModuleRunner";
|
||||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||
import Spinner from "../components/views/elements/Spinner";
|
||||
import { OpenToTabPayload } from "../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { UserTab } from "../components/views/dialogs/UserTab";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
|
||||
const TOAST_KEY = "setupencryption";
|
||||
|
||||
@ -104,10 +108,6 @@ export enum Kind {
|
||||
KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync",
|
||||
}
|
||||
|
||||
const onReject = (): void => {
|
||||
DeviceListener.sharedInstance().dismissEncryptionSetup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Show a toast prompting the user for some action related to setting up their encryption.
|
||||
*
|
||||
@ -123,7 +123,7 @@ export const showToast = (kind: Kind): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const onAccept = async (): Promise<void> => {
|
||||
const onPrimaryClick = async (): Promise<void> => {
|
||||
if (kind === Kind.VERIFY_THIS_SESSION) {
|
||||
Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true);
|
||||
} else {
|
||||
@ -142,6 +142,19 @@ export const showToast = (kind: Kind): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSecondaryClick = (): void => {
|
||||
if (kind === Kind.KEY_STORAGE_OUT_OF_SYNC) {
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
props: { showResetIdentity: true },
|
||||
};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
} else {
|
||||
DeviceListener.sharedInstance().dismissEncryptionSetup();
|
||||
}
|
||||
};
|
||||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: TOAST_KEY,
|
||||
title: getTitle(kind),
|
||||
@ -149,9 +162,9 @@ export const showToast = (kind: Kind): void => {
|
||||
props: {
|
||||
description: getDescription(kind),
|
||||
primaryLabel: getSetupCaption(kind),
|
||||
onPrimaryClick: onAccept,
|
||||
onPrimaryClick,
|
||||
secondaryLabel: getSecondaryButtonLabel(kind),
|
||||
onSecondaryClick: onReject,
|
||||
onSecondaryClick,
|
||||
overrideWidth: kind === Kind.KEY_STORAGE_OUT_OF_SYNC ? "366px" : undefined,
|
||||
},
|
||||
component: GenericToast,
|
||||
|
@ -57,9 +57,9 @@ describe("<UserSettingsDialog />", () => {
|
||||
|
||||
let sdkContext: SdkContextClass;
|
||||
const defaultProps = { onFinished: jest.fn() };
|
||||
const getComponent = (props: Partial<typeof defaultProps & { initialTabId?: UserTab }> = {}): ReactElement => (
|
||||
<UserSettingsDialog sdkContext={sdkContext} {...defaultProps} {...props} />
|
||||
);
|
||||
const getComponent = (
|
||||
props: Partial<typeof defaultProps & { initialTabId?: UserTab; props: Record<string, any> }> = {},
|
||||
): ReactElement => <UserSettingsDialog sdkContext={sdkContext} {...defaultProps} {...props} />;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -25,7 +25,7 @@ describe("<ResetIdentityPanel />", () => {
|
||||
|
||||
const onFinish = jest.fn();
|
||||
const { asFragment } = render(
|
||||
<ResetIdentityPanel onFinish={onFinish} onCancelClick={jest.fn()} />,
|
||||
<ResetIdentityPanel variant="compromised" onFinish={onFinish} onCancelClick={jest.fn()} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
@ -34,4 +34,13 @@ describe("<ResetIdentityPanel />", () => {
|
||||
expect(matrixClient.getCrypto()!.resetEncryption).toHaveBeenCalled();
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display the 'forgot recovery key' variant correctly", async () => {
|
||||
const onFinish = jest.fn();
|
||||
const { asFragment } = render(
|
||||
<ResetIdentityPanel variant="forgot" onFinish={onFinish} onCancelClick={jest.fn()} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,185 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant correctly 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"
|
||||
>
|
||||
Forgot your recovery key? You’ll need 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 that’s 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>
|
||||
</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>
|
||||
`;
|
||||
|
||||
exports[`<ResetIdentityPanel /> should reset the encryption when the continue button is clicked 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
|
@ -12,7 +12,10 @@ import { waitFor } from "@testing-library/dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { EncryptionUserSettingsTab } from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab";
|
||||
import {
|
||||
EncryptionUserSettingsTab,
|
||||
State,
|
||||
} from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
import Modal from "../../../../../../../src/Modal";
|
||||
import { accessSecretStorage } from "../../../../../../../src/SecurityManager";
|
||||
@ -43,8 +46,8 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
return render(<EncryptionUserSettingsTab />, withClientContextRenderOptions(matrixClient));
|
||||
function renderComponent(props: { initialState?: State } = {}) {
|
||||
return render(<EncryptionUserSettingsTab {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
}
|
||||
|
||||
it("should display a loading state when the encryption state is computed", () => {
|
||||
@ -139,4 +142,12 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should enter reset flow when showResetIdentity is set", () => {
|
||||
renderComponent({ initialState: "reset_identity_forgot" });
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -7,9 +7,18 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
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";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("SetupEncryptionToast", () => {
|
||||
beforeEach(() => {
|
||||
@ -19,7 +28,18 @@ describe("SetupEncryptionToast", () => {
|
||||
it("should render the 'set up recovery' toast", async () => {
|
||||
showToast(Kind.SET_UP_RECOVERY);
|
||||
|
||||
await expect(screen.findByText("Set up recovery")).resolves.toBeInTheDocument();
|
||||
await expect(await screen.findByRole("heading", { name: "Set up recovery" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should dismiss toast when 'not now' button clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup");
|
||||
|
||||
showToast(Kind.SET_UP_RECOVERY);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Not now" }));
|
||||
|
||||
expect(DeviceListener.sharedInstance().dismissEncryptionSetup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render the 'key storage out of sync' toast", async () => {
|
||||
@ -27,4 +47,17 @@ describe("SetupEncryptionToast", () => {
|
||||
|
||||
await expect(screen.findByText("Your key storage is out of sync.")).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open settings to the reset flow when 'forgot recovery key' clicked", async () => {
|
||||
showToast(Kind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByText("Forgot recovery key?"));
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_user_settings",
|
||||
initialTabId: "USER_ENCRYPTION_TAB",
|
||||
props: { showResetIdentity: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user