[Labs] Sign in with QR on new EW using generated QR for MSC4108 v2024 (#33184)

* PoC Sign in with QR on new EW using generated QR for MSC4108 v2024

* Revert package.json changes

* Prettier

* Fix i18n

* Tidy up

* Remove unused state variable

* Iterate tests

* Partial revert

* Iterate

* Wire up qr_login route

* Iterate UI

* Fix React dev mode double rendering issue

* Fix react key warning

* Hide flow header on login

* Re-roll qr code on channel expiry

* Switch to AbortSignal

* Improve auto-retry QR UX

* Ensure we only show sign in with QR button if enabled

* XXX: enable labs flag on Netlify builds

* Tweak QR code sizing

* Move qr login flow into a dialog to match designs

* Fix null deviceId

* Remove duplicate log

* Iterate

* Fix tests

* Fix types

* Fix tests

* Fix tests

* Make Netlify more useful

* Make Netlify more useful v2

* Update copy

* Refactor QR link flow to use new SDK methods

Requires https://github.com/matrix-org/matrix-js-sdk/pull/5283
For element-hq/wat-internal#188
Split out from https://github.com/element-hq/element-web/pull/33184

* Link to js-sdk branch

* Update tests

* Simplify

* Revert js-sdk linking

* Iterate

* Iterate

* Refactor to handle most of the TODOs

* Remove unused code

* Remove unused code

* Use js-sdk isSignInWithQRAvailable API to simplify code

* Restore app-test.ts

* Improve coverage

* Improve coverage

* Remove unused prop/state

* Iterate

* Fix tests

* Iterate

* Tests

* Handle TODOs

* Docs

* Remove redundant call to crossSignDevice()

* Workaround to remove training slash on the serverName before auto-discovery

* Revert "Workaround to remove training slash on the serverName before auto-discovery"

This reverts commit 0335a8fdd1b8e8d949ab7fca17c76f8fab335b58.

* setLoggedIn not to be used with OIDC flows as it clears storage

as per docs on setLoggedIn we should use restoreSessionFromStorage

* Don't show the security_code_prompt unconditionally(i.e. for the web logging in mobile flow)

* Update LoginWithQRFlow-test.tsx.snap

* Update MatrixChat-test from setLoggedInSpy to restoreSessionSpy

* Add todo for server switch

* Add todo about handling base URL or server name

* Handle server name or base URL being returned

* Format

* Fix loading state height

* Handle the homeserver URL differing during QR code login

* Comments

* Comments

* Register OIDC client ID after homeserver swap

* Make QrLoginDialog async to minimise the impact on bundle size

* Handle unsupported HS earlier in the flow

* Iterate

* Delint

* Fix test

* Discard changes to apps/web/element.io/develop/config.json

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
Co-authored-by: David Langley <langley.dave@gmail.com>
This commit is contained in:
Michael Telatynski
2026-06-04 13:50:17 +00:00
committed by GitHub
co-authored by Hugh Nimmo-Smith David Langley
parent d1a6137c90
commit 486fa57b68
24 changed files with 3041 additions and 570 deletions
@@ -142,6 +142,8 @@ import { isOnlyAdmin } from "../../utils/membership";
import { ModuleApi } from "../../modules/Api.ts";
import { type IScreen } from "../../vector/routing.ts";
import { type URLParams } from "../../vector/url_utils.ts";
import { type QrLoginCredentials } from "../views/auth/LoginWithQR.tsx";
import { configureFromCompletedOAuthLogin } from "../../Lifecycle";
// legacy export
export { default as Views } from "../../Views";
@@ -827,6 +829,26 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewSomethingBehindModal();
break;
}
case Action.ViewQrLogin: {
if (this.isLoggedInViewPageDisplayed()) {
logger.warn("Ignoring payload due to unexpected call outside auth flows", payload);
} else {
Modal.createDialog(
lazy(() => import("../../async-components/views/dialogs/QrLoginDialog")),
{
serverConfig: this.getServerProperties().serverConfig,
onLoggedIn: this.onUserCompletedQrLoginFlow,
},
"mx_LoginWithQR_dialog",
false,
true,
);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
}
break;
}
case "view_welcome_page":
this.viewWelcome();
break;
@@ -1858,6 +1880,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
params: params,
});
PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN);
} else if (screen === "qr_login") {
dis.fire(Action.ViewQrLogin);
} else if (screen === "forgot_password") {
dis.dispatch({
action: "start_password_recovery",
@@ -2131,6 +2155,36 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
};
/**
* After successful qr login, load & persist the credentials, as well as the secrets bundle.
*/
private onUserCompletedQrLoginFlow = async ({
secrets,
deviceId,
...tokenResponse
}: QrLoginCredentials): Promise<void> => {
// Persist credentials + OIDC settings, then hydrate the client from storage.
// setLoggedIn would clear storage and drop the OIDC settings; see its docstring.
await configureFromCompletedOAuthLogin(tokenResponse);
await Lifecycle.restoreSessionFromStorage();
if (secrets) {
const crypto = MatrixClientPeg.safeGet().getCrypto();
if (crypto?.importSecretsBundle) {
// This imports the secrets and cross-signs the device in one go
await crypto.importSecretsBundle(secrets);
} else {
logger.warn(
"Crypto not initialised or no importSecretsBundle() method, cannot import secrets from QR login",
);
}
} else {
logger.warn("No secrets received from QR login");
}
this.onShowPostLoginScreen();
};
/** Called when {@link Views.E2E_SETUP} or {@link Views.COMPLETE_SECURITY} have completed. */
private onCompleteSecurityE2eSetupFinished = async (): Promise<void> => {
const forceVerify = await this.shouldForceVerification();
@@ -2220,7 +2274,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
);
}
} else if (this.state.view === Views.WELCOME) {
view = <Welcome />;
view = <Welcome {...this.getServerProperties()} />;
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = (