Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions apps/web/res/css/views/auth/_LoginWithQR.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,45 @@ Please see LICENSE files in the repository root for full details.
font-size: $font-15px;
}

.mx_UserSettingsDialog .mx_LoginWithQR {
.mx_LoginWithQR {
min-height: 350px;
display: flex;
flex-direction: column;
font: var(--cpd-font-body-md-regular);

h1 {
font-size: $font-24px;
margin-bottom: 0;

svg {
&.normal {
color: $secondary-content;
}
&.error {
color: $alert;
}
&.success {
color: $accent;
}
height: 1.3em;
margin-right: $spacing-8;
vertical-align: middle;
}
}

h2 {
margin-top: $spacing-24;
}

.mx_QRCode {
margin: $spacing-28 0;
}

.mx_LoginWithQR_qrWrapper {
display: flex;
}
}
padding: $spacing-28 0;

.mx_LoginWithQR {
min-height: 350px;
display: flex;
flex-direction: column;

h1 > svg {
&.normal {
color: $secondary-content;
}
&.error {
color: $alert;
}
&.success {
color: $accent;
.mx_Spinner {
/* Match the size of the QR code to prevent jumps */
height: 200px;
width: 200px;
}
height: 1.3em;
margin-right: $spacing-8;
vertical-align: middle;
}

.mx_LoginWithQR_confirmationDigits {
Expand Down
25 changes: 20 additions & 5 deletions apps/web/src/components/views/auth/LoginWithQR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
} from "matrix-js-sdk/src/rendezvous";
import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";

import { Click, Mode, Phase } from "./LoginWithQR-types";
import LoginWithQRFlow from "./LoginWithQRFlow";
Expand All @@ -32,7 +33,6 @@
interface IState {
phase: Phase;
rendezvous?: MSC4108SignInWithQR;
mediaPermissionError?: boolean;
verificationUri?: string;
userCode?: string;
checkCode?: string;
Expand Down Expand Up @@ -78,13 +78,15 @@
}
}

private async updateMode(mode: Mode): Promise<void> {
this.setState({ phase: Phase.Loading });
private async updateMode(mode: Mode, showLoading = true): Promise<void> {
if (this.state.rendezvous) {
const rendezvous = this.state.rendezvous;
rendezvous.onFailure = undefined;
this.setState({ rendezvous: undefined });
}
if (showLoading) {
this.setState({ phase: Phase.Loading });
}
if (mode === Mode.Show) {
await this.generateAndShowCode();
}
Expand Down Expand Up @@ -187,9 +189,23 @@
}
};

private onFailure = (reason: RendezvousFailureReason): void => {
private onFailure = async (reason: RendezvousFailureReason): Promise<void> => {

Check warning on line 192 in apps/web/src/components/views/auth/LoginWithQR.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'onFailure' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ3PE8BqIZeTBOHujqS4&open=AZ3PE8BqIZeTBOHujqS4&pullRequest=33303
if (this.state.phase === Phase.Error) return; // Already in failed state
logger.info(`Rendezvous failed: ${reason}`);

// Generate a new rendezvous channel & qr code if we hit expiry whilst still showing the QR code
if (reason === ClientRendezvousFailureReason.Expired && this.state.phase === Phase.ShowingQR) {
try {
this.reset();
// Add a sleep to make the UX looks less flickery and more intentional
await sleep(1000);
await this.updateMode(Mode.Show, false);
return;
} catch (e) {
logger.warn("Failed to re-roll qr code on expiry", e);
}
}

this.setState({ phase: Phase.Error, failureReason: reason });
};

Expand All @@ -200,7 +216,6 @@
failureReason: undefined,
userCode: undefined,
checkCode: undefined,
mediaPermissionError: false,
});
}

