Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit c4664d8

Browse files
committed
Refactor + improve test coverage for QR login
1 parent c497046 commit c4664d8

File tree

9 files changed

+1584
-378
lines changed

9 files changed

+1584
-378
lines changed

src/components/views/auth/LoginWithQR.tsx

Lines changed: 39 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,8 @@ import { logger } from 'matrix-js-sdk/src/logger';
2222
import { MatrixClient } from 'matrix-js-sdk/src/client';
2323

2424
import { _t } from "../../../languageHandler";
25-
import AccessibleButton from '../elements/AccessibleButton';
26-
import QRCode from '../elements/QRCode';
27-
import Spinner from '../elements/Spinner';
28-
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
29-
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
30-
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
31-
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
3225
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
26+
import LoginWithQRFlow from './LoginWithQRFlow';
3327

3428
/**
3529
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
@@ -41,7 +35,7 @@ export enum Mode {
4135
Show = "show",
4236
}
4337

44-
enum Phase {
38+
export enum Phase {
4539
Loading,
4640
ShowingQR,
4741
Connecting,
@@ -51,6 +45,14 @@ enum Phase {
5145
Error,
5246
}
5347

48+
export enum Click {
49+
Cancel,
50+
Decline,
51+
Approve,
52+
TryAgain,
53+
Back,
54+
}
55+
5456
interface IProps {
5557
client: MatrixClient;
5658
mode: Mode;
@@ -68,7 +70,7 @@ interface IState {
6870
/**
6971
* A component that allows sign in and E2EE set up with a QR code.
7072
*
71-
* It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes.
73+
* It implements `login.reciprocate` capabilities and showing QR codes.
7274
*
7375
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
7476
*/
@@ -138,6 +140,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
138140
this.props.onFinished(true);
139141
return;
140142
}
143+
this.setState({ phase: Phase.Verifying });
141144
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
142145
this.props.onFinished(true);
143146
} catch (e) {
@@ -197,200 +200,41 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
197200
});
198201
}
199202

