Refactor QR link flow to use new SDK methods (#33309)
* 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 * Use js-sdk isSignInWithQRAvailable API to simplify code
This commit is contained in:
@@ -9,9 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React from "react";
|
||||
import {
|
||||
ClientRendezvousFailureReason,
|
||||
linkNewDeviceByGeneratingQR,
|
||||
MSC4108FailureReason,
|
||||
MSC4108RendezvousSession,
|
||||
MSC4108SecureChannel,
|
||||
MSC4108SignInWithQR,
|
||||
RendezvousError,
|
||||
type RendezvousFailureReason,
|
||||
@@ -55,6 +54,7 @@ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
|
||||
*/
|
||||
export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
private finished = false;
|
||||
private abortController?: AbortController;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -69,35 +69,31 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.updateMode(this.props.mode).then(() => {});
|
||||
void this.updateMode(this.props.mode);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.updateMode(this.props.mode).then(() => {});
|
||||
void this.updateMode(this.props.mode);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
this.abortController?.abort();
|
||||
this.abortController = new AbortController();
|
||||
this.setState({ rendezvous: undefined });
|
||||
if (showLoading) {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
}
|
||||
|
||||
if (mode === Mode.Show) {
|
||||
await this.generateAndShowCode();
|
||||
await this.generateAndShowCode(this.abortController);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
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(MSC4108FailureReason.UserCancelled);
|
||||
if (!this.finished) {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,24 +102,18 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
this.props.onFinished(success);
|
||||
}
|
||||
|
||||
private generateAndShowCode = async (): Promise<void> => {
|
||||
private generateAndShowCode = async (abortController: AbortController): Promise<void> => {
|
||||
let rendezvous: MSC4108SignInWithQR;
|
||||
try {
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
});
|
||||
await transport.send("");
|
||||
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
|
||||
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
|
||||
|
||||
await rendezvous.generateCode();
|
||||
rendezvous = await linkNewDeviceByGeneratingQR(this.props.client, this.onFailure, abortController.signal);
|
||||
if (abortController.signal.aborted) return;
|
||||
this.setState({
|
||||
phase: Phase.ShowingQR,
|
||||
rendezvous,
|
||||
failureReason: undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
logger.error("Error whilst generating QR code", e);
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport });
|
||||
return;
|
||||
@@ -142,8 +132,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
|
||||
// we ask the user to confirm that the channel is secure
|
||||
} catch (e: RendezvousError | unknown) {
|
||||
if (abortController.signal.aborted) return;
|
||||
logger.error("Error whilst approving login", e);
|
||||
await rendezvous?.cancel(
|
||||
await rendezvous.cancel(
|
||||
e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown,
|
||||
);
|
||||
}
|
||||
@@ -210,6 +201,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
public reset(): void {
|
||||
this.abortController?.abort();
|
||||
this.setState({
|
||||
rendezvous: undefined,
|
||||
verificationUri: undefined,
|
||||
|
||||
@@ -7,48 +7,35 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
type IServerVersions,
|
||||
type OidcClientConfig,
|
||||
type MatrixClient,
|
||||
DEVICE_CODE_SCOPE,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code";
|
||||
import { Text } from "@vector-im/compound-web";
|
||||
import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { SettingsSubsection } from "../shared/SettingsSubsection";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||
|
||||
interface IProps {
|
||||
onShowQr: () => void;
|
||||
versions?: IServerVersions;
|
||||
oidcClientConfig?: OidcClientConfig;
|
||||
isCrossSigningReady?: boolean;
|
||||
}
|
||||
|
||||
export function shouldShowQr(
|
||||
cli: MatrixClient,
|
||||
isCrossSigningReady: boolean,
|
||||
oidcClientConfig?: OidcClientConfig,
|
||||
versions?: IServerVersions,
|
||||
): boolean {
|
||||
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
||||
export async function shouldShowQrForLinkNewDevice(cli: MatrixClient, isCrossSigningReady: boolean): Promise<boolean> {
|
||||
const doesServerHaveSupport = await isSignInWithQRAvailable(cli);
|
||||
|
||||
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
||||
|
||||
return (
|
||||
!!deviceAuthorizationGrantSupported &&
|
||||
msc4108Supported &&
|
||||
!!cli.getCrypto()?.exportSecretsBundle &&
|
||||
isCrossSigningReady
|
||||
);
|
||||
return doesServerHaveSupport && !!cli.getCrypto()?.exportSecretsBundle && isCrossSigningReady;
|
||||
}
|
||||
|
||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => {
|
||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, isCrossSigningReady }) => {
|
||||
const cli = useMatrixClientContext();
|
||||
const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions);
|
||||
const offerShowQr = useAsyncMemo(
|
||||
() => shouldShowQrForLinkNewDevice(cli, !!isCrossSigningReady),
|
||||
[cli, isCrossSigningReady],
|
||||
false,
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
||||
|
||||
@@ -162,14 +162,6 @@ const SessionManagerTab: React.FC<{
|
||||
const disableMultipleSignout = !!accountManagement?.endpoint;
|
||||
const userId = matrixClient?.getUserId();
|
||||
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||
const oidcClientConfig = useAsyncMemo(async () => {
|
||||
try {
|
||||
return await matrixClient?.getAuthMetadata();
|
||||
} catch (e) {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
}, [matrixClient]);
|
||||
const isCrossSigningReady = useAsyncMemo(
|
||||
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
|
||||
[matrixClient],
|
||||
@@ -279,12 +271,7 @@ const SessionManagerTab: React.FC<{
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection>
|
||||
<LoginWithQRSection
|
||||
onShowQr={onShowQrClicked}
|
||||
versions={clientVersions}
|
||||
oidcClientConfig={oidcClientConfig}
|
||||
isCrossSigningReady={isCrossSigningReady}
|
||||
/>
|
||||
<LoginWithQRSection onShowQr={onShowQrClicked} isCrossSigningReady={isCrossSigningReady} />
|
||||
<SecurityRecommendations
|
||||
devices={devices}
|
||||
goToFilteredList={onGoToFilteredList}
|
||||
|
||||
Reference in New Issue
Block a user