Skip to content

Commit 3249b08

Browse files
committed
Iterate
1 parent 91dc619 commit 3249b08

9 files changed

Lines changed: 170 additions & 98 deletions

File tree

apps/web/src/PosthogTrackers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
2828
[Views.CONFIRM_LOCK_THEFT]: "ConfirmStartup",
2929
[Views.WELCOME]: "Welcome",
3030
[Views.LOGIN]: "Login",
31+
[Views.QR_LOGIN]: "Login", // XXX: we should get a new analytics identifier for this
3132
[Views.REGISTER]: "Register",
3233
[Views.FORGOT_PASSWORD]: "ForgotPassword",
3334
[Views.COMPLETE_SECURITY]: "CompleteSecurity",

apps/web/src/Views.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ enum Views {
100100
// we are showing the login view
101101
LOGIN,
102102

103+
// we are showing the login with QR code view
104+
QR_LOGIN,
105+
103106
// we are showing the registration view
104107
REGISTER,
105108

apps/web/src/components/structures/MatrixChat.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ import { isOnlyAdmin } from "../../utils/membership";
142142
import { ModuleApi } from "../../modules/Api.ts";
143143
import { type IScreen } from "../../vector/routing.ts";
144144
import { type URLParams } from "../../vector/url_utils.ts";
145+
import QrLogin from "./auth/QrLogin.tsx";
145146

146147
// legacy export
147148
export { default as Views } from "../../Views";
@@ -2224,7 +2225,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
22242225
);
22252226
}
22262227
} else if (this.state.view === Views.WELCOME) {
2227-
view = <Welcome />;
2228+
view = <Welcome {...this.getServerProperties()} />;
22282229
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
22292230
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
22302231
view = (
@@ -2267,6 +2268,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
22672268
{...this.getServerProperties()}
22682269
/>
22692270
);
2271+
} else if (this.state.view === Views.QR_LOGIN && SettingsStore.getValue("feature_login_with_qr")) {
2272+
view = (
2273+
<QrLogin
2274+
isSyncing={this.state.pendingInitialSync}
2275+
onLoggedIn={this.onUserCompletedLoginFlow}
2276+
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
2277+
fragmentAfterLogin={fragmentAfterLogin}
2278+
{...this.getServerProperties()}
2279+
/>
2280+
);
22702281
} else if (this.state.view === Views.SOFT_LOGOUT) {
22712282
view = (
22722283
<SoftLogout

apps/web/src/components/structures/auth/Login.tsx

Lines changed: 18 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@ Please see LICENSE files in the repository root for full details.
99
import React, { type JSX, memo, type ReactNode } from "react";
1010
import classNames from "classnames";
1111
import { logger } from "matrix-js-sdk/src/logger";
12-
import { type SSOFlow, type MatrixClient, SSOAction } from "matrix-js-sdk/src/matrix";
12+
import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix";
1313
import { Button } from "@vector-im/compound-web";
14-
import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
15-
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
16-
import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous";
1714

1815
import { _t, UserFriendlyError } from "../../../languageHandler";
19-
import Login, { type LoginWithQrFlow, type ClientLoginFlow, type OidcNativeFlow } from "../../../Login";
16+
import Login, { type ClientLoginFlow, type OidcNativeFlow } from "../../../Login";
2017
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
2118
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
2219
import AuthPage from "../../views/auth/AuthPage";
@@ -36,9 +33,6 @@ import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig
3633
import { filterBoolean } from "../../../utils/arrays";
3734
import { startOidcLogin } from "../../../utils/oidc/authorize";
3835
import { ModuleApi } from "../../../modules/Api.ts";
39-
import LoginWithQR from "../../views/auth/LoginWithQR.tsx";
40-
import { Mode } from "../../views/auth/LoginWithQR-types.ts";
41-
import createMatrixClient from "../../../utils/createMatrixClient.ts";
4236

4337
interface IProps {
4438
serverConfig: ValidatedServerConfig;
@@ -57,8 +51,7 @@ interface IProps {
5751

5852
// Called when the user has logged in. Params:
5953
// - The object returned by the login API
60-
// - alreadySignedIn: true if the user was already signed in (e.g. QR login) and only the post login setup is needed
61-
onLoggedIn(data: IMatrixClientCreds, alreadySignedIn?: boolean): void;
54+
onLoggedIn(data: IMatrixClientCreds): void;
6255

6356
// login shouldn't know or care how registration, password recovery, etc is done.
6457
onRegisterClick?(): void;
@@ -86,9 +79,6 @@ interface IState {
8679
serverIsAlive: boolean;
8780
serverErrorIsFatal: boolean;
8881
serverDeadError?: ReactNode;
89-
90-
loginWithQrInProgress: boolean;
91-
loginWithQrClient?: MatrixClient;
9282
}
9383

9484
type OnPasswordLogin = {
@@ -120,8 +110,6 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
120110
serverIsAlive: true,
121111
serverErrorIsFatal: false,
122112
serverDeadError: "",
123-
124-
loginWithQrInProgress: false,
125113
};
126114

127115
// map from login step type to a function which will render a control
@@ -135,7 +123,6 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
135123
// eslint-disable-next-line @typescript-eslint/naming-convention
136124
"m.login.sso": () => this.renderSsoStep("sso"),
137125
"oidcNativeFlow": () => this.renderOidcNativeStep(),
138-
"loginWithQrFlow": () => this.renderLoginWithQRStep(),
139126
};
140127
}
141128

@@ -415,7 +402,7 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
415402
if (!this.state.flows) return null;
416403

417404
// this is the ideal order we want to show the flows in
418-
const order = ["loginWithQrFlow", "oidcNativeFlow", "m.login.password", "m.login.sso"];
405+
const order = ["oidcNativeFlow", "m.login.password", "m.login.sso"];
419406

420407
const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type)));
421408
return (
@@ -485,42 +472,6 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
485472
);
486473
};
487474

488-
private startLoginWithQR = (): void => {
489-
if (this.state.loginWithQrInProgress) return;
490-
// pick our device ID
491-
const deviceId = secureRandomString(10);
492-
const loginWithQrClient = createMatrixClient({
493-
baseUrl: this.loginLogic.getHomeserverUrl(),
494-
idBaseUrl: this.loginLogic.getIdentityServerUrl(),
495-
deviceId,
496-
});
497-
this.setState({ loginWithQrInProgress: true, loginWithQrClient });
498-
};
499-
500-
private renderLoginWithQRStep = (): JSX.Element => {
501-
return (
502-
<>
503-
<Button className="mx_Login_fullWidthButton" kind="primary" size="sm" onClick={this.startLoginWithQR}>
504-
<QrCodeIcon />
505-
{_t("auth|sign_in_with_qr")}
506-
</Button>
507-
</>
508-
);
509-
};
510-
511-
private onLoginWithQRFinished = (success: boolean, credentials?: IMatrixClientCreds): void => {
512-
if (credentials) {
513-
this.props.onLoggedIn(credentials, true);
514-
} else if (!success) {
515-
this.state.loginWithQrClient?.stopClient();
516-
this.setState({ loginWithQrInProgress: false, loginWithQrClient: undefined });
517-
}
518-
};
519-
520-
private get qrClientId(): string {
521-
return (this.state.flows?.find((flow) => flow.type === "loginWithQrFlow") as LoginWithQrFlow).clientId ?? "";
522-
}
523-
524475
public render(): React.ReactNode {
525476
const loader =
526477
this.isBusy() && !this.state.busyLoggingIn ? (
@@ -581,35 +532,20 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
581532
<AuthPage>
582533
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
583534
<AuthBody>
584-
{this.state.loginWithQrInProgress ? (
585-
<>
586-
<LoginWithQR
587-
onFinished={this.onLoginWithQRFinished}
588-
mode={Mode.Show}
589-
intent={RendezvousIntent.LOGIN_ON_NEW_DEVICE}
590-
client={this.state.loginWithQrClient!}
591-
clientId={this.qrClientId}
592-
/>
593-
</>
594-
) : (
595-
<>
596-
{" "}
597-
<h1>
598-
{_t("action|sign_in")}
599-
{loader}
600-
</h1>
601-
{errorTextSection}
602-
{serverDeadSection}
603-
<ServerPicker
604-
serverConfig={this.props.serverConfig}
605-
onServerConfigChange={this.props.onServerConfigChange}
606-
disabled={this.isBusy()}
607-
/>
608-
{this.renderLoginComponentForFlows()}
609-
{this.props.children}
610-
{footer}
611-
</>
612-
)}
535+
<h1>
536+
{_t("action|sign_in")}
537+
{loader}
538+
</h1>
539+
{errorTextSection}
540+
{serverDeadSection}
541+
<ServerPicker
542+
serverConfig={this.props.serverConfig}
543+
onServerConfigChange={this.props.onServerConfigChange}
544+
disabled={this.isBusy()}
545+
/>
546+
{this.renderLoginComponentForFlows()}
547+
{this.props.children}
548+
{footer}
613549
</AuthBody>
614550
</AuthPage>
615551
);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import React, { type FC, type JSX, useCallback, useMemo } from "react";
8+
import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous";
9+
import { createClient } from "matrix-js-sdk/src/matrix";
10+
11+
import AuthPage from "../../views/auth/AuthPage";
12+
import AuthHeader from "../../views/auth/AuthHeader";
13+
import AuthBody from "../../views/auth/AuthBody";
14+
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
15+
import type { IMatrixClientCreds } from "../../../MatrixClientPeg.ts";
16+
import LoginWithQR from "../../views/auth/LoginWithQR.tsx";
17+
import { Mode } from "../../views/auth/LoginWithQR-types.ts";
18+
import { useAsyncMemo } from "../../../hooks/useAsyncMemo.ts";
19+
import { getOidcClientId } from "../../../utils/oidc/registerClient.ts";
20+
import SdkConfig from "../../../SdkConfig.ts";
21+
import Spinner from "../../views/elements/Spinner.tsx";
22+
23+
interface Props {
24+
serverConfig: ValidatedServerConfig;
25+
isSyncing?: boolean;
26+
fragmentAfterLogin: string;
27+
defaultDeviceDisplayName?: string; // TODO is this useful?
28+
onLoggedIn(this: void, credentials: IMatrixClientCreds, alreadySignedIn: boolean): void;
29+
}
30+
31+
const QrLogin: FC<Props> = ({ serverConfig, onLoggedIn }) => {
32+
const tempClient = useMemo(() => createClient({ baseUrl: serverConfig.hsUrl }), [serverConfig]);
33+
const onFinished = useCallback(
34+
(success: boolean, credentials?: IMatrixClientCreds) => {
35+
if (success) {
36+
onLoggedIn(credentials!, true);
37+
} else {
38+
// TODO handle
39+
}
40+
},
41+
[onLoggedIn],
42+
);
43+
const clientId = useAsyncMemo(
44+
() => getOidcClientId(serverConfig.delegatedAuthentication!, SdkConfig.get().oidc_static_clients),
45+
[serverConfig],
46+
);
47+
48+
let body: JSX.Element;
49+
if (clientId === undefined) {
50+
body = <Spinner />;
51+
} else {
52+
body = (
53+
<LoginWithQR
54+
intent={RendezvousIntent.LOGIN_ON_NEW_DEVICE}
55+
client={tempClient}
56+
onFinished={onFinished}
57+
mode={Mode.Show}
58+
clientId={clientId}
59+
/>
60+
);
61+
}
62+
63+
// TODO className
64+
return (
65+
<AuthPage>
66+
<AuthHeader />
67+
<AuthBody className="mx_AuthBody_forgot-password">{body}</AuthBody>
68+
</AuthPage>
69+
);
70+
};
71+
72+
export default QrLogin;

apps/web/src/components/views/auth/DefaultWelcome.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,56 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import React from "react";
8+
import React, { type JSX } from "react";
99
import { Button, Heading, Text } from "@vector-im/compound-web";
10+
import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous";
11+
import { createClient } from "matrix-js-sdk/src/matrix";
12+
import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
1013

1114
import { _t } from "../../../languageHandler";
1215
import SdkConfig from "../../../SdkConfig.ts";
1316
import { MatrixClientPeg } from "../../../MatrixClientPeg.ts";
1417
import { isElementBranded } from "../../../branding.ts";
18+
import { useFeatureEnabled } from "../../../hooks/useSettings.ts";
19+
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig.ts";
20+
import { useAsyncMemo } from "../../../hooks/useAsyncMemo.ts";
21+
import Spinner from "../elements/Spinner.tsx";
1522

16-
const DefaultWelcome: React.FC = () => {
23+
interface Props {
24+
serverConfig: ValidatedServerConfig;
25+
}
26+
27+
const DefaultWelcome: React.FC<Props> = ({ serverConfig }) => {
1728
const brand = SdkConfig.get("brand");
1829
const branding = SdkConfig.getObject("branding");
1930
const logoUrl = branding.get("auth_header_logo_url");
2031

2132
const showGuestFunctions = !!MatrixClientPeg.get();
2233
const isElement = isElementBranded();
2334

24-
return (
25-
<div className="mx_DefaultWelcome">
26-
<a href={branding.get("logo_link_url")} target="_blank" rel="noopener" className="mx_DefaultWelcome_logo">
27-
<img src={logoUrl} alt={brand} />
28-
</a>
29-
<Heading as="h1" weight="semibold">
30-
{isElement ? _t("welcome|title_element") : _t("welcome|title_generic", { brand })}
31-
</Heading>
32-
{isElement && <Text size="md">{_t("welcome|tagline_element")}</Text>}
35+
const isQrLoginEnabled = useFeatureEnabled("feature_login_with_qr");
36+
const showQrButton = useAsyncMemo(() => {
37+
const tempClient = createClient({
38+
baseUrl: serverConfig.hsUrl,
39+
});
40+
return isSignInWithQRAvailable(tempClient);
41+
}, [serverConfig]);
42+
43+
const loading = isQrLoginEnabled && showQrButton === undefined;
3344

45+
let body: JSX.Element;
46+
if (loading) {
47+
body = <Spinner />;
48+
} else {
49+
body = (
3450
<div className="mx_DefaultWelcome_buttons">
51+
{showQrButton && (
52+
<Button as="a" href="#/qr_login" kind="primary" size="sm" Icon={QrCodeIcon}>
53+
{_t("auth|sign_in_with_qr")}
54+
</Button>
55+
)}
3556
<Button as="a" href="#/login" kind="primary" size="sm">
36-
{_t("action|sign_in")}
57+
{showQrButton ? _t("auth|sign_in_manually") : _t("action|sign_in")}
3758
</Button>
3859
<Button as="a" href="#/register" kind="secondary" size="sm">
3960
{_t("action|create_account")}
@@ -44,6 +65,20 @@ const DefaultWelcome: React.FC = () => {
4465
</Button>
4566
)}
4667
</div>
68+
);
69+
}
70+
71+
return (
72+
<div className="mx_DefaultWelcome">
73+
<a href={branding.get("logo_link_url")} target="_blank" rel="noopener" className="mx_DefaultWelcome_logo">
74+
<img src={logoUrl} alt={brand} />
75+
</a>
76+
<Heading as="h1" weight="semibold">
77+
{isElement ? _t("welcome|title_element") : _t("welcome|title_generic", { brand })}
78+
</Heading>
79+
{isElement && <Text size="md">{_t("welcome|tagline_element")}</Text>}
80+
81+
{body}
4782
</div>
4883
);
4984
};

0 commit comments

Comments
 (0)