mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-08 12:11:07 +01:00
"Verify this device" redesign (#30596)
* add variant of ResetIdentityBody for when the user has no verif. methods * no longer distinguish between the using having a passphrase or not * use vertical stack of buttons via EncryptionCard and update wording * swap logic order to match rendering order * use the same dialog when no verification options available * make it agree with the design more * allow signing out on initial login * apply styling changes and remove duplicate elements * fix and add tests * add missing snapshot * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use a boolean property to disable blurring instead of adding a class * change string identifiers * apply changes from review -- simplify logic * change class name to avoid confusion --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
1e0cdf7b14
commit
9ad239f87f
@ -38,7 +38,7 @@ test.describe("Dehydration", () => {
|
||||
// Reset the identity key
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Set up recovery
|
||||
@ -106,7 +106,7 @@ test.describe("Dehydration", () => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Oh no, we forgot our recovery key - reset our identity
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Can't confirm" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
||||
).toBeVisible();
|
||||
|
||||
@ -36,13 +36,13 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||
// Click the "Use another device" button, and have the bot client auto-accept it.
|
||||
async function initiateAliceVerificationRequest(page: Page): Promise<JSHandle<VerificationRequest>> {
|
||||
// alice bot waits for verification request
|
||||
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
|
||||
|
||||
// Click on "Verify with another device"
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with another device" }).click();
|
||||
// Click on "Use another device"
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Use another device" }).click();
|
||||
|
||||
// alice bot responds yes to verification request from alice
|
||||
return promiseVerificationRequest;
|
||||
@ -203,7 +203,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
/** Helper for the three tests above which verify by recovery key */
|
||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
||||
await page.getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||
await page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
|
||||
// Enter the recovery key
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
|
||||
@ -224,9 +224,9 @@ export async function logIntoElement(page: Page, credentials: Credentials) {
|
||||
export async function logIntoElementAndVerify(page: Page, credentials: Credentials, recoveryKey: string) {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Use recovery key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "Use recovery key" });
|
||||
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||
@ -272,7 +272,7 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
await app.page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
|
||||
@ -186,7 +186,7 @@ test.describe("Login", () => {
|
||||
await page.goto("/");
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||
});
|
||||
@ -219,7 +219,7 @@ test.describe("Login", () => {
|
||||
await page.goto("/");
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||
});
|
||||
@ -254,10 +254,10 @@ test.describe("Login", () => {
|
||||
await page.goto("/");
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
const h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
await expect(h2).toBeVisible();
|
||||
|
||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
await expect(h2.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||
@ -274,18 +274,18 @@ test.describe("Login", () => {
|
||||
// Load the page and see that we are asked to verify
|
||||
await page.goto("/#/welcome");
|
||||
await login(page, homeserver, credentials);
|
||||
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
let h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
await expect(h2).toBeVisible();
|
||||
|
||||
// Click "Verify with another device"
|
||||
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||
// Click "Use another device"
|
||||
await page.getByRole("button", { name: "Use another device" }).click();
|
||||
|
||||
// Cancel the new dialog
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
// Check that we are still being asked to verify
|
||||
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
await expect(h2).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -303,18 +303,18 @@ test.describe("Login", () => {
|
||||
await page.goto("/");
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
||||
|
||||
// Start the reset process
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// First try cancelling and restarting
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Then click outside the dialog and restart
|
||||
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Finally we actually continue
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
@ -129,8 +129,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// We should be in (we see an error because we have no recovery key).
|
||||
await expect(page.getByText("Unable to verify this device")).toBeVisible();
|
||||
// We should be in
|
||||
await expect(page.getByText("Confirm your identity")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("with force_verification on", () => {
|
||||
@ -162,7 +162,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// We should be being warned that we need to verify (but we can't)
|
||||
await expect(page.getByText("Unable to verify this device")).toBeVisible();
|
||||
await expect(page.getByText("Confirm your identity")).toBeVisible();
|
||||
|
||||
// And there should be no way to close this prompt
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
||||
@ -210,7 +210,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
||||
|
||||
// When we start verifying with another device
|
||||
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||
await page.getByRole("button", { name: "Use another device" }).click();
|
||||
|
||||
// And then cancel it
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
@ -227,7 +227,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
* Perform interactive emoji verification for a new device.
|
||||
*/
|
||||
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
|
||||
await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
|
||||
await deviceToVerifyPage.getByRole("button", { name: "Use another device" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
|
||||
|
||||
@ -160,15 +160,15 @@ test.describe("Encryption tab", () => {
|
||||
|
||||
// We will reset our identity
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// First try cancelling and restarting
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Then click outside the dialog and restart
|
||||
await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Finally we actually continue
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
@ -43,7 +43,7 @@ class Helpers {
|
||||
*/
|
||||
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
|
||||
// Select the security phrase
|
||||
await this.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
await this.page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
await this.enterRecoveryKey(recoveryKey);
|
||||
await this.page.getByRole("button", { name: "Done" }).click();
|
||||
}
|
||||
|
||||
@ -6,13 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_SetupEncryptionBody_reset {
|
||||
color: $light-fg-color;
|
||||
margin-top: $font-14px;
|
||||
|
||||
.mx_SetupEncryptionBody_reset_link {
|
||||
&.mx_AccessibleButton_kind_link_inline {
|
||||
color: $alert;
|
||||
}
|
||||
}
|
||||
.mx_SetupEncryptionBody {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: flex;
|
||||
margin: 100px auto auto;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33);
|
||||
background-color: $authpage-modal-bg-color;
|
||||
|
||||
@media only screen and (max-height: 768px) {
|
||||
@ -29,4 +28,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
@media only screen and (max-width: 480px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Apply a blurred shadow around the modal */
|
||||
&.mx_AuthPage_modal_withBlur {
|
||||
box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,11 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_CompleteSecurityBody {
|
||||
width: 600px;
|
||||
color: $authpage-primary-color;
|
||||
background-color: $background;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
padding: 20px 20px 60px 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
h2 {
|
||||
|
||||
@ -30,6 +30,13 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* extra class for specifying that we don't need a border */
|
||||
&.mx_EncryptionCard_noBorder {
|
||||
border: 0px none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EncryptionCard_buttons {
|
||||
|
||||
@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Glass } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
|
||||
@ -22,15 +23,17 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
phase?: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to verify their device when they first log in.
|
||||
*/
|
||||
export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.start();
|
||||
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||
this.state = { phase: store.phase };
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
@ -40,7 +43,7 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
|
||||
this.setState({ phase: store.phase });
|
||||
};
|
||||
|
||||
private onSkipClick = (): void => {
|
||||
@ -55,20 +58,14 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { phase, lostKeys } = this.state;
|
||||
const { phase } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === Phase.Intro) {
|
||||
if (lostKeys) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("encryption|verification|after_new_login|unable_to_verify");
|
||||
} else {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("encryption|verification|after_new_login|verify_this_device");
|
||||
}
|
||||
// We don't specify an icon nor title since `SetupEncryptionBody` provides its own
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("encryption|verification|after_new_login|device_verified");
|
||||
@ -98,7 +95,8 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthPage addBlur={false}>
|
||||
<Glass className="mx_Dialog_border">
|
||||
<CompleteSecurityBody>
|
||||
<h1 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
@ -106,9 +104,10 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
{skipButton}
|
||||
</h1>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} allowLogout={true} />
|
||||
</div>
|
||||
</CompleteSecurityBody>
|
||||
</Glass>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX } from "react";
|
||||
import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
||||
import DevicesIcon from "@vector-im/compound-design-tokens/assets/web/icons/devices";
|
||||
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
@ -17,25 +19,38 @@ import Modal from "../../../Modal";
|
||||
import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog";
|
||||
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
|
||||
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
|
||||
import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog";
|
||||
|
||||
function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean {
|
||||
return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations);
|
||||
}
|
||||
import { EncryptionCard } from "../../views/settings/encryption/EncryptionCard";
|
||||
import { EncryptionCardButtons } from "../../views/settings/encryption/EncryptionCardButtons";
|
||||
import { EncryptionCardEmphasisedContent } from "../../views/settings/encryption/EncryptionCardEmphasisedContent";
|
||||
import ExternalLink from "../../views/elements/ExternalLink";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
/**
|
||||
* Offer the user an option to log out, instead of setting up encryption.
|
||||
*
|
||||
* This is used when this component is shown when the user is initially
|
||||
* prompted to set up encryption, before the user is shown the main chat
|
||||
* interface.
|
||||
*
|
||||
* Defaults to `false` if omitted.
|
||||
*/
|
||||
allowLogout?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase?: Phase;
|
||||
verificationRequest: VerificationRequest | null;
|
||||
backupInfo: KeyBackupInfo | null;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to set up encryption by verifying the current device.
|
||||
*/
|
||||
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
@ -48,7 +63,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -67,7 +81,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
});
|
||||
};
|
||||
|
||||
@ -112,8 +125,8 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
store.returnAfterSkip();
|
||||
};
|
||||
|
||||
private onResetClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
private onCantConfirmClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
Modal.createDialog(ResetIdentityDialog, {
|
||||
onReset: () => {
|
||||
// The user completed the reset process - close this dialog
|
||||
@ -121,10 +134,14 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
},
|
||||
variant: "confirm",
|
||||
variant: store.lostKeys() ? "no_verification_method" : "confirm",
|
||||
});
|
||||
};
|
||||
|
||||
private onSignOutClick = (): void => {
|
||||
dispatcher.dispatch({ action: "logout" });
|
||||
};
|
||||
|
||||
private onDoneClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
@ -136,7 +153,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const { phase, lostKeys } = this.state;
|
||||
const { phase } = this.state;
|
||||
|
||||
if (this.state.verificationRequest && cli.getUser(this.state.verificationRequest.otherUserId)) {
|
||||
return (
|
||||
@ -149,69 +166,59 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
/>
|
||||
);
|
||||
} else if (phase === Phase.Intro) {
|
||||
if (lostKeys) {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|no_key_or_device")}</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="primary" onClick={this.onResetClick}>
|
||||
{_t("encryption|verification|reset_proceed_prompt")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("encryption|verification|verify_using_key_or_phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("encryption|verification|verify_using_key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = (
|
||||
<AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{recoveryKeyPrompt}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = (
|
||||
<AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{_t("encryption|verification|verify_using_device")}
|
||||
</AccessibleButton>
|
||||
<Button kind="primary" onClick={this.onVerifyClick}>
|
||||
<DevicesIcon /> {_t("encryption|verification|use_another_device")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (store.keyInfo) {
|
||||
useRecoveryKeyButton = (
|
||||
<Button kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{_t("encryption|verification|use_recovery_key")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
let signOutButton;
|
||||
if (this.props.allowLogout) {
|
||||
signOutButton = (
|
||||
<Button kind="tertiary" onClick={this.onSignOutClick}>
|
||||
{_t("action|sign_out")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|verification_description")}</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<EncryptionCard
|
||||
title={_t("encryption|verification|confirm_identity_title")}
|
||||
Icon={LockIcon}
|
||||
className="mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<EncryptionCardEmphasisedContent>
|
||||
<span>{_t("encryption|verification|confirm_identity_description")}</span>
|
||||
<span>
|
||||
<ExternalLink href="https://element.io/help#encryption-device-verification">
|
||||
{_t("action|learn_more")}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</EncryptionCardEmphasisedContent>
|
||||
<EncryptionCardButtons>
|
||||
{verifyButton}
|
||||
{useRecoveryKeyButton}
|
||||
</div>
|
||||
<div className="mx_SetupEncryptionBody_reset">
|
||||
{_t("encryption|reset_all_button", undefined, {
|
||||
a: (sub) => (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
className="mx_SetupEncryptionBody_reset_link"
|
||||
onClick={this.onResetClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button kind="secondary" onClick={this.onCantConfirmClick}>
|
||||
{_t("encryption|verification|cant_confirm")}
|
||||
</Button>
|
||||
{signOutButton}
|
||||
</EncryptionCardButtons>
|
||||
</EncryptionCard>
|
||||
);
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
let message: JSX.Element;
|
||||
if (this.state.backupInfo) {
|
||||
|
||||
@ -8,11 +8,22 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AuthFooter from "./AuthFooter";
|
||||
|
||||
export default class AuthPage extends React.PureComponent<React.PropsWithChildren> {
|
||||
interface IProps {
|
||||
/**
|
||||
* Whether to add a blurred shadow around the modal.
|
||||
*
|
||||
* If the modal component provides its own shadow or blurring, this can be
|
||||
* disabled. Defaults to `true`.
|
||||
*/
|
||||
addBlur?: boolean;
|
||||
}
|
||||
|
||||
export default class AuthPage extends React.PureComponent<React.PropsWithChildren<IProps>> {
|
||||
private static welcomeBackgroundUrl?: string;
|
||||
|
||||
// cache the url as a static to prevent it changing without refreshing
|
||||
@ -58,14 +69,26 @@ export default class AuthPage extends React.PureComponent<React.PropsWithChildre
|
||||
const modalContentStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
background: "rgba(255, 255, 255, 0.59)",
|
||||
borderRadius: "8px",
|
||||
};
|
||||
|
||||
let modalBlur;
|
||||
if (this.props.addBlur !== false) {
|
||||
// Blur out the background: add a `div` which covers the content behind the modal,
|
||||
// and blurs it out, and make the modal's background semitransparent.
|
||||
modalBlur = <div className="mx_AuthPage_modalBlur" style={blurStyle} />;
|
||||
modalContentStyle.background = "rgba(255, 255, 255, 0.59)";
|
||||
}
|
||||
|
||||
const modalClasses = classNames({
|
||||
mx_AuthPage_modal: true,
|
||||
mx_AuthPage_modal_withBlur: this.props.addBlur !== false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_AuthPage" style={pageStyle}>
|
||||
<div className="mx_AuthPage_modal" style={modalStyle}>
|
||||
<div className="mx_AuthPage_modalBlur" style={blurStyle} />
|
||||
<div className={modalClasses} style={modalStyle}>
|
||||
{modalBlur}
|
||||
<div className="mx_AuthPage_modalContent" style={modalContentStyle}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
@ -10,55 +10,19 @@ import React from "react";
|
||||
|
||||
import SetupEncryptionBody from "../../../structures/auth/SetupEncryptionBody";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { SetupEncryptionStore, Phase } from "../../../../stores/SetupEncryptionStore";
|
||||
|
||||
function iconFromPhase(phase?: Phase): string {
|
||||
if (phase === Phase.Done) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../../../../../res/img/e2e/verified-deprecated.svg").default;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../../../../../res/img/e2e/warning-deprecated.svg").default;
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
interface IState {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
|
||||
private store: SetupEncryptionStore;
|
||||
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.store = SetupEncryptionStore.sharedInstance();
|
||||
this.state = { icon: iconFromPhase(this.store.phase) };
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.store.on("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.store.removeListener("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
this.setState({ icon: iconFromPhase(this.store.phase) });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<BaseDialog
|
||||
headerImage={this.state.icon}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("encryption|verify_toast_title")}
|
||||
>
|
||||
<BaseDialog onFinished={this.props.onFinished} fixedWidth={false}>
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@ -48,8 +48,12 @@ interface ResetIdentityBodyProps {
|
||||
* "forgot" is shown when the user chose 'Forgot recovery key?' during `SetupEncryptionToast`.
|
||||
*
|
||||
* "confirm" is shown when the user chose 'Reset all' during `SetupEncryptionBody`.
|
||||
*
|
||||
* "no_verification_method" is shown when the device is unverified and has no way of
|
||||
* obtaining the existing keys, and hence the identity needs to be reset to have
|
||||
* a cross-signed device.
|
||||
*/
|
||||
export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm";
|
||||
export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm" | "no_verification_method";
|
||||
|
||||
/**
|
||||
* User interface component allowing the user to reset their cryptographic identity.
|
||||
@ -124,5 +128,7 @@ function titleForVariant(variant: ResetIdentityBodyVariant): string {
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_sync_failed");
|
||||
case "forgot":
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_forgot");
|
||||
case "no_verification_method":
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_cant_confirm");
|
||||
}
|
||||
}
|
||||
|
||||
@ -969,7 +969,6 @@
|
||||
"title": "Recovery Method Removed",
|
||||
"warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
||||
},
|
||||
"reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>",
|
||||
"set_up_recovery": "Set up recovery",
|
||||
"set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.",
|
||||
"set_up_toast_title": "Set up Secure Backup",
|
||||
@ -992,16 +991,18 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Device verified",
|
||||
"skip_verification": "Skip verification for now",
|
||||
"unable_to_verify": "Unable to verify this device",
|
||||
"verify_this_device": "Verify this device"
|
||||
},
|
||||
"cancelled": "You cancelled verification.",
|
||||
"cancelled_self": "You cancelled verification on your other device.",
|
||||
"cancelled_user": "%(displayName)s cancelled verification.",
|
||||
"cancelling": "Cancelling…",
|
||||
"cant_confirm": "Can't confirm?",
|
||||
"complete_action": "Got It",
|
||||
"complete_description": "You've successfully verified this user.",
|
||||
"complete_title": "Verified!",
|
||||
"confirm_identity_description": "Verify this device to set up secure messaging",
|
||||
"confirm_identity_title": "Confirm your identity",
|
||||
"error_starting_description": "We were unable to start a chat with the other user.",
|
||||
"error_starting_title": "Error starting verification",
|
||||
"explainer": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
@ -1027,7 +1028,6 @@
|
||||
"text": "Supply the ID and fingerprint of one of your own devices to verify it. NOTE this allows the other device to send and receive messages as you. IF SOMEONE TOLD YOU TO PASTE SOMETHING HERE, IT IS LIKELY YOU ARE BEING SCAMMED!",
|
||||
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
|
||||
},
|
||||
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your cryptographic identity.",
|
||||
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||
"other_party_cancelled": "The other party cancelled the verification.",
|
||||
"prompt_encrypted": "Verify all users in a room to ensure it's secure.",
|
||||
@ -1043,7 +1043,6 @@
|
||||
"request_toast_accept_user": "Verify User",
|
||||
"request_toast_decline_counter": "Ignore (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s from %(ip)s",
|
||||
"reset_proceed_prompt": "Proceed with reset",
|
||||
"sas_caption_self": "Verify this device by confirming the following number appears on its screen.",
|
||||
"sas_caption_user": "Verify this user by confirming the following number appears on their screen.",
|
||||
"sas_description": "Compare a unique set of emoji if you don't have a camera on either device",
|
||||
@ -1066,7 +1065,8 @@
|
||||
"unverified_sessions_toast_description": "Review to ensure your account is safe",
|
||||
"unverified_sessions_toast_reject": "Later",
|
||||
"unverified_sessions_toast_title": "You have unverified sessions",
|
||||
"verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.",
|
||||
"use_another_device": "Use another device",
|
||||
"use_recovery_key": "Use recovery key",
|
||||
"verification_dialog_title_device": "Verify other device",
|
||||
"verification_dialog_title_user": "Verification Request",
|
||||
"verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.",
|
||||
@ -1076,9 +1076,6 @@
|
||||
"verify_emoji_prompt": "Verify by comparing unique emoji.",
|
||||
"verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.",
|
||||
"verify_later": "I'll verify later",
|
||||
"verify_using_device": "Verify with another device",
|
||||
"verify_using_key": "Verify with Recovery Key",
|
||||
"verify_using_key_or_phrase": "Verify with Recovery Key or Phrase",
|
||||
"waiting_for_user_accept": "Waiting for %(displayName)s to accept…",
|
||||
"waiting_other_device": "Waiting for you to verify on your other device…",
|
||||
"waiting_other_device_details": "Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…",
|
||||
@ -2589,6 +2586,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_cant_confirm": "You need to reset your identity",
|
||||
"breadcrumb_title_forgot": "Forgot your recovery key? You’ll need to reset your identity.",
|
||||
"breadcrumb_title_sync_failed": "Failed to sync key storage. You need to reset your identity.",
|
||||
"breadcrumb_warning": "Only do this if you believe your account has been compromised.",
|
||||
|
||||
@ -1081,10 +1081,10 @@ describe("<MatrixChat />", () => {
|
||||
getComponent();
|
||||
|
||||
// Then we are asked to verify our device
|
||||
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await screen.findByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
|
||||
// Sanity: we are not racing with another screen update, so this heading stays visible
|
||||
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await screen.findByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
});
|
||||
it("should not open app after cancelling device verify if unskippable verification is on", async () => {
|
||||
// See https://github.com/element-hq/element-web/issues/29230
|
||||
@ -1100,9 +1100,9 @@ describe("<MatrixChat />", () => {
|
||||
// And MatrixChat is rendered
|
||||
getComponent();
|
||||
|
||||
// When we click "Verify with another device"
|
||||
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
|
||||
const verify = screen.getByRole("button", { name: "Verify with another device" });
|
||||
// When we click "Use another device"
|
||||
await screen.findByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
const verify = screen.getByRole("button", { name: "Use another device" });
|
||||
act(() => verify.click());
|
||||
|
||||
// And close the device verification dialog
|
||||
@ -1110,7 +1110,7 @@ describe("<MatrixChat />", () => {
|
||||
act(() => closeButton.click());
|
||||
|
||||
// Then we are not allowed in - we are still being asked to verify
|
||||
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await screen.findByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
});
|
||||
|
||||
describe("when query params have a loginToken", () => {
|
||||
@ -1153,7 +1153,7 @@ describe("<MatrixChat />", () => {
|
||||
);
|
||||
|
||||
// Then we are not allowed in - we are being asked to verify
|
||||
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await screen.findByRole("heading", { name: "Confirm your identity", level: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -1397,7 +1397,7 @@ describe("<MatrixChat />", () => {
|
||||
await flushPromises();
|
||||
|
||||
// Complete security begin screen is rendered
|
||||
expect(screen.getByText("Unable to verify this device")).toBeInTheDocument();
|
||||
expect(screen.getByText("Confirm your identity")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should setup e2e when server supports cross signing", async () => {
|
||||
|
||||
@ -113,7 +113,7 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
class="mx_AuthPage_modal mx_AuthPage_modal_withBlur"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
@ -122,7 +122,7 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
|
||||
/>
|
||||
<div
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
|
||||
style="display: flex; z-index: 1; border-radius: 8px; background: rgba(255, 255, 255, 0.59);"
|
||||
>
|
||||
<div
|
||||
class="mx_Welcome"
|
||||
@ -231,7 +231,7 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
class="mx_AuthPage_modal mx_AuthPage_modal_withBlur"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
@ -240,7 +240,7 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
|
||||
/>
|
||||
<div
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
|
||||
style="display: flex; z-index: 1; border-radius: 8px; background: rgba(255, 255, 255, 0.59);"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthHeader"
|
||||
|
||||
@ -83,13 +83,66 @@ describe("CompleteSecurity", () => {
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// No recovery methods are available, so only the "Can't confirm?" button should be visible
|
||||
expect(screen.queryByRole("button", { name: "Can't confirm?" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Use another device" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Use recovery key" })).not.toBeInTheDocument();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Proceed with reset" }).click());
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears
|
||||
expect(screen.getByRole("heading", { name: "You need to reset your identity" })).toBeInTheDocument();
|
||||
expect(panel.getByRole("button", { name: "Continue" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Allows verifying with another device if one is available", async () => {
|
||||
// Given a store and a dialog based on it
|
||||
const store = new SetupEncryptionStore();
|
||||
jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => {
|
||||
store.hasDevicesToVerifyAgainst = true;
|
||||
store.phase = Phase.Intro;
|
||||
store.emit("update");
|
||||
});
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// The snapshot should have "Use another device" and "Can't confirm?"
|
||||
// buttons, but no "Use recovery key".
|
||||
expect(panel.asFragment()).toMatchSnapshot();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears, and should have a different
|
||||
// title from when there were no verification methods available.
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Allows verifying with recovery key if one is available", async () => {
|
||||
// Given a store and a dialog based on it
|
||||
const store = new SetupEncryptionStore();
|
||||
jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => {
|
||||
store.keyInfo = {} as any;
|
||||
store.phase = Phase.Intro;
|
||||
store.emit("update");
|
||||
});
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// The snapshot should have "Use recovery key" and "Can't confirm?"
|
||||
// buttons, but no "Use another device".
|
||||
expect(panel.asFragment()).toMatchSnapshot();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears, and should have a different
|
||||
// title from when there were no verification methods available.
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
||||
).toBeInTheDocument();
|
||||
expect(panel.getByRole("button", { name: "Continue" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,322 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CompleteSecurity Allows verifying with another device if one is available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: 8px;"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_border _glass_sepwu_8"
|
||||
>
|
||||
<div
|
||||
class="mx_CompleteSecurityBody"
|
||||
>
|
||||
<h1
|
||||
class="mx_CompleteSecurity_header"
|
||||
>
|
||||
<div
|
||||
aria-label="Skip verification for now"
|
||||
class="mx_AccessibleButton mx_CompleteSecurity_skip"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_CompleteSecurity_body"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_o77nw_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Confirm your identity
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="flex 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;"
|
||||
>
|
||||
<span>
|
||||
Verify this device to set up secure messaging
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/help#encryption-device-verification"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
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="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
Use another device
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Can't confirm?
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CompleteSecurity Allows verifying with recovery key if one is available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: 8px;"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_border _glass_sepwu_8"
|
||||
>
|
||||
<div
|
||||
class="mx_CompleteSecurityBody"
|
||||
>
|
||||
<h1
|
||||
class="mx_CompleteSecurity_header"
|
||||
>
|
||||
<div
|
||||
aria-label="Skip verification for now"
|
||||
class="mx_AccessibleButton mx_CompleteSecurity_skip"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_CompleteSecurity_body"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_o77nw_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Confirm your identity
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="flex 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;"
|
||||
>
|
||||
<span>
|
||||
Verify this device to set up secure messaging
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/help#encryption-device-verification"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Use recovery key
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Can't confirm?
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -6,7 +6,7 @@ exports[`<AuthPage /> should match snapshot 1`] = `
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
class="mx_AuthPage_modal mx_AuthPage_modal_withBlur"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
@ -15,7 +15,7 @@ exports[`<AuthPage /> should match snapshot 1`] = `
|
||||
/>
|
||||
<div
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
|
||||
style="display: flex; z-index: 1; border-radius: 8px; background: rgba(255, 255, 255, 0.59);"
|
||||
/>
|
||||
</div>
|
||||
<footer
|
||||
|
||||
@ -46,7 +46,7 @@ describe("SetupEncryptionDialog", () => {
|
||||
|
||||
// And we hit the Proceed with reset button.
|
||||
// (The createDialog mock above simulates the user doing the reset)
|
||||
await act(async () => screen.getByRole("button", { name: "Proceed with reset" }).click());
|
||||
await act(async () => screen.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the phase has been set to Finished
|
||||
expect(store.phase).toBe(Phase.Finished);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user