200-
private cancelClicked = async (e: React.FormEvent) => {
201-
e.preventDefault();
202-
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
203-
this.reset();
204-
this.props.onFinished(false);
205-
};
206-
207-
private declineClicked = async (e: React.FormEvent) => {
208-
e.preventDefault();
209-
await this.state.rendezvous?.declineLoginOnExistingDevice();
210-
this.reset();
211-
this.props.onFinished(false);
212-
};
213-
214-
private tryAgainClicked = async (e: React.FormEvent) => {
215-
e.preventDefault();
216-
this.reset();
217-
await this.updateMode(this.props.mode);
218-
};
219-
220-
private onBackClick = async () => {
221-
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
222-
223-
this.props.onFinished(false);
224-
};
225-
226-
private cancelButton = () => <AccessibleButton
227-
kind="primary_outline"
228-
onClick={this.cancelClicked}
229-
>
230-
{ _t("Cancel") }
231-
</AccessibleButton>;
232-
233-
private simpleSpinner = (description?: string): JSX.Element => {
234-
return <div className="mx_LoginWithQR_spinner">
235-
<div>
236-
<Spinner />
237-
{ description && <p>{ description }</p> }
238-
</div>
239-
</div>;
240-
};
241-
242-
public render() {
243-
let title: string;
244-
let titleIcon: JSX.Element | undefined;
245-
let main: JSX.Element | undefined;
246-
let buttons: JSX.Element | undefined;
247-
let backButton = true;
248-
let cancellationMessage: string | undefined;
249-
let centreTitle = false;
250-
251-
switch (this.state.phase) {
252-
case Phase.Error:
253-
switch (this.state.failureReason) {
254-
case RendezvousFailureReason.Expired:
255-
cancellationMessage = _t("The linking wasn't completed in the required time.");
256-
break;
257-
case RendezvousFailureReason.InvalidCode:
258-
cancellationMessage = _t("The scanned code is invalid.");
259-
break;
260-
case RendezvousFailureReason.UnsupportedAlgorithm:
261-
cancellationMessage = _t("Linking with this device is not supported.");
262-
break;
263-
case RendezvousFailureReason.UserDeclined:
264-
cancellationMessage = _t("The request was declined on the other device.");
265-
break;
266-
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
267-
cancellationMessage = _t("The other device is already signed in.");
268-
break;
269-
case RendezvousFailureReason.OtherDeviceNotSignedIn:
270-
cancellationMessage = _t("The other device isn't signed in.");
271-
break;
272-
case RendezvousFailureReason.UserCancelled:
273-
cancellationMessage = _t("The request was cancelled.");
274-
break;
275-
case RendezvousFailureReason.Unknown:
276-
cancellationMessage = _t("An unexpected error occurred.");
277-
break;
278-
case RendezvousFailureReason.HomeserverLacksSupport:
279-
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
280-
break;
281-
default:
282-
cancellationMessage = _t("The request was cancelled.");
283-
break;
284-
}
285-
title = _t("Connection failed");
286-
centreTitle = true;
287-
titleIcon = <WarningBadge className="error" />;
288-
backButton = false;
289-
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
290-
buttons = <>
291-
<AccessibleButton
292-
kind="primary"
293-
onClick={this.tryAgainClicked}
294-
>
295-
{ _t("Try again") }
296-
</AccessibleButton>
297-
{ this.cancelButton() }
298-
</>;
203+
private onClick = async (type: Click) => {
204+
switch (type) {
205+
case Click.Cancel:
206+
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
207+
this.reset();
208+
this.props.onFinished(false);
299209
break;
300-
case Phase.Connected:
301-
title = _t("Devices connected");
302-
titleIcon = <DevicesIcon className="normal" />;
303-
backButton = false;
304-
main = <>
305-
<p>{ _t("Check that the code below matches with your other device:") }</p>
306-
<div className="mx_LoginWithQR_confirmationDigits">
307-
{ this.state.confirmationDigits }
308-
</div>
309-
<div className="mx_LoginWithQR_confirmationAlert">
310-
<div>
311-
<InfoIcon />
312-
</div>
313-
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
314-
</div>
315-
</>;
316-
317-
buttons = <>
318-
<AccessibleButton
319-
data-testid="decline-login-button"
320-
kind="primary_outline"
321-
onClick={this.declineClicked}
322-
>
323-
{ _t("Cancel") }
324-
</AccessibleButton>
325-
<AccessibleButton
326-
data-testid="approve-login-button"
327-
kind="primary"
328-
onClick={this.approveLogin}
329-
>
330-
{ _t("Approve") }
331-
</AccessibleButton>
332-
</>;
333-
break;
334-
case Phase.ShowingQR:
335-
title =_t("Sign in with QR code");
336-
if (this.state.rendezvous) {
337-
const code = <div className="mx_LoginWithQR_qrWrapper">
338-
<QRCode data={[{ data: Buffer.from(this.state.rendezvous.code), mode: 'byte' }]} className="mx_QRCode" />
339-
</div>;
340-
main = <>
341-
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
342-
<ol>
343-
<li>{ _t("Start at the sign in screen") }</li>
344-
<li>{ _t("Select 'Scan QR code'") }</li>
345-
<li>{ _t("Review and approve the sign in") }</li>
346-
</ol>
347-
{ code }
348-
</>;
349-
} else {
350-
main = this.simpleSpinner();
351-
buttons = this.cancelButton();
352-
}
353-
break;
354-
case Phase.Loading:
355-
main = this.simpleSpinner();
210+
case Click.Approve:
211+
await this.approveLogin();
356212
break;
357-
case Phase.Connecting:
358-
main = this.simpleSpinner(_t("Connecting..."));
359-
buttons = this.cancelButton();
213+
case Click.Decline:
214+
await this.state.rendezvous?.declineLoginOnExistingDevice();
215+
this.reset();
216+
this.props.onFinished(false);
360217
break;
361-
case Phase.WaitingForDevice:
362-
main = this.simpleSpinner(_t("Waiting for device to sign in"));
363-
buttons = this.cancelButton();
218+
case Click.TryAgain:
219+
this.reset();
220+
await this.updateMode(this.props.mode);
364221
break;
365-
case Phase.Verifying:
366-
title = _t("Success");
367-
centreTitle = true;
368-
main = this.simpleSpinner(_t("Completing set up of your new device"));
222+
case Click.Back:
223+
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
224+
this.props.onFinished(false);
369225
break;
370226
}
227+
};
371228

229+
public render() {
372230
return (
373-
<div data-testid="login-with-qr" className="mx_LoginWithQR">
374-
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
375-
{ backButton ?
376-
<AccessibleButton
377-
data-testid="back-button"
378-
className="mx_LoginWithQR_BackButton"
379-
onClick={this.onBackClick}
380-
title="Back"
381-
>
382-
<BackButtonIcon />
383-
</AccessibleButton>
384-
: null }
385-
<h1>{ titleIcon }{ title }</h1>
386-
</div>
387-
<div className="mx_LoginWithQR_main">
388-
{ main }
389-
</div>
390-
<div className="mx_LoginWithQR_buttons">
391-
{ buttons }
392-
</div>
393-
</div>
231+
<LoginWithQRFlow
232+
onClick={this.onClick}
233+
phase={this.state.phase}
234+
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
235+
confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
236+
failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
237+
/>
394238
);
395239
}
396240
}

0 commit comments

Comments
 (0)