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
@@ -19,6 +19,7 @@ import {
} 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";
@@ -32,7 +33,6 @@ interface IProps {
interface IState {
phase: Phase;
rendezvous?: MSC4108SignInWithQR;
mediaPermissionError?: boolean;
verificationUri?: string;
userCode?: string;
checkCode?: string;
@@ -78,13 +78,15 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
}
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();
}
@@ -187,9 +189,23 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
};
private onFailure = (reason: RendezvousFailureReason): void => {
private onFailure = async (reason: RendezvousFailureReason): Promise<void> => {
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 });
};
@@ -200,7 +216,6 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
failureReason: undefined,
userCode: undefined,
checkCode: undefined,
mediaPermissionError: false,
});
}
@@ -226,39 +226,39 @@ export default class LoginWithQRFlow extends React.Component<Props> {
</>
);
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>
))}
</ol>
</>
);
break;
}
case Phase.Loading:
main = this.simpleSpinner();
break;