Re-generate QR code if the channel expires before scan (#33303)

* Re-generate QR code if the channel expires before scan

* Tweak styling

* Remove unused state variable

* Update tests

* Add tests
This commit is contained in:
Michael Telatynski
2026-04-28 08:26:56 +00:00
committed by GitHub
parent 4e9655dc6b
commit 9213790158
6 changed files with 179 additions and 100 deletions
@@ -17,7 +17,7 @@ import {
} 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");
@@ -68,6 +68,7 @@ describe("<LoginWithQR />", () => {
mockedFlow.mockReset();
jest.resetAllMocks();
client = makeClient();
jest.useFakeTimers();
});
afterEach(() => {
@@ -105,6 +106,29 @@ describe("<LoginWithQR />", () => {
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({});
@@ -115,6 +139,34 @@ describe("<LoginWithQR />", () => {
await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown));
});
test("should show error if check code doesn't match", async () => {
jest.spyOn(global.window, "open");
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");
@@ -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 () => {
@@ -813,12 +813,12 @@ exports[`<LoginWithQRFlow /> renders QR code 1`] = `
class="mx_LoginWithQR_qrWrapper"
>
<div
class="mx_QRCode mx_QRCode"
class="mx_QRCode"
>
<img
alt="QR Code"
class="mx_VerificationQRCode"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKxSURBVO3BQW7kQAwEwSxC//9yro88NSBI4/UQjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrl4qEk/CaVkyR0Kl0STlS6JPwmlSeKNUqxRinWKBcvU3lTEp5IwonKHSpvSsKbijVKsUYp1igXH5aEO1TuSMKJSpeELgmdyh1JuEPlk4o1SrFGKdYoF19OpUvCHSqTFGuUYo1SrFEuhlHpktCpdEnoVL5ZsUYp1ijFGuXiw1T+J5VPUvlLijVKsUYp1igXL0vCb0pCp9IloVN5Igl/WbFGKdYoxRrl4iGVvyQJnUqXhDtUvkmxRinWKMUa5eKhJNyh0iXhk5JwotIl4UTljiR0Kp9UrFGKNUqxRok/eFESOpUuCZ3KN0nCiUqXhDtUnijWKMUapVijxB88kIQ3qXRJeELlJAmdSpeEJ1S6JJyoPFGsUYo1SrFGuXiZSpeETqVLwonKSRKeUDlROUlCp3Ki8knFGqVYoxRrlPiDB5LwhMpJEjqVO5LQqXRJ6FROktCp3JGETuVNxRqlWKMUa5T4gy+WhE7lJAmdyh1JuEOlS8KJyhPFGqVYoxRrlIuHkvCbVO5Iwh1J+GbFGqVYoxRrlIuXqbwpCXckoVPpktAl4UTlJAmdyonKJxVrlGKNUqxRLj4sCXeovCkJJyonSehU/rJijVKsUYo1ysWXUzlJQqfyRBJOknCHypuKNUqxRinWKBdfLgmdSqdykoROpVPpknCHykkSOpUnijVKsUYp1igXH6byPyWhU+lUTpLQqZwk4SQJncqbijVKsUYp1igXL0vC/5SEO5LQqZwkoVPpVO5IQqfyRLFGKdYoxRol/mCNUaxRijVKsUYp1ijFGqVYoxRrlGKNUqxRijVKsUYp1ijFGqVYoxRrlH+MbAvtLaAtKAAAAABJRU5ErkJggg=="
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAklEQVR4AewaftIAAAPWSURBVO3BQY5jBxYDweSD7n9lTu+8aQJu+Y+gsjMi/QVJv3VImg5J0yFpOiRNh6TpkDQdkqZD0nRImg5J0yFpOiRNh6TpkDQdkqZD0nRImg5J0yFpOiRNh6TpkDQdkqZD0nRImg5J0yFpevFhSfjJ2vKOJCxtWZLwjrYsSfjJ2vIph6TpkDQdkqZD0nRImg5J04sv0pZvkIRPSsI72vK0tnyDJHyDQ9J0SJoOSdMhaTokTYek6cUPkYSnteVJSXhHW5YkLElY2vKkJDytLd/ukDQdkqZD0nRImg5J0yFpeqHHtGVJwtPaov+vQ9J0SJoOSdMhaTokTYek6YU+oi1LEpa2LElY2qJ/7pA0HZKmQ9J0SJoOSdMhaXrxQ7Tl36ot364t/0WHpOmQNB2SpkPSdEiaXnyRJPxkSVjasiRhacsnJUF/OSRNh6TpkDQdkqZD0nRIml58WFv+i5KwtGVJwtPaor/nkDQdkqZD0nRImg5J0yFpevFhSXhaW5YkfLskvKMtSxLe0ZYnJWFpy7c7JE2HpOmQNB2SpkPSdEia0l/4EklY2rIkYWmL/r4k/Km2LEl4Wls+5ZA0HZKmQ9J0SJoOSdMhaUp/4YOS8C3a8jtJ+KS2vCMJS1uWJHxKW5YkvKMtn3JImg5J0yFpOiRNh6TpkDS9+CJtWZKwtGVJwp9qyzuS8ElteUdb/lQSlra8oy3f7pA0HZKmQ9J0SJoOSdMhaUp/4YOS8Elt+VNJWNrytCQsbVmSsLTlHUn4nbY8LQlLW77BIWk6JE2HpOmQNB2SpkPSlP6CHpGEpS3vSMLSlqcl4UltWZLwjrZ8yiFpOiRNh6TpkDQdkqZD0vTiw5Lwk7XlaUl4WhL0zx2SpkPSdEiaDknTIWk6JE0vvkhbvkESnpaEpS1LEpYkvKMtfyoJS1ve0ZZvd0iaDknTIWk6JE2HpOnFD5GEp7XlGyThHW15RxKWtugvh6TpkDQdkqZD0nRImg5J0ws9pi3vSMLSlk9Kwp9KwtPa8g0OSdMhaTokTYek6ZA0HZKmF3pMEpa2LG15RxKWtixtWZLwpLa8IwlLWz7lkDQdkqZD0nRImg5J0yFpevFDtOXfKglLW5a2vCMJS1v+VBLekYSlLd/gkDQdkqZD0nRImg5J0yFpevFFkvBvlYSnJWFpyzuS8DttWdrytCQsbfmUQ9J0SJoOSdMhaTokTYekKf0FSb91SJoOSdMhaTokTYek6ZA0HZKmQ9J0SJoOSdMhaTokTYek6ZA0HZKmQ9J0SJoOSdMhaTokTYek6ZA0HZKmQ9J0SJoOSdP/AHKVDJXnVp/qAAAAAElFTkSuQmCC"
/>
</div>
</div>
@@ -1203,47 +1203,61 @@ exports[`<LoginWithQRFlow /> renders spinner whilst QR generating 1`] = `
<div
class="mx_LoginWithQR_main"
>
<div
class="mx_LoginWithQR_spinner"
<h1
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
<div>
<div
class="mx_Spinner"
Scan the QR code with another device
</h1>
<div
class="mx_LoginWithQR_qrWrapper"
>
<div
class="mx_Spinner"
>
<svg
aria-label="Loading…"
class="_icon_1855a_18"
data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar"
style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-label="Loading…"
class="_icon_1855a_18"
data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar"
style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/>
</svg>
</div>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
<ol>
<li>
Open Element on your other device
</li>
<li>
<span>
Select "
<strong>
Sign in with QR code
</strong>
"
</span>
</li>
<li>
Scan the QR code shown here
</li>
<li>
Follow the remaining instructions
</li>
</ol>
</div>
<div
class="mx_LoginWithQR_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
data-testid="cancel-button"
role="button"
tabindex="0"
>
Cancel
</div>
</div>
/>
</div>
</div>
`;