Expand Down
62 changes: 31 additions & 31 deletions apps/web/src/components/views/auth/LoginWithQRFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,39 +226,39 @@
</>
);
break;
case Phase.ShowingQR:
if (this.props.code) {
const data = this.props.code;
case Phase.ShowingQR: {
const steps = [
_t("auth|qr_code_login|open_element_other_device", {
brand: SdkConfig.get().brand,
}),
_t("auth|qr_code_login|select_qr_code", {
scanQRCode: <strong>{_t("auth|qr_code_login|scan_qr_code")}</strong>,
}),
_t("auth|qr_code_login|point_the_camera"),
_t("auth|qr_code_login|follow_remaining_instructions"),
];

main = (
<>
<Heading as="h1" size="sm" weight="semibold">
{_t("auth|qr_code_login|scan_code_instruction")}
</Heading>
<div className="mx_LoginWithQR_qrWrapper">
<QRCode data={[{ data, mode: "byte" }]} className="mx_QRCode" />
</div>
<ol>
<li>
{_t("auth|qr_code_login|open_element_other_device", {
brand: SdkConfig.get().brand,
})}
</li>
<li>
{_t("auth|qr_code_login|select_qr_code", {
scanQRCode: <strong>{_t("auth|qr_code_login|scan_qr_code")}</strong>,
})}
</li>
<li>{_t("auth|qr_code_login|point_the_camera")}</li>
<li>{_t("auth|qr_code_login|follow_remaining_instructions")}</li>
</ol>
</>
);
} else {
main = this.simpleSpinner();
buttons = this.cancelButton();
}
main = (
<>
<Heading as="h1" size="sm" weight="semibold">
{_t("auth|qr_code_login|scan_code_instruction")}
</Heading>
<div className="mx_LoginWithQR_qrWrapper">
{this.props.code ? (
<QRCode data={[{ data: this.props.code, mode: "byte" }]} width={200} />
) : (
<Spinner />
)}
</div>
<ol>
{steps.map((step, i) => (
<li key={i}>{step}</li>

Check warning on line 255 in apps/web/src/components/views/auth/LoginWithQRFlow.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ3PE8I0IZeTBOHujqS5&open=AZ3PE8I0IZeTBOHujqS5&pullRequest=33303
))}
</ol>
</>
);
break;
}
case Phase.Loading:
main = this.simpleSpinner();
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
} from "matrix-js-sdk/src/rendezvous";
import { HTTPError, type MatrixClient } from "matrix-js-sdk/src/matrix";

import LoginWithQR from "../../../../../../src/components/views/auth/LoginWithQR";
import LoginWithQR, { LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";

jest.mock("matrix-js-sdk/src/rendezvous");
Expand Down Expand Up @@ -68,6 +68,7 @@
mockedFlow.mockReset();
jest.resetAllMocks();
client = makeClient();
jest.useFakeTimers();
});

afterEach(() => {
Expand Down Expand Up @@ -105,6 +106,29 @@
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
});

test("should open a new channel if expires before qr scan", async () => {
const onFinished = jest.fn();
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
render(getComponent({ client, onFinished }));

await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.ShowingQR,
onClick: expect.any(Function),
}),
);

const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();

// Expire the channel
const onFailure = mocked(MSC4108SignInWithQR).mock.calls[0][3];
onFailure!(ClientRendezvousFailureReason.Expired);
await jest.runAllTimersAsync();
await waitFor(() => expect(mocked(MSC4108SignInWithQR).mock.instances).toHaveLength(2));
});

test("failed to connect", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
Expand All @@ -115,6 +139,34 @@
await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown));
});

test("should show error if check code doesn't match", async () => {
jest.spyOn(global.window, "open");

Check warning on line 143 in apps/web/test/unit-tests/components/views/settings/devices/LoginWithQR-test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ3PPMPQxjT9LIk5CHdJ&open=AZ3PPMPQxjT9LIk5CHdJ&pullRequest=33303

render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
verificationUri: "mock-verification-uri",
});

await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
onClick: expect.any(Function),
}),
);

const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve, "12");

await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
failureReason: LoginWithQRFailureReason.CheckCodeMismatch,
onClick: expect.any(Function),
}),
);
});

test("reciprocates login", async () => {
jest.spyOn(global.window, "open");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ describe("<LoginWithQRFlow />", () => {

it("renders spinner whilst QR generating", async () => {
const { container } = render(getComponent({ phase: Phase.ShowingQR }));
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
expect(screen.getAllByTestId("spinner")).toHaveLength(1);
expect(container).toMatchSnapshot();
fireEvent.click(screen.getByTestId("cancel-button"));
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
});

it("renders QR code", async () => {
Expand Down
Loading
Loading