MSC4108 support OIDC QR code login (#12370)
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
parent
ca7760789b
commit
1677ed1be0
24 changed files with 1558 additions and 733 deletions
|
@ -764,7 +764,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const tabPayload = payload as OpenToTabPayload;
|
||||
Modal.createDialog(
|
||||
UserSettingsDialog,
|
||||
{ initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores },
|
||||
{ ...payload.props, initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores },
|
||||
/*className=*/ undefined,
|
||||
/*isPriority=*/ false,
|
||||
/*isStatic=*/ true,
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef, ReactNode } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { discoverAndValidateOIDCIssuerWellKnown, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -52,6 +52,8 @@ import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
|
|||
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
|
||||
import { SDKContext } from "../../contexts/SDKContext";
|
||||
import { shouldShowFeedback } from "../../utils/Feedback";
|
||||
import { shouldShowQr } from "../views/settings/devices/LoginWithQRSection";
|
||||
import { Features } from "../../settings/Settings";
|
||||
|
||||
interface IProps {
|
||||
isPanelCollapsed: boolean;
|
||||
|
@ -66,6 +68,8 @@ interface IState {
|
|||
isHighContrast: boolean;
|
||||
selectedSpace?: Room | null;
|
||||
showLiveAvatarAddon: boolean;
|
||||
showQrLogin: boolean;
|
||||
supportsQrLogin: boolean;
|
||||
}
|
||||
|
||||
const toRightOf = (rect: PartialDOMRect): MenuProps => {
|
||||
|
@ -103,6 +107,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
|
||||
showQrLogin: false,
|
||||
supportsQrLogin: false,
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
|
@ -126,6 +132,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
this.checkQrLoginSupport();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -140,6 +147,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private checkQrLoginSupport = async (): Promise<void> => {
|
||||
if (!this.context.client || !SettingsStore.getValue(Features.OidcNativeFlow)) return;
|
||||
|
||||
const { issuer } = await this.context.client.getAuthIssuer().catch(() => ({ issuer: undefined }));
|
||||
if (issuer) {
|
||||
const [oidcClientConfig, versions, wellKnown, isCrossSigningReady] = await Promise.all([
|
||||
discoverAndValidateOIDCIssuerWellKnown(issuer),
|
||||
this.context.client.getVersions(),
|
||||
this.context.client.waitForClientWellKnown(),
|
||||
this.context.client.getCrypto()?.isCrossSigningReady(),
|
||||
]);
|
||||
|
||||
const supportsQrLogin = shouldShowQr(
|
||||
this.context.client,
|
||||
!!isCrossSigningReady,
|
||||
oidcClientConfig,
|
||||
versions,
|
||||
wellKnown,
|
||||
);
|
||||
this.setState({ supportsQrLogin, showQrLogin: true });
|
||||
}
|
||||
};
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
if (SettingsStore.getValue("use_system_theme")) {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
@ -237,11 +267,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
|
||||
};
|
||||
|
||||
private onSettingsOpen = (ev: ButtonEvent, tabId?: string): void => {
|
||||
private onSettingsOpen = (ev: ButtonEvent, tabId?: string, props?: Record<string, any>): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId };
|
||||
const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId, props };
|
||||
defaultDispatcher.dispatch(payload);
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
@ -363,9 +393,33 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let linkNewDeviceButton: JSX.Element | undefined;
|
||||
if (this.state.showQrLogin) {
|
||||
const extraProps: Omit<
|
||||
React.ComponentProps<typeof IconizedContextMenuOption>,
|
||||
"iconClassname" | "label" | "onClick"
|
||||
> = {};
|
||||
if (!this.state.supportsQrLogin) {
|
||||
extraProps.disabled = true;
|
||||
extraProps.title = _t("user_menu|link_new_device_not_supported");
|
||||
extraProps.caption = _t("user_menu|link_new_device_not_supported_caption");
|
||||
extraProps.placement = "right";
|
||||
}
|
||||
|
||||
linkNewDeviceButton = (
|
||||
<IconizedContextMenuOption
|
||||
{...extraProps}
|
||||
iconClassName="mx_UserMenu_iconQr"
|
||||
label={_t("user_menu|link_new_device")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.SessionManager, { showMsc4108QrCode: true })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{homeButton}
|
||||
{linkNewDeviceButton}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("notifications|enable_prompt_toast_title")}
|
||||
|
|
|
@ -27,17 +27,21 @@ export enum Mode {
|
|||
export enum Phase {
|
||||
Loading,
|
||||
ShowingQR,
|
||||
Connecting,
|
||||
Connected,
|
||||
// The following are specific to MSC4108
|
||||
OutOfBandConfirmation,
|
||||
WaitingForDevice,
|
||||
Verifying,
|
||||
Error,
|
||||
/**
|
||||
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
|
||||
*/
|
||||
LegacyConnected,
|
||||
}
|
||||
|
||||
export enum Click {
|
||||
Cancel,
|
||||
Decline,
|
||||
Approve,
|
||||
TryAgain,
|
||||
Back,
|
||||
ShowQr,
|
||||
}
|
||||
|
|
|
@ -16,39 +16,61 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
MSC3906RendezvousPayload,
|
||||
ClientRendezvousFailureReason,
|
||||
LegacyRendezvousFailureReason,
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv2RendezvousChannel,
|
||||
MSC3906Rendezvous,
|
||||
MSC4108FailureReason,
|
||||
MSC4108RendezvousSession,
|
||||
MSC4108SecureChannel,
|
||||
MSC4108SignInWithQR,
|
||||
RendezvousError,
|
||||
RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
|
||||
import LoginWithQRFlow from "./LoginWithQRFlow";
|
||||
import { Click, Mode, Phase } from "./LoginWithQR-types";
|
||||
import LoginWithQRFlow from "./LoginWithQRFlow";
|
||||
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
client: MatrixClient;
|
||||
mode: Mode;
|
||||
legacy: boolean;
|
||||
onFinished(...args: any): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
rendezvous?: MSC3906Rendezvous;
|
||||
confirmationDigits?: string;
|
||||
failureReason?: FailureReason;
|
||||
rendezvous?: MSC3906Rendezvous | MSC4108SignInWithQR;
|
||||
mediaPermissionError?: boolean;
|
||||
|
||||
// MSC3906
|
||||
confirmationDigits?: string;
|
||||
|
||||
// MSC4108
|
||||
verificationUri?: string;
|
||||
userCode?: string;
|
||||
checkCode?: string;
|
||||
failureReason?: FailureReason;
|
||||
lastScannedCode?: Buffer;
|
||||
homeserverBaseUrl?: string;
|
||||
}
|
||||
|
||||
export enum LoginWithQRFailureReason {
|
||||
/**
|
||||
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
|
||||
*/
|
||||
RateLimited = "rate_limited",
|
||||
CheckCodeMismatch = "check_code_mismatch",
|
||||
}
|
||||
|
||||
export type FailureReason = LegacyRendezvousFailureReason | LoginWithQRFailureReason;
|
||||
export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
|
||||
|
||||
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
|
||||
// However, we want to keep this implementation around for some time.
|
||||
|
@ -62,6 +84,8 @@ export type FailureReason = LegacyRendezvousFailureReason | LoginWithQRFailureRe
|
|||
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||
*/
|
||||
export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
private finished = false;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -70,6 +94,10 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
};
|
||||
}
|
||||
|
||||
private get ourIntent(): RendezvousIntent {
|
||||
return RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.updateMode(this.props.mode).then(() => {});
|
||||
}
|
||||
|
@ -85,27 +113,36 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
if (this.state.rendezvous) {
|
||||
const rendezvous = this.state.rendezvous;
|
||||
rendezvous.onFailure = undefined;
|
||||
await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
if (rendezvous instanceof MSC3906Rendezvous) {
|
||||
await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
}
|
||||
this.setState({ rendezvous: undefined });
|
||||
}
|
||||
if (mode === Mode.Show) {
|
||||
await this.generateCode();
|
||||
await this.generateAndShowCode();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
if (this.state.rendezvous) {
|
||||
if (this.state.rendezvous && !this.finished) {
|
||||
// eslint-disable-next-line react/no-direct-mutation-state
|
||||
this.state.rendezvous.onFailure = undefined;
|
||||
// calling cancel will call close() as well to clean up the resources
|
||||
this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled).then(() => {});
|
||||
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
|
||||
this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
} else {
|
||||
this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private approveLogin = async (): Promise<void> => {
|
||||
if (!this.state.rendezvous) {
|
||||
private async legacyApproveLogin(): Promise<void> {
|
||||
if (!(this.state.rendezvous instanceof MSC3906Rendezvous)) {
|
||||
throw new Error("Rendezvous not found");
|
||||
}
|
||||
if (!this.props.client) {
|
||||
throw new Error("No client to approve login with");
|
||||
}
|
||||
this.setState({ phase: Phase.Loading });
|
||||
|
||||
try {
|
||||
|
@ -125,7 +162,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
}
|
||||
if (!this.props.client.getCrypto()) {
|
||||
// no E2EE to set up
|
||||
this.props.onFinished(true);
|
||||
this.onFinished(true);
|
||||
return;
|
||||
}
|
||||
this.setState({ phase: Phase.Verifying });
|
||||
|
@ -136,7 +173,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
} finally {
|
||||
this.setState({ rendezvous: undefined });
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
this.onFinished(true);
|
||||
} catch (e) {
|
||||
logger.error("Error whilst approving sign in", e);
|
||||
if (e instanceof HTTPError && e.httpStatus === 429) {
|
||||
|
@ -144,27 +181,38 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited });
|
||||
return;
|
||||
}
|
||||
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private generateCode = async (): Promise<void> => {
|
||||
let rendezvous: MSC3906Rendezvous;
|
||||
private onFinished(success: boolean): void {
|
||||
this.finished = true;
|
||||
this.props.onFinished(success);
|
||||
}
|
||||
|
||||
private generateAndShowCode = async (): Promise<void> => {
|
||||
let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous;
|
||||
try {
|
||||
const fallbackRzServer = this.props.client.getClientWellKnown()?.["io.element.rendezvous"]?.server;
|
||||
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
fallbackRzServer,
|
||||
});
|
||||
const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server;
|
||||
|
||||
const channel = new MSC3903ECDHv2RendezvousChannel<MSC3906RendezvousPayload>(
|
||||
transport,
|
||||
undefined,
|
||||
this.onFailure,
|
||||
);
|
||||
|
||||
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
|
||||
if (this.props.legacy) {
|
||||
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
fallbackRzServer,
|
||||
});
|
||||
const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure);
|
||||
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
|
||||
} else {
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
fallbackRzServer,
|
||||
});
|
||||
await transport.send("");
|
||||
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
|
||||
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
|
||||
}
|
||||
|
||||
await rendezvous.generateCode();
|
||||
this.setState({
|
||||
|
@ -174,23 +222,84 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
});
|
||||
} catch (e) {
|
||||
logger.error("Error whilst generating QR code", e);
|
||||
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport });
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const confirmationDigits = await rendezvous.startAfterShowingCode();
|
||||
this.setState({ phase: Phase.Connected, confirmationDigits });
|
||||
} catch (e) {
|
||||
logger.error("Error whilst doing QR login", e);
|
||||
// only set to error phase if it hasn't already been set by onFailure or similar
|
||||
if (this.state.phase !== Phase.Error) {
|
||||
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
|
||||
if (rendezvous instanceof MSC3906Rendezvous) {
|
||||
const confirmationDigits = await rendezvous.startAfterShowingCode();
|
||||
this.setState({ phase: Phase.LegacyConnected, confirmationDigits });
|
||||
} else if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) {
|
||||
// MSC4108-Flow: NewScanned
|
||||
await rendezvous.negotiateProtocols();
|
||||
const { verificationUri } = await rendezvous.deviceAuthorizationGrant();
|
||||
this.setState({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
verificationUri,
|
||||
});
|
||||
}
|
||||
|
||||
// we ask the user to confirm that the channel is secure
|
||||
} catch (e: RendezvousError | unknown) {
|
||||
logger.error("Error whilst approving login", e);
|
||||
if (rendezvous instanceof MSC3906Rendezvous) {
|
||||
// only set to error phase if it hasn't already been set by onFailure or similar
|
||||
if (this.state.phase !== Phase.Error) {
|
||||
this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown });
|
||||
}
|
||||
} else {
|
||||
await rendezvous?.cancel(
|
||||
e instanceof RendezvousError
|
||||
? (e.code as MSC4108FailureReason)
|
||||
: ClientRendezvousFailureReason.Unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onFailure = (reason: LegacyRendezvousFailureReason): void => {
|
||||
private approveLogin = async (checkCode: string | undefined): Promise<void> => {
|
||||
if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) {
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
throw new Error("Rendezvous not found");
|
||||
}
|
||||
|
||||
if (!this.state.lastScannedCode && this.state.rendezvous?.checkCode !== checkCode) {
|
||||
this.setState({ failureReason: LoginWithQRFailureReason.CheckCodeMismatch });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) {
|
||||
// MSC4108-Flow: NewScanned
|
||||
this.setState({ phase: Phase.Loading });
|
||||
|
||||
if (this.state.verificationUri) {
|
||||
window.open(this.state.verificationUri, "_blank");
|
||||
}
|
||||
|
||||
this.setState({ phase: Phase.WaitingForDevice });
|
||||
|
||||
// send secrets
|
||||
await this.state.rendezvous.shareSecrets();
|
||||
|
||||
// done
|
||||
this.onFinished(true);
|
||||
} else {
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
throw new Error("New device flows around OIDC are not yet implemented");
|
||||
}
|
||||
} catch (e: RendezvousError | unknown) {
|
||||
logger.error("Error whilst approving sign in", e);
|
||||
this.setState({
|
||||
phase: Phase.Error,
|
||||
failureReason: e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onFailure = (reason: RendezvousFailureReason): void => {
|
||||
if (this.state.phase === Phase.Error) return; // Already in failed state
|
||||
logger.info(`Rendezvous failed: ${reason}`);
|
||||
this.setState({ phase: Phase.Error, failureReason: reason });
|
||||
};
|
||||
|
@ -199,44 +308,72 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
rendezvous: undefined,
|
||||
confirmationDigits: undefined,
|
||||
verificationUri: undefined,
|
||||
failureReason: undefined,
|
||||
userCode: undefined,
|
||||
checkCode: undefined,
|
||||
homeserverBaseUrl: undefined,
|
||||
lastScannedCode: undefined,
|
||||
mediaPermissionError: false,
|
||||
});
|
||||
}
|
||||
|
||||
private onClick = async (type: Click): Promise<void> => {
|
||||
private onClick = async (type: Click, checkCode?: string): Promise<void> => {
|
||||
switch (type) {
|
||||
case Click.Cancel:
|
||||
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
|
||||
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
} else {
|
||||
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
|
||||
}
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
this.onFinished(false);
|
||||
break;
|
||||
case Click.Approve:
|
||||
await this.approveLogin();
|
||||
await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode));
|
||||
break;
|
||||
case Click.Decline:
|
||||
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
break;
|
||||
case Click.TryAgain:
|
||||
this.reset();
|
||||
await this.updateMode(this.props.mode);
|
||||
this.onFinished(false);
|
||||
break;
|
||||
case Click.Back:
|
||||
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
this.props.onFinished(false);
|
||||
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
|
||||
await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled);
|
||||
} else {
|
||||
await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled);
|
||||
}
|
||||
this.onFinished(false);
|
||||
break;
|
||||
case Click.ShowQr:
|
||||
await this.updateMode(Mode.Show);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.rendezvous instanceof MSC3906Rendezvous) {
|
||||
return (
|
||||
<LoginWithQRFlow
|
||||
onClick={this.onClick}
|
||||
phase={this.state.phase}
|
||||
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
|
||||
confirmationDigits={
|
||||
this.state.phase === Phase.LegacyConnected ? this.state.confirmationDigits : undefined
|
||||
}
|
||||
failureReason={this.state.failureReason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LoginWithQRFlow
|
||||
onClick={this.onClick}
|
||||
phase={this.state.phase}
|
||||
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
|
||||
confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
|
||||
failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
|
||||
failureReason={this.state.failureReason}
|
||||
userCode={this.state.userCode}
|
||||
checkCode={this.state.checkCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import { LegacyRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
import React, { createRef, ReactNode } from "react";
|
||||
import {
|
||||
ClientRendezvousFailureReason,
|
||||
LegacyRendezvousFailureReason,
|
||||
MSC4108FailureReason,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg";
|
||||
import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg";
|
||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||
import { Heading, Text } from "@vector-im/compound-web";
|
||||
import { Heading, MFAInput, Text } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -30,13 +34,24 @@ import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
|
|||
import { Click, Phase } from "./LoginWithQR-types";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR";
|
||||
import { XOR } from "../../../@types/common";
|
||||
import { ErrorMessage } from "../../structures/ErrorMessage";
|
||||
|
||||
/**
|
||||
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
|
||||
*/
|
||||
interface MSC3906Props extends Pick<Props, "phase" | "onClick" | "failureReason"> {
|
||||
code?: string;
|
||||
confirmationDigits?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
phase: Phase;
|
||||
code?: string;
|
||||
onClick(type: Click): Promise<void>;
|
||||
code?: Uint8Array;
|
||||
onClick(type: Click, checkCodeEntered?: string): Promise<void>;
|
||||
failureReason?: FailureReason;
|
||||
confirmationDigits?: string;
|
||||
userCode?: string;
|
||||
checkCode?: string;
|
||||
}
|
||||
|
||||
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
|
||||
|
@ -46,17 +61,19 @@ interface Props {
|
|||
/**
|
||||
* A component that implements the UI for sign in and E2EE set up with a QR code.
|
||||
*
|
||||
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||
* This supports the unstable features of MSC3906 and MSC4108
|
||||
*/
|
||||
export default class LoginWithQRFlow extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
export default class LoginWithQRFlow extends React.Component<XOR<Props, MSC3906Props>> {
|
||||
private checkCodeInput = createRef<HTMLInputElement>();
|
||||
|
||||
public constructor(props: XOR<Props, MSC3906Props>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
private handleClick = (type: Click): ((e: React.FormEvent) => Promise<void>) => {
|
||||
return async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
await this.props.onClick(type);
|
||||
await this.props.onClick(type, type === Click.Approve ? this.checkCodeInput.current?.value : undefined);
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -90,24 +107,26 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
let message: ReactNode | undefined;
|
||||
|
||||
switch (this.props.failureReason) {
|
||||
case LegacyRendezvousFailureReason.UnsupportedAlgorithm:
|
||||
case LegacyRendezvousFailureReason.UnsupportedTransport:
|
||||
case LegacyRendezvousFailureReason.HomeserverLacksSupport:
|
||||
case MSC4108FailureReason.UnsupportedProtocol:
|
||||
case LegacyRendezvousFailureReason.UnsupportedProtocol:
|
||||
title = _t("auth|qr_code_login|error_unsupported_protocol_title");
|
||||
message = _t("auth|qr_code_login|error_unsupported_protocol");
|
||||
break;
|
||||
|
||||
case MSC4108FailureReason.UserCancelled:
|
||||
case LegacyRendezvousFailureReason.UserCancelled:
|
||||
title = _t("auth|qr_code_login|error_user_cancelled_title");
|
||||
message = _t("auth|qr_code_login|error_user_cancelled");
|
||||
break;
|
||||
|
||||
case MSC4108FailureReason.AuthorizationExpired:
|
||||
case ClientRendezvousFailureReason.Expired:
|
||||
case LegacyRendezvousFailureReason.Expired:
|
||||
title = _t("auth|qr_code_login|error_expired_title");
|
||||
message = _t("auth|qr_code_login|error_expired");
|
||||
break;
|
||||
|
||||
case LegacyRendezvousFailureReason.InvalidCode:
|
||||
case ClientRendezvousFailureReason.InsecureChannelDetected:
|
||||
title = _t("auth|qr_code_login|error_insecure_channel_detected_title");
|
||||
message = (
|
||||
<>
|
||||
|
@ -125,13 +144,13 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
);
|
||||
break;
|
||||
|
||||
case LegacyRendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||
case ClientRendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||
success = true;
|
||||
title = _t("auth|qr_code_login|error_other_device_already_signed_in_title");
|
||||
message = _t("auth|qr_code_login|error_other_device_already_signed_in");
|
||||
break;
|
||||
|
||||
case LegacyRendezvousFailureReason.UserDeclined:
|
||||
case ClientRendezvousFailureReason.UserDeclined:
|
||||
title = _t("auth|qr_code_login|error_user_declined_title");
|
||||
message = _t("auth|qr_code_login|error_user_declined");
|
||||
break;
|
||||
|
@ -141,8 +160,16 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
message = _t("auth|qr_code_login|error_rate_limited");
|
||||
break;
|
||||
|
||||
case LegacyRendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||
case LegacyRendezvousFailureReason.Unknown:
|
||||
case ClientRendezvousFailureReason.ETagMissing:
|
||||
title = _t("error|something_went_wrong");
|
||||
message = _t("auth|qr_code_login|error_etag_missing");
|
||||
break;
|
||||
|
||||
case MSC4108FailureReason.DeviceAlreadyExists:
|
||||
case MSC4108FailureReason.DeviceNotFound:
|
||||
case MSC4108FailureReason.UnexpectedMessageReceived:
|
||||
case ClientRendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||
case ClientRendezvousFailureReason.Unknown:
|
||||
default:
|
||||
title = _t("error|something_went_wrong");
|
||||
message = _t("auth|qr_code_login|error_unexpected");
|
||||
|
@ -150,18 +177,6 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
}
|
||||
className = "mx_LoginWithQR_error";
|
||||
backButton = false;
|
||||
buttons = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
data-testid="try-again-button"
|
||||
kind="primary"
|
||||
onClick={this.handleClick(Click.TryAgain)}
|
||||
>
|
||||
{_t("action|try_again")}
|
||||
</AccessibleButton>
|
||||
{this.cancelButton()}
|
||||
</>
|
||||
);
|
||||
main = (
|
||||
<>
|
||||
<div
|
||||
|
@ -179,7 +194,7 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
);
|
||||
break;
|
||||
}
|
||||
case Phase.Connected:
|
||||
case Phase.LegacyConnected:
|
||||
backButton = false;
|
||||
main = (
|
||||
<>
|
||||
|
@ -213,9 +228,62 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
</>
|
||||
);
|
||||
break;
|
||||
case Phase.OutOfBandConfirmation:
|
||||
backButton = false;
|
||||
main = (
|
||||
<>
|
||||
<Heading as="h1" size="sm" weight="semibold">
|
||||
{_t("auth|qr_code_login|check_code_heading")}
|
||||
</Heading>
|
||||
<Text size="md">{_t("auth|qr_code_login|check_code_explainer")}</Text>
|
||||
<label htmlFor="mx_LoginWithQR_checkCode">
|
||||
{_t("auth|qr_code_login|check_code_input_label")}
|
||||
</label>
|
||||
<MFAInput
|
||||
className="mx_LoginWithQR_checkCode_input mx_no_textinput"
|
||||
ref={this.checkCodeInput}
|
||||
length={2}
|
||||
autoFocus
|
||||
id="mx_LoginWithQR_checkCode"
|
||||
data-invalid={
|
||||
this.props.failureReason === LoginWithQRFailureReason.CheckCodeMismatch
|
||||
? true
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ErrorMessage
|
||||
message={
|
||||
this.props.failureReason === LoginWithQRFailureReason.CheckCodeMismatch
|
||||
? _t("auth|qr_code_login|check_code_mismatch")
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
data-testid="approve-login-button"
|
||||
kind="primary"
|
||||
onClick={this.handleClick(Click.Approve)}
|
||||
>
|
||||
{_t("action|continue")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid="decline-login-button"
|
||||
kind="primary_outline"
|
||||
onClick={this.handleClick(Click.Decline)}
|
||||
>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case Phase.ShowingQR:
|
||||
if (this.props.code) {
|
||||
const data = Buffer.from(this.props.code ?? "");
|
||||
const data =
|
||||
typeof this.props.code !== "string" ? this.props.code : Buffer.from(this.props.code ?? "");
|
||||
|
||||
main = (
|
||||
<>
|
||||
|
@ -249,12 +317,19 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||
case Phase.Loading:
|
||||
main = this.simpleSpinner();
|
||||
break;
|
||||
case Phase.Connecting:
|
||||
main = this.simpleSpinner(_t("auth|qr_code_login|connecting"));
|
||||
buttons = this.cancelButton();
|
||||
break;
|
||||
case Phase.WaitingForDevice:
|
||||
main = this.simpleSpinner(_t("auth|qr_code_login|waiting_for_device"));
|
||||
main = (
|
||||
<>
|
||||
{this.simpleSpinner(_t("auth|qr_code_login|waiting_for_device"))}
|
||||
{this.props.userCode ? (
|
||||
<div>
|
||||
<p>{_t("auth|qr_code_login|security_code")}</p>
|
||||
<p>{_t("auth|qr_code_login|security_code_prompt")}</p>
|
||||
<p>{this.props.userCode}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
buttons = this.cancelButton();
|
||||
break;
|
||||
case Phase.Verifying:
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
|
@ -41,6 +41,7 @@ import { useSettingValue } from "../../../hooks/useSettings";
|
|||
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
showMsc4108QrCode?: boolean;
|
||||
sdkContext: SdkContextClass;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
@ -80,6 +81,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
|
|||
export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const voipEnabled = useSettingValue<boolean>(UIFeature.Voip);
|
||||
const mjolnirEnabled = useSettingValue<boolean>("feature_mjolnir");
|
||||
// store this prop in state as changing tabs back and forth should clear it
|
||||
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
|
||||
|
||||
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
|
@ -98,7 +101,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
|||
UserTab.SessionManager,
|
||||
_td("settings|sessions|title"),
|
||||
"mx_UserSettingsDialog_sessionsIcon",
|
||||
<SessionManagerTab />,
|
||||
<SessionManagerTab showMsc4108QrCode={showMsc4108QrCode} />,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
|
@ -205,7 +208,12 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
|||
return tabs as NonEmptyArray<Tab<UserTab>>;
|
||||
};
|
||||
|
||||
const [activeTabId, setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.General, props.initialTabId);
|
||||
const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.General, props.initialTabId);
|
||||
const setActiveTabId = (tabId: UserTab): void => {
|
||||
_setActiveTabId(tabId);
|
||||
// Clear this so switching away from the tab and back to it will not show the QR code again
|
||||
setShowMsc4108QrCode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
// XXX: SDKContext is provided within the LoggedInView subtree.
|
||||
|
|
|
@ -21,18 +21,26 @@ import {
|
|||
GET_LOGIN_TOKEN_CAPABILITY,
|
||||
Capabilities,
|
||||
IClientWellKnown,
|
||||
OidcClientConfig,
|
||||
MatrixClient,
|
||||
DEVICE_CODE_SCOPE,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { Icon as QrCodeIcon } from "@vector-im/compound-design-tokens/icons/qr-code.svg";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import { Features } from "../../../../settings/Settings";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
onShowQr: () => void;
|
||||
versions?: IServerVersions;
|
||||
capabilities?: Capabilities;
|
||||
wellKnown?: IClientWellKnown;
|
||||
oidcClientConfig?: OidcClientConfig;
|
||||
isCrossSigningReady?: boolean;
|
||||
}
|
||||
|
||||
function shouldShowQrLegacy(
|
||||
|
@ -50,8 +58,40 @@ function shouldShowQrLegacy(
|
|||
return getLoginTokenSupported && msc3886Supported;
|
||||
}
|
||||
|
||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, capabilities, wellKnown }) => {
|
||||
const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities);
|
||||
export function shouldShowQr(
|
||||
cli: MatrixClient,
|
||||
isCrossSigningReady: boolean,
|
||||
oidcClientConfig?: OidcClientConfig,
|
||||
versions?: IServerVersions,
|
||||
wellKnown?: IClientWellKnown,
|
||||
): boolean {
|
||||
const msc4108Supported =
|
||||
!!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server;
|
||||
|
||||
const deviceAuthorizationGrantSupported =
|
||||
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
||||
|
||||
return (
|
||||
deviceAuthorizationGrantSupported &&
|
||||
msc4108Supported &&
|
||||
SettingsStore.getValue(Features.OidcNativeFlow) &&
|
||||
!!cli.getCrypto()?.exportSecretsBundle &&
|
||||
isCrossSigningReady
|
||||
);
|
||||
}
|
||||
|
||||
const LoginWithQRSection: React.FC<IProps> = ({
|
||||
onShowQr,
|
||||
versions,
|
||||
capabilities,
|
||||
wellKnown,
|
||||
oidcClientConfig,
|
||||
isCrossSigningReady,
|
||||
}) => {
|
||||
const cli = useMatrixClientContext();
|
||||
const offerShowQr = oidcClientConfig
|
||||
? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown)
|
||||
: shouldShowQrLegacy(versions, wellKnown, capabilities);
|
||||
|
||||
// don't show anything if no method is available
|
||||
if (!offerShowQr) {
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -32,7 +32,6 @@ import { ExtendedDevice } from "../../devices/types";
|
|||
import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import LoginWithQRSection from "../../devices/LoginWithQRSection";
|
||||
import LoginWithQR from "../../../auth/LoginWithQR";
|
||||
import { Mode } from "../../../auth/LoginWithQR-types";
|
||||
import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo";
|
||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
|
@ -41,6 +40,10 @@ import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionH
|
|||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
|
||||
// We import `LoginWithQR` asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle.
|
||||
const LoginWithQR = lazy(() => import("../../../auth/LoginWithQR"));
|
||||
|
||||
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
|
@ -148,7 +151,9 @@ const useSignOut = (
|
|||
};
|
||||
};
|
||||
|
||||
const SessionManagerTab: React.FC = () => {
|
||||
const SessionManagerTab: React.FC<{
|
||||
showMsc4108QrCode?: boolean;
|
||||
}> = ({ showMsc4108QrCode }) => {
|
||||
const {
|
||||
devices,
|
||||
dehydratedDeviceId,
|
||||
|
@ -186,6 +191,20 @@ const SessionManagerTab: React.FC = () => {
|
|||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||
const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]);
|
||||
const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]);
|
||||
const oidcClientConfig = useAsyncMemo(async () => {
|
||||
try {
|
||||
const authIssuer = await matrixClient?.getAuthIssuer();
|
||||
if (authIssuer) {
|
||||
return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
}, [matrixClient]);
|
||||
const isCrossSigningReady = useAsyncMemo(
|
||||
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
|
||||
[matrixClient],
|
||||
);
|
||||
|
||||
const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => {
|
||||
if (expandedDeviceIds.includes(deviceId)) {
|
||||
|
@ -268,7 +287,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
|
||||
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>(showMsc4108QrCode ? Mode.Show : null);
|
||||
|
||||
const onQrFinish = useCallback(() => {
|
||||
setSignInWithQrMode(null);
|
||||
|
@ -279,7 +298,16 @@ const SessionManagerTab: React.FC = () => {
|
|||
}, [setSignInWithQrMode]);
|
||||
|
||||
if (signInWithQrMode) {
|
||||
return <LoginWithQR mode={signInWithQrMode} onFinished={onQrFinish} client={matrixClient} />;
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LoginWithQR
|
||||
mode={signInWithQrMode}
|
||||
onFinished={onQrFinish}
|
||||
client={matrixClient}
|
||||
legacy={!oidcClientConfig && !showMsc4108QrCode}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -290,6 +318,8 @@ const SessionManagerTab: React.FC = () => {
|
|||
versions={clientVersions}
|
||||
capabilities={capabilities}
|
||||
wellKnown={wellKnown}
|
||||
oidcClientConfig={oidcClientConfig}
|
||||
isCrossSigningReady={isCrossSigningReady}
|
||||
/>
|
||||
<SecurityRecommendations
|
||||
devices={devices}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue