Hide recovery key when prompting for verification (#30471)

* Separate security_key_title from security_key_label since they differ in designs

See https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=92-8818&t=02JILBe2n7sx7ljU-1

In parallel with this, I have updated security_key_title in localazy.

* Hide recovery key on entry screen after login
This commit is contained in:
Andy Balaam
2025-08-05 14:57:40 +00:00
committed by GitHub
parent 1e6f9dd096
commit c1a163cbc9
6 changed files with 31 additions and 17 deletions
@@ -209,7 +209,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems // We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
// (cf https://github.com/element-hq/element-web/issues/30089) // (cf https://github.com/element-hq/element-web/issues/30089)
await dialog.locator("textarea").pressSequentially(recoveryKey); await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
await dialog.getByRole("button", { name: "Continue", disabled: false }).click(); await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click(); await page.getByRole("button", { name: "Done" }).click();
+2 -2
View File
@@ -228,7 +228,7 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
await useSecurityKey.click(); await useSecurityKey.click();
} }
// Fill in the recovery key // Fill in the recovery key
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey); await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click(); await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click(); await page.getByRole("button", { name: "Done" }).click();
} }
@@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption"); const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click(); 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: "Verify with Recovery Key" }).click();
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey); 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: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click(); await app.page.getByRole("button", { name: "Done" }).click();
await app.settings.closeDialog(); await app.settings.closeDialog();
@@ -15,6 +15,23 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_AccessSecretStorageDialog_primaryContainer { .mx_AccessSecretStorageDialog_primaryContainer {
.mx_AccessSecretStorageDialog_recoveryKeyEntry {
/*
* Be specific here to avoid "margin: 9px" from _common.pcss
*/
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) {
input {
/*
* From figma: https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=102-43729&t=QmewENUd7f6Tmw9U-1
*/
width: 448px;
height: 70px;
margin: 0px;
border: 1px solid;
}
}
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback { .mx_AccessSecretStorageDialog_recoveryKeyFeedback {
&::before { &::before {
content: ""; content: "";
@@ -6,14 +6,13 @@ 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. Please see LICENSE files in the repository root for full details.
*/ */
import { Button } from "@vector-im/compound-web"; import { Button, PasswordInput } from "@vector-im/compound-web";
import LockSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid"; import LockSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
import { debounce } from "lodash"; import { debounce } from "lodash";
import classNames from "classnames"; import classNames from "classnames";
import React, { type ChangeEvent, type FormEvent } from "react"; import React, { type ChangeEvent, type FormEvent } from "react";
import { type SecretStorage } from "matrix-js-sdk/src/matrix"; import { type SecretStorage } from "matrix-js-sdk/src/matrix";
import Field from "../../elements/Field";
import { Flex } from "../../../../shared-components/utils/Flex"; import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "../../settings/encryption/EncryptionCard"; import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
@@ -53,7 +52,7 @@ interface IState {
* Access Secure Secret Storage by requesting the user's passphrase. * Access Secure Secret Storage by requesting the user's passphrase.
*/ */
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> { export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
private inputRef = React.createRef<HTMLTextAreaElement>(); private inputRef = React.createRef<HTMLInputElement>();
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@@ -119,7 +118,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
}); });
} }
private onRecoveryKeyChange = (ev: ChangeEvent<HTMLTextAreaElement>): void => { private onRecoveryKeyChange = (ev: ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
recoveryKey: ev.target.value, recoveryKey: ev.target.value,
}); });
@@ -181,17 +180,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
autoComplete="off" autoComplete="off"
> >
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry"> <div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
<Field <PasswordInput
inputRef={this.inputRef} ref={this.inputRef}
element="textarea"
rows={2}
cols={45}
id="mx_securityKey" id="mx_securityKey"
label={_t("encryption|access_secret_storage_dialog|security_key_title")} title={_t("encryption|access_secret_storage_dialog|security_key_label")}
placeholder={_t("encryption|access_secret_storage_dialog|security_key_label")}
value={this.state.recoveryKey} value={this.state.recoveryKey}
onChange={this.onRecoveryKeyChange} onChange={this.onRecoveryKeyChange}
autoFocus={true} autoFocus={true}
forceValidity={this.state.recoveryKeyCorrect ?? undefined}
autoComplete="off" autoComplete="off"
/> />
</div> </div>
+1
View File
@@ -920,6 +920,7 @@
}, },
"privacy_warning": "Make sure nobody can see this screen!", "privacy_warning": "Make sure nobody can see this screen!",
"restoring": "Restoring keys from backup", "restoring": "Restoring keys from backup",
"security_key_label": "Recovery key",
"security_key_title": "Recovery key" "security_key_title": "Recovery key"
}, },
"bootstrap_title": "Setting up keys", "bootstrap_title": "Setting up keys",
@@ -31,7 +31,7 @@ describe("AccessSecretStorageDialog", () => {
const enterRecoveryKey = async (valueToEnter: string = recoveryKey): Promise<void> => { const enterRecoveryKey = async (valueToEnter: string = recoveryKey): Promise<void> => {
await act(async () => { await act(async () => {
fireEvent.change(screen.getByRole("textbox"), { fireEvent.change(screen.getByTitle("Recovery key"), {
target: { target: {
value: valueToEnter, value: valueToEnter,
}, },
@@ -67,7 +67,7 @@ describe("AccessSecretStorageDialog", () => {
renderComponent({ onFinished, checkPrivateKey }); renderComponent({ onFinished, checkPrivateKey });
// check that the input field is focused // check that the input field is focused
expect(screen.getByRole("textbox")).toHaveFocus(); expect(screen.getByTitle("Recovery key")).toHaveFocus();
await enterRecoveryKey(); await enterRecoveryKey();
await submitDialog(); await submitDialog();
@@ -111,7 +111,7 @@ describe("AccessSecretStorageDialog", () => {
renderComponent({ checkPrivateKey, keyInfo }); renderComponent({ checkPrivateKey, keyInfo });
await enterRecoveryKey(); await enterRecoveryKey();
expect(screen.getByRole("textbox")).toHaveValue(recoveryKey); expect(screen.getByTitle("Recovery key")).toHaveValue(recoveryKey);
await expect(screen.findByText("The recovery key you entered is not correct.")).resolves.toBeInTheDocument(); await expect(screen.findByText("The recovery key you entered is not correct.")).resolves.toBeInTheDocument();
expect(screen.getByText("Continue")).toHaveAttribute("aria-disabled", "true"); expect(screen.getByText("Continue")).toHaveAttribute("aria-disabled", "true");