From 0fdc4d2d62c44d3ebd9352d3b5565c116a592cdd Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 17:07:19 -0400 Subject: [PATCH 01/15] add variant of ResetIdentityBody for when the user has no verif. methods --- src/components/structures/auth/SetupEncryptionBody.tsx | 3 ++- .../views/settings/encryption/ResetIdentityBody.tsx | 8 +++++++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index b76a623e2c3..19ad947e634 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -114,6 +114,7 @@ export default class SetupEncryptionBody extends React.Component private onResetClick = (ev: ButtonEvent): void => { ev.preventDefault(); + const store = SetupEncryptionStore.sharedInstance(); Modal.createDialog(ResetIdentityDialog, { onReset: () => { // The user completed the reset process - close this dialog @@ -121,7 +122,7 @@ export default class SetupEncryptionBody extends React.Component const store = SetupEncryptionStore.sharedInstance(); store.done(); }, - variant: "confirm", + variant: store.lostKeys() ? "no_verification_method" : "confirm", }); }; diff --git a/src/components/views/settings/encryption/ResetIdentityBody.tsx b/src/components/views/settings/encryption/ResetIdentityBody.tsx index a6b0b2c12ee..91b7d65b5d4 100644 --- a/src/components/views/settings/encryption/ResetIdentityBody.tsx +++ b/src/components/views/settings/encryption/ResetIdentityBody.tsx @@ -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"); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1c12f8e7edb..2f154aed005 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2561,6 +2561,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.", From 63dce71ad93dc36bbee0d141c584ca184df82f2d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 17:27:52 -0400 Subject: [PATCH 02/15] no longer distinguish between the using having a passphrase or not --- .../structures/auth/SetupEncryptionBody.tsx | 15 ++------------- src/i18n/strings/en_EN.json | 1 - 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 19ad947e634..a20a3f62d8d 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -9,7 +9,6 @@ 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 { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -21,10 +20,6 @@ import AccessibleButton, { type ButtonEvent } from "../../views/elements/Accessi 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); -} - interface IProps { onFinished: () => void; } @@ -164,18 +159,12 @@ export default class SetupEncryptionBody extends React.Component ); } 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) { + if (store.keyInfo) { useRecoveryKeyButton = ( - {recoveryKeyPrompt} + {_t("encryption|verification|verify_using_key")} ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2f154aed005..fc8c0decaa5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1076,7 +1076,6 @@ "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)…", From b101488596982fc985e9b4a328dd3acb3d2b1b9c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 18:07:42 -0400 Subject: [PATCH 03/15] use vertical stack of buttons via EncryptionCard and update wording --- .../structures/auth/_SetupEncryptionBody.pcss | 10 ----- .../structures/auth/SetupEncryptionBody.tsx | 43 +++++++++++-------- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/res/css/structures/auth/_SetupEncryptionBody.pcss b/res/css/structures/auth/_SetupEncryptionBody.pcss index ac6384313c7..678063812fe 100644 --- a/res/css/structures/auth/_SetupEncryptionBody.pcss +++ b/res/css/structures/auth/_SetupEncryptionBody.pcss @@ -6,13 +6,3 @@ 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; - } - } -} diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index a20a3f62d8d..a68e174e715 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -9,6 +9,7 @@ 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 LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -19,6 +20,10 @@ import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import Spinner from "../../views/elements/Spinner"; import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog"; +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"; interface IProps { onFinished: () => void; @@ -179,27 +184,27 @@ export default class SetupEncryptionBody extends React.Component } return ( -
-

{_t("encryption|verification|verification_description")}

- -
+ + + {_t("encryption|verification|verification_description")} + + + {_t("action|learn_more")} + + + + {verifyButton} {useRecoveryKeyButton} -
-
- {_t("encryption|reset_all_button", undefined, { - a: (sub) => ( - - {sub} - - ), - })} -
-
+ + {_t("encryption|verification|cant_confirm")} + + + ); } } else if (phase === Phase.Done) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fc8c0decaa5..b2cb12c63b5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -997,9 +997,11 @@ "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_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.", @@ -1064,7 +1066,7 @@ "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.", + "verification_description": "Verify this device to set up secure messaging", "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.", From 91250c4de9b96e7249f34c786dd4770fac1e8918 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 18:15:10 -0400 Subject: [PATCH 04/15] swap logic order to match rendering order --- .../structures/auth/SetupEncryptionBody.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index a68e174e715..51cad09ecfd 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -165,15 +165,6 @@ export default class SetupEncryptionBody extends React.Component } else { const store = SetupEncryptionStore.sharedInstance(); - let useRecoveryKeyButton; - if (store.keyInfo) { - useRecoveryKeyButton = ( - - {_t("encryption|verification|verify_using_key")} - - ); - } - let verifyButton; if (store.hasDevicesToVerifyAgainst) { verifyButton = ( @@ -183,6 +174,15 @@ export default class SetupEncryptionBody extends React.Component ); } + let useRecoveryKeyButton; + if (store.keyInfo) { + useRecoveryKeyButton = ( + + {_t("encryption|verification|verify_using_key")} + + ); + } + return ( Date: Wed, 27 Aug 2025 19:08:25 -0400 Subject: [PATCH 05/15] use the same dialog when no verification options available --- .../structures/auth/SetupEncryptionBody.tsx | 99 ++++++++----------- src/i18n/strings/en_EN.json | 3 - 2 files changed, 41 insertions(+), 61 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 51cad09ecfd..7c5487ce9a1 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -33,7 +33,6 @@ interface IState { phase?: Phase; verificationRequest: VerificationRequest | null; backupInfo: KeyBackupInfo | null; - lostKeys: boolean; } export default class SetupEncryptionBody extends React.Component { @@ -48,7 +47,6 @@ export default class SetupEncryptionBody extends React.Component // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, - lostKeys: store.lostKeys(), }; } @@ -67,7 +65,6 @@ export default class SetupEncryptionBody extends React.Component phase: store.phase, verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, - lostKeys: store.lostKeys(), }); }; @@ -112,7 +109,7 @@ export default class SetupEncryptionBody extends React.Component store.returnAfterSkip(); }; - private onResetClick = (ev: ButtonEvent): void => { + private onCantConfirmClick = (ev: ButtonEvent): void => { ev.preventDefault(); const store = SetupEncryptionStore.sharedInstance(); Modal.createDialog(ResetIdentityDialog, { @@ -137,7 +134,7 @@ export default class SetupEncryptionBody extends React.Component 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 ( @@ -150,63 +147,49 @@ export default class SetupEncryptionBody extends React.Component /> ); } else if (phase === Phase.Intro) { - if (lostKeys) { - return ( -
-

{_t("encryption|verification|no_key_or_device")}

- -
- - {_t("encryption|verification|reset_proceed_prompt")} - -
-
+ const store = SetupEncryptionStore.sharedInstance(); + + let verifyButton; + if (store.hasDevicesToVerifyAgainst) { + verifyButton = ( + + {_t("encryption|verification|verify_using_device")} + ); - } else { - const store = SetupEncryptionStore.sharedInstance(); + } - let verifyButton; - if (store.hasDevicesToVerifyAgainst) { - verifyButton = ( - - {_t("encryption|verification|verify_using_device")} - - ); - } - - let useRecoveryKeyButton; - if (store.keyInfo) { - useRecoveryKeyButton = ( - - {_t("encryption|verification|verify_using_key")} - - ); - } - - return ( - - - {_t("encryption|verification|verification_description")} - - - {_t("action|learn_more")} - - - - - {verifyButton} - {useRecoveryKeyButton} - - {_t("encryption|verification|cant_confirm")} - - - + let useRecoveryKeyButton; + if (store.keyInfo) { + useRecoveryKeyButton = ( + + {_t("encryption|verification|verify_using_key")} + ); } + + return ( + + + {_t("encryption|verification|verification_description")} + + + {_t("action|learn_more")} + + + + + {verifyButton} + {useRecoveryKeyButton} + + {_t("encryption|verification|cant_confirm")} + + + + ); } else if (phase === Phase.Done) { let message: JSX.Element; if (this.state.backupInfo) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2cb12c63b5..96881baa03b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -967,7 +967,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? Reset all", "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", @@ -1027,7 +1026,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 verification keys.", "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 +1041,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", From a7a34a0f7d52784871896b6de6635898599510a9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 19:25:44 -0400 Subject: [PATCH 06/15] make it agree with the design more --- .../structures/auth/SetupEncryptionBody.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 7c5487ce9a1..d5cd19b90bc 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -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 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,7 +19,7 @@ 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"; import { EncryptionCard } from "../../views/settings/encryption/EncryptionCard"; @@ -109,8 +111,7 @@ export default class SetupEncryptionBody extends React.Component store.returnAfterSkip(); }; - private onCantConfirmClick = (ev: ButtonEvent): void => { - ev.preventDefault(); + private onCantConfirmClick = (): void => { const store = SetupEncryptionStore.sharedInstance(); Modal.createDialog(ResetIdentityDialog, { onReset: () => { @@ -152,18 +153,20 @@ export default class SetupEncryptionBody extends React.Component let verifyButton; if (store.hasDevicesToVerifyAgainst) { verifyButton = ( - - {_t("encryption|verification|verify_using_device")} - + ); } let useRecoveryKeyButton; if (store.keyInfo) { useRecoveryKeyButton = ( - + + ); + } ); } @@ -184,9 +187,9 @@ export default class SetupEncryptionBody extends React.Component {verifyButton} {useRecoveryKeyButton} - +
); From d145fa47e94721de0afbe2be65679b5294649478 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 19:26:10 -0400 Subject: [PATCH 07/15] allow signing out on initial login --- .../structures/auth/CompleteSecurity.tsx | 2 +- .../structures/auth/SetupEncryptionBody.tsx | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 5bade7b24a0..87e34432693 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -106,7 +106,7 @@ export default class CompleteSecurity extends React.Component { {skipButton}
- +
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index d5cd19b90bc..65006bb997c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -26,9 +26,20 @@ 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; + /** + * Allow the user to log out, rather than setting up encryption. + * + * This is only 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 { @@ -124,6 +135,10 @@ export default class SetupEncryptionBody extends React.Component }); }; + private onSignOutClick = (): void => { + dispatcher.dispatch({ action: "logout" }); + }; + private onDoneClick = (): void => { const store = SetupEncryptionStore.sharedInstance(); store.done(); @@ -167,6 +182,13 @@ export default class SetupEncryptionBody extends React.Component ); } + + let signOutButton; + if (this.props.allowLogout) { + signOutButton = ( + ); } @@ -190,6 +212,7 @@ export default class SetupEncryptionBody extends React.Component + {signOutButton} ); From fcf3b1f169b07c129fdface980af2977b20e2c69 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 19:34:51 -0400 Subject: [PATCH 08/15] apply styling changes and remove duplicate elements --- .../structures/auth/_SetupEncryptionBody.pcss | 3 ++ res/css/views/auth/_AuthPage.pcss | 9 ++++ res/css/views/auth/_CompleteSecurityBody.pcss | 3 +- .../settings/encryption/_EncryptionCard.pcss | 7 +++ .../structures/auth/CompleteSecurity.tsx | 43 +++++++++---------- .../structures/auth/SetupEncryptionBody.tsx | 3 ++ src/components/views/auth/AuthPage.tsx | 14 +++++- .../security/SetupEncryptionDialog.tsx | 40 +---------------- src/i18n/strings/en_EN.json | 5 +-- 9 files changed, 60 insertions(+), 67 deletions(-) diff --git a/res/css/structures/auth/_SetupEncryptionBody.pcss b/res/css/structures/auth/_SetupEncryptionBody.pcss index 678063812fe..204e1f5c597 100644 --- a/res/css/structures/auth/_SetupEncryptionBody.pcss +++ b/res/css/structures/auth/_SetupEncryptionBody.pcss @@ -6,3 +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 { + width: 600px; +} diff --git a/res/css/views/auth/_AuthPage.pcss b/res/css/views/auth/_AuthPage.pcss index 3ae17122be1..5f8ef7c6a92 100644 --- a/res/css/views/auth/_AuthPage.pcss +++ b/res/css/views/auth/_AuthPage.pcss @@ -29,4 +29,13 @@ Please see LICENSE files in the repository root for full details. @media only screen and (max-width: 480px) { margin-top: 0; } + + /* Extra class to indicate that the component is providing its own blurring, + * so we don't apply ours */ + &.mx_AuthPage_modal_noBlur { + box-shadow: none; + .mx_AuthPage_modalContent { + background: none !important; + } + } } diff --git a/res/css/views/auth/_CompleteSecurityBody.pcss b/res/css/views/auth/_CompleteSecurityBody.pcss index f0701290051..3aa9cbf518a 100644 --- a/res/css/views/auth/_CompleteSecurityBody.pcss +++ b/res/css/views/auth/_CompleteSecurityBody.pcss @@ -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 { diff --git a/res/css/views/settings/encryption/_EncryptionCard.pcss b/res/css/views/settings/encryption/_EncryptionCard.pcss index 5598facfa95..5aba3fe7d75 100644 --- a/res/css/views/settings/encryption/_EncryptionCard.pcss +++ b/res/css/views/settings/encryption/_EncryptionCard.pcss @@ -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 { diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 87e34432693..f30b48397cb 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -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 { 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 { 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 { } 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 = ; - title = _t("encryption|verification|after_new_login|unable_to_verify"); - } else { - icon = ; - 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 = ; title = _t("encryption|verification|after_new_login|device_verified"); @@ -98,17 +95,19 @@ export default class CompleteSecurity extends React.Component { } return ( - - -

- {icon} - {title} - {skipButton} -

-
- -
-
+ + + +

+ {icon} + {title} + {skipButton} +

+
+ +
+
+
); } diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 65006bb997c..d1d9b8b2203 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -48,6 +48,9 @@ interface IState { backupInfo: KeyBackupInfo | null; } +/** + * Component to set up encryption by verifying the current device. + */ export default class SetupEncryptionBody extends React.Component { public constructor(props: IProps) { super(props); diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index dd3facaa79a..6d6b66d734d 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -8,11 +8,19 @@ 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 { +interface IProps { + /** + * An additional class to apply to the modal wrapper. + */ + modalClass?: string; +} + +export default class AuthPage extends React.PureComponent> { private static welcomeBackgroundUrl?: string; // cache the url as a static to prevent it changing without refreshing @@ -62,9 +70,11 @@ export default class AuthPage extends React.PureComponent -
+
{this.props.children} diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 2f845b627a9..dabea9660db 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -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 { - private store: SetupEncryptionStore; +export default class SetupEncryptionDialog extends React.Component { 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 ( - + ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 96881baa03b..770dd255826 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -989,7 +989,6 @@ "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.", @@ -1073,8 +1072,8 @@ "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_device": "Use another device", + "verify_using_key": "Use recovery key", "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)…", From c032ed2c0367608322d5cbedead757f8b77749e2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 19:36:18 -0400 Subject: [PATCH 09/15] fix and add tests --- playwright/e2e/crypto/dehydration.spec.ts | 4 +- .../e2e/crypto/device-verification.spec.ts | 8 +-- playwright/e2e/crypto/utils.ts | 6 +- playwright/e2e/login/login-consent.spec.ts | 30 +++++----- playwright/e2e/oidc/oidc-native.spec.ts | 10 ++-- .../encryption-tab.spec.ts | 6 +- .../e2e/settings/encryption-user-tab/index.ts | 2 +- .../components/structures/MatrixChat-test.tsx | 16 +++--- .../structures/auth/CompleteSecurity-test.tsx | 57 ++++++++++++++++++- .../security/SetupEncryptionDialog-test.tsx | 2 +- 10 files changed, 97 insertions(+), 44 deletions(-) diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 379fc36cf99..4483593f1cb 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -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(); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index ab36c37a76b..bd1c0ca691b 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -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> { // 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"); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 0521df236e2..a5fdbf0a680 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -218,9 +218,9 @@ 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 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. @@ -262,7 +262,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(); diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index 23baf023fa3..51486b1ce03 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -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(); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 8b49942dd35..f084febdebe 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -133,8 +133,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", () => { @@ -166,7 +166,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(); @@ -214,7 +214,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(); @@ -231,7 +231,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(); diff --git a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts index ed1daecf350..64ecfa1b83f 100644 --- a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts +++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts @@ -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(); diff --git a/playwright/e2e/settings/encryption-user-tab/index.ts b/playwright/e2e/settings/encryption-user-tab/index.ts index a7351fd2b4f..327199b6d9e 100644 --- a/playwright/e2e/settings/encryption-user-tab/index.ts +++ b/playwright/e2e/settings/encryption-user-tab/index.ts @@ -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(); } diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 928b5d7ffd3..e16e42d0d4f 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1081,10 +1081,10 @@ describe("", () => { 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("", () => { // 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("", () => { 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("", () => { ); // 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("", () => { 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 () => { diff --git a/test/unit-tests/components/structures/auth/CompleteSecurity-test.tsx b/test/unit-tests/components/structures/auth/CompleteSecurity-test.tsx index c5efd81f0d3..95024c6113e 100644 --- a/test/unit-tests/components/structures/auth/CompleteSecurity-test.tsx +++ b/test/unit-tests/components/structures/auth/CompleteSecurity-test.tsx @@ -83,13 +83,66 @@ describe("CompleteSecurity", () => { jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); const panel = await act(() => render( {}} />)); + // 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( {}} />)); + + // 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( {}} />)); + + // 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(); }); }); diff --git a/test/unit-tests/components/views/dialogs/security/SetupEncryptionDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/SetupEncryptionDialog-test.tsx index 0b5273e7258..bfaa1b1f595 100644 --- a/test/unit-tests/components/views/dialogs/security/SetupEncryptionDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/SetupEncryptionDialog-test.tsx @@ -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); From a2e32aa7471869e3882409caf93a5e8e9cf4f951 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 27 Aug 2025 20:16:42 -0400 Subject: [PATCH 10/15] add missing snapshot --- .../CompleteSecurity-test.tsx.snap | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap diff --git a/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap b/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap new file mode 100644 index 00000000000..b54780b6871 --- /dev/null +++ b/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap @@ -0,0 +1,330 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CompleteSecurity Allows verifying with another device if one is available 1`] = ` + +
+
+
+
+
+
+

+
+

+
+
+
+
+ + + +
+

+ Confirm your identity +

+
+
+ + Verify this device to set up secure messaging + + + + Learn more + + + +
+
+ + + +
+
+
+
+
+
+
+ +
+ +`; + +exports[`CompleteSecurity Allows verifying with recovery key if one is available 1`] = ` + +
+
+
+
+
+
+

+
+

+
+
+
+
+ + + +
+

+ Confirm your identity +

+
+
+ + Verify this device to set up secure messaging + + + + Learn more + + + +
+
+ + + +
+
+
+
+
+
+
+ +
+ +`; From 52a474cbe884f5f35ad0bb9281657ec2bd1cebe5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Sep 2025 17:15:41 -0400 Subject: [PATCH 11/15] Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/components/structures/auth/SetupEncryptionBody.tsx | 4 ++-- src/components/views/auth/AuthPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index d1d9b8b2203..83876f79c0a 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -31,9 +31,9 @@ import dispatcher from "../../../dispatcher/dispatcher"; interface IProps { onFinished: () => void; /** - * Allow the user to log out, rather than setting up encryption. + * Offer the user an option to log out, instead of setting up encryption. * - * This is only used when this component is shown when the user is initially + * 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. * diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index 6d6b66d734d..e6cbaa9e9c5 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -15,7 +15,7 @@ import AuthFooter from "./AuthFooter"; interface IProps { /** - * An additional class to apply to the modal wrapper. + * A CSS class to apply to the modal wrapper, in addition to the default `mx_AuthPage_modal`. */ modalClass?: string; } From 1d34e2aae3001deec9341e40ecd1e2d60547aef3 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Sep 2025 22:59:36 -0400 Subject: [PATCH 12/15] use a boolean property to disable blurring instead of adding a class --- res/css/views/auth/_AuthPage.pcss | 5 +---- .../structures/auth/CompleteSecurity.tsx | 2 +- src/components/views/auth/AuthPage.tsx | 21 +++++++++++++++---- .../CompleteSecurity-test.tsx.snap | 12 ++--------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/res/css/views/auth/_AuthPage.pcss b/res/css/views/auth/_AuthPage.pcss index 5f8ef7c6a92..f75daf10838 100644 --- a/res/css/views/auth/_AuthPage.pcss +++ b/res/css/views/auth/_AuthPage.pcss @@ -30,12 +30,9 @@ Please see LICENSE files in the repository root for full details. margin-top: 0; } - /* Extra class to indicate that the component is providing its own blurring, + /* Extra class to indicate that the component is providing its own blurring/shadow, * so we don't apply ours */ &.mx_AuthPage_modal_noBlur { box-shadow: none; - .mx_AuthPage_modalContent { - background: none !important; - } } } diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index f30b48397cb..07a4852895e 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -95,7 +95,7 @@ export default class CompleteSecurity extends React.Component { } return ( - +

diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index e6cbaa9e9c5..200c51ea2bb 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -15,9 +15,12 @@ import AuthFooter from "./AuthFooter"; interface IProps { /** - * A CSS class to apply to the modal wrapper, in addition to the default `mx_AuthPage_modal`. + * 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`. */ - modalClass?: string; + addBlur?: boolean; } export default class AuthPage extends React.PureComponent> { @@ -70,12 +73,22 @@ export default class AuthPage extends React.PureComponent; + } else { + delete modalContentStyle.background; + } + + const modalClasses = classNames({ + mx_AuthPage_modal: true, + mx_AuthPage_modal_noBlur: this.props.addBlur === false, + }); return (
-
+ {modalBlur}
{this.props.children}
diff --git a/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap b/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap index b54780b6871..f5937b0b20c 100644 --- a/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap +++ b/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap @@ -9,13 +9,9 @@ exports[`CompleteSecurity Allows verifying with another device if one is availab class="mx_AuthPage_modal mx_AuthPage_modal_noBlur" style="position: relative;" > -
-
Date: Wed, 3 Sep 2025 23:55:32 -0400 Subject: [PATCH 13/15] change string identifiers --- src/components/structures/auth/SetupEncryptionBody.tsx | 6 +++--- src/i18n/strings/en_EN.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 83876f79c0a..5098265e67f 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -172,7 +172,7 @@ export default class SetupEncryptionBody extends React.Component if (store.hasDevicesToVerifyAgainst) { verifyButton = ( ); } @@ -181,7 +181,7 @@ export default class SetupEncryptionBody extends React.Component if (store.keyInfo) { useRecoveryKeyButton = ( ); } @@ -202,7 +202,7 @@ export default class SetupEncryptionBody extends React.Component className="mx_EncryptionCard_noBorder mx_SetupEncryptionBody" > - {_t("encryption|verification|verification_description")} + {_t("encryption|verification|confirm_identity_description")} {_t("action|learn_more")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9ca12b316e3..25c3c3aae3e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1000,6 +1000,7 @@ "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", @@ -1063,7 +1064,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 this device to set up secure messaging", + "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.", @@ -1073,8 +1075,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": "Use another device", - "verify_using_key": "Use recovery key", "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)…", From ce42a8dee18144eb8d5e8552c635809f0e64229e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 11 Sep 2025 13:47:58 -0400 Subject: [PATCH 14/15] apply changes from review -- simplify logic --- res/css/views/auth/_AuthPage.pcss | 8 +++----- src/components/views/auth/AuthPage.tsx | 8 ++++---- .../structures/__snapshots__/MatrixChat-test.tsx.snap | 8 ++++---- .../auth/__snapshots__/CompleteSecurity-test.tsx.snap | 4 ++-- .../views/auth/__snapshots__/AuthPage-test.tsx.snap | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/res/css/views/auth/_AuthPage.pcss b/res/css/views/auth/_AuthPage.pcss index f75daf10838..a209435c421 100644 --- a/res/css/views/auth/_AuthPage.pcss +++ b/res/css/views/auth/_AuthPage.pcss @@ -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) { @@ -30,9 +29,8 @@ Please see LICENSE files in the repository root for full details. margin-top: 0; } - /* Extra class to indicate that the component is providing its own blurring/shadow, - * so we don't apply ours */ - &.mx_AuthPage_modal_noBlur { - box-shadow: none; + /* Apply a blurred shadow around the modal */ + &.mx_AuthPage_modal_blur { + box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33); } } diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index 200c51ea2bb..00fa6d8208a 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -69,20 +69,20 @@ export default class AuthPage extends React.PureComponent; - } else { - delete modalContentStyle.background; + modalContentStyle.background = "rgba(255, 255, 255, 0.59)"; } const modalClasses = classNames({ mx_AuthPage_modal: true, - mx_AuthPage_modal_noBlur: this.props.addBlur === false, + mx_AuthPage_modal_blur: this.props.addBlur !== false, }); return ( diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 7eee28dbb84..fdf6c6f76b3 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -113,7 +113,7 @@ exports[` Multi-tab lockout waits for other tab to stop during sta class="mx_AuthPage" >
Multi-tab lockout waits for other tab to stop during sta />
with a soft-logged-out session should show the soft-logo class="mx_AuthPage" >
with a soft-logged-out session should show the soft-logo />
should match snapshot 1`] = ` class="mx_AuthPage" >
should match snapshot 1`] = ` />