Prepare for OIDC QR Login PR (#12463)

* Move LoginWithQRSection to the top of the settings tab

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Refactor LoginWithQRSection to a Functional Component

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Extract LoginWithQR types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update LoginWithQRFlow styling & copy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Re-add missing buttons and update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Use compound spacings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-04-30 18:18:55 +01:00 committed by GitHub
parent 1c79bbb1ae
commit 641a20ce63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 598 additions and 389 deletions

View file

@ -32,36 +32,10 @@ limitations under the License.
margin-top: $spacing-8; 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; font-size: $font-15px;
} }
.mx_UserSettingsDialog .mx_LoginWithQR { .mx_UserSettingsDialog .mx_LoginWithQR {
.mx_AccessibleButton + .mx_AccessibleButton {
margin-left: $spacing-12;
}
font: var(--cpd-font-body-md-regular); font: var(--cpd-font-body-md-regular);
h1 { h1 {
@ -69,18 +43,14 @@ limitations under the License.
margin-bottom: 0; margin-bottom: 0;
} }
li { h2 {
line-height: 1.8; margin-top: $spacing-24;
} }
.mx_QRCode { .mx_QRCode {
margin: $spacing-28 0; margin: $spacing-28 0;
} }
.mx_LoginWithQR_buttons {
text-align: center;
}
.mx_LoginWithQR_qrWrapper { .mx_LoginWithQR_qrWrapper {
display: flex; display: flex;
} }
@ -91,12 +61,6 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.mx_LoginWithQR_centreTitle {
h1 {
text-align: center;
}
}
h1 > svg { h1 > svg {
&.normal { &.normal {
color: $secondary-content; color: $secondary-content;
@ -137,11 +101,69 @@ limitations under the License.
} }
ol { ol {
list-style-position: inside;
padding-inline-start: 0; padding-inline-start: 0;
list-style: none; /* list markers do not support the outlined number styling we need */
li::marker { li {
color: $accent; 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;
}
/* 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 { .mx_LoginWithQR_breadcrumbs {
font-size: $font-13px; font-size: $font-13px;
color: var(--cpd-color-text-secondary); color: $secondary-content;
} }
.mx_LoginWithQR_main { .mx_LoginWithQR_main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; 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 { .mx_QRCode {

View file

@ -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,
}

View file

@ -24,34 +24,7 @@ import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
import LoginWithQRFlow from "./LoginWithQRFlow"; import LoginWithQRFlow from "./LoginWithQRFlow";
import { Click, Mode, Phase } from "./LoginWithQR-types";
/**
* 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,
}
interface IProps { interface IProps {
client: MatrixClient; client: MatrixClient;

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; 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 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 { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import QRCode from "../elements/QRCode"; import QRCode from "../elements/QRCode";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; 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 SdkConfig from "../../../SdkConfig";
import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR";
interface IProps { interface Props {
phase: Phase; phase: Phase;
code?: string; code?: string;
onClick(type: Click): Promise<void>; onClick(type: Click): Promise<void>;
@ -39,8 +44,8 @@ interface IProps {
* *
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
*/ */
export default class LoginWithQRFlow extends React.Component<IProps> { export default class LoginWithQRFlow extends React.Component<Props> {
public constructor(props: IProps) { public constructor(props: Props) {
super(props); super(props);
} }
@ -72,49 +77,75 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
let main: JSX.Element | undefined; let main: JSX.Element | undefined;
let buttons: JSX.Element | undefined; let buttons: JSX.Element | undefined;
let backButton = true; let backButton = true;
let cancellationMessage: string | undefined; let className = "";
let centreTitle = false;
switch (this.props.phase) { 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) { switch (this.props.failureReason) {
case RendezvousFailureReason.Expired: case LegacyRendezvousFailureReason.UnsupportedAlgorithm:
cancellationMessage = _t("auth|qr_code_login|error_linking_incomplete"); 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; break;
case RendezvousFailureReason.InvalidCode:
cancellationMessage = _t("auth|qr_code_login|error_invalid_scanned_code"); case LegacyRendezvousFailureReason.UserCancelled:
title = _t("auth|qr_code_login|error_user_cancelled_title");
message = _t("auth|qr_code_login|error_user_cancelled");
break; break;
case RendezvousFailureReason.UnsupportedAlgorithm:
cancellationMessage = _t("auth|qr_code_login|error_device_unsupported"); case LegacyRendezvousFailureReason.Expired:
title = _t("auth|qr_code_login|error_expired_title");
message = _t("auth|qr_code_login|error_expired");
break; break;
case RendezvousFailureReason.UserDeclined:
cancellationMessage = _t("auth|qr_code_login|error_request_declined"); case LegacyRendezvousFailureReason.InvalidCode:
title = _t("auth|qr_code_login|error_insecure_channel_detected_title");
message = (
<>
{_t("auth|qr_code_login|error_insecure_channel_detected")}
<Text as="h2" size="lg" weight="semibold" data-testid="cancellation-message">
{_t("auth|qr_code_login|error_insecure_channel_detected_instructions")}
</Text>
<ol>
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_1")}</li>
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_2")}</li>
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_3")}</li>
</ol>
</>
);
break; break;
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
cancellationMessage = _t("auth|qr_code_login|error_device_already_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; break;
case RendezvousFailureReason.OtherDeviceNotSignedIn:
cancellationMessage = _t("auth|qr_code_login|error_device_not_signed_in"); case LegacyRendezvousFailureReason.UserDeclined:
break; title = _t("auth|qr_code_login|error_user_declined_title");
case RendezvousFailureReason.UserCancelled: message = _t("auth|qr_code_login|error_user_declined");
cancellationMessage = _t("auth|qr_code_login|error_request_cancelled");
break; break;
case LoginWithQRFailureReason.RateLimited: case LoginWithQRFailureReason.RateLimited:
cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); title = _t("error|something_went_wrong");
break; message = _t("auth|qr_code_login|error_rate_limited");
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");
break; break;
case LegacyRendezvousFailureReason.OtherDeviceNotSignedIn:
case LegacyRendezvousFailureReason.Unknown:
default: 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; break;
} }
centreTitle = true; className = "mx_LoginWithQR_error";
backButton = false; backButton = false;
main = <p data-testid="cancellation-message">{cancellationMessage}</p>;
buttons = ( buttons = (
<> <>
<AccessibleButton <AccessibleButton
@ -127,7 +158,23 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
{this.cancelButton()} {this.cancelButton()}
</> </>
); );
main = (
<>
<div
className={classNames("mx_LoginWithQR_icon", {
"mx_LoginWithQR_icon--critical": !success,
})}
>
{success ? <CheckCircleSolidIcon width="32px" /> : <ErrorIcon width="32px" />}
</div>
<Heading as="h1" size="sm" weight="semibold">
{title}
</Heading>
{typeof message === "object" ? message : <p data-testid="cancellation-message">{message}</p>}
</>
);
break; break;
}
case Phase.Connected: case Phase.Connected:
backButton = false; backButton = false;
main = ( main = (
@ -145,13 +192,6 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
buttons = ( buttons = (
<> <>
<AccessibleButton
data-testid="decline-login-button"
kind="primary_outline"
onClick={this.handleClick(Click.Decline)}
>
{_t("action|cancel")}
</AccessibleButton>
<AccessibleButton <AccessibleButton
data-testid="approve-login-button" data-testid="approve-login-button"
kind="primary" kind="primary"
@ -159,23 +199,28 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
> >
{_t("action|approve")} {_t("action|approve")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton
data-testid="decline-login-button"
kind="primary_outline"
onClick={this.handleClick(Click.Decline)}
>
{_t("action|cancel")}
</AccessibleButton>
</> </>
); );
break; break;
case Phase.ShowingQR: case Phase.ShowingQR:
if (this.props.code) { if (this.props.code) {
const code = ( const data = Buffer.from(this.props.code ?? "");
<div className="mx_LoginWithQR_qrWrapper">
<QRCode
data={[{ data: Buffer.from(this.props.code ?? ""), mode: "byte" }]}
className="mx_QRCode"
/>
</div>
);
main = ( main = (
<> <>
<h1>{_t("auth|qr_code_login|scan_code_instruction")}</h1> <Heading as="h1" size="sm" weight="semibold">
{code} {_t("auth|qr_code_login|scan_code_instruction")}
</Heading>
<div className="mx_LoginWithQR_qrWrapper">
<QRCode data={[{ data, mode: "byte" }]} className="mx_QRCode" />
</div>
<ol> <ol>
<li> <li>
{_t("auth|qr_code_login|open_element_other_device", { {_t("auth|qr_code_login|open_element_other_device", {
@ -209,14 +254,12 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
buttons = this.cancelButton(); buttons = this.cancelButton();
break; break;
case Phase.Verifying: case Phase.Verifying:
centreTitle = true;
main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup"));
break; break;
} }
return ( return (
<div data-testid="login-with-qr" className="mx_LoginWithQR"> <div data-testid="login-with-qr" className={classNames("mx_LoginWithQR", className)}>
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
{backButton ? ( {backButton ? (
<div className="mx_LoginWithQR_heading"> <div className="mx_LoginWithQR_heading">
<AccessibleButton <AccessibleButton
@ -232,7 +275,6 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
</div> </div>
</div> </div>
) : null} ) : null}
</div>
<div className="mx_LoginWithQR_main">{main}</div> <div className="mx_LoginWithQR_main">{main}</div>
<div className="mx_LoginWithQR_buttons">{buttons}</div> <div className="mx_LoginWithQR_buttons">{buttons}</div>
</div> </div>

View file

@ -35,21 +35,23 @@ interface IProps {
wellKnown?: IClientWellKnown; wellKnown?: IClientWellKnown;
} }
export default class LoginWithQRSection extends React.Component<IProps> { function shouldShowQrLegacy(
public constructor(props: IProps) { versions?: IServerVersions,
super(props); wellKnown?: IClientWellKnown,
} capabilities?: Capabilities,
): boolean {
public render(): JSX.Element | null { // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886:
// 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 // 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<IGetLoginTokenCapability>(this.props.capabilities); const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
const getLoginTokenSupported = const getLoginTokenSupported =
!!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled; !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled;
const msc3886Supported = const msc3886Supported =
!!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server;
this.props.wellKnown?.["io.element.rendezvous"]?.server; return getLoginTokenSupported && msc3886Supported;
const offerShowQr = getLoginTokenSupported && msc3886Supported; }
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, capabilities, wellKnown }) => {
const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities);
// don't show anything if no method is available // don't show anything if no method is available
if (!offerShowQr) { if (!offerShowQr) {
@ -59,15 +61,14 @@ export default class LoginWithQRSection extends React.Component<IProps> {
return ( return (
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}> <SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
<div className="mx_LoginWithQRSection"> <div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText"> <p className="mx_SettingsTab_subsectionText">{_t("settings|sessions|sign_in_with_qr_description")}</p>
{_t("settings|sessions|sign_in_with_qr_description")} <AccessibleButton onClick={onShowQr} kind="primary">
</p>
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
<QrCodeIcon height={20} width={20} /> <QrCodeIcon height={20} width={20} />
{_t("settings|sessions|sign_in_with_qr_button")} {_t("settings|sessions|sign_in_with_qr_button")}
</AccessibleButton> </AccessibleButton>
</div> </div>
</SettingsSubsection> </SettingsSubsection>
); );
} };
}
export default LoginWithQRSection;

View file

@ -32,7 +32,8 @@ import { ExtendedDevice } from "../../devices/types";
import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices"; import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices";
import SettingsTab from "../SettingsTab"; import SettingsTab from "../SettingsTab";
import LoginWithQRSection from "../../devices/LoginWithQRSection"; 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 { useAsyncMemo } from "../../../../../hooks/useAsyncMemo";
import QuestionDialog from "../../../dialogs/QuestionDialog"; import QuestionDialog from "../../../dialogs/QuestionDialog";
import { FilterVariation } from "../../devices/filter"; import { FilterVariation } from "../../devices/filter";
@ -284,6 +285,12 @@ const SessionManagerTab: React.FC = () => {
return ( return (
<SettingsTab> <SettingsTab>
<SettingsSection heading={_t("settings|sessions|title")}> <SettingsSection heading={_t("settings|sessions|title")}>
<LoginWithQRSection
onShowQr={onShowQrClicked}
versions={clientVersions}
capabilities={capabilities}
wellKnown={wellKnown}
/>
<SecurityRecommendations <SecurityRecommendations
devices={devices} devices={devices}
goToFilteredList={onGoToFilteredList} goToFilteredList={onGoToFilteredList}
@ -337,12 +344,6 @@ const SessionManagerTab: React.FC = () => {
/> />
</SettingsSubsection> </SettingsSubsection>
)} )}
<LoginWithQRSection
onShowQr={onShowQrClicked}
versions={clientVersions}
capabilities={capabilities}
wellKnown={wellKnown}
/>
</SettingsSection> </SettingsSection>
</SettingsTab> </SettingsTab>
); );

View file

@ -249,21 +249,29 @@
"completing_setup": "Completing set up of your new device", "completing_setup": "Completing set up of your new device",
"confirm_code_match": "Check that the code below matches with your other device:", "confirm_code_match": "Check that the code below matches with your other device:",
"connecting": "Connecting…", "connecting": "Connecting…",
"error_device_already_signed_in": "The other device is already signed in.", "error_expired": "Sign in expired. Please try again.",
"error_device_not_signed_in": "The other device isn't signed in.", "error_expired_title": "The sign in was not completed in time",
"error_device_unsupported": "Linking with this device is not supported.", "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_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", "error_insecure_channel_detected_instructions": "Now what?",
"error_invalid_scanned_code": "The scanned code is invalid.", "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_linking_incomplete": "The linking wasn't completed in the required time.", "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 dont 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_rate_limited": "Too many attempts in a short time. Wait some time before trying again.",
"error_request_cancelled": "The request was cancelled.", "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.",
"error_request_declined": "The request was declined on the other device.", "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.",
"error_unexpected": "An unexpected error occurred.", "error_unsupported_protocol_title": "Other device not compatible",
"follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "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", "open_element_other_device": "Open %(brand)s on your other device",
"point_the_camera": "Point the camera at the QR code shown here", "point_the_camera": "Point the camera at the QR code shown here",
"scan_code_instruction": "Scan the QR code with another device", "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\"", "select_qr_code": "Select \"%(scanQRCode)s\"",
"sign_in_new_device": "Sign in new device", "sign_in_new_device": "Sign in new device",
"waiting_for_device": "Waiting for device to sign in" "waiting_for_device": "Waiting for device to sign in"

View file

@ -20,7 +20,8 @@ import React from "react";
import { MSC3906Rendezvous, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { MSC3906Rendezvous, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; 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"; import type { MatrixClient } from "matrix-js-sdk/src/matrix";
jest.mock("matrix-js-sdk/src/rendezvous"); jest.mock("matrix-js-sdk/src/rendezvous");

View file

@ -19,12 +19,8 @@ import React from "react";
import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow"; import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow";
import { import { LoginWithQRFailureReason, FailureReason } from "../../../../../src/components/views/auth/LoginWithQR";
Click, import { Click, Phase } from "../../../../../src/components/views/auth/LoginWithQR-types";
Phase,
LoginWithQRFailureReason,
FailureReason,
} from "../../../../../src/components/views/auth/LoginWithQR";
describe("<LoginWithQRFlow />", () => { describe("<LoginWithQRFlow />", () => {
const onClick = jest.fn(); const onClick = jest.fn();

View file

@ -3,19 +3,28 @@
exports[`<LoginWithQRFlow /> errors renders data_mismatch 1`] = ` exports[`<LoginWithQRFlow /> errors renders data_mismatch 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The request was cancelled. An unexpected error occurred. The request to connect your other device has been cancelled.
</p> </p>
</div> </div>
<div <div
@ -45,19 +54,28 @@ exports[`<LoginWithQRFlow /> errors renders data_mismatch 1`] = `
exports[`<LoginWithQRFlow /> errors renders expired 1`] = ` exports[`<LoginWithQRFlow /> errors renders expired 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
The sign in was not completed in time
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The linking wasn't completed in the required time. Sign in expired. Please try again.
</p> </p>
</div> </div>
<div <div
@ -87,19 +105,28 @@ exports[`<LoginWithQRFlow /> errors renders expired 1`] = `
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = ` exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Other device not compatible
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The homeserver doesn't support signing in another device. This device does not support signing in to the other device with a QR code.
</p> </p>
</div> </div>
<div <div
@ -129,20 +156,42 @@ exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
exports[`<LoginWithQRFlow /> errors renders invalid_code 1`] = ` exports[`<LoginWithQRFlow /> errors renders invalid_code 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<p <div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Connection not secure
</h1>
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.
<h2
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The scanned code is invalid. Now what?
</p> </h2>
<ol>
<li>
Try signing in to the other device again with a QR code in case this was a network problem
</li>
<li>
If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi
</li>
<li>
If that doesn't work, sign in manually
</li>
</ol>
</div> </div>
<div <div
class="mx_LoginWithQR_buttons" class="mx_LoginWithQR_buttons"
@ -171,19 +220,28 @@ exports[`<LoginWithQRFlow /> errors renders invalid_code 1`] = `
exports[`<LoginWithQRFlow /> errors renders other_device_already_signed_in 1`] = ` exports[`<LoginWithQRFlow /> errors renders other_device_already_signed_in 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Your other device is already signed in
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The other device is already signed in. You dont need to do anything else.
</p> </p>
</div> </div>
<div <div
@ -213,19 +271,28 @@ exports[`<LoginWithQRFlow /> errors renders other_device_already_signed_in 1`] =
exports[`<LoginWithQRFlow /> errors renders other_device_not_signed_in 1`] = ` exports[`<LoginWithQRFlow /> errors renders other_device_not_signed_in 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The other device isn't signed in. An unexpected error occurred. The request to connect your other device has been cancelled.
</p> </p>
</div> </div>
<div <div
@ -255,15 +322,24 @@ exports[`<LoginWithQRFlow /> errors renders other_device_not_signed_in 1`] = `
exports[`<LoginWithQRFlow /> errors renders rate_limited 1`] = ` exports[`<LoginWithQRFlow /> errors renders rate_limited 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
@ -297,19 +373,28 @@ exports[`<LoginWithQRFlow /> errors renders rate_limited 1`] = `
exports[`<LoginWithQRFlow /> errors renders unknown 1`] = ` exports[`<LoginWithQRFlow /> errors renders unknown 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Something went wrong!
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
An unexpected error occurred. An unexpected error occurred. The request to connect your other device has been cancelled.
</p> </p>
</div> </div>
<div <div
@ -339,19 +424,28 @@ exports[`<LoginWithQRFlow /> errors renders unknown 1`] = `
exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = ` exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Other device not compatible
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
Linking with this device is not supported. This device does not support signing in to the other device with a QR code.
</p> </p>
</div> </div>
<div <div
@ -381,19 +475,28 @@ exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = `
exports[`<LoginWithQRFlow /> errors renders unsupported_transport 1`] = ` exports[`<LoginWithQRFlow /> errors renders unsupported_transport 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Other device not compatible
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The request was cancelled. This device does not support signing in to the other device with a QR code.
</p> </p>
</div> </div>
<div <div
@ -423,19 +526,28 @@ exports[`<LoginWithQRFlow /> errors renders unsupported_transport 1`] = `
exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = ` exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in request cancelled
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The request was cancelled. The sign in was cancelled on the other device.
</p> </p>
</div> </div>
<div <div
@ -465,19 +577,28 @@ exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = ` exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
<div> <div>
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR mx_LoginWithQR_error"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class="mx_LoginWithQR_centreTitle"
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<div
class="mx_LoginWithQR_icon mx_LoginWithQR_icon--critical"
>
<div
width="32px"
/>
</div>
<h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Sign in declined
</h1>
<p <p
data-testid="cancellation-message" data-testid="cancellation-message"
> >
The request was declined on the other device. You declined the request from your other device to sign in.
</p> </p>
</div> </div>
<div <div
@ -509,9 +630,6 @@ exports[`<LoginWithQRFlow /> renders QR code 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class=""
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -534,11 +652,12 @@ exports[`<LoginWithQRFlow /> renders QR code 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
<h1> <h1
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Scan the QR code with another device Scan the QR code with another device
</h1> </h1>
<div <div
@ -562,7 +681,7 @@ exports[`<LoginWithQRFlow /> renders QR code 1`] = `
<span> <span>
Select " Select "
<b> <b>
Scan QR code Sign in with QR code
</b> </b>
" "
</span> </span>
@ -571,7 +690,7 @@ exports[`<LoginWithQRFlow /> renders QR code 1`] = `
Point the camera at the QR code shown here Point the camera at the QR code shown here
</li> </li>
<li> <li>
Follow the remaining instructions to verify your other device Follow the instructions to link your other device
</li> </li>
</ol> </ol>
</div> </div>
@ -588,9 +707,6 @@ exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
> >
<div
class=""
/>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
@ -616,14 +732,6 @@ exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
<div <div
class="mx_LoginWithQR_buttons" class="mx_LoginWithQR_buttons"
> >
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
data-testid="decline-login-button"
role="button"
tabindex="0"
>
Cancel
</div>
<div <div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="approve-login-button" data-testid="approve-login-button"
@ -632,6 +740,14 @@ exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
> >
Approve Approve
</div> </div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
data-testid="decline-login-button"
role="button"
tabindex="0"
>
Cancel
</div>
</div> </div>
</div> </div>
</div> </div>
@ -642,9 +758,6 @@ exports[`<LoginWithQRFlow /> renders spinner while connecting 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class=""
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -667,7 +780,6 @@ exports[`<LoginWithQRFlow /> renders spinner while connecting 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
@ -713,9 +825,6 @@ exports[`<LoginWithQRFlow /> renders spinner while loading 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class=""
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -738,7 +847,6 @@ exports[`<LoginWithQRFlow /> renders spinner while loading 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
@ -772,9 +880,6 @@ exports[`<LoginWithQRFlow /> renders spinner while signing in 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class=""
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -797,7 +902,6 @@ exports[`<LoginWithQRFlow /> renders spinner while signing in 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
@ -843,9 +947,6 @@ exports[`<LoginWithQRFlow /> renders spinner while verifying 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -868,7 +969,6 @@ exports[`<LoginWithQRFlow /> renders spinner while verifying 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >
@ -905,9 +1005,6 @@ exports[`<LoginWithQRFlow /> renders spinner whilst QR generating 1`] = `
<div <div
class="mx_LoginWithQR" class="mx_LoginWithQR"
data-testid="login-with-qr" data-testid="login-with-qr"
>
<div
class=""
> >
<div <div
class="mx_LoginWithQR_heading" class="mx_LoginWithQR_heading"
@ -930,7 +1027,6 @@ exports[`<LoginWithQRFlow /> renders spinner whilst QR generating 1`] = `
Link new device Link new device
</div> </div>
</div> </div>
</div>
<div <div
class="mx_LoginWithQR_main" class="mx_LoginWithQR_main"
> >