diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 6a112c7c82c..c4904952b6c 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -32,36 +32,10 @@ limitations under the License. margin-top: $spacing-8; } - .mx_LoginWithQR_separator { - display: flex; - align-items: center; - text-align: center; - - &::before, - &::after { - content: ""; - flex: 1; - border-bottom: 1px solid $quinary-content; - } - - &:not(:empty) { - &::before { - margin-right: 1em; - } - &::after { - margin-left: 1em; - } - } - } - font-size: $font-15px; } .mx_UserSettingsDialog .mx_LoginWithQR { - .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: $spacing-12; - } - font: var(--cpd-font-body-md-regular); h1 { @@ -69,18 +43,14 @@ limitations under the License. margin-bottom: 0; } - li { - line-height: 1.8; + h2 { + margin-top: $spacing-24; } .mx_QRCode { margin: $spacing-28 0; } - .mx_LoginWithQR_buttons { - text-align: center; - } - .mx_LoginWithQR_qrWrapper { display: flex; } @@ -91,12 +61,6 @@ limitations under the License. display: flex; flex-direction: column; - .mx_LoginWithQR_centreTitle { - h1 { - text-align: center; - } - } - h1 > svg { &.normal { color: $secondary-content; @@ -137,11 +101,69 @@ limitations under the License. } ol { - list-style-position: inside; padding-inline-start: 0; + list-style: none; /* list markers do not support the outlined number styling we need */ + + li { + position: relative; + padding-left: var(--cpd-space-7x); + color: 1px solid $input-placeholder; + margin-bottom: var(--cpd-space-4x); + line-height: 20px; + text-align: initial; + } - li::marker { - color: $accent; + /* Circled number list item marker */ + li::before { + content: counter(list-item); + position: absolute; + left: 0; + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + border-radius: 50%; + border: 1px solid $input-placeholder; + box-sizing: border-box; + text-align: center; + } + } + + label[for="mx_LoginWithQR_checkCode"] { + margin-top: var(--cpd-space-6x); + color: var(--cpd-color-text-primary); + margin-bottom: var(--cpd-space-1x); + } + + .mx_LoginWithQR_icon { + width: 56px; + height: 56px; + border-radius: 8px; + box-sizing: border-box; + padding: var(--cpd-space-3x); + gap: 10px; + + background-color: var(--cpd-color-bg-success-subtle); + svg { + color: var(--cpd-color-icon-success-primary); + } + + &.mx_LoginWithQR_icon--critical { + background-color: var(--cpd-color-bg-critical-subtle); + svg { + color: var(--cpd-color-icon-critical-primary); + } + } + } + + .mx_LoginWithQR_checkCode_input { + margin-bottom: var(--cpd-space-1x); + text-align: initial; + + input { + /* Workaround for one of the input rules in _common.pcss being not specific enough */ + padding: 0; + padding-inline-start: calc(40px / 2 - (1ch / 2)); } } @@ -164,13 +186,39 @@ limitations under the License. .mx_LoginWithQR_breadcrumbs { font-size: $font-13px; - color: var(--cpd-color-text-secondary); + color: $secondary-content; } .mx_LoginWithQR_main { display: flex; flex-direction: column; flex-grow: 1; + align-items: center; + color: $primary-content; + text-align: center; + + p { + color: $secondary-content; + } + } + + &.mx_LoginWithQR_error .mx_LoginWithQR_main { + max-width: 400px; + margin: 0 auto; + } + + .mx_LoginWithQR_buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-16; + margin-top: var(--cpd-space-6x); + + .mx_AccessibleButton { + width: 300px; + height: 48px; + box-sizing: border-box; + } } .mx_QRCode { diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts new file mode 100644 index 00000000000..33f709d79c4 --- /dev/null +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -0,0 +1,43 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * The intention of this enum is to have a mode that scans a QR code instead of generating one. + */ +export enum Mode { + /** + * A QR code with be generated and shown + */ + Show = "show", +} + +export enum Phase { + Loading, + ShowingQR, + Connecting, + Connected, + WaitingForDevice, + Verifying, + Error, +} + +export enum Click { + Cancel, + Decline, + Approve, + TryAgain, + Back, +} diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 1e2efb5106f..feb869deaf4 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -24,34 +24,7 @@ import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import LoginWithQRFlow from "./LoginWithQRFlow"; - -/** - * The intention of this enum is to have a mode that scans a QR code instead of generating one. - */ -export enum Mode { - /** - * A QR code with be generated and shown - */ - Show = "show", -} - -export enum Phase { - Loading, - ShowingQR, - Connecting, - Connected, - WaitingForDevice, - Verifying, - Error, -} - -export enum Click { - Cancel, - Decline, - Approve, - TryAgain, - Back, -} +import { Click, Mode, Phase } from "./LoginWithQR-types"; interface IProps { client: MatrixClient; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 05c8d95c423..b5daddbe0cd 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,19 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import React, { ReactNode } from "react"; +import { RendezvousFailureReason as LegacyRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; +import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg"; +import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; +import { Heading, Text } from "@vector-im/compound-web"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR"; +import { Click, Phase } from "./LoginWithQR-types"; import SdkConfig from "../../../SdkConfig"; +import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR"; -interface IProps { +interface Props { phase: Phase; code?: string; onClick(type: Click): Promise; @@ -39,8 +44,8 @@ interface IProps { * * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ -export default class LoginWithQRFlow extends React.Component { - public constructor(props: IProps) { +export default class LoginWithQRFlow extends React.Component { + public constructor(props: Props) { super(props); } @@ -72,49 +77,75 @@ export default class LoginWithQRFlow extends React.Component { let main: JSX.Element | undefined; let buttons: JSX.Element | undefined; let backButton = true; - let cancellationMessage: string | undefined; - let centreTitle = false; + let className = ""; switch (this.props.phase) { - case Phase.Error: + case Phase.Error: { + let success = false; + let title: string | undefined; + let message: ReactNode | undefined; + switch (this.props.failureReason) { - case RendezvousFailureReason.Expired: - cancellationMessage = _t("auth|qr_code_login|error_linking_incomplete"); - break; - case RendezvousFailureReason.InvalidCode: - cancellationMessage = _t("auth|qr_code_login|error_invalid_scanned_code"); + case LegacyRendezvousFailureReason.UnsupportedAlgorithm: + case LegacyRendezvousFailureReason.UnsupportedTransport: + case LegacyRendezvousFailureReason.HomeserverLacksSupport: + title = _t("auth|qr_code_login|error_unsupported_protocol_title"); + message = _t("auth|qr_code_login|error_unsupported_protocol"); break; - case RendezvousFailureReason.UnsupportedAlgorithm: - cancellationMessage = _t("auth|qr_code_login|error_device_unsupported"); + + case LegacyRendezvousFailureReason.UserCancelled: + title = _t("auth|qr_code_login|error_user_cancelled_title"); + message = _t("auth|qr_code_login|error_user_cancelled"); break; - case RendezvousFailureReason.UserDeclined: - cancellationMessage = _t("auth|qr_code_login|error_request_declined"); + + case LegacyRendezvousFailureReason.Expired: + title = _t("auth|qr_code_login|error_expired_title"); + message = _t("auth|qr_code_login|error_expired"); break; - case RendezvousFailureReason.OtherDeviceAlreadySignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_already_signed_in"); + + case LegacyRendezvousFailureReason.InvalidCode: + title = _t("auth|qr_code_login|error_insecure_channel_detected_title"); + message = ( + <> + {_t("auth|qr_code_login|error_insecure_channel_detected")} + + + {_t("auth|qr_code_login|error_insecure_channel_detected_instructions")} + +
    +
  1. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_1")}
  2. +
  3. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_2")}
  4. +
  5. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_3")}
  6. +
+ + ); break; - case RendezvousFailureReason.OtherDeviceNotSignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_not_signed_in"); + + case LegacyRendezvousFailureReason.OtherDeviceAlreadySignedIn: + success = true; + title = _t("auth|qr_code_login|error_other_device_already_signed_in_title"); + message = _t("auth|qr_code_login|error_other_device_already_signed_in"); break; - case RendezvousFailureReason.UserCancelled: - cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); + + case LegacyRendezvousFailureReason.UserDeclined: + title = _t("auth|qr_code_login|error_user_declined_title"); + message = _t("auth|qr_code_login|error_user_declined"); break; + case LoginWithQRFailureReason.RateLimited: - cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); - break; - case RendezvousFailureReason.Unknown: - cancellationMessage = _t("auth|qr_code_login|error_unexpected"); - break; - case RendezvousFailureReason.HomeserverLacksSupport: - cancellationMessage = _t("auth|qr_code_login|error_homeserver_lacks_support"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_rate_limited"); break; + + case LegacyRendezvousFailureReason.OtherDeviceNotSignedIn: + case LegacyRendezvousFailureReason.Unknown: default: - cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_unexpected"); break; } - centreTitle = true; + className = "mx_LoginWithQR_error"; backButton = false; - main =

{cancellationMessage}

; buttons = ( <> { {this.cancelButton()} ); + main = ( + <> +
+ {success ? : } +
+ + {title} + + {typeof message === "object" ? message :

{message}

} + + ); break; + } case Phase.Connected: backButton = false; main = ( @@ -145,13 +192,6 @@ export default class LoginWithQRFlow extends React.Component { buttons = ( <> - - {_t("action|cancel")} - { > {_t("action|approve")} + + {_t("action|cancel")} + ); break; case Phase.ShowingQR: if (this.props.code) { - const code = ( -
- -
- ); + const data = Buffer.from(this.props.code ?? ""); + main = ( <> -

{_t("auth|qr_code_login|scan_code_instruction")}

- {code} + + {_t("auth|qr_code_login|scan_code_instruction")} + +
+ +
  1. {_t("auth|qr_code_login|open_element_other_device", { @@ -209,30 +254,27 @@ export default class LoginWithQRFlow extends React.Component { buttons = this.cancelButton(); break; case Phase.Verifying: - centreTitle = true; main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); break; } return ( -
    -
    - {backButton ? ( -
    - - - -
    - {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")} -
    +
    + {backButton ? ( +
    + + + +
    + {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")}
    - ) : null} -
    +
    + ) : null}
    {main}
    {buttons}
    diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index c85b80c3a7a..b83668b6b84 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -35,39 +35,40 @@ interface IProps { wellKnown?: IClientWellKnown; } -export default class LoginWithQRSection extends React.Component { - public constructor(props: IProps) { - super(props); - } - - public render(): JSX.Element | null { - // Needs server support for get_login_token and MSC3886: - // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn(this.props.capabilities); - const getLoginTokenSupported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled; - const msc3886Supported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || - this.props.wellKnown?.["io.element.rendezvous"]?.server; - const offerShowQr = getLoginTokenSupported && msc3886Supported; +function shouldShowQrLegacy( + versions?: IServerVersions, + wellKnown?: IClientWellKnown, + capabilities?: Capabilities, +): boolean { + // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: + // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability + const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); + const getLoginTokenSupported = + !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; + const msc3886Supported = + !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; + return getLoginTokenSupported && msc3886Supported; +} - // don't show anything if no method is available - if (!offerShowQr) { - return null; - } +const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown }) => { + const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities); - return ( - -
    -

    - {_t("settings|sessions|sign_in_with_qr_description")} -

    - - - {_t("settings|sessions|sign_in_with_qr_button")} - -
    -
    - ); + // don't show anything if no method is available + if (!offerShowQr) { + return null; } -} + + return ( + +
    +

    {_t("settings|sessions|sign_in_with_qr_description")}

    + + + {_t("settings|sessions|sign_in_with_qr_button")} + +
    +
    + ); +}; + +export default LoginWithQRSection; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 61c8e85f8d1..c711b7e39e5 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -32,7 +32,8 @@ import { ExtendedDevice } from "../../devices/types"; import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices"; import SettingsTab from "../SettingsTab"; import LoginWithQRSection from "../../devices/LoginWithQRSection"; -import LoginWithQR, { Mode } from "../../../auth/LoginWithQR"; +import LoginWithQR from "../../../auth/LoginWithQR"; +import { Mode } from "../../../auth/LoginWithQR-types"; import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo"; import QuestionDialog from "../../../dialogs/QuestionDialog"; import { FilterVariation } from "../../devices/filter"; @@ -284,6 +285,12 @@ const SessionManagerTab: React.FC = () => { return ( + { /> )} - ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 816f909ec31..ddd6e3e09c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,21 +249,29 @@ "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", "connecting": "Connecting…", - "error_device_already_signed_in": "The other device is already signed in.", - "error_device_not_signed_in": "The other device isn't signed in.", - "error_device_unsupported": "Linking with this device is not supported.", - "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", - "error_invalid_scanned_code": "The scanned code is invalid.", - "error_linking_incomplete": "The linking wasn't completed in the required time.", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", - "error_request_cancelled": "The request was cancelled.", - "error_request_declined": "The request was declined on the other device.", - "error_unexpected": "An unexpected error occurred.", - "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", + "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", + "error_user_declined": "You declined the request from your other device to sign in.", + "error_user_declined_title": "Sign in declined", + "follow_remaining_instructions": "Follow the instructions to link your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", - "scan_qr_code": "Scan QR code", + "scan_qr_code": "Sign in with QR code", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", "waiting_for_device": "Waiting for device to sign in" diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index cdbb46a8b68..7a1a269064e 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -20,7 +20,8 @@ import React from "react"; import { MSC3906Rendezvous, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; -import LoginWithQR, { Click, Mode, Phase } from "../../../../../src/components/views/auth/LoginWithQR"; +import LoginWithQR from "../../../../../src/components/views/auth/LoginWithQR"; +import { Click, Mode, Phase } from "../../../../../src/components/views/auth/LoginWithQR-types"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; jest.mock("matrix-js-sdk/src/rendezvous"); diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 614a8d3ffd4..21863c84a49 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -19,12 +19,8 @@ import React from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow"; -import { - Click, - Phase, - LoginWithQRFailureReason, - FailureReason, -} from "../../../../../src/components/views/auth/LoginWithQR"; +import { LoginWithQRFailureReason, FailureReason } from "../../../../../src/components/views/auth/LoginWithQR"; +import { Click, Phase } from "../../../../../src/components/views/auth/LoginWithQR-types"; describe("", () => { const onClick = jest.fn(); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index fb323ed875c..395a86f994d 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -3,19 +3,28 @@ exports[` errors renders data_mismatch 1`] = `
    -
    +
    +
    +
    +

    + Something went wrong! +

    - The request was cancelled. + An unexpected error occurred. The request to connect your other device has been cancelled.

    errors renders data_mismatch 1`] = ` exports[` errors renders expired 1`] = `
    -
    +
    +
    +
    +

    + The sign in was not completed in time +

    - The linking wasn't completed in the required time. + Sign in expired. Please try again.

    errors renders expired 1`] = ` exports[` errors renders homeserver_lacks_support 1`] = `
    -
    +
    +
    +
    +

    + Other device not compatible +

    - The homeserver doesn't support signing in another device. + This device does not support signing in to the other device with a QR code.

    errors renders homeserver_lacks_support 1`] = ` exports[` errors renders invalid_code 1`] = `
    -
    -

    +

    +
    +

    + Connection not secure +

    + A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them. +

    - The scanned code is invalid. -

    + Now what? +

    +
      +
    1. + Try signing in to the other device again with a QR code in case this was a network problem +
    2. +
    3. + If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi +
    4. +
    5. + If that doesn't work, sign in manually +
    6. +
    errors renders invalid_code 1`] = ` exports[` errors renders other_device_already_signed_in 1`] = `
    -
    +
    +
    +
    +

    + Your other device is already signed in +

    - The other device is already signed in. + You don’t need to do anything else.

    errors renders other_device_already_signed_in 1`] = exports[` errors renders other_device_not_signed_in 1`] = `
    -
    +
    +
    +
    +

    + Something went wrong! +

    - The other device isn't signed in. + An unexpected error occurred. The request to connect your other device has been cancelled.

    errors renders other_device_not_signed_in 1`] = ` exports[` errors renders rate_limited 1`] = `
    -
    +
    +
    +
    +

    + Something went wrong! +

    @@ -297,19 +373,28 @@ exports[` errors renders rate_limited 1`] = ` exports[` errors renders unknown 1`] = `

    -
    +
    +
    +
    +

    + Something went wrong! +

    - An unexpected error occurred. + An unexpected error occurred. The request to connect your other device has been cancelled.

    errors renders unknown 1`] = ` exports[` errors renders unsupported_algorithm 1`] = `
    -
    +
    +
    +
    +

    + Other device not compatible +

    - Linking with this device is not supported. + This device does not support signing in to the other device with a QR code.

    errors renders unsupported_algorithm 1`] = ` exports[` errors renders unsupported_transport 1`] = `
    -
    +
    +
    +
    +

    + Other device not compatible +

    - The request was cancelled. + This device does not support signing in to the other device with a QR code.

    errors renders unsupported_transport 1`] = ` exports[` errors renders user_cancelled 1`] = `
    -
    +
    +
    +
    +

    + Sign in request cancelled +

    - The request was cancelled. + The sign in was cancelled on the other device.

    errors renders user_cancelled 1`] = ` exports[` errors renders user_declined 1`] = `
    -
    +
    +
    +
    +

    + Sign in declined +

    - The request was declined on the other device. + You declined the request from your other device to sign in.

    renders QR code 1`] = ` data-testid="login-with-qr" >
    -
    -
    -
    -
    - Sessions - / - Link new device -
    +
    +
    +
    + Sessions + / + Link new device
    -

    +

    Scan the QR code with another device

    renders QR code 1`] = ` Select " - Scan QR code + Sign in with QR code " @@ -571,7 +690,7 @@ exports[` renders QR code 1`] = ` Point the camera at the QR code shown here
  2. - Follow the remaining instructions to verify your other device + Follow the instructions to link your other device
@@ -588,9 +707,6 @@ exports[` renders code when connected 1`] = ` class="mx_LoginWithQR" data-testid="login-with-qr" > -
@@ -617,20 +733,20 @@ exports[` renders code when connected 1`] = ` class="mx_LoginWithQR_buttons" >
- Cancel + Approve
- Approve + Cancel
@@ -644,28 +760,24 @@ exports[` renders spinner while connecting 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while loading 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while signing in 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while verifying 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner whilst QR generating 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device