From 85cfabc750dfa252e52095513df9af369153c96d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 22:29:33 +0100 Subject: [PATCH 01/73] Support for login + E2EE set up with QR --- package.json | 1 + res/css/_components.pcss | 1 + res/css/structures/auth/_Login.pcss | 19 + res/css/views/auth/_LoginWithQR.pcss | 193 +++++++ .../tabs/user/_SecurityUserSettingsTab.pcss | 4 + res/img/element-icons/back.svg | 3 + res/img/element-icons/devices.svg | 11 + res/img/element-icons/qrcode.svg | 4 + src/IConfigOptions.ts | 11 + src/SdkConfig.ts | 8 + src/components/structures/auth/Login.tsx | 73 ++- src/components/views/auth/LoginWithQR.tsx | 534 ++++++++++++++++++ .../views/settings/DevicesPanel.tsx | 10 + .../tabs/user/SecurityUserSettingsTab.tsx | 83 ++- src/i18n/strings/en_EN.json | 32 ++ yarn.lock | 42 ++ 16 files changed, 1003 insertions(+), 26 deletions(-) create mode 100644 res/css/views/auth/_LoginWithQR.pcss create mode 100644 res/img/element-icons/back.svg create mode 100644 res/img/element-icons/devices.svg create mode 100644 res/img/element-icons/qrcode.svg create mode 100644 src/components/views/auth/LoginWithQR.tsx diff --git a/package.json b/package.json index 402e6e13da3..2441e7ea7bc 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "react-blurhash": "^0.1.3", "react-dom": "17.0.2", "react-focus-lock": "^2.5.1", + "react-qr-reader": "^3.0.0-beta-1", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-html": "^2.3.2", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index faaf2089484..de8050640d1 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -95,6 +95,7 @@ @import "./views/auth/_CountryDropdown.pcss"; @import "./views/auth/_InteractiveAuthEntryComponents.pcss"; @import "./views/auth/_LanguageSelector.pcss"; +@import "./views/auth/_LoginWithQR.pcss"; @import "./views/auth/_PassphraseField.pcss"; @import "./views/auth/_Welcome.pcss"; @import "./views/avatars/_BaseAvatar.pcss"; diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index 2638daf8769..7d673aa8750 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -101,3 +101,22 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { align-content: center; padding: 14px; } + +.mx_Login_withQR_or { + font-size: 14px; + text-align: center; + margin-top: -14px; + color: #737D8C; + margin-bottom: 10px; +} + +.mx_Login_withQR { + display: block !important; + margin-bottom: 8px; + svg { + height: 1em; + vertical-align: middle; + margin-right: 10px; + padding-bottom: 2px; + } +} diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss new file mode 100644 index 00000000000..ed5f818e92a --- /dev/null +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -0,0 +1,193 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_AuthPage .mx_LoginWithQR { + .mx_AccessibleButton { + display: block !important; + } + + .mx_AccessibleButton + .mx_AccessibleButton { + margin-top: 8px; + } + + .mx_LoginWithQR_separator { + display: flex; + align-items: center; + text-align: center; + + &::before, &::after { + content: ''; + flex: 1; + border-bottom: 1px solid #E3E8F0; + } + + &:not(:empty) { + &::before { + margin-right: 1em; + } + &::after { + margin-left: 1em; + } + } + } + + .mx_LoginWithQR_QRScanner { + margin: 20px auto; + } + + font-size: $font-15px; +} + +.mx_UserSettingsDialog .mx_LoginWithQR { + .mx_AccessibleButton + .mx_AccessibleButton { + margin-left: 12px; + } + + font-size: $font-14px; + + h1 { + font-size: $font-24px; + margin-bottom: 0; + } + + li { + line-height: 1.8; + } + + .mx_LoginWithQR_QRScanner { + margin: 46px 0; + } + + .mx_QRCode { + padding: 20px 0; + margin: 26px 0; + } + + .mx_LoginWithQR_buttons { + text-align: center; + } + + .mx_LoginWithQR_qrWrapper { + display: flex; + } +} + +.mx_LoginWithQR { + min-height: 350px; + display: flex; + flex-direction: column; + + h1 > svg { + &.normal { + color: #737D8C; + } + &.error { + color: #FF5B55; + } + &.success { + color: #0DBD8B; + } + height: 1.3em; + margin-right: 8px; + vertical-align: middle; + } + + .mx_LoginWithQR_confirmationDigits { + text-align: center; + margin: 50px auto; + font-weight: 600; + font-size: $font-24px; + color: #17191C; + } + + .mx_LoginWithQR_confirmationAlert { + border: 1px solid #C1C6CD; + border-radius: 8px; + padding: 8px; + line-height: 1.5em; + display: flex; + + svg { + height: 30px; + } + } + + .mx_LoginWithQR_separator { + margin: 1em 0; + } + + ol { + list-style-position: inside; + padding-inline-start: 0; + + li::marker { + color: #0DBD8B; + } + } + + .mx_LoginWithQR_BackButton { + height: 12px; + margin-bottom: 24px; + svg { + height: 100%; + } + } + + .mx_LoginWithQR_main { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .mx_QRCode { + border: 1px solid #E3E8F0; + border-radius: 8px; + display: flex; + justify-content: center; + } + + .mx_LoginWithQR_QRScanner { + width: 350px; + height: 350px; + background-color: #222; + border-radius: 8px; + border: 1px solid transparent; + + .mx_QRViewFinder { + top: 0; + left: 0; + z-index: 1; + box-sizing: border-box; + border: 50px solid rgba(0, 0, 0, 0.2); + position: absolute; + width: 100%; + height: 100%; + stroke-width: 2; + stroke: rgba(255, 255, 255, 1); + } + + video { + object-fit: cover; + border-radius: 8px; + border: 1px solid transparent; + } + } + + .mx_LoginWithQR_spinner { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } +} diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss index 3dad2a49a16..4d528aba6c9 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss @@ -26,6 +26,10 @@ limitations under the License. margin-right: 10px; } +.mx_SecurityUserSettingsTab_loginWithQr .mx_AccessibleButton { + margin-right: 10px; +} + .mx_SecurityUserSettingsTab { .mx_SettingsTab_section { .mx_AccessibleButton_kind_link { diff --git a/res/img/element-icons/back.svg b/res/img/element-icons/back.svg new file mode 100644 index 00000000000..62aef5df274 --- /dev/null +++ b/res/img/element-icons/back.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/res/img/element-icons/devices.svg b/res/img/element-icons/devices.svg new file mode 100644 index 00000000000..6c26cfe97ee --- /dev/null +++ b/res/img/element-icons/devices.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/element-icons/qrcode.svg b/res/img/element-icons/qrcode.svg new file mode 100644 index 00000000000..7787141ad53 --- /dev/null +++ b/res/img/element-icons/qrcode.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index b45461618e1..0a8408abfdc 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -183,6 +183,17 @@ export interface IConfigOptions { // length per voice chunk in seconds chunk_length?: number; }; + + login_with_qr?: { + login?: { + enable_showing?: boolean; + }; + reciprocate?: { + enable_showing?: boolean; + enable_scanning?: boolean; + }; + default_http_transport_server?: string; + }; } export interface ISsoRedirectOptions { diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 0d3400f4bba..c5b2d12cfcd 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -49,6 +49,14 @@ export const DEFAULTS: IConfigOptions = { voice_broadcast: { chunk_length: 60 * 1000, // one minute }, + login_with_qr: { // TODO remove these defaults before merging: + login: { + enable_showing: true, + }, + reciprocate: { + enable_showing: true, + }, + }, }; export default class SdkConfig { diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index c9fc7e001d9..7e289b1dc4e 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -38,6 +38,8 @@ import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; import AccessibleButton from '../../views/elements/AccessibleButton'; import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig'; +import LoginWithQR, { Mode } from '../../views/auth/LoginWithQR'; +import { Icon as QRIcon } from '../../../../res/img/element-icons/qrcode.svg'; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -98,6 +100,7 @@ interface IState { serverIsAlive: boolean; serverErrorIsFatal: boolean; serverDeadError?: ReactNode; + loginWithQrInProgress: boolean; } /* @@ -128,6 +131,7 @@ export default class LoginComponent extends React.PureComponent serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", + loginWithQrInProgress: false, }; // map from login step type to a function which will render a control @@ -140,6 +144,7 @@ export default class LoginComponent extends React.PureComponent 'm.login.cas': () => this.renderSsoStep("cas"), // eslint-disable-next-line @typescript-eslint/naming-convention 'm.login.sso': () => this.renderSsoStep("sso"), + 'loginWithQR': () => this.renderLoginWithQRStep(), }; } @@ -497,10 +502,16 @@ export default class LoginComponent extends React.PureComponent // this is the ideal order we want to show the flows in const order = [ "m.login.password", + "loginWithQR", "m.login.sso", ]; - const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean); + const qrSupported = SdkConfig.get().login_with_qr?.login?.enable_showing; + + const flows = order.map(type => + (type === 'loginWithQR' && qrSupported) + ? { type: 'loginWithQR' } : this.state.flows.find(flow => flow.type === type), + ).filter(Boolean); return { flows.map(flow => { const stepRenderer = this.stepRendererMap[flow.type]; @@ -543,6 +554,32 @@ export default class LoginComponent extends React.PureComponent ); }; + private startLoginWithQR = () => { + this.setState({ loginWithQrInProgress: true }); + }; + + private renderLoginWithQRStep = () => { + return ( + <> +

or

+ + + { _t("Sign in with QR code") } + + + ); + }; + + private onLoginWithQRFinished = (success: boolean) => { + if (!success) { + this.setState({ loginWithQrInProgress: false }); + } + }; + render() { const loader = this.isBusy() && !this.state.busyLoggingIn ?
: null; @@ -599,20 +636,26 @@ export default class LoginComponent extends React.PureComponent return ( - -

- { _t('Sign in') } - { loader } -

- { errorTextSection } - { serverDeadSection } - - { this.renderLoginComponentForFlows() } - { footer } -
+ { this.state.loginWithQrInProgress ? + + + + : + +

+ { _t('Sign in') } + { loader } +

+ { errorTextSection } + { serverDeadSection } + + { this.renderLoginComponentForFlows() } + { footer } +
+ }
); } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx new file mode 100644 index 00000000000..37c0737569b --- /dev/null +++ b/src/components/views/auth/LoginWithQR.tsx @@ -0,0 +1,534 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { buildChannelFromCode, Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; +import { SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; +import { ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; +import { QrReader, OnResultFunction } from 'react-qr-reader'; +import { logger } from 'matrix-js-sdk/src/logger'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; + +import { _t } from "../../../languageHandler"; +import AccessibleButton from '../elements/AccessibleButton'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import QRCode from '../elements/QRCode'; +import defaultDispatcher from '../../../dispatcher/dispatcher'; +import { Action } from '../../../dispatcher/actions'; +import SdkConfig from '../../../SdkConfig'; +import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig'; +import Spinner from '../elements/Spinner'; +import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg"; +import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; +import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; +import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; +import { setLoggedIn } from '../../../Lifecycle'; + +export enum Mode { + SCAN = "scan", + SHOW = "show", +} + +interface IProps { + device: 'new' | 'existing'; + serverConfig?: ValidatedServerConfig; + client?: MatrixClient; + mode: Mode; + onFinished(...args: any): void; +} + +interface IState { + scannedRendezvous?: Rendezvous; + generatedRendezvous?: Rendezvous; + scannedCode?: string; + confirmationDigits?: string; + cancelled?: RendezvousFailureReason; + mediaPermissionError?: boolean; + scanning: boolean; +} + +export default class LoginWithQR extends React.Component { + constructor(props) { + super(props); + + this.state = { + scanning: false, + }; + } + + componentDidMount(): void { + void this.updateMode(this.props.mode); + } + + componentDidUpdate(prevProps: Readonly): void { + if (prevProps.mode !== this.props.mode) { + void this.updateMode(this.props.mode); + } + } + + private async updateMode(mode: Mode) { + if (mode === Mode.SCAN) { + if (this.state.generatedRendezvous) { + this.state.generatedRendezvous.onFailure = undefined; + this.state.generatedRendezvous.channel.transport.onFailure = undefined; + await this.state.generatedRendezvous.userCancelled(); + this.setState({ generatedRendezvous: undefined }); + } + } else { + if (this.state.scannedRendezvous) { + this.state.scannedRendezvous.onFailure = undefined; + this.state.scannedRendezvous.channel.transport.onFailure = undefined; + await this.state.scannedRendezvous.userCancelled(); + this.setState({ scannedRendezvous: undefined }); + } + await this.generateCode(); + } + } + + private get rendezvous(): Rendezvous | undefined { + return this.state.generatedRendezvous ?? this.state.scannedRendezvous; + } + + public componentWillUnmount(): void { + if (this.rendezvous) { + this.rendezvous.onFailure = undefined; + this.rendezvous.channel.transport.onFailure = undefined; + // calling cancel will call close() as well to clean up the resources + void this.rendezvous.userCancelled(); + } + } + + private approveLogin = async (): Promise => { + if (!this.rendezvous) { + throw new Error('Rendezvous not found'); + } + const newDeviceId = await this.rendezvous.confirmLoginOnExistingDevice(); + if (!newDeviceId) { + // user denied + return; + } + const cli = MatrixClientPeg.get(); + if (!cli.crypto) { + // alert(`New device signed in: ${newDeviceId}. Not signing cross-signing as no crypto setup`); + this.props.onFinished(true); + return; + } + const didCrossSign = await this.rendezvous.crossSign(); + if (didCrossSign) { + // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); + } else { + // alert(`New device signed in, but no keys received for cross signing: ${newDeviceId}`); + } + this.props.onFinished(true); + }; + + private generateCode = async () => { + try { + const fallbackServer = SdkConfig.get().login_with_qr?.default_http_transport_server + ?? 'https://rendezvous.lab.element.dev'; // FIXME: remove this default value + + const transport = new SimpleHttpRendezvousTransport({ + onFailure: this.onFailure, + client: this.props.client, + hsUrl: this.props.serverConfig?.hsUrl, + fallbackRzServer: fallbackServer, + }); + + const channel = new ECDHv1RendezvousChannel(transport); + + const generatedRendezvous = new Rendezvous(channel, this.props.client); + + generatedRendezvous.onFailure = this.onFailure; + await generatedRendezvous.generateCode(); + logger.info(generatedRendezvous.code); + this.setState({ + generatedRendezvous, + cancelled: undefined, + }); + + const confirmationDigits = await generatedRendezvous.startAfterShowingCode(); + this.setState({ confirmationDigits }); + + if (this.isNewDevice) { + const creds = await generatedRendezvous.completeLoginOnNewDevice(); + if (creds) { + await setLoggedIn({ + accessToken: creds.accessToken, + userId: creds.userId, + deviceId: creds.deviceId, + homeserverUrl: creds.homeserverUrl, + }); + await generatedRendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); + this.props.onFinished(true); + defaultDispatcher.dispatch({ + action: Action.ViewHomePage, + }); + } + } + } catch (e) { + logger.error(e); + if (this.rendezvous) { + await this.rendezvous.cancel(RendezvousFailureReason.Unknown); + } + } + }; + + private onFailure = (reason: RendezvousFailureReason) => { + logger.info(`Rendezvous cancelled: ${reason}`); + this.setState({ cancelled: reason }); + }; + + reset() { + this.setState({ + scannedRendezvous: undefined, + generatedRendezvous: undefined, + confirmationDigits: undefined, + cancelled: undefined, + scannedCode: undefined, + scanning: false, + }); + } + + private get isExistingDevice(): boolean { + return this.props.device === 'existing'; + } + + private get isNewDevice(): boolean { + return this.props.device === 'new'; + } + + private processScannedCode = async (scannedCode: string) => { + // try { + // const parsed = JSON.parse(scannedCode); + // } catch (err) { + // this.setState({ cancelled: RendezvousFailureReason.InvalidCode }); + // return; + // } + try { + if (this.state.scannedCode === scannedCode) { + return; // suppress duplicate scans + } + if (this.rendezvous) { + await this.rendezvous.userCancelled(); + this.reset(); + } + + const { channel, intent: theirIntent } = await buildChannelFromCode( + scannedCode, + this.onFailure, + ); + + const scannedRendezvous = new Rendezvous(channel, this.props.client); + this.setState({ + scannedCode, + scannedRendezvous, + cancelled: undefined, + }); + + const confirmationDigits = await scannedRendezvous.startAfterScanningCode(theirIntent); + this.setState({ confirmationDigits }); + + if (this.isNewDevice) { + const creds = await scannedRendezvous.completeLoginOnNewDevice(); + if (creds) { + await setLoggedIn({ + accessToken: creds.accessToken, + userId: creds.userId, + deviceId: creds.deviceId, + homeserverUrl: creds.homeserverUrl, + }); + await scannedRendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); + this.props.onFinished(true); + defaultDispatcher.dispatch({ + action: Action.ViewHomePage, + }); + } + } + } catch (e) { + alert(e); + } + }; + + private cancelClicked = () => { + void (async () => { + await this.rendezvous?.userCancelled(); + this.reset(); + this.props.onFinished(false); + })(); + }; + + private declineClicked = () => { + void (async () => { + await this.rendezvous?.declineLoginOnExistingDevice(); + this.reset(); + this.props.onFinished(false); + })(); + }; + + private tryAgainClicked = () => { + this.reset(); + + if (this.props.mode === Mode.SHOW) { + void this.generateCode(); + } + }; + + private onQrResult: OnResultFunction = (result, error) => { + if (result) { + this.processScannedCode(result.getText()); + } + }; + + private viewFinder() { + return + + + + + ; + } + + private requestMediaPermissions = async () => { + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + this.setState({ mediaPermissionError: false }); + } catch (err) { + this.setState({ mediaPermissionError: true }); + } + }; + + private onBackClick = () => { + void this.state.generatedRendezvous?.userCancelled(); + void this.state.scannedRendezvous?.userCancelled(); + + this.props.onFinished(false); + }; + + private onDoScanQRClicked = () => { + this.setState({ scanning: true }); + void this.requestMediaPermissions(); + }; + + render() { + let title: string; + let titleIcon: JSX.Element | undefined; + let main: JSX.Element | undefined; + let buttons: JSX.Element | undefined; + let backButton = true; + + if (this.state.cancelled) { + let cancellationMessage: string; + switch (this.state.cancelled) { + case RendezvousFailureReason.Expired: + cancellationMessage = _t("The linking wasn't completed in the required time."); + break; + case RendezvousFailureReason.InvalidCode: + cancellationMessage = _t("The scanned code is invalid."); + break; + case RendezvousFailureReason.UnsupportedAlgorithm: + cancellationMessage = _t("Linking with this device is not supported."); + break; + case RendezvousFailureReason.UserDeclined: + cancellationMessage = _t("The request was declined on the other device."); + break; + case RendezvousFailureReason.OtherDeviceAlreadySignedIn: + cancellationMessage = _t("The other device is already signed in."); + break; + case RendezvousFailureReason.OtherDeviceNotSignedIn: + cancellationMessage = _t("The other device isn't signed in."); + break; + case RendezvousFailureReason.UserCancelled: + cancellationMessage = _t("The request was cancelled."); + break; + case RendezvousFailureReason.Unknown: + cancellationMessage = _t("An unexpected error occurred."); + break; + case RendezvousFailureReason.HomeserverLacksSupport: + cancellationMessage = _t("The homeserver doesn't support signing in another device."); + break; + default: + cancellationMessage = _t("The request was cancelled."); + break; + } + title = _t("Connection failed"); + titleIcon = ; + backButton = false; + main =

{ cancellationMessage }

; + buttons = <> + + { _t("Try again") } + + + { _t("Cancel") } + + ; + } else if (this.state.confirmationDigits) { + title = _t("Devices connected"); + titleIcon = ; + backButton = false; + if (this.isNewDevice) { + main = <> +

{ _t("Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:") }

+
+ { this.state.confirmationDigits } +
+ ; + buttons = <> +
+ { _t("No match?") } +
+ + { _t("Cancel") } + + ; + } else { + main = <> +

{ _t("Confirm that the code below matches with your mobile device:") }

+
+ { this.state.confirmationDigits } +
+
+
+ +
+
{ _t("Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.") }
+
+ ; + + buttons = <> + + { _t("Cancel") } + + + { _t("Confirm") } + + ; + } + } else if (this.props.mode === Mode.SHOW) { + title =_t("Sign in with QR code"); + if (this.state.generatedRendezvous) { + const code =
+ +
; + + if (this.isExistingDevice) { + main = <> +

{ _t("Scan the QR code below with your device that's signed out.") }

+
    +
  1. { _t("Start at the sign in screen") }
  2. +
  3. { _t("Select 'Scan QR code'") }
  4. +
+ { code } + ; + } else { + main = <> +

{ _t("Scan the QR code below with your device that's already signed in:") }

+
    +
  1. { _t("Open the app on your mobile device") }
  2. +
  3. { _t("Go to Settings -> Security & Privacy") }
  4. +
  5. { _t("Select 'Scan QR code'") }
  6. +
+ { code } + ; + } + } else { + main =
; + buttons = <> + + { _t("Cancel") } + + ; + } + } else if (this.props.mode === Mode.SCAN) { + title = _t("Sign in with QR code"); + if (!this.state.scanning) { + if (this.isExistingDevice) { + main = <> +

+ { _t("Use the camera on this device to scan the QR code shown on your other device:") } +

+
    +
  1. { _t("Start at the sign in screen") }
  2. +
  3. { _t("Select 'Show QR code'") }
  4. +
+ ; + } else { + main = <> +

+ { _t("Use the camera on this device to scan the QR code shown on your signed in device:") } +

+
    +
  1. { _t("Open Element on your other device") }
  2. +
  3. { _t("Go to Settings -> Security & Privacy") }
  4. +
  5. { _t("Select 'Link a device'") }
  6. +
  7. { _t("Select 'Show QR code on this device'") }
  8. +
+ ; + } + buttons = <> + + { _t("Scan QR code") } + + ; + } else { + main = <> +

{ _t("Line up the QR code in the square below:") }

+ + ; + } + } + + return ( +
+
+ { backButton ? + + : null } +

{ titleIcon }{ title }

+
+
+ { main } +
+
+ { buttons } +
+
+ ); + } +} diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index f32f7997fed..b9121409ed3 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -19,6 +19,7 @@ import classNames from 'classnames'; import { IMyDevice } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { CryptoEvent } from 'matrix-js-sdk/src/crypto'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; @@ -52,13 +53,22 @@ export default class DevicesPanel extends React.Component { } public componentDidMount(): void { + const cli = MatrixClientPeg.get(); + cli.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.loadDevices(); } public componentWillUnmount(): void { + const cli = MatrixClientPeg.get(); + cli.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.unmounted = true; } + private onDevicesUpdated = (users: string[]) => { + if (!users.includes(MatrixClientPeg.get().getUserId())) return; + this.loadDevices(); + }; + private loadDevices(): void { const cli = MatrixClientPeg.get(); cli.getDevices().then( diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index f4e4e55513d..550bc670271 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -38,6 +38,8 @@ import InlineSpinner from "../../../elements/InlineSpinner"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; +import SdkConfig from '../../../../../SdkConfig'; +import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; interface IIgnoredUserProps { userId: string; @@ -72,6 +74,7 @@ interface IState { waitingUnignored: string[]; managingInvites: boolean; invitedRoomIds: Set; + showLoginWithQR: Mode | null; } export default class SecurityUserSettingsTab extends React.Component { @@ -88,6 +91,7 @@ export default class SecurityUserSettingsTab extends React.Component { + this.setState({ showLoginWithQR: Mode.SHOW }); + }; + + private onScanQRClicked = (): void => { + this.setState({ showLoginWithQR: Mode.SCAN }); + }; + + private onLoginWithQRFinished = (): void => { + this.setState({ showLoginWithQR: null }); + }; + public render(): JSX.Element { const secureBackup = (
@@ -365,19 +381,64 @@ export default class SecurityUserSettingsTab extends React.Component ; + let loginWithQRSection: JSX.Element | undefined; + + if (SdkConfig.get().login_with_qr?.reciprocate?.enable_scanning || + SdkConfig.get().login_with_qr?.reciprocate?.enable_showing) { + const features = SdkConfig.get().login_with_qr?.reciprocate; + let description: string; + if (features.enable_scanning && features.enable_showing) { + description = _t("You can use this device to sign in a new device with a QR code. There are two ways " + + "to do this:"); + } else if (features.enable_scanning) { + description = _t("You can use this device to sign in a new device with a QR code. You will need to " + + "use this device to scan the QR code shown on your other device that's signed out."); + } else { + description = _t("You can use this device to sign in a new device with a QR code. You will need to " + + "scan the QR code shown on this device with your device that's signed out."); + } + + const scanQR = features.enable_scanning ? Scan QR Code : null; + + const showQR = features.enable_showing ? Show QR Code : null; + + loginWithQRSection = <> +
{ _t("Sign in with QR code") }
+
+

{ description }

+ { scanQR } + { showQR } +
+ ; + } + + const client = MatrixClientPeg.get(); + return (
- { warning } - { devicesSection } -
{ _t("Encryption") }
-
- { secureBackup } - { eventIndex } - { crossSigning } - -
- { privacySection } - { advancedSection } + { this.state.showLoginWithQR ? + : + <> + { warning } + { devicesSection } + { loginWithQRSection } +
{ _t("Encryption") }
+
+ { secureBackup } + { eventIndex } + { crossSigning } + +
+ { privacySection } + { advancedSection } + + }
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0913b46bc5e..5610b003005 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1582,6 +1582,10 @@ "Sessions": "Sessions", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", + "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", + "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", + "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", + "Sign in with QR code": "Sign in with QR code", "Other sessions": "Other sessions", "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", @@ -3170,6 +3174,34 @@ "Submit": "Submit", "Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.", "Start authentication": "Start authentication", + "The linking wasn't completed in the required time.": "The linking wasn't completed in the required time.", + "The scanned code is invalid.": "The scanned code is invalid.", + "Linking with this device is not supported.": "Linking with this device is not supported.", + "The request was declined on the other device.": "The request was declined on the other device.", + "The other device is already signed in.": "The other device is already signed in.", + "The other device isn't signed in.": "The other device isn't signed in.", + "The request was cancelled.": "The request was cancelled.", + "An unexpected error occurred.": "An unexpected error occurred.", + "The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.", + "Devices connected": "Devices connected", + "Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:": "Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:", + "No match?": "No match?", + "Confirm that the code below matches with your mobile device:": "Confirm that the code below matches with your mobile device:", + "Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.": "Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.", + "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", + "Start at the sign in screen": "Start at the sign in screen", + "Select 'Scan QR code'": "Select 'Scan QR code'", + "Scan the QR code below with your device that's already signed in:": "Scan the QR code below with your device that's already signed in:", + "Open the app on your mobile device": "Open the app on your mobile device", + "Go to Settings -> Security & Privacy": "Go to Settings -> Security & Privacy", + "Use the camera on this device to scan the QR code shown on your other device:": "Use the camera on this device to scan the QR code shown on your other device:", + "Select 'Show QR code'": "Select 'Show QR code'", + "Use the camera on this device to scan the QR code shown on your signed in device:": "Use the camera on this device to scan the QR code shown on your signed in device:", + "Open Element on your other device": "Open Element on your other device", + "Select 'Link a device'": "Select 'Link a device'", + "Select 'Show QR code on this device'": "Select 'Show QR code on this device'", + "Scan QR code": "Scan QR code", + "Line up the QR code in the square below:": "Line up the QR code in the square below:", "Enter password": "Enter password", "Nice, strong password!": "Nice, strong password!", "Password is allowed, but unsafe": "Password is allowed, but unsafe", diff --git a/yarn.lock b/yarn.lock index 9ca41fe8a3c..9aaa8a0d803 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2621,6 +2621,27 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" +"@zxing/browser@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7" + integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng== + optionalDependencies: + "@zxing/text-encoding" "^0.9.0" + +"@zxing/library@^0.18.3": + version "0.18.6" + resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c" + integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw== + dependencies: + ts-custom-error "^3.0.0" + optionalDependencies: + "@zxing/text-encoding" "~0.9.0" + +"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + abab@^2.0.3, abab@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -8114,6 +8135,15 @@ react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-qr-reader@^3.0.0-beta-1: + version "3.0.0-beta-1" + resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68" + integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw== + dependencies: + "@zxing/browser" "0.0.7" + "@zxing/library" "^0.18.3" + rollup "^2.67.2" + react-redux@^7.2.0: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -8446,6 +8476,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup@^2.67.2: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + rrweb-snapshot@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" @@ -9233,6 +9270,11 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-custom-error@^3.0.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.2.2.tgz#06a567ea92f5df0b61c25722bb4e3772f79c5e5b" + integrity sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" From ee6df8810f3803329e70937b02940a813814a729 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 22:40:11 +0100 Subject: [PATCH 02/73] Whitespace --- res/css/views/auth/_LoginWithQR.pcss | 356 +++++++++++++-------------- 1 file changed, 178 insertions(+), 178 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index ed5f818e92a..a9bc42ae2d0 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -12,182 +12,182 @@ limitations under the License. */ .mx_AuthPage .mx_LoginWithQR { - .mx_AccessibleButton { - display: block !important; - } - - .mx_AccessibleButton + .mx_AccessibleButton { - margin-top: 8px; - } - - .mx_LoginWithQR_separator { - display: flex; - align-items: center; - text-align: center; - - &::before, &::after { - content: ''; - flex: 1; - border-bottom: 1px solid #E3E8F0; - } - - &:not(:empty) { - &::before { - margin-right: 1em; - } - &::after { - margin-left: 1em; - } - } - } - - .mx_LoginWithQR_QRScanner { - margin: 20px auto; - } - - font-size: $font-15px; -} - -.mx_UserSettingsDialog .mx_LoginWithQR { - .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: 12px; - } - - font-size: $font-14px; - - h1 { - font-size: $font-24px; - margin-bottom: 0; - } - - li { - line-height: 1.8; - } - - .mx_LoginWithQR_QRScanner { - margin: 46px 0; - } - - .mx_QRCode { - padding: 20px 0; - margin: 26px 0; - } - - .mx_LoginWithQR_buttons { - text-align: center; - } - - .mx_LoginWithQR_qrWrapper { - display: flex; - } -} - -.mx_LoginWithQR { - min-height: 350px; - display: flex; - flex-direction: column; - - h1 > svg { - &.normal { - color: #737D8C; - } - &.error { - color: #FF5B55; - } - &.success { - color: #0DBD8B; - } - height: 1.3em; - margin-right: 8px; - vertical-align: middle; - } - - .mx_LoginWithQR_confirmationDigits { - text-align: center; - margin: 50px auto; - font-weight: 600; - font-size: $font-24px; - color: #17191C; - } - - .mx_LoginWithQR_confirmationAlert { - border: 1px solid #C1C6CD; - border-radius: 8px; - padding: 8px; - line-height: 1.5em; - display: flex; - - svg { - height: 30px; - } - } - - .mx_LoginWithQR_separator { - margin: 1em 0; - } - - ol { - list-style-position: inside; - padding-inline-start: 0; - - li::marker { - color: #0DBD8B; - } - } - - .mx_LoginWithQR_BackButton { - height: 12px; - margin-bottom: 24px; - svg { - height: 100%; - } - } - - .mx_LoginWithQR_main { - display: flex; - flex-direction: column; - flex-grow: 1; - } - - .mx_QRCode { - border: 1px solid #E3E8F0; - border-radius: 8px; - display: flex; - justify-content: center; - } - - .mx_LoginWithQR_QRScanner { - width: 350px; - height: 350px; - background-color: #222; - border-radius: 8px; - border: 1px solid transparent; - - .mx_QRViewFinder { - top: 0; - left: 0; - z-index: 1; - box-sizing: border-box; - border: 50px solid rgba(0, 0, 0, 0.2); - position: absolute; - width: 100%; - height: 100%; - stroke-width: 2; - stroke: rgba(255, 255, 255, 1); - } - - video { - object-fit: cover; - border-radius: 8px; - border: 1px solid transparent; - } - } - - .mx_LoginWithQR_spinner { - flex-grow: 1; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - } + .mx_AccessibleButton { + display: block !important; + } + + .mx_AccessibleButton + .mx_AccessibleButton { + margin-top: 8px; + } + + .mx_LoginWithQR_separator { + display: flex; + align-items: center; + text-align: center; + + &::before, &::after { + content: ''; + flex: 1; + border-bottom: 1px solid #E3E8F0; + } + + &:not(:empty) { + &::before { + margin-right: 1em; + } + &::after { + margin-left: 1em; + } + } + } + + .mx_LoginWithQR_QRScanner { + margin: 20px auto; + } + + font-size: $font-15px; + } + + .mx_UserSettingsDialog .mx_LoginWithQR { + .mx_AccessibleButton + .mx_AccessibleButton { + margin-left: 12px; + } + + font-size: $font-14px; + + h1 { + font-size: $font-24px; + margin-bottom: 0; + } + + li { + line-height: 1.8; + } + + .mx_LoginWithQR_QRScanner { + margin: 46px 0; + } + + .mx_QRCode { + padding: 20px 0; + margin: 26px 0; + } + + .mx_LoginWithQR_buttons { + text-align: center; + } + + .mx_LoginWithQR_qrWrapper { + display: flex; + } + } + + .mx_LoginWithQR { + min-height: 350px; + display: flex; + flex-direction: column; + + h1 > svg { + &.normal { + color: #737D8C; + } + &.error { + color: #FF5B55; + } + &.success { + color: #0DBD8B; + } + height: 1.3em; + margin-right: 8px; + vertical-align: middle; + } + + .mx_LoginWithQR_confirmationDigits { + text-align: center; + margin: 50px auto; + font-weight: 600; + font-size: $font-24px; + color: #17191C; + } + + .mx_LoginWithQR_confirmationAlert { + border: 1px solid #C1C6CD; + border-radius: 8px; + padding: 8px; + line-height: 1.5em; + display: flex; + + svg { + height: 30px; + } + } + + .mx_LoginWithQR_separator { + margin: 1em 0; + } + + ol { + list-style-position: inside; + padding-inline-start: 0; + + li::marker { + color: #0DBD8B; + } + } + + .mx_LoginWithQR_BackButton { + height: 12px; + margin-bottom: 24px; + svg { + height: 100%; + } + } + + .mx_LoginWithQR_main { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .mx_QRCode { + border: 1px solid #E3E8F0; + border-radius: 8px; + display: flex; + justify-content: center; + } + + .mx_LoginWithQR_QRScanner { + width: 350px; + height: 350px; + background-color: #222; + border-radius: 8px; + border: 1px solid transparent; + + .mx_QRViewFinder { + top: 0; + left: 0; + z-index: 1; + box-sizing: border-box; + border: 50px solid rgba(0, 0, 0, 0.2); + position: absolute; + width: 100%; + height: 100%; + stroke-width: 2; + stroke: rgba(255, 255, 255, 1); + } + + video { + object-fit: cover; + border-radius: 8px; + border: 1px solid transparent; + } + } + + .mx_LoginWithQR_spinner { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } } From b4222dc4ec35ad9fcb3be1c357e6b0528ab5a8d2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:12:58 +0100 Subject: [PATCH 03/73] Padding --- res/css/structures/auth/_Login.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index 7d673aa8750..4f9b2208727 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -112,7 +112,7 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { .mx_Login_withQR { display: block !important; - margin-bottom: 8px; + margin-bottom: 20px; svg { height: 1em; vertical-align: middle; From 0ae60e47f896d3c6988a99fee938dd9cc9ccda50 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:17:19 +0100 Subject: [PATCH 04/73] Refactor of fetch --- src/components/views/auth/LoginWithQR.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 37c0737569b..80b6e22c42d 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -142,6 +142,7 @@ export default class LoginWithQR extends React.Component { client: this.props.client, hsUrl: this.props.serverConfig?.hsUrl, fallbackRzServer: fallbackServer, + fetch: MatrixClientPeg.get().http.fetch, }); const channel = new ECDHv1RendezvousChannel(transport); @@ -226,6 +227,7 @@ export default class LoginWithQR extends React.Component { const { channel, intent: theirIntent } = await buildChannelFromCode( scannedCode, this.onFailure, + MatrixClientPeg.get().http.fetch, ); const scannedRendezvous = new Rendezvous(channel, this.props.client); From b5040dbc153f6df443ef1ae4f01c9b36a17a8577 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:19:05 +0100 Subject: [PATCH 05/73] Whitespace --- res/css/views/auth/_LoginWithQR.pcss | 246 +++++++++++++-------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index a9bc42ae2d0..b5840e74e41 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -62,132 +62,132 @@ limitations under the License. li { line-height: 1.8; - } - - .mx_LoginWithQR_QRScanner { - margin: 46px 0; - } - - .mx_QRCode { - padding: 20px 0; - margin: 26px 0; - } - - .mx_LoginWithQR_buttons { - text-align: center; - } - - .mx_LoginWithQR_qrWrapper { - display: flex; - } - } - - .mx_LoginWithQR { - min-height: 350px; - display: flex; - flex-direction: column; - - h1 > svg { - &.normal { - color: #737D8C; - } - &.error { - color: #FF5B55; - } - &.success { - color: #0DBD8B; - } - height: 1.3em; - margin-right: 8px; - vertical-align: middle; - } - - .mx_LoginWithQR_confirmationDigits { - text-align: center; - margin: 50px auto; - font-weight: 600; - font-size: $font-24px; - color: #17191C; - } - - .mx_LoginWithQR_confirmationAlert { - border: 1px solid #C1C6CD; - border-radius: 8px; - padding: 8px; - line-height: 1.5em; - display: flex; + } - svg { - height: 30px; - } - } + .mx_LoginWithQR_QRScanner { + margin: 46px 0; + } - .mx_LoginWithQR_separator { - margin: 1em 0; - } + .mx_QRCode { + padding: 20px 0; + margin: 26px 0; + } - ol { - list-style-position: inside; - padding-inline-start: 0; - - li::marker { - color: #0DBD8B; - } - } - - .mx_LoginWithQR_BackButton { - height: 12px; - margin-bottom: 24px; - svg { - height: 100%; - } - } - - .mx_LoginWithQR_main { - display: flex; - flex-direction: column; - flex-grow: 1; - } + .mx_LoginWithQR_buttons { + text-align: center; + } - .mx_QRCode { - border: 1px solid #E3E8F0; - border-radius: 8px; - display: flex; - justify-content: center; - } - - .mx_LoginWithQR_QRScanner { - width: 350px; - height: 350px; - background-color: #222; - border-radius: 8px; - border: 1px solid transparent; - - .mx_QRViewFinder { - top: 0; - left: 0; - z-index: 1; - box-sizing: border-box; - border: 50px solid rgba(0, 0, 0, 0.2); - position: absolute; - width: 100%; - height: 100%; - stroke-width: 2; - stroke: rgba(255, 255, 255, 1); - } - - video { - object-fit: cover; - border-radius: 8px; - border: 1px solid transparent; - } - } + .mx_LoginWithQR_qrWrapper { + display: flex; + } +} - .mx_LoginWithQR_spinner { - flex-grow: 1; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - } +.mx_LoginWithQR { + min-height: 350px; + display: flex; + flex-direction: column; + + h1 > svg { + &.normal { + color: #737D8C; + } + &.error { + color: #FF5B55; + } + &.success { + color: #0DBD8B; + } + height: 1.3em; + margin-right: 8px; + vertical-align: middle; + } + + .mx_LoginWithQR_confirmationDigits { + text-align: center; + margin: 50px auto; + font-weight: 600; + font-size: $font-24px; + color: #17191C; + } + + .mx_LoginWithQR_confirmationAlert { + border: 1px solid #C1C6CD; + border-radius: 8px; + padding: 8px; + line-height: 1.5em; + display: flex; + + svg { + height: 30px; + } + } + + .mx_LoginWithQR_separator { + margin: 1em 0; + } + + ol { + list-style-position: inside; + padding-inline-start: 0; + + li::marker { + color: #0DBD8B; + } + } + + .mx_LoginWithQR_BackButton { + height: 12px; + margin-bottom: 24px; + svg { + height: 100%; + } + } + + .mx_LoginWithQR_main { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .mx_QRCode { + border: 1px solid #E3E8F0; + border-radius: 8px; + display: flex; + justify-content: center; + } + + .mx_LoginWithQR_QRScanner { + width: 350px; + height: 350px; + background-color: #222; + border-radius: 8px; + border: 1px solid transparent; + + .mx_QRViewFinder { + top: 0; + left: 0; + z-index: 1; + box-sizing: border-box; + border: 50px solid rgba(0, 0, 0, 0.2); + position: absolute; + width: 100%; + height: 100%; + stroke-width: 2; + stroke: rgba(255, 255, 255, 1); + } + + video { + object-fit: cover; + border-radius: 8px; + border: 1px solid transparent; + } + } + + .mx_LoginWithQR_spinner { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } } From 2789ace03829b2754b8c5ce600c74438ca711749 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:26:33 +0100 Subject: [PATCH 06/73] CSS whitespace --- res/css/views/auth/_LoginWithQR.pcss | 246 +++++++++++++-------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index b5840e74e41..40b6b761716 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -46,9 +46,9 @@ limitations under the License. } font-size: $font-15px; - } +} - .mx_UserSettingsDialog .mx_LoginWithQR { +.mx_UserSettingsDialog .mx_LoginWithQR { .mx_AccessibleButton + .mx_AccessibleButton { margin-left: 12px; } @@ -62,132 +62,132 @@ limitations under the License. li { line-height: 1.8; - } + } - .mx_LoginWithQR_QRScanner { - margin: 46px 0; - } + .mx_LoginWithQR_QRScanner { + margin: 46px 0; + } - .mx_QRCode { - padding: 20px 0; - margin: 26px 0; - } + .mx_QRCode { + padding: 20px 0; + margin: 26px 0; + } - .mx_LoginWithQR_buttons { - text-align: center; - } + .mx_LoginWithQR_buttons { + text-align: center; + } - .mx_LoginWithQR_qrWrapper { - display: flex; - } + .mx_LoginWithQR_qrWrapper { + display: flex; + } } .mx_LoginWithQR { - min-height: 350px; - display: flex; - flex-direction: column; - - h1 > svg { - &.normal { - color: #737D8C; - } - &.error { - color: #FF5B55; - } - &.success { - color: #0DBD8B; - } - height: 1.3em; - margin-right: 8px; - vertical-align: middle; - } - - .mx_LoginWithQR_confirmationDigits { - text-align: center; - margin: 50px auto; - font-weight: 600; - font-size: $font-24px; - color: #17191C; - } - - .mx_LoginWithQR_confirmationAlert { - border: 1px solid #C1C6CD; - border-radius: 8px; - padding: 8px; - line-height: 1.5em; - display: flex; - - svg { - height: 30px; - } - } - - .mx_LoginWithQR_separator { - margin: 1em 0; - } - - ol { - list-style-position: inside; - padding-inline-start: 0; - - li::marker { - color: #0DBD8B; - } - } - - .mx_LoginWithQR_BackButton { - height: 12px; - margin-bottom: 24px; - svg { - height: 100%; - } - } - - .mx_LoginWithQR_main { - display: flex; - flex-direction: column; - flex-grow: 1; - } - - .mx_QRCode { - border: 1px solid #E3E8F0; - border-radius: 8px; - display: flex; - justify-content: center; - } - - .mx_LoginWithQR_QRScanner { - width: 350px; - height: 350px; - background-color: #222; - border-radius: 8px; - border: 1px solid transparent; - - .mx_QRViewFinder { - top: 0; - left: 0; - z-index: 1; - box-sizing: border-box; - border: 50px solid rgba(0, 0, 0, 0.2); - position: absolute; - width: 100%; - height: 100%; - stroke-width: 2; - stroke: rgba(255, 255, 255, 1); - } - - video { - object-fit: cover; - border-radius: 8px; - border: 1px solid transparent; - } - } - - .mx_LoginWithQR_spinner { - flex-grow: 1; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - } + min-height: 350px; + display: flex; + flex-direction: column; + + h1 > svg { + &.normal { + color: #737D8C; + } + &.error { + color: #FF5B55; + } + &.success { + color: #0DBD8B; + } + height: 1.3em; + margin-right: 8px; + vertical-align: middle; + } + + .mx_LoginWithQR_confirmationDigits { + text-align: center; + margin: 50px auto; + font-weight: 600; + font-size: $font-24px; + color: #17191C; + } + + .mx_LoginWithQR_confirmationAlert { + border: 1px solid #C1C6CD; + border-radius: 8px; + padding: 8px; + line-height: 1.5em; + display: flex; + + svg { + height: 30px; + } + } + + .mx_LoginWithQR_separator { + margin: 1em 0; + } + + ol { + list-style-position: inside; + padding-inline-start: 0; + + li::marker { + color: #0DBD8B; + } + } + + .mx_LoginWithQR_BackButton { + height: 12px; + margin-bottom: 24px; + svg { + height: 100%; + } + } + + .mx_LoginWithQR_main { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .mx_QRCode { + border: 1px solid #E3E8F0; + border-radius: 8px; + display: flex; + justify-content: center; + } + + .mx_LoginWithQR_QRScanner { + width: 350px; + height: 350px; + background-color: #222; + border-radius: 8px; + border: 1px solid transparent; + + .mx_QRViewFinder { + top: 0; + left: 0; + z-index: 1; + box-sizing: border-box; + border: 50px solid rgba(0, 0, 0, 0.2); + position: absolute; + width: 100%; + height: 100%; + stroke-width: 2; + stroke: rgba(255, 255, 255, 1); + } + + video { + object-fit: cover; + border-radius: 8px; + border: 1px solid transparent; + } + } + + .mx_LoginWithQR_spinner { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } } From 80fef387da667b2f544e42c01bfdfd775a6697a4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 12:54:19 +0100 Subject: [PATCH 07/73] Add link to MSC3906 --- src/components/views/auth/LoginWithQR.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 80b6e22c42d..5d870702a36 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -57,6 +57,13 @@ interface IState { scanning: boolean; } +/** + * A component that allows sign in and E2EE set up with a QR code. + * + * It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes. + * + * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 + */ export default class LoginWithQR extends React.Component { constructor(props) { super(props); From d97e8a304348841f26bfe83bfe1833b2ed3edbb8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:07:05 +0100 Subject: [PATCH 08/73] Handle incorrect typing in MatrixClientPeg.get() --- src/components/views/auth/LoginWithQR.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 5d870702a36..e2e38ea14a3 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -149,7 +149,7 @@ export default class LoginWithQR extends React.Component { client: this.props.client, hsUrl: this.props.serverConfig?.hsUrl, fallbackRzServer: fallbackServer, - fetch: MatrixClientPeg.get().http.fetch, + fetchFn: MatrixClientPeg.get()?.http.fetch, }); const channel = new ECDHv1RendezvousChannel(transport); @@ -234,7 +234,7 @@ export default class LoginWithQR extends React.Component { const { channel, intent: theirIntent } = await buildChannelFromCode( scannedCode, this.onFailure, - MatrixClientPeg.get().http.fetch, + MatrixClientPeg.get()?.http.fetch, ); const scannedRendezvous = new Rendezvous(channel, this.props.client); From 5709df63f3babf46641590ffdb0df1bd6dcbddf8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:16:29 +0100 Subject: [PATCH 09/73] Use unstable class name --- src/components/views/auth/LoginWithQR.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e2e38ea14a3..0ca8721e8b9 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -13,8 +13,8 @@ limitations under the License. import React from 'react'; import { buildChannelFromCode, Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; -import { SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; -import { ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; +import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; +import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; import { QrReader, OnResultFunction } from 'react-qr-reader'; import { logger } from 'matrix-js-sdk/src/logger'; import { MatrixClient } from 'matrix-js-sdk/src/client'; @@ -144,7 +144,7 @@ export default class LoginWithQR extends React.Component { const fallbackServer = SdkConfig.get().login_with_qr?.default_http_transport_server ?? 'https://rendezvous.lab.element.dev'; // FIXME: remove this default value - const transport = new SimpleHttpRendezvousTransport({ + const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, client: this.props.client, hsUrl: this.props.serverConfig?.hsUrl, @@ -152,7 +152,7 @@ export default class LoginWithQR extends React.Component { fetchFn: MatrixClientPeg.get()?.http.fetch, }); - const channel = new ECDHv1RendezvousChannel(transport); + const channel = new MSC3903ECDHv1RendezvousChannel(transport); const generatedRendezvous = new Rendezvous(channel, this.props.client); From 93b2b6e10f43daf53e0fdbbaffbdc3f9bcafb725 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:40:45 +0100 Subject: [PATCH 10/73] fix: use unstable class name --- src/components/views/auth/LoginWithQR.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 0ca8721e8b9..85f427c3cb8 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -12,7 +12,7 @@ limitations under the License. */ import React from 'react'; -import { buildChannelFromCode, Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; +import { buildChannelFromCode, MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; import { QrReader, OnResultFunction } from 'react-qr-reader'; @@ -48,8 +48,8 @@ interface IProps { } interface IState { - scannedRendezvous?: Rendezvous; - generatedRendezvous?: Rendezvous; + scannedRendezvous?: MSC3906Rendezvous; + generatedRendezvous?: MSC3906Rendezvous; scannedCode?: string; confirmationDigits?: string; cancelled?: RendezvousFailureReason; @@ -102,7 +102,7 @@ export default class LoginWithQR extends React.Component { } } - private get rendezvous(): Rendezvous | undefined { + private get rendezvous(): MSC3906Rendezvous | undefined { return this.state.generatedRendezvous ?? this.state.scannedRendezvous; } @@ -154,7 +154,7 @@ export default class LoginWithQR extends React.Component { const channel = new MSC3903ECDHv1RendezvousChannel(transport); - const generatedRendezvous = new Rendezvous(channel, this.props.client); + const generatedRendezvous = new MSC3906Rendezvous(channel, this.props.client); generatedRendezvous.onFailure = this.onFailure; await generatedRendezvous.generateCode(); @@ -237,7 +237,7 @@ export default class LoginWithQR extends React.Component { MatrixClientPeg.get()?.http.fetch, ); - const scannedRendezvous = new Rendezvous(channel, this.props.client); + const scannedRendezvous = new MSC3906Rendezvous(channel, this.props.client); this.setState({ scannedCode, scannedRendezvous, From ac389552027082fd0a74be6ad087622fcfd35fd5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 16:55:14 +0100 Subject: [PATCH 11/73] Use default fetch client instead --- src/components/views/auth/LoginWithQR.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 85f427c3cb8..19a3569abab 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -149,7 +149,6 @@ export default class LoginWithQR extends React.Component { client: this.props.client, hsUrl: this.props.serverConfig?.hsUrl, fallbackRzServer: fallbackServer, - fetchFn: MatrixClientPeg.get()?.http.fetch, }); const channel = new MSC3903ECDHv1RendezvousChannel(transport); @@ -234,7 +233,6 @@ export default class LoginWithQR extends React.Component { const { channel, intent: theirIntent } = await buildChannelFromCode( scannedCode, this.onFailure, - MatrixClientPeg.get()?.http.fetch, ); const scannedRendezvous = new MSC3906Rendezvous(channel, this.props.client); From 8f647e220b20e5404782881fa37d5f08b90d77f3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 23:04:34 +0100 Subject: [PATCH 12/73] Update to revised function name --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 19a3569abab..6b28affbb61 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -130,7 +130,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(true); return; } - const didCrossSign = await this.rendezvous.crossSign(); + const didCrossSign = await this.rendezvous.verifyNewDeviceOnExistingDevice(); if (didCrossSign) { // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); } else { From 996a9cf9d3b5b75db47717cfe8425874d352f488 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 04:51:54 +0100 Subject: [PATCH 13/73] Refactor device manager panel and make it work with new sessions manager --- .../settings/devices/SignInWithQRSection.tsx | 96 ++++++++++++++++ .../views/settings/devices/useOwnDevices.ts | 7 ++ .../tabs/user/SecurityUserSettingsTab.tsx | 40 +------ .../settings/tabs/user/SessionManagerTab.tsx | 103 ++++++++++-------- 4 files changed, 161 insertions(+), 85 deletions(-) create mode 100644 src/components/views/settings/devices/SignInWithQRSection.tsx diff --git a/src/components/views/settings/devices/SignInWithQRSection.tsx b/src/components/views/settings/devices/SignInWithQRSection.tsx new file mode 100644 index 00000000000..a02b3d1bf92 --- /dev/null +++ b/src/components/views/settings/devices/SignInWithQRSection.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../../languageHandler'; +import { MatrixClientPeg } from '../../../../MatrixClientPeg'; +import SdkConfig from '../../../../SdkConfig'; +import AccessibleButton from '../../elements/AccessibleButton'; +import Spinner from '../../elements/Spinner'; +import SettingsSubsection from '../shared/SettingsSubsection'; + +interface IProps { + onShowQr: () => void; + onScanQr: () => void; +} + +interface IState { + supported: boolean | null; +} + +export default class SignInWithQRSection extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + supported: null, + }; + } + + public componentDidMount(): void { + MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3882").then((supported) => { + this.setState({ supported }); + }); + } + + public render(): JSX.Element { + if (!SdkConfig.get().login_with_qr?.reciprocate?.enable_scanning && + !SdkConfig.get().login_with_qr?.reciprocate?.enable_showing) { + return null; + } + + const features = SdkConfig.get().login_with_qr?.reciprocate; + let description: string; + if (features.enable_scanning && features.enable_showing) { + description = _t("You can use this device to sign in a new device with a QR code. There are two ways " + + "to do this:"); + } else if (features.enable_scanning) { + description = _t("You can use this device to sign in a new device with a QR code. You will need to " + + "use this device to scan the QR code shown on your other device that's signed out."); + } else { + description = _t("You can use this device to sign in a new device with a QR code. You will need to " + + "scan the QR code shown on this device with your device that's signed out."); + } + + const scanQR = features.enable_scanning ? { _t("Scan QR code") } : null; + + const showQR = features.enable_showing ? { _t("Show QR code") } : null; + + return + { this.state.supported === null && } + { this.state.supported === true && + <> +

{ description }

+ { scanQR } + { showQR } + + } + { this.state.supported === false && +

{ _t("This homeserver doesn't support signing in with QR codes.") }

+ } +
; + } +} diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index c3b8cb0212a..5655b68dc78 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -38,6 +38,7 @@ import { getDeviceClientInformation } from "../../../../utils/device/clientInfor import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types"; import { useEventEmitter } from "../../../../hooks/useEventEmitter"; import { parseUserAgent } from "../../../../utils/device/parseUserAgent"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; const isDeviceVerified = ( matrixClient: MatrixClient, @@ -179,6 +180,12 @@ export const useOwnDevices = (): DevicesState => { refreshDevices(); }, [refreshDevices]); + useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => { + if (users.includes(userId)) { + refreshDevices(); + } + }); + useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => { const type = event.getType(); if (type.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 550bc670271..6a7217f25d0 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -40,6 +40,7 @@ import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/Ana import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; import SdkConfig from '../../../../../SdkConfig'; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; +import SignInWithQRSection from '../../devices/SignInWithQRSection'; interface IIgnoredUserProps { userId: string; @@ -379,45 +380,9 @@ export default class SecurityUserSettingsTab extends React.Component
+ ; - let loginWithQRSection: JSX.Element | undefined; - - if (SdkConfig.get().login_with_qr?.reciprocate?.enable_scanning || - SdkConfig.get().login_with_qr?.reciprocate?.enable_showing) { - const features = SdkConfig.get().login_with_qr?.reciprocate; - let description: string; - if (features.enable_scanning && features.enable_showing) { - description = _t("You can use this device to sign in a new device with a QR code. There are two ways " + - "to do this:"); - } else if (features.enable_scanning) { - description = _t("You can use this device to sign in a new device with a QR code. You will need to " + - "use this device to scan the QR code shown on your other device that's signed out."); - } else { - description = _t("You can use this device to sign in a new device with a QR code. You will need to " + - "scan the QR code shown on this device with your device that's signed out."); - } - - const scanQR = features.enable_scanning ? Scan QR Code : null; - - const showQR = features.enable_showing ? Show QR Code : null; - - loginWithQRSection = <> -
{ _t("Sign in with QR code") }
-
-

{ description }

- { scanQR } - { showQR } -
- ; - } - const client = MatrixClientPeg.get(); return ( @@ -427,7 +392,6 @@ export default class SecurityUserSettingsTab extends React.Component { warning } { devicesSection } - { loginWithQRSection }
{ _t("Encryption") }
{ secureBackup } diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2c94d5a5c2f..6ba95b00108 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -32,6 +32,8 @@ import SecurityRecommendations from '../../devices/SecurityRecommendations'; import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types'; import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices'; import SettingsTab from '../SettingsTab'; +import SignInWithQRSection from '../../devices/SignInWithQRSection'; +import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; const useSignOut = ( matrixClient: MatrixClient, @@ -171,53 +173,60 @@ const SessionManagerTab: React.FC = () => { setSelectedDeviceIds([]); }, [filter, setSelectedDeviceIds]); - return - - saveDeviceName(currentDeviceId, deviceName)} - onVerifyCurrentDevice={onVerifyCurrentDevice} - onSignOutCurrentDevice={onSignOutCurrentDevice} - /> - { - shouldShowOtherSessions && - - - - } - ; + const [signInWithQrMode, setSignInWithQrMode] = useState(); + + return signInWithQrMode ? + setSignInWithQrMode(null)} device="existing" client={matrixClient} /> + : + + + saveDeviceName(currentDeviceId, deviceName)} + onVerifyCurrentDevice={onVerifyCurrentDevice} + onSignOutCurrentDevice={onSignOutCurrentDevice} + /> + { + shouldShowOtherSessions && + + + + } + setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)}/> + + ; }; export default SessionManagerTab; From 9e505f7c2563b5946a2ceaf55f7f692002bfc97a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 06:41:18 +0100 Subject: [PATCH 14/73] Lint fix --- src/components/views/settings/devices/useOwnDevices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 5655b68dc78..f56ed85c87b 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -31,6 +31,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { MatrixError } from "matrix-js-sdk/src/http-api"; import { logger } from "matrix-js-sdk/src/logger"; import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { _t } from "../../../../languageHandler"; @@ -38,7 +39,6 @@ import { getDeviceClientInformation } from "../../../../utils/device/clientInfor import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types"; import { useEventEmitter } from "../../../../hooks/useEventEmitter"; import { parseUserAgent } from "../../../../utils/device/parseUserAgent"; -import { CryptoEvent } from "matrix-js-sdk/src/crypto"; const isDeviceVerified = ( matrixClient: MatrixClient, From 42a2ec110755d4f9b34b5b64163df2534a8c3259 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 06:41:50 +0100 Subject: [PATCH 15/73] Add missing interstitials and update wording --- res/css/views/auth/_LoginWithQR.pcss | 12 +- .../tabs/user/_SecurityUserSettingsTab.pcss | 4 - src/components/structures/auth/Login.tsx | 2 +- src/components/views/auth/LoginWithQR.tsx | 449 +++++++++--------- ...thQRSection.tsx => LoginWithQRSection.tsx} | 27 +- .../tabs/user/SecurityUserSettingsTab.tsx | 5 +- .../settings/tabs/user/SessionManagerTab.tsx | 4 +- src/i18n/strings/en_EN.json | 23 +- 8 files changed, 277 insertions(+), 249 deletions(-) rename src/components/views/settings/devices/{SignInWithQRSection.tsx => LoginWithQRSection.tsx} (82%) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 40b6b761716..09eccb0e6ce 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -11,6 +11,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_LoginWithQRSection .mx_AccessibleButton { + margin-right: 10px; +} + .mx_AuthPage .mx_LoginWithQR { .mx_AccessibleButton { display: block !important; @@ -69,7 +73,7 @@ limitations under the License. } .mx_QRCode { - padding: 20px 0; + padding: 0 40px; margin: 26px 0; } @@ -87,6 +91,12 @@ limitations under the License. display: flex; flex-direction: column; + .mx_LoginWithQR_centreTitle { + h1 { + text-align: centre; + } + } + h1 > svg { &.normal { color: #737D8C; diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss index 4d528aba6c9..3dad2a49a16 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss @@ -26,10 +26,6 @@ limitations under the License. margin-right: 10px; } -.mx_SecurityUserSettingsTab_loginWithQr .mx_AccessibleButton { - margin-right: 10px; -} - .mx_SecurityUserSettingsTab { .mx_SettingsTab_section { .mx_AccessibleButton_kind_link { diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 7e289b1dc4e..d63d7f487f3 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -638,7 +638,7 @@ export default class LoginComponent extends React.PureComponent { this.state.loginWithQrInProgress ? - + : diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 6b28affbb61..efafd7f076f 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -39,8 +39,19 @@ export enum Mode { SHOW = "show", } +enum Phase { + LOADING, + SCAN_INSTRUCTIONS, + SCANNING_QR, + SHOWING_QR, + CONNECTING, + CONNECTED, + WAITING_FOR_DEVICE, + VERIFYING, + ERROR, +} + interface IProps { - device: 'new' | 'existing'; serverConfig?: ValidatedServerConfig; client?: MatrixClient; mode: Mode; @@ -48,13 +59,12 @@ interface IProps { } interface IState { - scannedRendezvous?: MSC3906Rendezvous; - generatedRendezvous?: MSC3906Rendezvous; - scannedCode?: string; + phase: Phase; + rendezvous?: MSC3906Rendezvous; + lastScannedCode?: string; confirmationDigits?: string; - cancelled?: RendezvousFailureReason; + failureReason?: RendezvousFailureReason; mediaPermissionError?: boolean; - scanning: boolean; } /** @@ -69,7 +79,7 @@ export default class LoginWithQR extends React.Component { super(props); this.state = { - scanning: false, + phase: Phase.LOADING, }; } @@ -84,42 +94,37 @@ export default class LoginWithQR extends React.Component { } private async updateMode(mode: Mode) { - if (mode === Mode.SCAN) { - if (this.state.generatedRendezvous) { - this.state.generatedRendezvous.onFailure = undefined; - this.state.generatedRendezvous.channel.transport.onFailure = undefined; - await this.state.generatedRendezvous.userCancelled(); - this.setState({ generatedRendezvous: undefined }); - } - } else { - if (this.state.scannedRendezvous) { - this.state.scannedRendezvous.onFailure = undefined; - this.state.scannedRendezvous.channel.transport.onFailure = undefined; - await this.state.scannedRendezvous.userCancelled(); - this.setState({ scannedRendezvous: undefined }); - } + this.setState({ phase: Phase.LOADING }); + if (this.state.rendezvous) { + this.state.rendezvous.onFailure = undefined; + this.state.rendezvous.channel.transport.onFailure = undefined; + await this.state.rendezvous.userCancelled(); + this.setState({ rendezvous: undefined }); + } + if (mode === Mode.SHOW) { await this.generateCode(); + } else { + this.setState({ phase: Phase.SCAN_INSTRUCTIONS }); } } - private get rendezvous(): MSC3906Rendezvous | undefined { - return this.state.generatedRendezvous ?? this.state.scannedRendezvous; - } - public componentWillUnmount(): void { - if (this.rendezvous) { - this.rendezvous.onFailure = undefined; - this.rendezvous.channel.transport.onFailure = undefined; + if (this.state.rendezvous) { + // eslint-disable-next-line react/no-direct-mutation-state + this.state.rendezvous.onFailure = undefined; + // eslint-disable-next-line react/no-direct-mutation-state + this.state.rendezvous.channel.transport.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - void this.rendezvous.userCancelled(); + void this.state.rendezvous.userCancelled(); } } private approveLogin = async (): Promise => { - if (!this.rendezvous) { + if (!this.state.rendezvous) { throw new Error('Rendezvous not found'); } - const newDeviceId = await this.rendezvous.confirmLoginOnExistingDevice(); + this.setState({ phase: Phase.WAITING_FOR_DEVICE }); + const newDeviceId = await this.state.rendezvous.confirmLoginOnExistingDevice(); if (!newDeviceId) { // user denied return; @@ -130,7 +135,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(true); return; } - const didCrossSign = await this.rendezvous.verifyNewDeviceOnExistingDevice(); + const didCrossSign = await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); if (didCrossSign) { // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); } else { @@ -141,8 +146,7 @@ export default class LoginWithQR extends React.Component { private generateCode = async () => { try { - const fallbackServer = SdkConfig.get().login_with_qr?.default_http_transport_server - ?? 'https://rendezvous.lab.element.dev'; // FIXME: remove this default value + const fallbackServer = SdkConfig.get().login_with_qr?.default_http_transport_server; const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, @@ -153,21 +157,22 @@ export default class LoginWithQR extends React.Component { const channel = new MSC3903ECDHv1RendezvousChannel(transport); - const generatedRendezvous = new MSC3906Rendezvous(channel, this.props.client); + const rendezvous = new MSC3906Rendezvous(channel, this.props.client); - generatedRendezvous.onFailure = this.onFailure; - await generatedRendezvous.generateCode(); - logger.info(generatedRendezvous.code); + rendezvous.onFailure = this.onFailure; + await rendezvous.generateCode(); + logger.info(rendezvous.code); this.setState({ - generatedRendezvous, - cancelled: undefined, + phase: Phase.SHOWING_QR, + rendezvous, + failureReason: undefined, }); - const confirmationDigits = await generatedRendezvous.startAfterShowingCode(); - this.setState({ confirmationDigits }); + const confirmationDigits = await rendezvous.startAfterShowingCode(); + this.setState({ phase: Phase.CONNECTED, confirmationDigits }); if (this.isNewDevice) { - const creds = await generatedRendezvous.completeLoginOnNewDevice(); + const creds = await rendezvous.completeLoginOnNewDevice(); if (creds) { await setLoggedIn({ accessToken: creds.accessToken, @@ -175,7 +180,7 @@ export default class LoginWithQR extends React.Component { deviceId: creds.deviceId, homeserverUrl: creds.homeserverUrl, }); - await generatedRendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); + await rendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); this.props.onFinished(true); defaultDispatcher.dispatch({ action: Action.ViewHomePage, @@ -184,49 +189,41 @@ export default class LoginWithQR extends React.Component { } } catch (e) { logger.error(e); - if (this.rendezvous) { - await this.rendezvous.cancel(RendezvousFailureReason.Unknown); + if (this.state.rendezvous) { + await this.state.rendezvous.cancel(RendezvousFailureReason.Unknown); } } }; private onFailure = (reason: RendezvousFailureReason) => { - logger.info(`Rendezvous cancelled: ${reason}`); - this.setState({ cancelled: reason }); + logger.info(`Rendezvous failed: ${reason}`); + this.setState({ phase: Phase.ERROR, failureReason: reason }); }; reset() { this.setState({ - scannedRendezvous: undefined, - generatedRendezvous: undefined, + rendezvous: undefined, confirmationDigits: undefined, - cancelled: undefined, - scannedCode: undefined, - scanning: false, + failureReason: undefined, + lastScannedCode: undefined, }); } private get isExistingDevice(): boolean { - return this.props.device === 'existing'; + return !!this.props.client; } private get isNewDevice(): boolean { - return this.props.device === 'new'; + return !this.props.client; } private processScannedCode = async (scannedCode: string) => { - // try { - // const parsed = JSON.parse(scannedCode); - // } catch (err) { - // this.setState({ cancelled: RendezvousFailureReason.InvalidCode }); - // return; - // } try { - if (this.state.scannedCode === scannedCode) { + if (this.state.lastScannedCode === scannedCode) { return; // suppress duplicate scans } - if (this.rendezvous) { - await this.rendezvous.userCancelled(); + if (this.state.rendezvous) { + await this.state.rendezvous.userCancelled(); this.reset(); } @@ -235,18 +232,19 @@ export default class LoginWithQR extends React.Component { this.onFailure, ); - const scannedRendezvous = new MSC3906Rendezvous(channel, this.props.client); + const rendezvous = new MSC3906Rendezvous(channel, this.props.client); this.setState({ - scannedCode, - scannedRendezvous, - cancelled: undefined, + phase: Phase.CONNECTING, + lastScannedCode: scannedCode, + rendezvous, + failureReason: undefined, }); - const confirmationDigits = await scannedRendezvous.startAfterScanningCode(theirIntent); - this.setState({ confirmationDigits }); + const confirmationDigits = await rendezvous.startAfterScanningCode(theirIntent); + this.setState({ phase: Phase.CONNECTED, confirmationDigits }); if (this.isNewDevice) { - const creds = await scannedRendezvous.completeLoginOnNewDevice(); + const creds = await rendezvous.completeLoginOnNewDevice(); if (creds) { await setLoggedIn({ accessToken: creds.accessToken, @@ -254,7 +252,7 @@ export default class LoginWithQR extends React.Component { deviceId: creds.deviceId, homeserverUrl: creds.homeserverUrl, }); - await scannedRendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); + await rendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); this.props.onFinished(true); defaultDispatcher.dispatch({ action: Action.ViewHomePage, @@ -268,7 +266,7 @@ export default class LoginWithQR extends React.Component { private cancelClicked = () => { void (async () => { - await this.rendezvous?.userCancelled(); + await this.state.rendezvous?.userCancelled(); this.reset(); this.props.onFinished(false); })(); @@ -276,7 +274,7 @@ export default class LoginWithQR extends React.Component { private declineClicked = () => { void (async () => { - await this.rendezvous?.declineLoginOnExistingDevice(); + await this.state.rendezvous?.declineLoginOnExistingDevice(); this.reset(); this.props.onFinished(false); })(); @@ -285,9 +283,7 @@ export default class LoginWithQR extends React.Component { private tryAgainClicked = () => { this.reset(); - if (this.props.mode === Mode.SHOW) { - void this.generateCode(); - } + void this.updateMode(this.props.mode); }; private onQrResult: OnResultFunction = (result, error) => { @@ -315,168 +311,171 @@ export default class LoginWithQR extends React.Component { }; private onBackClick = () => { - void this.state.generatedRendezvous?.userCancelled(); - void this.state.scannedRendezvous?.userCancelled(); + void this.state.rendezvous?.userCancelled(); this.props.onFinished(false); }; private onDoScanQRClicked = () => { - this.setState({ scanning: true }); + this.setState({ phase: Phase.SCANNING_QR }); void this.requestMediaPermissions(); }; + private cancelButton = () => + { _t("Cancel") } + ; + + private simpleSpinner = (description?: string): JSX.Element => { + return
+
+ + { description &&

{ description }

} +
+
; + }; + render() { let title: string; let titleIcon: JSX.Element | undefined; let main: JSX.Element | undefined; let buttons: JSX.Element | undefined; let backButton = true; - - if (this.state.cancelled) { - let cancellationMessage: string; - switch (this.state.cancelled) { - case RendezvousFailureReason.Expired: - cancellationMessage = _t("The linking wasn't completed in the required time."); - break; - case RendezvousFailureReason.InvalidCode: - cancellationMessage = _t("The scanned code is invalid."); - break; - case RendezvousFailureReason.UnsupportedAlgorithm: - cancellationMessage = _t("Linking with this device is not supported."); - break; - case RendezvousFailureReason.UserDeclined: - cancellationMessage = _t("The request was declined on the other device."); - break; - case RendezvousFailureReason.OtherDeviceAlreadySignedIn: - cancellationMessage = _t("The other device is already signed in."); - break; - case RendezvousFailureReason.OtherDeviceNotSignedIn: - cancellationMessage = _t("The other device isn't signed in."); - break; - case RendezvousFailureReason.UserCancelled: - cancellationMessage = _t("The request was cancelled."); - break; - case RendezvousFailureReason.Unknown: - cancellationMessage = _t("An unexpected error occurred."); - break; - case RendezvousFailureReason.HomeserverLacksSupport: - cancellationMessage = _t("The homeserver doesn't support signing in another device."); - break; - default: - cancellationMessage = _t("The request was cancelled."); - break; - } - title = _t("Connection failed"); - titleIcon = ; - backButton = false; - main =

{ cancellationMessage }

; - buttons = <> - - { _t("Try again") } - - - { _t("Cancel") } - - ; - } else if (this.state.confirmationDigits) { - title = _t("Devices connected"); - titleIcon = ; - backButton = false; - if (this.isNewDevice) { - main = <> -

{ _t("Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:") }

-
- { this.state.confirmationDigits } -
- ; - buttons = <> -
- { _t("No match?") } -
- - { _t("Cancel") } - - ; - } else { - main = <> -

{ _t("Confirm that the code below matches with your mobile device:") }

-
- { this.state.confirmationDigits } -
-
-
- -
-
{ _t("Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.") }
-
- ; - + let cancellationMessage: string | undefined; + let centreTitle = false; + + switch (this.state.phase) { + case Phase.ERROR: + switch (this.state.failureReason) { + case RendezvousFailureReason.Expired: + cancellationMessage = _t("The linking wasn't completed in the required time."); + break; + case RendezvousFailureReason.InvalidCode: + cancellationMessage = _t("The scanned code is invalid."); + break; + case RendezvousFailureReason.UnsupportedAlgorithm: + cancellationMessage = _t("Linking with this device is not supported."); + break; + case RendezvousFailureReason.UserDeclined: + cancellationMessage = _t("The request was declined on the other device."); + break; + case RendezvousFailureReason.OtherDeviceAlreadySignedIn: + cancellationMessage = _t("The other device is already signed in."); + break; + case RendezvousFailureReason.OtherDeviceNotSignedIn: + cancellationMessage = _t("The other device isn't signed in."); + break; + case RendezvousFailureReason.UserCancelled: + cancellationMessage = _t("The request was cancelled."); + break; + case RendezvousFailureReason.Unknown: + cancellationMessage = _t("An unexpected error occurred."); + break; + case RendezvousFailureReason.HomeserverLacksSupport: + cancellationMessage = _t("The homeserver doesn't support signing in another device."); + break; + default: + cancellationMessage = _t("The request was cancelled."); + break; + } + title = _t("Connection failed"); + centreTitle = true; + titleIcon = ; + backButton = false; + main =

{ cancellationMessage }

; buttons = <> - - { _t("Cancel") } - - { _t("Confirm") } + { _t("Try again") } + { this.cancelButton()} ; - } - } else if (this.props.mode === Mode.SHOW) { - title =_t("Sign in with QR code"); - if (this.state.generatedRendezvous) { - const code =
- -
; - - if (this.isExistingDevice) { + break; + case Phase.CONNECTED: + title = _t("Devices connected"); + titleIcon = ; + backButton = false; + if (this.isNewDevice) { main = <> -

{ _t("Scan the QR code below with your device that's signed out.") }

-
    -
  1. { _t("Start at the sign in screen") }
  2. -
  3. { _t("Select 'Scan QR code'") }
  4. -
- { code } +

{ _t("Check that the same code is shown on your other device before proceeding:") }

+
+ { this.state.confirmationDigits } +
+ ; + buttons = <> +
+ { _t("No match?") } +
+ { this.cancelButton() } ; } else { main = <> -

{ _t("Scan the QR code below with your device that's already signed in:") }

-
    -
  1. { _t("Open the app on your mobile device") }
  2. -
  3. { _t("Go to Settings -> Security & Privacy") }
  4. -
  5. { _t("Select 'Scan QR code'") }
  6. -
- { code } +

{ _t("Check that the code below matches with your other device:") }

+
+ { this.state.confirmationDigits } +
+
+
+ +
+
{ _t("By approving access for this device, it will have full access to your account.") }
+
+ ; + + buttons = <> + + { _t("Cancel") } + + + { _t("Approve") } + ; } - } else { - main =
; - buttons = <> - - { _t("Cancel") } - - ; - } - } else if (this.props.mode === Mode.SCAN) { - title = _t("Sign in with QR code"); - if (!this.state.scanning) { + break; + case Phase.SHOWING_QR: + title =_t("Sign in with QR code"); + if (this.state.rendezvous) { + const code =
+ +
; + + if (this.isExistingDevice) { + main = <> +

{ _t("Scan the QR code below with your device that's signed out.") }

+
    +
  1. { _t("Start at the sign in screen") }
  2. +
  3. { _t("Select 'Scan QR code'") }
  4. +
+ { code } + ; + } else { + main = <> +

{ _t("Scan the QR code below with your device that's already signed in:") }

+
    +
  1. { _t("Open the app on your other device") }
  2. +
  3. { _t("Go to Settings -> Security & Privacy") }
  4. +
  5. { _t("Select 'Scan QR code'") }
  6. +
+ { code } + ; + } + } else { + main = this.simpleSpinner(); + buttons = this.cancelButton(); + } + break; + case Phase.SCAN_INSTRUCTIONS: + title = _t("Sign in with QR code"); if (this.isExistingDevice) { main = <>

@@ -508,7 +507,9 @@ export default class LoginWithQR extends React.Component { { _t("Scan QR code") } ; - } else { + break; + case Phase.SCANNING_QR: + title =_t("Sign in with QR code"); main = <>

{ _t("Line up the QR code in the square below:") }

{ ViewFinder={this.viewFinder} /> ; - } + break; + case Phase.LOADING: + main = this.simpleSpinner(); + break; + case Phase.CONNECTING: + main = this.simpleSpinner(_t("Connecting...")); + buttons = this.cancelButton(); + break; + case Phase.WAITING_FOR_DEVICE: + main = this.simpleSpinner(_t("Waiting for device to sign in")); + buttons = this.cancelButton(); + break; + case Phase.VERIFYING: + title = _t("Success"); + centreTitle = true; + main = this.simpleSpinner(_t("Completing set up of your new device")); + break; } return (
-
+
{ backButton ? : null } diff --git a/src/components/views/settings/devices/SignInWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx similarity index 82% rename from src/components/views/settings/devices/SignInWithQRSection.tsx rename to src/components/views/settings/devices/LoginWithQRSection.tsx index a02b3d1bf92..482eeb29690 100644 --- a/src/components/views/settings/devices/SignInWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -32,7 +32,7 @@ interface IState { supported: boolean | null; } -export default class SignInWithQRSection extends React.Component { +export default class LoginWithQRSection extends React.Component { constructor(props: IProps) { super(props); @@ -78,19 +78,20 @@ export default class SignInWithQRSection extends React.Component return - { this.state.supported === null && } - { this.state.supported === true && - <> -

{ description }

- { scanQR } - { showQR } - - } - { this.state.supported === false && -

{ _t("This homeserver doesn't support signing in with QR codes.") }

- } +
+ { this.state.supported === null && } + { this.state.supported === true && + <> +

{ description }

+ { scanQR } + { showQR } + + } + { this.state.supported === false && +

{ _t("This homeserver doesn't support signing in with QR codes.") }

+ } +
; } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 6a7217f25d0..61bf95365cc 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -38,9 +38,8 @@ import InlineSpinner from "../../../elements/InlineSpinner"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; -import SdkConfig from '../../../../../SdkConfig'; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; -import SignInWithQRSection from '../../devices/SignInWithQRSection'; +import LoginWithQRSection from '../../devices/LoginWithQRSection'; interface IIgnoredUserProps { userId: string; @@ -380,7 +379,7 @@ export default class SecurityUserSettingsTab extends React.Component
- + ; const client = MatrixClientPeg.get(); diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 6ba95b00108..863813593d8 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -32,7 +32,7 @@ import SecurityRecommendations from '../../devices/SecurityRecommendations'; import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types'; import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices'; import SettingsTab from '../SettingsTab'; -import SignInWithQRSection from '../../devices/SignInWithQRSection'; +import LoginWithQRSection from '../../devices/LoginWithQRSection'; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; const useSignOut = ( @@ -224,7 +224,7 @@ const SessionManagerTab: React.FC = () => { /> } - setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)}/> + setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)}/> ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5610b003005..bc6819c47fd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1582,10 +1582,6 @@ "Sessions": "Sessions", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", - "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", - "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", - "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", - "Sign in with QR code": "Sign in with QR code", "Other sessions": "Other sessions", "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", @@ -1785,6 +1781,13 @@ "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "View all": "View all", + "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", + "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", + "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", + "Scan QR code": "Scan QR code", + "Show QR code": "Show QR code", + "Sign in with QR code": "Sign in with QR code", + "This homeserver doesn't support signing in with QR codes.": "This homeserver doesn't support signing in with QR codes.", "Failed to set pusher state": "Failed to set pusher state", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", @@ -3184,15 +3187,15 @@ "An unexpected error occurred.": "An unexpected error occurred.", "The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.", "Devices connected": "Devices connected", - "Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:": "Check your mobile device, the code below should be displayed. Confirm that the code below matches with that device:", + "Check that the same code is shown on your other device before proceeding:": "Check that the same code is shown on your other device before proceeding:", "No match?": "No match?", - "Confirm that the code below matches with your mobile device:": "Confirm that the code below matches with your mobile device:", - "Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.": "Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.", + "Check that the code below matches with your other device:": "Check that the code below matches with your other device:", + "By approving access for this device, it will have full access to your account.": "By approving access for this device, it will have full access to your account.", "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", "Start at the sign in screen": "Start at the sign in screen", "Select 'Scan QR code'": "Select 'Scan QR code'", "Scan the QR code below with your device that's already signed in:": "Scan the QR code below with your device that's already signed in:", - "Open the app on your mobile device": "Open the app on your mobile device", + "Open the app on your other device": "Open the app on your other device", "Go to Settings -> Security & Privacy": "Go to Settings -> Security & Privacy", "Use the camera on this device to scan the QR code shown on your other device:": "Use the camera on this device to scan the QR code shown on your other device:", "Select 'Show QR code'": "Select 'Show QR code'", @@ -3200,8 +3203,10 @@ "Open Element on your other device": "Open Element on your other device", "Select 'Link a device'": "Select 'Link a device'", "Select 'Show QR code on this device'": "Select 'Show QR code on this device'", - "Scan QR code": "Scan QR code", "Line up the QR code in the square below:": "Line up the QR code in the square below:", + "Connecting...": "Connecting...", + "Waiting for device to sign in": "Waiting for device to sign in", + "Completing set up of your new device": "Completing set up of your new device", "Enter password": "Enter password", "Nice, strong password!": "Nice, strong password!", "Password is allowed, but unsafe": "Password is allowed, but unsafe", From 2c3df977f77a22f4ada97fd12f5911632b212130 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 06:44:23 +0100 Subject: [PATCH 16/73] Linting --- src/components/views/auth/LoginWithQR.tsx | 4 ++-- .../views/settings/tabs/user/SecurityUserSettingsTab.tsx | 2 +- src/components/views/settings/tabs/user/SessionManagerTab.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index efafd7f076f..03f61cf086b 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -323,7 +323,7 @@ export default class LoginWithQR extends React.Component { private cancelButton = () => { _t("Cancel") } ; @@ -392,7 +392,7 @@ export default class LoginWithQR extends React.Component { > { _t("Try again") } - { this.cancelButton()} + { this.cancelButton() } ; break; case Phase.CONNECTED: diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 61bf95365cc..54fc0a48b48 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -387,7 +387,7 @@ export default class SecurityUserSettingsTab extends React.Component { this.state.showLoginWithQR ? - : + : <> { warning } { devicesSection } diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 863813593d8..9889e3bfb2e 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -176,7 +176,7 @@ const SessionManagerTab: React.FC = () => { const [signInWithQrMode, setSignInWithQrMode] = useState(); return signInWithQrMode ? - setSignInWithQrMode(null)} device="existing" client={matrixClient} /> + setSignInWithQrMode(null)} client={matrixClient} /> : Date: Fri, 14 Oct 2022 06:50:33 +0100 Subject: [PATCH 17/73] i18n --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5870553b9d5..62e97b63043 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1781,9 +1781,6 @@ "Filter devices": "Filter devices", "Show": "Show", "%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sessions selected", - "Security recommendations": "Security recommendations", - "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", - "View all": "View all", "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", @@ -1791,6 +1788,9 @@ "Show QR code": "Show QR code", "Sign in with QR code": "Sign in with QR code", "This homeserver doesn't support signing in with QR codes.": "This homeserver doesn't support signing in with QR codes.", + "Security recommendations": "Security recommendations", + "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", + "View all": "View all", "Failed to set pusher state": "Failed to set pusher state", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", From 073f52c053c0f16e551fd4d07623e3126079d0ef Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 06:53:38 +0100 Subject: [PATCH 18/73] Lint --- .../views/settings/tabs/user/SecurityUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 54fc0a48b48..edf9a249b13 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -379,7 +379,7 @@ export default class SecurityUserSettingsTab extends React.Component
- + ; const client = MatrixClientPeg.get(); From fd1aeb3cfcb1610f8fa8504d980679fbcf9df3d2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 11:54:06 +0100 Subject: [PATCH 19/73] Use sensible sdk config name for fallback server --- src/IConfigOptions.ts | 2 +- src/SdkConfig.ts | 8 -------- src/components/views/auth/LoginWithQR.tsx | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 0a8408abfdc..5f9711d6edd 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -192,7 +192,7 @@ export interface IConfigOptions { enable_showing?: boolean; enable_scanning?: boolean; }; - default_http_transport_server?: string; + fallback_http_transport_server?: string; }; } diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index c5b2d12cfcd..0d3400f4bba 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -49,14 +49,6 @@ export const DEFAULTS: IConfigOptions = { voice_broadcast: { chunk_length: 60 * 1000, // one minute }, - login_with_qr: { // TODO remove these defaults before merging: - login: { - enable_showing: true, - }, - reciprocate: { - enable_showing: true, - }, - }, }; export default class SdkConfig { diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 03f61cf086b..9ee28ffc648 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -146,7 +146,7 @@ export default class LoginWithQR extends React.Component { private generateCode = async () => { try { - const fallbackServer = SdkConfig.get().login_with_qr?.default_http_transport_server; + const fallbackServer = SdkConfig.get().login_with_qr?.fallback_http_transport_server; const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, From 53d14a6c982b2e267849c8b51f6fe24b47c9b764 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 13:43:45 +0100 Subject: [PATCH 20/73] Improve error handling for QR code generation --- src/components/views/auth/LoginWithQR.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 9ee28ffc648..014aa8a6ec0 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -145,6 +145,7 @@ export default class LoginWithQR extends React.Component { }; private generateCode = async () => { + let rendezvous: MSC3906Rendezvous; try { const fallbackServer = SdkConfig.get().login_with_qr?.fallback_http_transport_server; @@ -157,17 +158,22 @@ export default class LoginWithQR extends React.Component { const channel = new MSC3903ECDHv1RendezvousChannel(transport); - const rendezvous = new MSC3906Rendezvous(channel, this.props.client); + rendezvous = new MSC3906Rendezvous(channel, this.props.client); rendezvous.onFailure = this.onFailure; await rendezvous.generateCode(); - logger.info(rendezvous.code); this.setState({ phase: Phase.SHOWING_QR, rendezvous, failureReason: undefined, }); + } catch (e) { + logger.error('Error whilst generating QR code', e); + this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.HomeserverLacksSupport }); + return; + } + try { const confirmationDigits = await rendezvous.startAfterShowingCode(); this.setState({ phase: Phase.CONNECTED, confirmationDigits }); @@ -188,10 +194,11 @@ export default class LoginWithQR extends React.Component { } } } catch (e) { - logger.error(e); + logger.error('Error whilst doing QR login', e); if (this.state.rendezvous) { await this.state.rendezvous.cancel(RendezvousFailureReason.Unknown); } + this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.Unknown }); } }; From 87adf05cd9819a53adb3169d0e5d33501e9c9edc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 15:50:49 +0100 Subject: [PATCH 21/73] Refactor feature availability logic --- .../settings/devices/LoginWithQRSection.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 482eeb29690..17fe5fdded0 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -48,17 +48,20 @@ export default class LoginWithQRSection extends React.Component } public render(): JSX.Element { - if (!SdkConfig.get().login_with_qr?.reciprocate?.enable_scanning && - !SdkConfig.get().login_with_qr?.reciprocate?.enable_showing) { + const features = SdkConfig.get().login_with_qr?.reciprocate; + const offerScanQr = features.enable_scanning; + const offerShowQr = features.enable_showing; + + if (!offerScanQr && !offerShowQr) { return null; } - const features = SdkConfig.get().login_with_qr?.reciprocate; let description: string; - if (features.enable_scanning && features.enable_showing) { + + if (offerScanQr && offerShowQr) { description = _t("You can use this device to sign in a new device with a QR code. There are two ways " + "to do this:"); - } else if (features.enable_scanning) { + } else if (offerScanQr) { description = _t("You can use this device to sign in a new device with a QR code. You will need to " + "use this device to scan the QR code shown on your other device that's signed out."); } else { @@ -66,16 +69,6 @@ export default class LoginWithQRSection extends React.Component "scan the QR code shown on this device with your device that's signed out."); } - const scanQR = features.enable_scanning ? { _t("Scan QR code") } : null; - - const showQR = features.enable_showing ? { _t("Show QR code") } : null; - return @@ -84,8 +77,14 @@ export default class LoginWithQRSection extends React.Component { this.state.supported === true && <>

{ description }

- { scanQR } - { showQR } + { offerScanQr && { _t("Scan QR code") } } + { offerShowQr && { _t("Show QR code") } } } { this.state.supported === false && From d31026fbfb2772a1a1b98b40a6b327e281e9dcc3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 16:01:34 +0100 Subject: [PATCH 22/73] Hide device manager panel if no options available --- .../settings/devices/LoginWithQRSection.tsx | 50 ++++++++++--------- src/i18n/strings/en_EN.json | 3 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 17fe5fdded0..0d15e4526e2 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -20,7 +20,6 @@ import { _t } from '../../../../languageHandler'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import SdkConfig from '../../../../SdkConfig'; import AccessibleButton from '../../elements/AccessibleButton'; -import Spinner from '../../elements/Spinner'; import SettingsSubsection from '../shared/SettingsSubsection'; interface IProps { @@ -29,7 +28,8 @@ interface IProps { } interface IState { - supported: boolean | null; + msc3882Supported: boolean | null; + msc3886Supported: boolean | null; } export default class LoginWithQRSection extends React.Component { @@ -37,21 +37,31 @@ export default class LoginWithQRSection extends React.Component super(props); this.state = { - supported: null, + msc3882Supported: null, + msc3886Supported: null, }; } public componentDidMount(): void { - MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3882").then((supported) => { - this.setState({ supported }); + MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3882"). then((msc3882Supported) => { + this.setState({ msc3882Supported }); + }); + MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3886").then((msc3886Supported) => { + this.setState({ msc3886Supported }); }); } public render(): JSX.Element { const features = SdkConfig.get().login_with_qr?.reciprocate; - const offerScanQr = features.enable_scanning; - const offerShowQr = features.enable_showing; + // Needs to be enabled as a feature + server support MSC3882: + const offerScanQr = features.enable_scanning && this.state.msc3882Supported; + + // Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured: + const offerShowQr = features.enable_showing && this.state.msc3882Supported && + (this.state.msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); + + // don't show anything if no method is available if (!offerScanQr && !offerShowQr) { return null; } @@ -73,23 +83,15 @@ export default class LoginWithQRSection extends React.Component heading={_t('Sign in with QR code')} >
- { this.state.supported === null && } - { this.state.supported === true && - <> -

{ description }

- { offerScanQr && { _t("Scan QR code") } } - { offerShowQr && { _t("Show QR code") } } - - } - { this.state.supported === false && -

{ _t("This homeserver doesn't support signing in with QR codes.") }

- } +

{ description }

+ { offerScanQr && { _t("Scan QR code") } } + { offerShowQr && { _t("Show QR code") } }
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 62e97b63043..44ad5b7d3a8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1784,10 +1784,9 @@ "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", + "Sign in with QR code": "Sign in with QR code", "Scan QR code": "Scan QR code", "Show QR code": "Show QR code", - "Sign in with QR code": "Sign in with QR code", - "This homeserver doesn't support signing in with QR codes.": "This homeserver doesn't support signing in with QR codes.", "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "View all": "View all", From f0400f7e6785e9a7291ee0e617c61c3bd140d0b5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 17:03:20 +0100 Subject: [PATCH 23/73] Put sign in with QR behind lab setting --- src/components/structures/auth/Login.tsx | 24 +++++++++++++------ .../tabs/user/SecurityUserSettingsTab.tsx | 12 ++++++---- .../settings/tabs/user/SessionManagerTab.tsx | 11 +++++++-- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 7 ++++++ 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index d63d7f487f3..4cdcb6a3897 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -499,19 +499,25 @@ export default class LoginComponent extends React.PureComponent renderLoginComponentForFlows() { if (!this.state.flows) return null; + const qrFeatureEnabled = SettingsStore.getValue("feature_signin_with_qr_code"); // this is the ideal order we want to show the flows in - const order = [ + const order = qrFeatureEnabled ? [ "m.login.password", "loginWithQR", "m.login.sso", + ] : [ + "m.login.password", + "m.login.sso", ]; - const qrSupported = SdkConfig.get().login_with_qr?.login?.enable_showing; + const qrSupported = qrFeatureEnabled && SdkConfig.get().login_with_qr?.login?.enable_showing; - const flows = order.map(type => - (type === 'loginWithQR' && qrSupported) - ? { type: 'loginWithQR' } : this.state.flows.find(flow => flow.type === type), - ).filter(Boolean); + const flows = order.map(type => { + if (qrSupported && type === 'loginWithQR') { + return { type: 'loginWithQR' }; + } + return this.state.flows.find(flow => flow.type === type); + }).filter(Boolean); return { flows.map(flow => { const stepRenderer = this.stepRendererMap[flow.type]; @@ -559,6 +565,10 @@ export default class LoginComponent extends React.PureComponent }; private renderLoginWithQRStep = () => { + if (!SettingsStore.getValue("feature_signin_with_qr_code")) { + return null; + } + return ( <>

or

@@ -636,7 +646,7 @@ export default class LoginComponent extends React.PureComponent return ( - { this.state.loginWithQrInProgress ? + { SettingsStore.getValue("feature_signin_with_qr_code") && this.state.loginWithQrInProgress ? diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index edf9a249b13..2092e8bf941 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -363,6 +363,7 @@ export default class SecurityUserSettingsTab extends React.Component @@ -379,16 +380,19 @@ export default class SecurityUserSettingsTab extends React.Component
- + { signinWithQrEnabled ? + + : null + } ; const client = MatrixClientPeg.get(); return (
- { this.state.showLoginWithQR ? - : - <> + { signinWithQrEnabled && this.state.showLoginWithQR ? + + : <> { warning } { devicesSection }
{ _t("Encryption") }
diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 72985e78537..4761e379c91 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -34,6 +34,7 @@ import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices'; import SettingsTab from '../SettingsTab'; import LoginWithQRSection from '../../devices/LoginWithQRSection'; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; +import SettingsStore from '../../../../../settings/SettingsStore'; const useSignOut = ( matrixClient: MatrixClient, @@ -179,7 +180,9 @@ const SessionManagerTab: React.FC = () => { const [signInWithQrMode, setSignInWithQrMode] = useState(); - return signInWithQrMode ? + const signinWithQrEnabled = SettingsStore.getValue("feature_signin_with_qr_code"); + + return signinWithQrEnabled && signInWithQrMode ? setSignInWithQrMode(null)} client={matrixClient} /> : @@ -231,7 +234,11 @@ const SessionManagerTab: React.FC = () => { /> } - setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)} /> + { signinWithQrEnabled ? + setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)} /> + : null + } + ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 44ad5b7d3a8..9c586ee46d0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -929,6 +929,7 @@ "New session manager": "New session manager", "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", + "Enable sign in with QR code from session manager (requires compatible homeserver)": "Enable sign in with QR code from session manager (requires compatible homeserver)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 60fd06ef85c..b1ff08fad59 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -494,6 +494,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { , }, }, + "feature_signin_with_qr_code": { + isFeature: true, + labsGroup: LabGroup.Experimental, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Enable sign in with QR code from session manager (requires compatible homeserver)"), + default: false, + }, "baseFontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From eb88f78b2156d26e3e8c85ce2b944be61d77172c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 18:43:24 +0100 Subject: [PATCH 24/73] Reduce scope of PR to just showing code on existing device --- package.json | 1 - res/css/structures/auth/_Login.pcss | 19 --- src/IConfigOptions.ts | 4 - src/components/structures/auth/Login.tsx | 83 ++------- src/components/views/auth/LoginWithQR.tsx | 161 +----------------- .../settings/devices/LoginWithQRSection.tsx | 34 +--- .../tabs/user/SecurityUserSettingsTab.tsx | 6 +- .../settings/tabs/user/SessionManagerTab.tsx | 2 +- yarn.lock | 42 ----- 9 files changed, 31 insertions(+), 321 deletions(-) diff --git a/package.json b/package.json index 5988b4963ac..164fb406745 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,6 @@ "react-blurhash": "^0.1.3", "react-dom": "17.0.2", "react-focus-lock": "^2.5.1", - "react-qr-reader": "^3.0.0-beta-1", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-html": "^2.3.2", diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index 4f9b2208727..2638daf8769 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -101,22 +101,3 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { align-content: center; padding: 14px; } - -.mx_Login_withQR_or { - font-size: 14px; - text-align: center; - margin-top: -14px; - color: #737D8C; - margin-bottom: 10px; -} - -.mx_Login_withQR { - display: block !important; - margin-bottom: 20px; - svg { - height: 1em; - vertical-align: middle; - margin-right: 10px; - padding-bottom: 2px; - } -} diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 5f9711d6edd..05969045da4 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -185,12 +185,8 @@ export interface IConfigOptions { }; login_with_qr?: { - login?: { - enable_showing?: boolean; - }; reciprocate?: { enable_showing?: boolean; - enable_scanning?: boolean; }; fallback_http_transport_server?: string; }; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 4cdcb6a3897..b16886696b4 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -38,8 +38,6 @@ import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; import AccessibleButton from '../../views/elements/AccessibleButton'; import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig'; -import LoginWithQR, { Mode } from '../../views/auth/LoginWithQR'; -import { Icon as QRIcon } from '../../../../res/img/element-icons/qrcode.svg'; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -144,7 +142,6 @@ export default class LoginComponent extends React.PureComponent 'm.login.cas': () => this.renderSsoStep("cas"), // eslint-disable-next-line @typescript-eslint/naming-convention 'm.login.sso': () => this.renderSsoStep("sso"), - 'loginWithQR': () => this.renderLoginWithQRStep(), }; } @@ -499,25 +496,13 @@ export default class LoginComponent extends React.PureComponent renderLoginComponentForFlows() { if (!this.state.flows) return null; - const qrFeatureEnabled = SettingsStore.getValue("feature_signin_with_qr_code"); // this is the ideal order we want to show the flows in - const order = qrFeatureEnabled ? [ - "m.login.password", - "loginWithQR", - "m.login.sso", - ] : [ + const order = [ "m.login.password", "m.login.sso", ]; - const qrSupported = qrFeatureEnabled && SdkConfig.get().login_with_qr?.login?.enable_showing; - - const flows = order.map(type => { - if (qrSupported && type === 'loginWithQR') { - return { type: 'loginWithQR' }; - } - return this.state.flows.find(flow => flow.type === type); - }).filter(Boolean); + const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean); return { flows.map(flow => { const stepRenderer = this.stepRendererMap[flow.type]; @@ -560,36 +545,6 @@ export default class LoginComponent extends React.PureComponent ); }; - private startLoginWithQR = () => { - this.setState({ loginWithQrInProgress: true }); - }; - - private renderLoginWithQRStep = () => { - if (!SettingsStore.getValue("feature_signin_with_qr_code")) { - return null; - } - - return ( - <> -

or

- - - { _t("Sign in with QR code") } - - - ); - }; - - private onLoginWithQRFinished = (success: boolean) => { - if (!success) { - this.setState({ loginWithQrInProgress: false }); - } - }; - render() { const loader = this.isBusy() && !this.state.busyLoggingIn ?
: null; @@ -646,26 +601,20 @@ export default class LoginComponent extends React.PureComponent return ( - { SettingsStore.getValue("feature_signin_with_qr_code") && this.state.loginWithQrInProgress ? - - - - : - -

- { _t('Sign in') } - { loader } -

- { errorTextSection } - { serverDeadSection } - - { this.renderLoginComponentForFlows() } - { footer } -
- } + +

+ { _t('Sign in') } + { loader } +

+ { errorTextSection } + { serverDeadSection } + + { this.renderLoginComponentForFlows() } + { footer } +
); } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 014aa8a6ec0..92982b8f693 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -12,10 +12,9 @@ limitations under the License. */ import React from 'react'; -import { buildChannelFromCode, MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; +import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; -import { QrReader, OnResultFunction } from 'react-qr-reader'; import { logger } from 'matrix-js-sdk/src/logger'; import { MatrixClient } from 'matrix-js-sdk/src/client'; @@ -23,8 +22,6 @@ import { _t } from "../../../languageHandler"; import AccessibleButton from '../elements/AccessibleButton'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import QRCode from '../elements/QRCode'; -import defaultDispatcher from '../../../dispatcher/dispatcher'; -import { Action } from '../../../dispatcher/actions'; import SdkConfig from '../../../SdkConfig'; import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig'; import Spinner from '../elements/Spinner'; @@ -32,17 +29,13 @@ import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.s import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { setLoggedIn } from '../../../Lifecycle'; export enum Mode { - SCAN = "scan", SHOW = "show", } enum Phase { LOADING, - SCAN_INSTRUCTIONS, - SCANNING_QR, SHOWING_QR, CONNECTING, CONNECTED, @@ -61,7 +54,6 @@ interface IProps { interface IState { phase: Phase; rendezvous?: MSC3906Rendezvous; - lastScannedCode?: string; confirmationDigits?: string; failureReason?: RendezvousFailureReason; mediaPermissionError?: boolean; @@ -98,13 +90,11 @@ export default class LoginWithQR extends React.Component { if (this.state.rendezvous) { this.state.rendezvous.onFailure = undefined; this.state.rendezvous.channel.transport.onFailure = undefined; - await this.state.rendezvous.userCancelled(); + await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); this.setState({ rendezvous: undefined }); } if (mode === Mode.SHOW) { await this.generateCode(); - } else { - this.setState({ phase: Phase.SCAN_INSTRUCTIONS }); } } @@ -115,7 +105,7 @@ export default class LoginWithQR extends React.Component { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.channel.transport.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - void this.state.rendezvous.userCancelled(); + void this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); } } @@ -152,7 +142,6 @@ export default class LoginWithQR extends React.Component { const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, client: this.props.client, - hsUrl: this.props.serverConfig?.hsUrl, fallbackRzServer: fallbackServer, }); @@ -176,23 +165,6 @@ export default class LoginWithQR extends React.Component { try { const confirmationDigits = await rendezvous.startAfterShowingCode(); this.setState({ phase: Phase.CONNECTED, confirmationDigits }); - - if (this.isNewDevice) { - const creds = await rendezvous.completeLoginOnNewDevice(); - if (creds) { - await setLoggedIn({ - accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, - homeserverUrl: creds.homeserverUrl, - }); - await rendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); - this.props.onFinished(true); - defaultDispatcher.dispatch({ - action: Action.ViewHomePage, - }); - } - } } catch (e) { logger.error('Error whilst doing QR login', e); if (this.state.rendezvous) { @@ -212,7 +184,6 @@ export default class LoginWithQR extends React.Component { rendezvous: undefined, confirmationDigits: undefined, failureReason: undefined, - lastScannedCode: undefined, }); } @@ -224,56 +195,9 @@ export default class LoginWithQR extends React.Component { return !this.props.client; } - private processScannedCode = async (scannedCode: string) => { - try { - if (this.state.lastScannedCode === scannedCode) { - return; // suppress duplicate scans - } - if (this.state.rendezvous) { - await this.state.rendezvous.userCancelled(); - this.reset(); - } - - const { channel, intent: theirIntent } = await buildChannelFromCode( - scannedCode, - this.onFailure, - ); - - const rendezvous = new MSC3906Rendezvous(channel, this.props.client); - this.setState({ - phase: Phase.CONNECTING, - lastScannedCode: scannedCode, - rendezvous, - failureReason: undefined, - }); - - const confirmationDigits = await rendezvous.startAfterScanningCode(theirIntent); - this.setState({ phase: Phase.CONNECTED, confirmationDigits }); - - if (this.isNewDevice) { - const creds = await rendezvous.completeLoginOnNewDevice(); - if (creds) { - await setLoggedIn({ - accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, - homeserverUrl: creds.homeserverUrl, - }); - await rendezvous.completeVerificationOnNewDevice(MatrixClientPeg.get()); - this.props.onFinished(true); - defaultDispatcher.dispatch({ - action: Action.ViewHomePage, - }); - } - } - } catch (e) { - alert(e); - } - }; - private cancelClicked = () => { void (async () => { - await this.state.rendezvous?.userCancelled(); + await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); this.reset(); this.props.onFinished(false); })(); @@ -293,41 +217,12 @@ export default class LoginWithQR extends React.Component { void this.updateMode(this.props.mode); }; - private onQrResult: OnResultFunction = (result, error) => { - if (result) { - this.processScannedCode(result.getText()); - } - }; - - private viewFinder() { - return - - - - - ; - } - - private requestMediaPermissions = async () => { - try { - await navigator.mediaDevices.getUserMedia({ video: true }); - this.setState({ mediaPermissionError: false }); - } catch (err) { - this.setState({ mediaPermissionError: true }); - } - }; - private onBackClick = () => { - void this.state.rendezvous?.userCancelled(); + void this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); this.props.onFinished(false); }; - private onDoScanQRClicked = () => { - this.setState({ phase: Phase.SCANNING_QR }); - void this.requestMediaPermissions(); - }; - private cancelButton = () => { buttons = this.cancelButton(); } break; - case Phase.SCAN_INSTRUCTIONS: - title = _t("Sign in with QR code"); - if (this.isExistingDevice) { - main = <> -

- { _t("Use the camera on this device to scan the QR code shown on your other device:") } -

-
    -
  1. { _t("Start at the sign in screen") }
  2. -
  3. { _t("Select 'Show QR code'") }
  4. -
- ; - } else { - main = <> -

- { _t("Use the camera on this device to scan the QR code shown on your signed in device:") } -

-
    -
  1. { _t("Open Element on your other device") }
  2. -
  3. { _t("Go to Settings -> Security & Privacy") }
  4. -
  5. { _t("Select 'Link a device'") }
  6. -
  7. { _t("Select 'Show QR code on this device'") }
  8. -
- ; - } - buttons = <> - - { _t("Scan QR code") } - - ; - break; - case Phase.SCANNING_QR: - title =_t("Sign in with QR code"); - main = <> -

{ _t("Line up the QR code in the square below:") }

- - ; - break; case Phase.LOADING: main = this.simpleSpinner(); break; diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 0d15e4526e2..f20a18c2187 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -24,7 +24,6 @@ import SettingsSubsection from '../shared/SettingsSubsection'; interface IProps { onShowQr: () => void; - onScanQr: () => void; } interface IState { @@ -54,44 +53,27 @@ export default class LoginWithQRSection extends React.Component public render(): JSX.Element { const features = SdkConfig.get().login_with_qr?.reciprocate; - // Needs to be enabled as a feature + server support MSC3882: - const offerScanQr = features.enable_scanning && this.state.msc3882Supported; - // Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured: const offerShowQr = features.enable_showing && this.state.msc3882Supported && (this.state.msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); // don't show anything if no method is available - if (!offerScanQr && !offerShowQr) { + if (!offerShowQr) { return null; } - let description: string; - - if (offerScanQr && offerShowQr) { - description = _t("You can use this device to sign in a new device with a QR code. There are two ways " + - "to do this:"); - } else if (offerScanQr) { - description = _t("You can use this device to sign in a new device with a QR code. You will need to " + - "use this device to scan the QR code shown on your other device that's signed out."); - } else { - description = _t("You can use this device to sign in a new device with a QR code. You will need to " + - "scan the QR code shown on this device with your device that's signed out."); - } - return
-

{ description }

- { offerScanQr && { _t("Scan QR code") } } - { offerShowQr && { + _t("You can use this device to sign in a new device with a QR code. You will need to " + + "scan the QR code shown on this device with your device that's signed out.") + }

+ { _t("Show QR code") } } + kind="primary" + >{ _t("Show QR code") }
; } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 2092e8bf941..b140ba44181 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -259,10 +259,6 @@ export default class SecurityUserSettingsTab extends React.Component { - this.setState({ showLoginWithQR: Mode.SCAN }); - }; - private onLoginWithQRFinished = (): void => { this.setState({ showLoginWithQR: null }); }; @@ -381,7 +377,7 @@ export default class SecurityUserSettingsTab extends React.Component
{ signinWithQrEnabled ? - + : null } ; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 4761e379c91..a8bdcea713e 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -235,7 +235,7 @@ const SessionManagerTab: React.FC = () => { } { signinWithQrEnabled ? - setSignInWithQrMode(Mode.SCAN)} onShowQr={() => setSignInWithQrMode(Mode.SHOW)} /> + setSignInWithQrMode(Mode.SHOW)} /> : null } diff --git a/yarn.lock b/yarn.lock index 834c3693d3b..ab871f523f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2621,27 +2621,6 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" -"@zxing/browser@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7" - integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng== - optionalDependencies: - "@zxing/text-encoding" "^0.9.0" - -"@zxing/library@^0.18.3": - version "0.18.6" - resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c" - integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw== - dependencies: - ts-custom-error "^3.0.0" - optionalDependencies: - "@zxing/text-encoding" "~0.9.0" - -"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" - integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== - abab@^2.0.3, abab@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -8125,15 +8104,6 @@ react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-qr-reader@^3.0.0-beta-1: - version "3.0.0-beta-1" - resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68" - integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw== - dependencies: - "@zxing/browser" "0.0.7" - "@zxing/library" "^0.18.3" - rollup "^2.67.2" - react-redux@^7.2.0: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -8466,13 +8436,6 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^2.67.2: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== - optionalDependencies: - fsevents "~2.3.2" - rrweb-snapshot@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" @@ -9260,11 +9223,6 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -ts-custom-error@^3.0.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.2.2.tgz#06a567ea92f5df0b61c25722bb4e3772f79c5e5b" - integrity sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ== - tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" From 3abcaf0f8cb54689ca530ed55f8a010b2c223fea Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 18:49:21 +0100 Subject: [PATCH 25/73] i18n updates --- src/i18n/strings/en_EN.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9c586ee46d0..123ec9c7e6c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1782,11 +1782,8 @@ "Filter devices": "Filter devices", "Show": "Show", "%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sessions selected", - "You can use this device to sign in a new device with a QR code. There are two ways to do this:": "You can use this device to sign in a new device with a QR code. There are two ways to do this:", - "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to use this device to scan the QR code shown on your other device that's signed out.", - "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", "Sign in with QR code": "Sign in with QR code", - "Scan QR code": "Scan QR code", + "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", "Show QR code": "Show QR code", "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", @@ -3200,13 +3197,6 @@ "Scan the QR code below with your device that's already signed in:": "Scan the QR code below with your device that's already signed in:", "Open the app on your other device": "Open the app on your other device", "Go to Settings -> Security & Privacy": "Go to Settings -> Security & Privacy", - "Use the camera on this device to scan the QR code shown on your other device:": "Use the camera on this device to scan the QR code shown on your other device:", - "Select 'Show QR code'": "Select 'Show QR code'", - "Use the camera on this device to scan the QR code shown on your signed in device:": "Use the camera on this device to scan the QR code shown on your signed in device:", - "Open Element on your other device": "Open Element on your other device", - "Select 'Link a device'": "Select 'Link a device'", - "Select 'Show QR code on this device'": "Select 'Show QR code on this device'", - "Line up the QR code in the square below:": "Line up the QR code in the square below:", "Connecting...": "Connecting...", "Waiting for device to sign in": "Waiting for device to sign in", "Completing set up of your new device": "Completing set up of your new device", From 5142dbc4eca503589a43dfb60aa30f4ee23aada5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 23:49:36 +0100 Subject: [PATCH 26/73] Handle null features --- src/components/views/settings/devices/LoginWithQRSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index f20a18c2187..7a5a1036a79 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -54,7 +54,7 @@ export default class LoginWithQRSection extends React.Component const features = SdkConfig.get().login_with_qr?.reciprocate; // Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured: - const offerShowQr = features.enable_showing && this.state.msc3882Supported && + const offerShowQr = features?.enable_showing && this.state.msc3882Supported && (this.state.msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); // don't show anything if no method is available From ae77a1a4728f7d8b97a92c19f569b89a4db602bf Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 10:17:03 +0100 Subject: [PATCH 27/73] Testing for LoginWithQRSection --- src/components/views/auth/LoginWithQR.tsx | 8 +- .../settings/devices/LoginWithQRSection.tsx | 29 ++-- .../tabs/user/SecurityUserSettingsTab.tsx | 5 +- .../settings/tabs/user/SessionManagerTab.tsx | 4 +- .../devices/LoginWithQRSection-test.tsx | 143 ++++++++++++++++++ .../LoginWithQRSection-test.tsx.snap | 85 +++++++++++ 6 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 test/components/views/settings/devices/LoginWithQRSection-test.tsx create mode 100644 test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 92982b8f693..42fb90f1f90 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -20,10 +20,8 @@ import { MatrixClient } from 'matrix-js-sdk/src/client'; import { _t } from "../../../languageHandler"; import AccessibleButton from '../elements/AccessibleButton'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import QRCode from '../elements/QRCode'; import SdkConfig from '../../../SdkConfig'; -import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig'; import Spinner from '../elements/Spinner'; import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg"; import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; @@ -45,8 +43,7 @@ enum Phase { } interface IProps { - serverConfig?: ValidatedServerConfig; - client?: MatrixClient; + client: MatrixClient; mode: Mode; onFinished(...args: any): void; } @@ -119,8 +116,7 @@ export default class LoginWithQR extends React.Component { // user denied return; } - const cli = MatrixClientPeg.get(); - if (!cli.crypto) { + if (!this.props.client.crypto) { // alert(`New device signed in: ${newDeviceId}. Not signing cross-signing as no crypto setup`); this.props.onFinished(true); return; diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 7a5a1036a79..323e91b8dae 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -16,46 +16,35 @@ limitations under the License. import React from 'react'; +import type { IServerVersions } from 'matrix-js-sdk/src/matrix'; import { _t } from '../../../../languageHandler'; -import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import SdkConfig from '../../../../SdkConfig'; import AccessibleButton from '../../elements/AccessibleButton'; import SettingsSubsection from '../shared/SettingsSubsection'; interface IProps { onShowQr: () => void; + versions: IServerVersions; } -interface IState { - msc3882Supported: boolean | null; - msc3886Supported: boolean | null; -} +interface IState {} export default class LoginWithQRSection extends React.Component { constructor(props: IProps) { super(props); - this.state = { - msc3882Supported: null, - msc3886Supported: null, - }; - } - - public componentDidMount(): void { - MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3882"). then((msc3882Supported) => { - this.setState({ msc3882Supported }); - }); - MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3886").then((msc3886Supported) => { - this.setState({ msc3886Supported }); - }); + this.state = {}; } public render(): JSX.Element { const features = SdkConfig.get().login_with_qr?.reciprocate; + const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882']; + const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886']; + // Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured: - const offerShowQr = features?.enable_showing && this.state.msc3882Supported && - (this.state.msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); + const offerShowQr = features?.enable_showing && msc3882Supported && + (msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); // don't show anything if no method is available if (!offerShowQr) { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index b140ba44181..bb0f49b829e 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -40,6 +40,7 @@ import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/Ana import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; import LoginWithQRSection from '../../devices/LoginWithQRSection'; +import type { IServerVersions } from 'matrix-js-sdk/src/matrix'; interface IIgnoredUserProps { userId: string; @@ -75,6 +76,7 @@ interface IState { managingInvites: boolean; invitedRoomIds: Set; showLoginWithQR: Mode | null; + versions?: IServerVersions; } export default class SecurityUserSettingsTab extends React.Component { @@ -106,6 +108,7 @@ export default class SecurityUserSettingsTab extends React.Component this.setState({ versions })); } public componentWillUnmount(): void { @@ -377,7 +380,7 @@ export default class SecurityUserSettingsTab extends React.Component
{ signinWithQrEnabled ? - + : null } ; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index a8bdcea713e..bac7f955134 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -35,6 +35,7 @@ import SettingsTab from '../SettingsTab'; import LoginWithQRSection from '../../devices/LoginWithQRSection'; import LoginWithQR, { Mode } from '../../../auth/LoginWithQR'; import SettingsStore from '../../../../../settings/SettingsStore'; +import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo'; const useSignOut = ( matrixClient: MatrixClient, @@ -107,6 +108,7 @@ const SessionManagerTab: React.FC = () => { const matrixClient = useContext(MatrixClientContext); const userId = matrixClient.getUserId(); const currentUserMember = userId && matrixClient.getUser(userId) || undefined; + const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => { if (expandedDeviceIds.includes(deviceId)) { @@ -235,7 +237,7 @@ const SessionManagerTab: React.FC = () => { } { signinWithQrEnabled ? - setSignInWithQrMode(Mode.SHOW)} /> + setSignInWithQrMode(Mode.SHOW)} versions={clientVersions} /> : null } diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx new file mode 100644 index 00000000000..37f88967213 --- /dev/null +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -0,0 +1,143 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { render } from '@testing-library/react'; +import { mocked } from 'jest-mock'; +import { IServerVersions, MatrixClient } from 'matrix-js-sdk/src/matrix'; +import React from 'react'; + +import LoginWithQRSection from '../../../../../src/components/views/settings/devices/LoginWithQRSection'; +import { MatrixClientPeg } from '../../../../../src/MatrixClientPeg'; +import SdkConfig from '../../../../../src/SdkConfig'; +import { SettingLevel } from '../../../../../src/settings/SettingLevel'; +import SettingsStore from '../../../../../src/settings/SettingsStore'; + +function makeClient() { + return mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + isCryptoEnabled: jest.fn(), + getUserId: jest.fn(), + on: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + isRoomEncrypted: jest.fn().mockReturnValue(false), + mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + } as unknown as MatrixClient); +} + +function makeVersions(unstableFeatures: Record): IServerVersions { + return { + versions: [], + unstable_features: unstableFeatures, + }; +} + +describe('', () => { + beforeAll(() => { + jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(makeClient()); + }); + + const defaultProps = { + onShowQr: () => {}, + versions: undefined, + }; + + const getComponent = (props = {}) => + (); + + it('no support - all', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('no support - sdk', () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + }, + }); + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('no support - sdk + feature', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('no support - sdk + feature + MSC3882', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ versions: makeVersions({ 'org.matrix.msc3882': true }) })); + expect(container).toMatchSnapshot(); + }); + + it('supported - sdk + feature + MSC3882 + MSC3886', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ versions: makeVersions({ + 'org.matrix.msc3882': true, + 'org.matrix.msc3886': true, + }) })); + expect(container).toMatchSnapshot(); + }); + + it('supported - sdk + feature + MSC3882 + fallback', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + fallback_http_transport_server: 'https://rzserver', + }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ + versions: makeVersions({ + 'org.matrix.msc3882': true, + 'org.matrix.msc3886': false, + }), + })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap new file mode 100644 index 00000000000..62529a4f237 --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` no support - all 1`] = `
`; + +exports[` no support - sdk + feature + MSC3882 1`] = `
`; + +exports[` no support - sdk + feature 1`] = `
`; + +exports[` no support - sdk 1`] = `
`; + +exports[` supported - sdk + feature + MSC3882 + MSC3886 1`] = ` +
+
+
+

+ Sign in with QR code +

+
+
+
+

+ You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out. +

+
+ Show QR code +
+
+
+
+
+`; + +exports[` supported - sdk + feature + MSC3882 + fallback 1`] = ` +
+
+
+

+ Sign in with QR code +

+
+
+
+

+ You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out. +

+
+ Show QR code +
+
+
+
+
+`; From 4683d42e213a0f40fe50926d4fd263b1a88b9cff Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:50:05 +0100 Subject: [PATCH 28/73] Refactor to handle UIA --- src/components/views/auth/LoginWithQR.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 42fb90f1f90..0b2900206b9 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -27,6 +27,8 @@ import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.s import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; +import { LoginTokenPostResponse } from 'matrix-js-sdk/src/@types/auth'; +import { IAuthData } from 'matrix-js-sdk'; export enum Mode { SHOW = "show", @@ -110,8 +112,22 @@ export default class LoginWithQR extends React.Component { if (!this.state.rendezvous) { throw new Error('Rendezvous not found'); } + this.setState({ phase: Phase.LOADING }); + + logger.info("Requesting login token"); + + const loginTokenResponse = await this.props.client.requestLoginToken(); + + if (typeof (loginTokenResponse as IAuthData).session === 'string') { + // TODO: handle UIA response + throw new Error("UIA isn't supported yet"); + } + + const { login_token: loginToken } = loginTokenResponse as LoginTokenPostResponse; + this.setState({ phase: Phase.WAITING_FOR_DEVICE }); - const newDeviceId = await this.state.rendezvous.confirmLoginOnExistingDevice(); + + const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); if (!newDeviceId) { // user denied return; From 1135d3c0aee1ca4f89f56f670aa88eb0f40d709a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:50:44 +0100 Subject: [PATCH 29/73] Imports --- src/components/views/auth/LoginWithQR.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 0b2900206b9..319bce482e1 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -17,6 +17,8 @@ import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvo import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; import { logger } from 'matrix-js-sdk/src/logger'; import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { IAuthData } from 'matrix-js-sdk/src/matrix'; +import { LoginTokenPostResponse } from 'matrix-js-sdk/src/@types/auth'; import { _t } from "../../../languageHandler"; import AccessibleButton from '../elements/AccessibleButton'; @@ -27,8 +29,6 @@ import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.s import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { LoginTokenPostResponse } from 'matrix-js-sdk/src/@types/auth'; -import { IAuthData } from 'matrix-js-sdk'; export enum Mode { SHOW = "show", From 207cba3e4c0a2df3a3e0c8dd76396f21a9923a41 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 13:28:28 +0100 Subject: [PATCH 30/73] Reduce diff complexity --- .../tabs/user/SecurityUserSettingsTab.tsx | 35 ++--- .../settings/tabs/user/SessionManagerTab.tsx | 121 +++++++++--------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index bb0f49b829e..9268a11fcff 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -380,31 +380,32 @@ export default class SecurityUserSettingsTab extends React.Component
{ signinWithQrEnabled ? - + : null } ; const client = MatrixClientPeg.get(); + if (signinWithQrEnabled && this.state.showLoginWithQR) { + return
+ +
; + } + return (
- { signinWithQrEnabled && this.state.showLoginWithQR ? - - : <> - { warning } - { devicesSection } -
{ _t("Encryption") }
-
- { secureBackup } - { eventIndex } - { crossSigning } - -
- { privacySection } - { advancedSection } - - } + { warning } + { devicesSection } +
{ _t("Encryption") }
+
+ { secureBackup } + { eventIndex } + { crossSigning } + +
+ { privacySection } + { advancedSection }
); } diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index bac7f955134..3b6113a0148 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -184,65 +184,68 @@ const SessionManagerTab: React.FC = () => { const signinWithQrEnabled = SettingsStore.getValue("feature_signin_with_qr_code"); - return signinWithQrEnabled && signInWithQrMode ? - setSignInWithQrMode(null)} client={matrixClient} /> - : - - - saveDeviceName(currentDeviceId, deviceName)} - onVerifyCurrentDevice={onVerifyCurrentDevice} - onSignOutCurrentDevice={onSignOutCurrentDevice} - signOutAllOtherSessions={signOutAllOtherSessions} - /> - { - shouldShowOtherSessions && - - - - } - { signinWithQrEnabled ? - setSignInWithQrMode(Mode.SHOW)} versions={clientVersions} /> - : null - } - - - ; + if (signinWithQrEnabled && signInWithQrMode) { + return setSignInWithQrMode(null)} + client={matrixClient} + />; + } + + return + + saveDeviceName(currentDeviceId, deviceName)} + onVerifyCurrentDevice={onVerifyCurrentDevice} + onSignOutCurrentDevice={onSignOutCurrentDevice} + signOutAllOtherSessions={signOutAllOtherSessions} + /> + { + shouldShowOtherSessions && + + + + } + { signinWithQrEnabled ? + setSignInWithQrMode(Mode.SHOW)} versions={clientVersions} /> + : null + } + ; }; export default SessionManagerTab; From d96106b76a936a6579b1abbd7a5d68ea4d8ad813 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 13:29:51 +0100 Subject: [PATCH 31/73] Remove unnecessary change --- src/components/views/settings/tabs/user/SessionManagerTab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 3b6113a0148..885c00fc5f1 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -230,9 +230,7 @@ const SessionManagerTab: React.FC = () => { setSelectedDeviceIds={setSelectedDeviceIds} onFilterChange={setFilter} onDeviceExpandToggle={onDeviceExpandToggle} - onRequestDeviceVerification={ - requestDeviceVerification ? onTriggerDeviceVerification : undefined - } + onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined} onSignOutDevices={onSignOutOtherDevices} saveDeviceName={saveDeviceName} setPushNotifications={setPushNotifications} From e2ff60f149fb7eaeb12a289d3bcf8b133dc824e8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 13:31:14 +0100 Subject: [PATCH 32/73] Remove unused styles --- res/css/views/auth/_LoginWithQR.pcss | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 09eccb0e6ce..fb745ad9a26 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -45,10 +45,6 @@ limitations under the License. } } - .mx_LoginWithQR_QRScanner { - margin: 20px auto; - } - font-size: $font-15px; } @@ -68,10 +64,6 @@ limitations under the License. line-height: 1.8; } - .mx_LoginWithQR_QRScanner { - margin: 46px 0; - } - .mx_QRCode { padding: 0 40px; margin: 26px 0; @@ -166,33 +158,6 @@ limitations under the License. justify-content: center; } - .mx_LoginWithQR_QRScanner { - width: 350px; - height: 350px; - background-color: #222; - border-radius: 8px; - border: 1px solid transparent; - - .mx_QRViewFinder { - top: 0; - left: 0; - z-index: 1; - box-sizing: border-box; - border: 50px solid rgba(0, 0, 0, 0.2); - position: absolute; - width: 100%; - height: 100%; - stroke-width: 2; - stroke: rgba(255, 255, 255, 1); - } - - video { - object-fit: cover; - border-radius: 8px; - border: 1px solid transparent; - } - } - .mx_LoginWithQR_spinner { flex-grow: 1; display: flex; From 987bdc5304052f02af47aa2ef4800242cb46d938 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:27:25 +0100 Subject: [PATCH 33/73] Support UIA --- src/components/views/auth/LoginWithQR.tsx | 54 +++++++++--------- .../views/dialogs/InteractiveAuthDialog.tsx | 6 +- src/utils/UserInteractiveAuth.ts | 56 +++++++++++++++++++ 3 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 src/utils/UserInteractiveAuth.ts diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 319bce482e1..e01ecd32e41 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -17,8 +17,6 @@ import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvo import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; import { logger } from 'matrix-js-sdk/src/logger'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { IAuthData } from 'matrix-js-sdk/src/matrix'; -import { LoginTokenPostResponse } from 'matrix-js-sdk/src/@types/auth'; import { _t } from "../../../languageHandler"; import AccessibleButton from '../elements/AccessibleButton'; @@ -29,6 +27,7 @@ import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.s import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; +import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth'; export enum Mode { SHOW = "show", @@ -114,36 +113,37 @@ export default class LoginWithQR extends React.Component { } this.setState({ phase: Phase.LOADING }); - logger.info("Requesting login token"); - - const loginTokenResponse = await this.props.client.requestLoginToken(); - - if (typeof (loginTokenResponse as IAuthData).session === 'string') { - // TODO: handle UIA response - throw new Error("UIA isn't supported yet"); - } + try { + logger.info("Requesting login token"); - const { login_token: loginToken } = loginTokenResponse as LoginTokenPostResponse; + const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, { + matrixClient: this.props.client, + title: _t("Sign in new device"), + })(); - this.setState({ phase: Phase.WAITING_FOR_DEVICE }); + this.setState({ phase: Phase.WAITING_FOR_DEVICE }); - const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); - if (!newDeviceId) { - // user denied - return; - } - if (!this.props.client.crypto) { - // alert(`New device signed in: ${newDeviceId}. Not signing cross-signing as no crypto setup`); + const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); + if (!newDeviceId) { + // user denied + return; + } + if (!this.props.client.crypto) { + // alert(`New device signed in: ${newDeviceId}. Not signing cross-signing as no crypto setup`); + this.props.onFinished(true); + return; + } + const didCrossSign = await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); + if (didCrossSign) { + // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); + } else { + // alert(`New device signed in, but no keys received for cross signing: ${newDeviceId}`); + } this.props.onFinished(true); - return; - } - const didCrossSign = await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); - if (didCrossSign) { - // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); - } else { - // alert(`New device signed in, but no keys received for cross signing: ${newDeviceId}`); + } catch (e) { + logger.error('Error whilst approving sign in', e); + this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.Unknown }); } - this.props.onFinished(true); }; private generateCode = async () => { diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 6f10790811e..5d8fc2f952d 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -38,7 +38,7 @@ interface IDialogAesthetics { }; } -interface IProps extends IDialogProps { +export interface InteractiveAuthDialogProps extends IDialogProps { // matrix client to use for UI auth requests matrixClient: MatrixClient; @@ -82,8 +82,8 @@ interface IState { uiaStagePhase: number | string; } -export default class InteractiveAuthDialog extends React.Component { - constructor(props: IProps) { +export default class InteractiveAuthDialog extends React.Component { + constructor(props: InteractiveAuthDialogProps) { super(props); this.state = { diff --git a/src/utils/UserInteractiveAuth.ts b/src/utils/UserInteractiveAuth.ts new file mode 100644 index 00000000000..3385fd196a1 --- /dev/null +++ b/src/utils/UserInteractiveAuth.ts @@ -0,0 +1,56 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IAuthData } from "matrix-js-sdk/src/interactive-auth"; +import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; + +import Modal from "../Modal"; +import InteractiveAuthDialog, { InteractiveAuthDialogProps } from "../components/views/dialogs/InteractiveAuthDialog"; + +type FunctionWithUIA = (auth?: IAuthData, ...args: A[]) => Promise>; + +export function wrapRequestWithDialog( + requestFunction: FunctionWithUIA, + opts: Omit, +): ((...args: A[]) => Promise) { + return async function(...args): Promise { + return new Promise((resolve, reject) => { + console.log(opts); + const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA; + boundFunction(undefined, ...args) + .then((res) => resolve(res as R)) + .catch(error => { + if (error.httpStatus !== 401 || !error.data?.flows) { + // doesn't look like an interactive-auth failure + return reject(error); + } + + Modal.createDialog(InteractiveAuthDialog, { + ...opts, + authData: error.data, + makeRequest: (authData) => boundFunction(authData, ...args), + onFinished: (success, result) => { + if (success) { + resolve(result); + } else { + reject(result); + } + }, + }); + }); + }); + }; +} From 18a943e0df2cc66f76f02103c204772501f7d9ae Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:28:43 +0100 Subject: [PATCH 34/73] Tidy up --- src/components/views/auth/LoginWithQR.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e01ecd32e41..ea153bb7c39 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -129,16 +129,11 @@ export default class LoginWithQR extends React.Component { return; } if (!this.props.client.crypto) { - // alert(`New device signed in: ${newDeviceId}. Not signing cross-signing as no crypto setup`); + // no E2EE to set up this.props.onFinished(true); return; } - const didCrossSign = await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); - if (didCrossSign) { - // alert(`New device signed in, cross signed and marked as known: ${newDeviceId}`); - } else { - // alert(`New device signed in, but no keys received for cross signing: ${newDeviceId}`); - } + await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); this.props.onFinished(true); } catch (e) { logger.error('Error whilst approving sign in', e); From f6881043dd352f1aba9e8501cb22153a1d242210 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:29:14 +0100 Subject: [PATCH 35/73] i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 123ec9c7e6c..34ce56e7479 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3177,6 +3177,7 @@ "Submit": "Submit", "Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.", "Start authentication": "Start authentication", + "Sign in new device": "Sign in new device", "The linking wasn't completed in the required time.": "The linking wasn't completed in the required time.", "The scanned code is invalid.": "The scanned code is invalid.", "Linking with this device is not supported.": "Linking with this device is not supported.", From 20f31cac151ffa56527349f07c3e99508476aff0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:31:08 +0100 Subject: [PATCH 36/73] Remove additional unused parts of flow --- src/components/views/auth/LoginWithQR.tsx | 102 +++++++--------------- src/i18n/strings/en_EN.json | 5 -- 2 files changed, 33 insertions(+), 74 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index ea153bb7c39..b0771162d4f 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -194,14 +194,6 @@ export default class LoginWithQR extends React.Component { }); } - private get isExistingDevice(): boolean { - return !!this.props.client; - } - - private get isNewDevice(): boolean { - return !this.props.client; - } - private cancelClicked = () => { void (async () => { await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); @@ -308,48 +300,33 @@ export default class LoginWithQR extends React.Component { title = _t("Devices connected"); titleIcon = ; backButton = false; - if (this.isNewDevice) { - main = <> -

{ _t("Check that the same code is shown on your other device before proceeding:") }

-
- { this.state.confirmationDigits } -
- ; - buttons = <> -
- { _t("No match?") } + main = <> +

{ _t("Check that the code below matches with your other device:") }

+
+ { this.state.confirmationDigits } +
+
+
+
- { this.cancelButton() } - ; - } else { - main = <> -

{ _t("Check that the code below matches with your other device:") }

-
- { this.state.confirmationDigits } -
-
-
- -
-
{ _t("By approving access for this device, it will have full access to your account.") }
-
- ; +
{ _t("By approving access for this device, it will have full access to your account.") }
+
+ ; - buttons = <> - - { _t("Cancel") } - - - { _t("Approve") } - - ; - } + buttons = <> + + { _t("Cancel") } + + + { _t("Approve") } + + ; break; case Phase.SHOWING_QR: title =_t("Sign in with QR code"); @@ -357,27 +334,14 @@ export default class LoginWithQR extends React.Component { const code =
; - - if (this.isExistingDevice) { - main = <> -

{ _t("Scan the QR code below with your device that's signed out.") }

-
    -
  1. { _t("Start at the sign in screen") }
  2. -
  3. { _t("Select 'Scan QR code'") }
  4. -
- { code } - ; - } else { - main = <> -

{ _t("Scan the QR code below with your device that's already signed in:") }

-
    -
  1. { _t("Open the app on your other device") }
  2. -
  3. { _t("Go to Settings -> Security & Privacy") }
  4. -
  5. { _t("Select 'Scan QR code'") }
  6. -
- { code } - ; - } + main = <> +

{ _t("Scan the QR code below with your device that's signed out.") }

+
    +
  1. { _t("Start at the sign in screen") }
  2. +
  3. { _t("Select 'Scan QR code'") }
  4. +
+ { code } + ; } else { main = this.simpleSpinner(); buttons = this.cancelButton(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 34ce56e7479..e2567c61c80 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3188,16 +3188,11 @@ "An unexpected error occurred.": "An unexpected error occurred.", "The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.", "Devices connected": "Devices connected", - "Check that the same code is shown on your other device before proceeding:": "Check that the same code is shown on your other device before proceeding:", - "No match?": "No match?", "Check that the code below matches with your other device:": "Check that the code below matches with your other device:", "By approving access for this device, it will have full access to your account.": "By approving access for this device, it will have full access to your account.", "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", "Start at the sign in screen": "Start at the sign in screen", "Select 'Scan QR code'": "Select 'Scan QR code'", - "Scan the QR code below with your device that's already signed in:": "Scan the QR code below with your device that's already signed in:", - "Open the app on your other device": "Open the app on your other device", - "Go to Settings -> Security & Privacy": "Go to Settings -> Security & Privacy", "Connecting...": "Connecting...", "Waiting for device to sign in": "Waiting for device to sign in", "Completing set up of your new device": "Completing set up of your new device", From cbfc1d9b123687e6dad79bab382115d568d20103 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:33:49 +0100 Subject: [PATCH 37/73] Add extra instruction when showing QR code --- src/components/views/auth/LoginWithQR.tsx | 1 + src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index b0771162d4f..cc7919993e2 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -339,6 +339,7 @@ export default class LoginWithQR extends React.Component {
  1. { _t("Start at the sign in screen") }
  2. { _t("Select 'Scan QR code'") }
  3. +
  4. { _t("Review and approve the sign in") }
{ code } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e2567c61c80..0304e5708ab 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3193,6 +3193,7 @@ "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", "Start at the sign in screen": "Start at the sign in screen", "Select 'Scan QR code'": "Select 'Scan QR code'", + "Review and approve the sign in": "Review and approve the sign in", "Connecting...": "Connecting...", "Waiting for device to sign in": "Waiting for device to sign in", "Completing set up of your new device": "Completing set up of your new device", From 271153823f7eb7f2bed6bdc4970e412ada66a6b3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 14:47:00 +0100 Subject: [PATCH 38/73] Add getVersions to server mocks --- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 1 + test/test-utils/client.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index ed10e643369..4a2d1d435b0 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -92,6 +92,7 @@ describe('', () => { getPushers: jest.fn(), setPusher: jest.fn(), setLocalNotificationSettings: jest.fn(), + getVersions: jest.fn().mockResolvedValue({}), }); const defaultProps = {}; diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 6478743458c..6635e6c3062 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -103,6 +103,7 @@ export const mockClientMethodsServer = (): Partial Date: Sun, 16 Oct 2022 14:53:30 +0100 Subject: [PATCH 39/73] Use proper colours for theme support --- res/css/views/auth/_LoginWithQR.pcss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index fb745ad9a26..70634471acd 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -32,7 +32,7 @@ limitations under the License. &::before, &::after { content: ''; flex: 1; - border-bottom: 1px solid #E3E8F0; + border-bottom: 1px solid $quinary-content; } &:not(:empty) { @@ -65,7 +65,7 @@ limitations under the License. } .mx_QRCode { - padding: 0 40px; + padding: 12px 40px; margin: 26px 0; } @@ -91,13 +91,13 @@ limitations under the License. h1 > svg { &.normal { - color: #737D8C; + color: $secondary-content; } &.error { - color: #FF5B55; + color: $alert; } &.success { - color: #0DBD8B; + color: $accent; } height: 1.3em; margin-right: 8px; @@ -109,11 +109,11 @@ limitations under the License. margin: 50px auto; font-weight: 600; font-size: $font-24px; - color: #17191C; + color: $primary-content; } .mx_LoginWithQR_confirmationAlert { - border: 1px solid #C1C6CD; + border: 1px solid $quaternary-content; border-radius: 8px; padding: 8px; line-height: 1.5em; @@ -133,7 +133,7 @@ limitations under the License. padding-inline-start: 0; li::marker { - color: #0DBD8B; + color: $accent; } } @@ -152,7 +152,7 @@ limitations under the License. } .mx_QRCode { - border: 1px solid #E3E8F0; + border: 1px solid $quinary-content; border-radius: 8px; display: flex; justify-content: center; From d3cda5b5831a3a8046f9c38a3d592ca2cc416de1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 15:55:03 +0100 Subject: [PATCH 40/73] Test cases --- .../settings/devices/LoginWithQR-test.tsx | 106 +++++++++++++ .../__snapshots__/LoginWithQR-test.tsx.snap | 149 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 test/components/views/settings/devices/LoginWithQR-test.tsx create mode 100644 test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx new file mode 100644 index 00000000000..8d3f118f451 --- /dev/null +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -0,0 +1,106 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { render } from '@testing-library/react'; +import { mocked } from 'jest-mock'; +import React from 'react'; +import { sleep } from 'matrix-js-sdk/src/utils'; +import { act } from 'react-dom/test-utils'; + +import LoginWithQR, { Mode } from '../../../../../src/components/views/auth/LoginWithQR'; +import type { MatrixClient } from 'matrix-js-sdk/src/matrix'; +import type { SAS } from 'matrix-js-sdk/src/crypto/verification/SAS'; +import SdkConfig from '../../../../../src/SdkConfig'; +import { MSC3906Rendezvous } from 'matrix-js-sdk/src/rendezvous'; + +function makeClient() { + return mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + isCryptoEnabled: jest.fn(), + getUserId: jest.fn(), + on: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + isRoomEncrypted: jest.fn().mockReturnValue(false), + mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(true), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + } as unknown as MatrixClient); +} + +describe('', () => { + const client = makeClient(); + const defaultProps = { + mode: Mode.SHOW, + onFinished: () => {}, + }; + + beforeAll(() => { + global.Olm = { + SAS: jest.fn().mockImplementation(() => { + return { + get_pubkey: jest.fn().mockReturnValue('mock-public-key'), + free: jest.fn(), + } as unknown as SAS; + }), + } as unknown as typeof global.Olm; + }); + + beforeEach(() => { + SdkConfig.put({ + login_with_qr: {}, + }); + }); + + const getComponent = (props: { client: MatrixClient }) => + (); + + it('no support', async () => { + const { container } = render(getComponent({ client })); + await act(async () => { + await sleep(1000); + }); + expect(container).toMatchSnapshot(); + }); + + it('device connected', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + fallback_http_transport_server: 'https://rzserver', + }, + }); + + jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockImplementation( + async function(this: MSC3906Rendezvous) { + this.code = 'mock-code'; + }, + ); + + jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockImplementation(async function() { + return '1234-4567-7890'; + }); + + const { container } = render(getComponent({ client })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap new file mode 100644 index 00000000000..f96b81ef722 --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` device connected 1`] = ` +
+
+
+
+
+
+

+

+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[` no support 1`] = ` +
+
+
+

+
+ Connection failed +

+
+
+

+ The homeserver doesn't support signing in another device. +

+
+
+
+ Try again +
+
+ Cancel +
+
+
+
+`; + +exports[` show qr 1`] = ` +
+
+
+

+
+ Devices connected +

+
+
+

+ Check that the code below matches with your other device: +

+
+ 1234-4567-7890 +
+
+
+
+
+
+ By approving access for this device, it will have full access to your account. +
+
+
+
+
+ Cancel +
+
+ Approve +
+
+
+
+`; From 3dcbdd667bb6061588baebe3a94f1053107d7497 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 15:56:40 +0100 Subject: [PATCH 41/73] Lint --- test/components/views/settings/devices/LoginWithQR-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 8d3f118f451..712689691bc 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -19,12 +19,12 @@ import { mocked } from 'jest-mock'; import React from 'react'; import { sleep } from 'matrix-js-sdk/src/utils'; import { act } from 'react-dom/test-utils'; +import { MSC3906Rendezvous } from 'matrix-js-sdk/src/rendezvous'; import LoginWithQR, { Mode } from '../../../../../src/components/views/auth/LoginWithQR'; import type { MatrixClient } from 'matrix-js-sdk/src/matrix'; import type { SAS } from 'matrix-js-sdk/src/crypto/verification/SAS'; import SdkConfig from '../../../../../src/SdkConfig'; -import { MSC3906Rendezvous } from 'matrix-js-sdk/src/rendezvous'; function makeClient() { return mocked({ From b3d135e9a653d83541ef3ee4534ac0a4afab4ea8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 18:30:12 +0100 Subject: [PATCH 42/73] Remove obsolete snapshot --- .../__snapshots__/LoginWithQR-test.tsx.snap | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap index f96b81ef722..766d17ea270 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -88,62 +88,3 @@ exports[` no support 1`] = `
`; - -exports[` show qr 1`] = ` -
-
-
-

-
- Devices connected -

-
-
-

- Check that the code below matches with your other device: -

-
- 1234-4567-7890 -
-
-
-
-
-
- By approving access for this device, it will have full access to your account. -
-
-
-
-
- Cancel -
-
- Approve -
-
-
-
-`; From eed649b5ce2f2e8a79d88a97ce5fe5f4ad9cb2b0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 20:27:15 +0100 Subject: [PATCH 43/73] Don't override error if already set --- src/components/views/auth/LoginWithQR.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index cc7919993e2..3c9caccae57 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -152,11 +152,10 @@ export default class LoginWithQR extends React.Component { fallbackRzServer: fallbackServer, }); - const channel = new MSC3903ECDHv1RendezvousChannel(transport); + const channel = new MSC3903ECDHv1RendezvousChannel(transport, undefined, this.onFailure); - rendezvous = new MSC3906Rendezvous(channel, this.props.client); + rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); - rendezvous.onFailure = this.onFailure; await rendezvous.generateCode(); this.setState({ phase: Phase.SHOWING_QR, @@ -174,10 +173,10 @@ export default class LoginWithQR extends React.Component { this.setState({ phase: Phase.CONNECTED, confirmationDigits }); } catch (e) { logger.error('Error whilst doing QR login', e); - if (this.state.rendezvous) { - await this.state.rendezvous.cancel(RendezvousFailureReason.Unknown); + // 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: RendezvousFailureReason.Unknown }); } - this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.Unknown }); } }; From f103cfda9a0494482b9ff1ac7e2899134e3a3990 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:27:19 +0100 Subject: [PATCH 44/73] Remove unused var --- src/components/structures/auth/Login.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index b16886696b4..c9fc7e001d9 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -98,7 +98,6 @@ interface IState { serverIsAlive: boolean; serverErrorIsFatal: boolean; serverDeadError?: ReactNode; - loginWithQrInProgress: boolean; } /* @@ -129,7 +128,6 @@ export default class LoginComponent extends React.PureComponent serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - loginWithQrInProgress: false, }; // map from login step type to a function which will render a control From 91332069a6d1e6cd8f33657c1d29fffdc8f4d99c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:42:29 +0100 Subject: [PATCH 45/73] Update src/components/views/settings/devices/LoginWithQRSection.tsx Co-authored-by: Travis Ralston --- src/components/views/settings/devices/LoginWithQRSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 323e91b8dae..e4080bfe25a 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -30,7 +30,7 @@ interface IProps { interface IState {} export default class LoginWithQRSection extends React.Component { - constructor(props: IProps) { + public constructor(props: IProps) { super(props); this.state = {}; From 7187f89e611e712bb8d536415003af9df4d8b08e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:42:40 +0100 Subject: [PATCH 46/73] Update src/components/views/auth/LoginWithQR.tsx Co-authored-by: Travis Ralston --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 3c9caccae57..d47d8ae8a9a 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -237,7 +237,7 @@ export default class LoginWithQR extends React.Component {
; }; - render() { + public render() { let title: string; let titleIcon: JSX.Element | undefined; let main: JSX.Element | undefined; From e2b9a7f4481785aacdd22f8b7d357afb609a12f7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:42:52 +0100 Subject: [PATCH 47/73] Update src/components/views/auth/LoginWithQR.tsx Co-authored-by: Travis Ralston --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index d47d8ae8a9a..eda0fb0a2c5 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -185,7 +185,7 @@ export default class LoginWithQR extends React.Component { this.setState({ phase: Phase.ERROR, failureReason: reason }); }; - reset() { + public reset() { this.setState({ rendezvous: undefined, confirmationDigits: undefined, From 16a89046cf8ef02d7e225cb516150cbafafa0a06 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:43:03 +0100 Subject: [PATCH 48/73] Update src/components/views/auth/LoginWithQR.tsx Co-authored-by: Travis Ralston --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index eda0fb0a2c5..5832bcb1051 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -77,7 +77,7 @@ export default class LoginWithQR extends React.Component { void this.updateMode(this.props.mode); } - componentDidUpdate(prevProps: Readonly): void { + public componentDidUpdate(prevProps: Readonly): void { if (prevProps.mode !== this.props.mode) { void this.updateMode(this.props.mode); } From 070486521d05592d6242c7d29ea1c453fb8f96c3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:43:13 +0100 Subject: [PATCH 49/73] Update src/components/views/auth/LoginWithQR.tsx Co-authored-by: Travis Ralston --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 5832bcb1051..0d6cd1663c7 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -65,7 +65,7 @@ interface IState { * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ export default class LoginWithQR extends React.Component { - constructor(props) { + public constructor(props) { super(props); this.state = { From 49fd8f593a20e9b2cf3fcf7d9bcd5f55e4183190 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:43:25 +0100 Subject: [PATCH 50/73] Update src/components/views/auth/LoginWithQR.tsx Co-authored-by: Travis Ralston --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 0d6cd1663c7..02cde8ac952 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -73,7 +73,7 @@ export default class LoginWithQR extends React.Component { }; } - componentDidMount(): void { + public componentDidMount(): void { void this.updateMode(this.props.mode); } From 1b003f51abb83a09f117ea6be2fa68116dcb1b62 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:43:45 +0100 Subject: [PATCH 51/73] Update res/css/views/auth/_LoginWithQR.pcss Co-authored-by: Kerry --- res/css/views/auth/_LoginWithQR.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 70634471acd..cf9ad5f9d70 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -100,7 +100,7 @@ limitations under the License. color: $accent; } height: 1.3em; - margin-right: 8px; + margin-right: $spacing-8; vertical-align: middle; } From 212cd745c1a5399dbe9fa324757dab951f1f3530 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:48:42 +0100 Subject: [PATCH 52/73] Use spacing variables --- res/css/views/auth/_LoginWithQR.pcss | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index cf9ad5f9d70..79851af2b96 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -12,7 +12,7 @@ limitations under the License. */ .mx_LoginWithQRSection .mx_AccessibleButton { - margin-right: 10px; + margin-right: $spacing-12; } .mx_AuthPage .mx_LoginWithQR { @@ -21,7 +21,7 @@ limitations under the License. } .mx_AccessibleButton + .mx_AccessibleButton { - margin-top: 8px; + margin-top: $spacing-8; } .mx_LoginWithQR_separator { @@ -50,7 +50,7 @@ limitations under the License. .mx_UserSettingsDialog .mx_LoginWithQR { .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: 12px; + margin-left: $spacing-12; } font-size: $font-14px; @@ -65,8 +65,8 @@ limitations under the License. } .mx_QRCode { - padding: 12px 40px; - margin: 26px 0; + padding: $spacing-12 $spacing-40; + margin: $spacing-28 0; } .mx_LoginWithQR_buttons { @@ -106,7 +106,7 @@ limitations under the License. .mx_LoginWithQR_confirmationDigits { text-align: center; - margin: 50px auto; + margin: $spacing-48 auto; font-weight: 600; font-size: $font-24px; color: $primary-content; @@ -114,8 +114,8 @@ limitations under the License. .mx_LoginWithQR_confirmationAlert { border: 1px solid $quaternary-content; - border-radius: 8px; - padding: 8px; + border-radius: $spacing-8; + padding: $spacing-8; line-height: 1.5em; display: flex; @@ -138,8 +138,8 @@ limitations under the License. } .mx_LoginWithQR_BackButton { - height: 12px; - margin-bottom: 24px; + height: $spacing-12; + margin-bottom: $spacing-24; svg { height: 100%; } @@ -153,7 +153,7 @@ limitations under the License. .mx_QRCode { border: 1px solid $quinary-content; - border-radius: 8px; + border-radius: $spacing-8; display: flex; justify-content: center; } From 0a74bf3252b8874860b6f3f1bf229b72555f214a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:49:36 +0100 Subject: [PATCH 53/73] Remove debug --- src/utils/UserInteractiveAuth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/UserInteractiveAuth.ts b/src/utils/UserInteractiveAuth.ts index 3385fd196a1..e3088fb3cb4 100644 --- a/src/utils/UserInteractiveAuth.ts +++ b/src/utils/UserInteractiveAuth.ts @@ -28,7 +28,6 @@ export function wrapRequestWithDialog( ): ((...args: A[]) => Promise) { return async function(...args): Promise { return new Promise((resolve, reject) => { - console.log(opts); const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA; boundFunction(undefined, ...args) .then((res) => resolve(res as R)) From d9096c46b0706f5466986048d0a3737b94ea9ed7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:55:36 +0100 Subject: [PATCH 54/73] Style + docs --- src/components/views/auth/LoginWithQR.tsx | 60 ++++++++++--------- .../tabs/user/SecurityUserSettingsTab.tsx | 2 +- .../settings/tabs/user/SessionManagerTab.tsx | 2 +- .../settings/devices/LoginWithQR-test.tsx | 2 +- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 02cde8ac952..d5185ed9eb3 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -29,18 +29,24 @@ import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning- import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth'; +/** + * The intention of this enum is to have a mode that scans a QR code instead of generating one. + */ export enum Mode { - SHOW = "show", + /** + * A QR code with be generated and shown + */ + Show = "show", } enum Phase { - LOADING, - SHOWING_QR, - CONNECTING, - CONNECTED, - WAITING_FOR_DEVICE, - VERIFYING, - ERROR, + Loading, + ShowingQR, + Connecting, + Connected, + WaitingForDevice, + Verifying, + Error, } interface IProps { @@ -69,7 +75,7 @@ export default class LoginWithQR extends React.Component { super(props); this.state = { - phase: Phase.LOADING, + phase: Phase.Loading, }; } @@ -84,14 +90,14 @@ export default class LoginWithQR extends React.Component { } private async updateMode(mode: Mode) { - this.setState({ phase: Phase.LOADING }); + this.setState({ phase: Phase.Loading }); if (this.state.rendezvous) { this.state.rendezvous.onFailure = undefined; this.state.rendezvous.channel.transport.onFailure = undefined; await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); this.setState({ rendezvous: undefined }); } - if (mode === Mode.SHOW) { + if (mode === Mode.Show) { await this.generateCode(); } } @@ -111,7 +117,7 @@ export default class LoginWithQR extends React.Component { if (!this.state.rendezvous) { throw new Error('Rendezvous not found'); } - this.setState({ phase: Phase.LOADING }); + this.setState({ phase: Phase.Loading }); try { logger.info("Requesting login token"); @@ -121,7 +127,7 @@ export default class LoginWithQR extends React.Component { title: _t("Sign in new device"), })(); - this.setState({ phase: Phase.WAITING_FOR_DEVICE }); + this.setState({ phase: Phase.WaitingForDevice }); const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); if (!newDeviceId) { @@ -137,7 +143,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(true); } catch (e) { logger.error('Error whilst approving sign in', e); - this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.Unknown }); + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); } }; @@ -158,31 +164,31 @@ export default class LoginWithQR extends React.Component { await rendezvous.generateCode(); this.setState({ - phase: Phase.SHOWING_QR, + phase: Phase.ShowingQR, rendezvous, failureReason: undefined, }); } catch (e) { logger.error('Error whilst generating QR code', e); - this.setState({ phase: Phase.ERROR, failureReason: RendezvousFailureReason.HomeserverLacksSupport }); + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport }); return; } try { const confirmationDigits = await rendezvous.startAfterShowingCode(); - this.setState({ phase: Phase.CONNECTED, confirmationDigits }); + 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: RendezvousFailureReason.Unknown }); + if (this.state.phase !== Phase.Error) { + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); } } }; private onFailure = (reason: RendezvousFailureReason) => { logger.info(`Rendezvous failed: ${reason}`); - this.setState({ phase: Phase.ERROR, failureReason: reason }); + this.setState({ phase: Phase.Error, failureReason: reason }); }; public reset() { @@ -247,7 +253,7 @@ export default class LoginWithQR extends React.Component { let centreTitle = false; switch (this.state.phase) { - case Phase.ERROR: + case Phase.Error: switch (this.state.failureReason) { case RendezvousFailureReason.Expired: cancellationMessage = _t("The linking wasn't completed in the required time."); @@ -295,7 +301,7 @@ export default class LoginWithQR extends React.Component { { this.cancelButton() } ; break; - case Phase.CONNECTED: + case Phase.Connected: title = _t("Devices connected"); titleIcon = ; backButton = false; @@ -327,7 +333,7 @@ export default class LoginWithQR extends React.Component { ; break; - case Phase.SHOWING_QR: + case Phase.ShowingQR: title =_t("Sign in with QR code"); if (this.state.rendezvous) { const code =
@@ -347,18 +353,18 @@ export default class LoginWithQR extends React.Component { buttons = this.cancelButton(); } break; - case Phase.LOADING: + case Phase.Loading: main = this.simpleSpinner(); break; - case Phase.CONNECTING: + case Phase.Connecting: main = this.simpleSpinner(_t("Connecting...")); buttons = this.cancelButton(); break; - case Phase.WAITING_FOR_DEVICE: + case Phase.WaitingForDevice: main = this.simpleSpinner(_t("Waiting for device to sign in")); buttons = this.cancelButton(); break; - case Phase.VERIFYING: + case Phase.Verifying: title = _t("Success"); centreTitle = true; main = this.simpleSpinner(_t("Completing set up of your new device")); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 9268a11fcff..b8345032165 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -259,7 +259,7 @@ export default class SecurityUserSettingsTab extends React.Component { - this.setState({ showLoginWithQR: Mode.SHOW }); + this.setState({ showLoginWithQR: Mode.Show }); }; private onLoginWithQRFinished = (): void => { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 885c00fc5f1..2d6aef511dd 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -240,7 +240,7 @@ const SessionManagerTab: React.FC = () => { } { signinWithQrEnabled ? - setSignInWithQrMode(Mode.SHOW)} versions={clientVersions} /> + setSignInWithQrMode(Mode.Show)} versions={clientVersions} /> : null } ; diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 712689691bc..118a4093e56 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -48,7 +48,7 @@ function makeClient() { describe('', () => { const client = makeClient(); const defaultProps = { - mode: Mode.SHOW, + mode: Mode.Show, onFinished: () => {}, }; From 782c714b2380fa6a2ebbd3439cb9fc293e5b9bee Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 23:00:37 +0100 Subject: [PATCH 55/73] preventDefault --- src/components/views/auth/LoginWithQR.tsx | 28 +++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index d5185ed9eb3..8091f806419 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -199,26 +199,24 @@ export default class LoginWithQR extends React.Component { }); } - private cancelClicked = () => { - void (async () => { - await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); - this.reset(); - this.props.onFinished(false); - })(); + private cancelClicked = async (e: React.FormEvent) => { + e.preventDefault(); + await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); + this.reset(); + this.props.onFinished(false); }; - private declineClicked = () => { - void (async () => { - await this.state.rendezvous?.declineLoginOnExistingDevice(); - this.reset(); - this.props.onFinished(false); - })(); + private declineClicked = async (e: React.FormEvent) => { + e.preventDefault(); + await this.state.rendezvous?.declineLoginOnExistingDevice(); + this.reset(); + this.props.onFinished(false); }; - private tryAgainClicked = () => { + private tryAgainClicked = async (e: React.FormEvent) => { + e.preventDefault(); this.reset(); - - void this.updateMode(this.props.mode); + await this.updateMode(this.props.mode); }; private onBackClick = () => { From c8ddbe8b94ecb9bf063697ef1f8ef19fc6268369 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 23:07:23 +0100 Subject: [PATCH 56/73] Names of tests --- .../settings/devices/LoginWithQR-test.tsx | 4 +- .../devices/LoginWithQRSection-test.tsx | 124 +++++++++--------- 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 118a4093e56..4288f934d64 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -72,7 +72,7 @@ describe('', () => { const getComponent = (props: { client: MatrixClient }) => (); - it('no support', async () => { + it('no content in case of no support', async () => { const { container } = render(getComponent({ client })); await act(async () => { await sleep(1000); @@ -80,7 +80,7 @@ describe('', () => { expect(container).toMatchSnapshot(); }); - it('device connected', async () => { + it('show device connected confirmation screen', async () => { SdkConfig.put({ login_with_qr: { reciprocate: { diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx index 37f88967213..8b24cf64a05 100644 --- a/test/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -63,81 +63,85 @@ describe('', () => { const getComponent = (props = {}) => (); - it('no support - all', () => { - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); + describe('should not render', () => { + it('no support at all', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); - it('no support - sdk', () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, + it('only sdk enabled', () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, }, - }, + }); + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); }); - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); - it('no support - sdk + feature', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, + it('only sdk + feature enabled', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, }, - }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); - it('no support - sdk + feature + MSC3882', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, + it('only sdk + feature + MSC3882 enabled', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, }, - }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ versions: makeVersions({ 'org.matrix.msc3882': true }) })); + expect(container).toMatchSnapshot(); }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent({ versions: makeVersions({ 'org.matrix.msc3882': true }) })); - expect(container).toMatchSnapshot(); }); - it('supported - sdk + feature + MSC3882 + MSC3886', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, + describe('should render panel', () => { + it('enabled by sdk + feature + MSC3882 + MSC3886', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, }, - }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ versions: makeVersions({ + 'org.matrix.msc3882': true, + 'org.matrix.msc3886': true, + }) })); + expect(container).toMatchSnapshot(); }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent({ versions: makeVersions({ - 'org.matrix.msc3882': true, - 'org.matrix.msc3886': true, - }) })); - expect(container).toMatchSnapshot(); - }); - it('supported - sdk + feature + MSC3882 + fallback', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, + it('enabled by sdk + feature + MSC3882 + fallback', async () => { + SdkConfig.put({ + login_with_qr: { + reciprocate: { + enable_showing: true, + }, + fallback_http_transport_server: 'https://rzserver', }, - fallback_http_transport_server: 'https://rzserver', - }, + }); + await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + const { container } = render(getComponent({ + versions: makeVersions({ + 'org.matrix.msc3882': true, + 'org.matrix.msc3886': false, + }), + })); + expect(container).toMatchSnapshot(); }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent({ - versions: makeVersions({ - 'org.matrix.msc3882': true, - 'org.matrix.msc3886': false, - }), - })); - expect(container).toMatchSnapshot(); }); }); From 55d0757e81e3ada3865dc3eeaec4c286aaa0f92f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 23:12:52 +0100 Subject: [PATCH 57/73] Fixes for js-sdk refactor --- src/components/views/auth/LoginWithQR.tsx | 3 --- test/components/views/settings/devices/LoginWithQR-test.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 8091f806419..0ccf7997bc4 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -93,7 +93,6 @@ export default class LoginWithQR extends React.Component { this.setState({ phase: Phase.Loading }); if (this.state.rendezvous) { this.state.rendezvous.onFailure = undefined; - this.state.rendezvous.channel.transport.onFailure = undefined; await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); this.setState({ rendezvous: undefined }); } @@ -106,8 +105,6 @@ export default class LoginWithQR extends React.Component { if (this.state.rendezvous) { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; - // eslint-disable-next-line react/no-direct-mutation-state - this.state.rendezvous.channel.transport.onFailure = undefined; // calling cancel will call close() as well to clean up the resources void this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); } diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 4288f934d64..7449d0cac39 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -92,7 +92,7 @@ describe('', () => { jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockImplementation( async function(this: MSC3906Rendezvous) { - this.code = 'mock-code'; + (this as any).code = 'mock-code'; }, ); From a48a458ef6729d8d5880f8bf0ffe27003f537726 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 01:34:57 +0100 Subject: [PATCH 58/73] Update snapshots to match test names --- .../__snapshots__/LoginWithQR-test.tsx.snap | 90 +++++++++---------- .../LoginWithQRSection-test.tsx.snap | 12 +-- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap index 766d17ea270..e81c88b18c6 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -1,6 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` device connected 1`] = ` +exports[` no content in case of no support 1`] = ` +
+
+
+

+
+ Connection failed +

+
+
+

+ The homeserver doesn't support signing in another device. +

+
+
+
+ Try again +
+
+ Cancel +
+
+
+
+`; + +exports[` show device connected confirmation screen 1`] = `
device connected 1`] = `
`; - -exports[` no support 1`] = ` -
-
-
-

-
- Connection failed -

-
-
-

- The homeserver doesn't support signing in another device. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap index 62529a4f237..6d429340fc7 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` no support - all 1`] = `
`; +exports[` should not render no support at all 1`] = `
`; -exports[` no support - sdk + feature + MSC3882 1`] = `
`; +exports[` should not render only sdk + feature + MSC3882 enabled 1`] = `
`; -exports[` no support - sdk + feature 1`] = `
`; +exports[` should not render only sdk + feature enabled 1`] = `
`; -exports[` no support - sdk 1`] = `
`; +exports[` should not render only sdk enabled 1`] = `
`; -exports[` supported - sdk + feature + MSC3882 + MSC3886 1`] = ` +exports[` should render panel enabled by sdk + feature + MSC3882 + MSC3886 1`] = `
supported - sdk + feature + MSC3882 + MSC3886 1`
`; -exports[` supported - sdk + feature + MSC3882 + fallback 1`] = ` +exports[` should render panel enabled by sdk + feature + MSC3882 + fallback 1`] = `
Date: Tue, 18 Oct 2022 02:07:09 +0100 Subject: [PATCH 59/73] Refactor labs config to make deployment simpler --- src/IConfigOptions.ts | 7 -- src/components/views/auth/LoginWithQR.tsx | 3 - .../settings/devices/LoginWithQRSection.tsx | 8 +-- .../tabs/user/SecurityUserSettingsTab.tsx | 6 +- .../settings/tabs/user/SessionManagerTab.tsx | 6 +- src/settings/Settings.tsx | 7 +- .../settings/devices/LoginWithQR-test.tsx | 27 -------- .../devices/LoginWithQRSection-test.tsx | 64 ++----------------- .../__snapshots__/LoginWithQR-test.tsx.snap | 45 ------------- .../LoginWithQRSection-test.tsx.snap | 48 ++------------ 10 files changed, 24 insertions(+), 197 deletions(-) diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 05969045da4..b45461618e1 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -183,13 +183,6 @@ export interface IConfigOptions { // length per voice chunk in seconds chunk_length?: number; }; - - login_with_qr?: { - reciprocate?: { - enable_showing?: boolean; - }; - fallback_http_transport_server?: string; - }; } export interface ISsoRedirectOptions { diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 0ccf7997bc4..7987efa7971 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -147,12 +147,9 @@ export default class LoginWithQR extends React.Component { private generateCode = async () => { let rendezvous: MSC3906Rendezvous; try { - const fallbackServer = SdkConfig.get().login_with_qr?.fallback_http_transport_server; - const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, client: this.props.client, - fallbackRzServer: fallbackServer, }); const channel = new MSC3903ECDHv1RendezvousChannel(transport, undefined, this.onFailure); diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index e4080bfe25a..df4e8e2ca71 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -18,9 +18,9 @@ import React from 'react'; import type { IServerVersions } from 'matrix-js-sdk/src/matrix'; import { _t } from '../../../../languageHandler'; -import SdkConfig from '../../../../SdkConfig'; import AccessibleButton from '../../elements/AccessibleButton'; import SettingsSubsection from '../shared/SettingsSubsection'; +import SettingsStore from '../../../../settings/SettingsStore'; interface IProps { onShowQr: () => void; @@ -37,14 +37,12 @@ export default class LoginWithQRSection extends React.Component } public render(): JSX.Element { - const features = SdkConfig.get().login_with_qr?.reciprocate; - const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882']; const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886']; // Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured: - const offerShowQr = features?.enable_showing && msc3882Supported && - (msc3886Supported || !!SdkConfig.get().login_with_qr?.fallback_http_transport_server); + const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") && + msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs // don't show anything if no method is available if (!offerShowQr) { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index b8345032165..b960e65a61e 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -362,7 +362,7 @@ export default class SecurityUserSettingsTab extends React.Component @@ -379,7 +379,7 @@ export default class SecurityUserSettingsTab extends React.Component
- { signinWithQrEnabled ? + { showQrCodeEnabled ? : null } @@ -387,7 +387,7 @@ export default class SecurityUserSettingsTab extends React.Component
; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2d6aef511dd..115d3a087f7 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -182,9 +182,9 @@ const SessionManagerTab: React.FC = () => { const [signInWithQrMode, setSignInWithQrMode] = useState(); - const signinWithQrEnabled = SettingsStore.getValue("feature_signin_with_qr_code"); + const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show"); - if (signinWithQrEnabled && signInWithQrMode) { + if (showQrCodeEnabled && signInWithQrMode) { return setSignInWithQrMode(null)} @@ -239,7 +239,7 @@ const SessionManagerTab: React.FC = () => { /> } - { signinWithQrEnabled ? + { showQrCodeEnabled ? setSignInWithQrMode(Mode.Show)} versions={clientVersions} /> : null } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 203d8c9d2f0..89263dc9f0e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -494,11 +494,14 @@ export const SETTINGS: {[setting: string]: ISetting} = { , }, }, - "feature_signin_with_qr_code": { + "feature_qr_signin_reciprocate_show": { isFeature: true, labsGroup: LabGroup.Experimental, supportedLevels: LEVELS_FEATURE, - displayName: _td("Enable sign in with QR code from session manager (requires compatible homeserver)"), + displayName: _td( + "Allow a QR code to be shown in session manager to sign in a another device " + + "(requires compatible homeserver)", + ), default: false, }, "baseFontSize": { diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 7449d0cac39..d45a8edce29 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -64,9 +64,6 @@ describe('', () => { }); beforeEach(() => { - SdkConfig.put({ - login_with_qr: {}, - }); }); const getComponent = (props: { client: MatrixClient }) => @@ -79,28 +76,4 @@ describe('', () => { }); expect(container).toMatchSnapshot(); }); - - it('show device connected confirmation screen', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - fallback_http_transport_server: 'https://rzserver', - }, - }); - - jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockImplementation( - async function(this: MSC3906Rendezvous) { - (this as any).code = 'mock-code'; - }, - ); - - jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockImplementation(async function() { - return '1234-4567-7890'; - }); - - const { container } = render(getComponent({ client })); - expect(container).toMatchSnapshot(); - }); }); diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx index 8b24cf64a05..731b4a0da58 100644 --- a/test/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -69,79 +69,27 @@ describe('', () => { expect(container).toMatchSnapshot(); }); - it('only sdk enabled', () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - }, - }); + it('feature enabled', async () => { + await SettingsStore.setValue('feature_qr_signin_reciprocate_show', null, SettingLevel.DEVICE, true); const { container } = render(getComponent()); expect(container).toMatchSnapshot(); }); - it('only sdk + feature enabled', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - }, - }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); - - it('only sdk + feature + MSC3882 enabled', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - }, - }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + it('only feature + MSC3882 enabled', async () => { + await SettingsStore.setValue('feature_qr_signin_reciprocate_show', null, SettingLevel.DEVICE, true); const { container } = render(getComponent({ versions: makeVersions({ 'org.matrix.msc3882': true }) })); expect(container).toMatchSnapshot(); }); }); describe('should render panel', () => { - it('enabled by sdk + feature + MSC3882 + MSC3886', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - }, - }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); + it('enabled by feature + MSC3882 + MSC3886', async () => { + await SettingsStore.setValue('feature_qr_signin_reciprocate_show', null, SettingLevel.DEVICE, true); const { container } = render(getComponent({ versions: makeVersions({ 'org.matrix.msc3882': true, 'org.matrix.msc3886': true, }) })); expect(container).toMatchSnapshot(); }); - - it('enabled by sdk + feature + MSC3882 + fallback', async () => { - SdkConfig.put({ - login_with_qr: { - reciprocate: { - enable_showing: true, - }, - fallback_http_transport_server: 'https://rzserver', - }, - }); - await SettingsStore.setValue('feature_signin_with_qr_code', null, SettingLevel.DEVICE, true); - const { container } = render(getComponent({ - versions: makeVersions({ - 'org.matrix.msc3882': true, - 'org.matrix.msc3886': false, - }), - })); - expect(container).toMatchSnapshot(); - }); }); }); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap index e81c88b18c6..de4abcf2879 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -43,48 +43,3 @@ exports[` no content in case of no support 1`] = `
`; - -exports[` show device connected confirmation screen 1`] = ` -
-
-
-
-
-
-

-

-
-
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap index 6d429340fc7..2cf0d24cc6c 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap @@ -1,52 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should not render no support at all 1`] = `
`; - -exports[` should not render only sdk + feature + MSC3882 enabled 1`] = `
`; - -exports[` should not render only sdk + feature enabled 1`] = `
`; +exports[` should not render feature enabled 1`] = `
`; -exports[` should not render only sdk enabled 1`] = `
`; +exports[` should not render no support at all 1`] = `
`; -exports[` should render panel enabled by sdk + feature + MSC3882 + MSC3886 1`] = ` -
-
-
-

- Sign in with QR code -

-
-
-
-

- You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out. -

-
- Show QR code -
-
-
-
-
-`; +exports[` should not render only feature + MSC3882 enabled 1`] = `
`; -exports[` should render panel enabled by sdk + feature + MSC3882 + fallback 1`] = ` +exports[` should render panel enabled by feature + MSC3882 + MSC3886 1`] = `
Date: Tue, 18 Oct 2022 02:12:51 +0100 Subject: [PATCH 60/73] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 38fc6141588..aa2b252b18f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -931,7 +931,7 @@ "New session manager": "New session manager", "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", - "Enable sign in with QR code from session manager (requires compatible homeserver)": "Enable sign in with QR code from session manager (requires compatible homeserver)", + "Allow a QR code to be shown in session manager to sign in a another device (requires compatible homeserver)": "Allow a QR code to be shown in session manager to sign in a another device (requires compatible homeserver)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", From 479a6e13178360d77bd69b69632790780714f8b5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 02:12:59 +0100 Subject: [PATCH 61/73] Unused imports --- src/components/views/auth/LoginWithQR.tsx | 1 - test/components/views/settings/devices/LoginWithQR-test.tsx | 2 -- .../views/settings/devices/LoginWithQRSection-test.tsx | 1 - 3 files changed, 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 7987efa7971..96ddb01b4b6 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -21,7 +21,6 @@ import { MatrixClient } from 'matrix-js-sdk/src/client'; import { _t } from "../../../languageHandler"; import AccessibleButton from '../elements/AccessibleButton'; import QRCode from '../elements/QRCode'; -import SdkConfig from '../../../SdkConfig'; import Spinner from '../elements/Spinner'; import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg"; import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index d45a8edce29..ff79d0273ea 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -19,12 +19,10 @@ import { mocked } from 'jest-mock'; import React from 'react'; import { sleep } from 'matrix-js-sdk/src/utils'; import { act } from 'react-dom/test-utils'; -import { MSC3906Rendezvous } from 'matrix-js-sdk/src/rendezvous'; import LoginWithQR, { Mode } from '../../../../../src/components/views/auth/LoginWithQR'; import type { MatrixClient } from 'matrix-js-sdk/src/matrix'; import type { SAS } from 'matrix-js-sdk/src/crypto/verification/SAS'; -import SdkConfig from '../../../../../src/SdkConfig'; function makeClient() { return mocked({ diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx index 731b4a0da58..711f4710350 100644 --- a/test/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import LoginWithQRSection from '../../../../../src/components/views/settings/devices/LoginWithQRSection'; import { MatrixClientPeg } from '../../../../../src/MatrixClientPeg'; -import SdkConfig from '../../../../../src/SdkConfig'; import { SettingLevel } from '../../../../../src/settings/SettingLevel'; import SettingsStore from '../../../../../src/settings/SettingsStore'; From 2fa8bc0dcfe89d3e9fdc95d5dd38a28abb19a60a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 02:21:42 +0100 Subject: [PATCH 62/73] Typo --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aa2b252b18f..66042ac8516 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -931,7 +931,7 @@ "New session manager": "New session manager", "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", - "Allow a QR code to be shown in session manager to sign in a another device (requires compatible homeserver)": "Allow a QR code to be shown in session manager to sign in a another device (requires compatible homeserver)", + "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 89263dc9f0e..723b789ab01 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -499,7 +499,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { labsGroup: LabGroup.Experimental, supportedLevels: LEVELS_FEATURE, displayName: _td( - "Allow a QR code to be shown in session manager to sign in a another device " + + "Allow a QR code to be shown in session manager to sign in another device " + "(requires compatible homeserver)", ), default: false, From 0794cffb2fd512e7e5ea60469184269fe9d16ddc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 14:04:56 +0100 Subject: [PATCH 63/73] Stateless component --- .../views/settings/devices/LoginWithQRSection.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index df4e8e2ca71..20cdb37902e 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -27,13 +27,9 @@ interface IProps { versions: IServerVersions; } -interface IState {} - -export default class LoginWithQRSection extends React.Component { +export default class LoginWithQRSection extends React.Component { public constructor(props: IProps) { super(props); - - this.state = {}; } public render(): JSX.Element { From c6e0628423242f6fc04d703a3ddbbc2f97425bed Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 14:05:42 +0100 Subject: [PATCH 64/73] Whitespace --- res/css/views/auth/_LoginWithQR.pcss | 3 +++ src/components/views/auth/LoginWithQR.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 79851af2b96..390cf8311d0 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -1,9 +1,12 @@ /* Copyright 2022 The Matrix.org Foundation C.I.C. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 96ddb01b4b6..b947044f578 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -1,9 +1,12 @@ /* Copyright 2022 The Matrix.org Foundation C.I.C. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. From 343974fc5ebec3cfc5d5b72b0a828368741b3620 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 14:30:09 +0100 Subject: [PATCH 65/73] Use context not MatrixClientPeg --- .../views/settings/DevicesPanel.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index b9121409ed3..45a50625052 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -21,12 +21,12 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { CryptoEvent } from 'matrix-js-sdk/src/crypto'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import DevicesPanelEntry from "./DevicesPanelEntry"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices'; +import MatrixClientContext from '../../../contexts/MatrixClientContext'; interface IProps { className?: string; @@ -41,6 +41,8 @@ interface IState { } export default class DevicesPanel extends React.Component { + static contextType = MatrixClientContext; + context!: React.ContextType; private unmounted = false; constructor(props: IProps) { @@ -53,24 +55,22 @@ export default class DevicesPanel extends React.Component { } public componentDidMount(): void { - const cli = MatrixClientPeg.get(); - cli.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); + this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.loadDevices(); } public componentWillUnmount(): void { - const cli = MatrixClientPeg.get(); - cli.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); + this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.unmounted = true; } private onDevicesUpdated = (users: string[]) => { - if (!users.includes(MatrixClientPeg.get().getUserId())) return; + if (!users.includes(this.context.getUserId())) return; this.loadDevices(); }; private loadDevices(): void { - const cli = MatrixClientPeg.get(); + const cli = this.context; cli.getDevices().then( (resp) => { if (this.unmounted) { return; } @@ -121,7 +121,7 @@ export default class DevicesPanel extends React.Component { private isDeviceVerified(device: IMyDevice): boolean | null { try { - const cli = MatrixClientPeg.get(); + const cli = this.context; const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id); return this.state.crossSigningInfo.checkDeviceTrust( this.state.crossSigningInfo, @@ -194,7 +194,7 @@ export default class DevicesPanel extends React.Component { try { await deleteDevicesWithInteractiveAuth( - MatrixClientPeg.get(), + this.context, this.state.selectedDevices, (success) => { if (success) { @@ -218,7 +218,7 @@ export default class DevicesPanel extends React.Component { }; private renderDevice = (device: IMyDevice): JSX.Element => { - const myDeviceId = MatrixClientPeg.get().getDeviceId(); + const myDeviceId = this.context.getDeviceId(); const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId)); const isOwnDevice = device.device_id === myDeviceId; @@ -256,7 +256,7 @@ export default class DevicesPanel extends React.Component { return ; } - const myDeviceId = MatrixClientPeg.get().getDeviceId(); + const myDeviceId = this.context.getDeviceId(); const myDevice = devices.find((device) => (device.device_id === myDeviceId)); if (!myDevice) { From 710edf93bb2e91dffd62bcb8c62349cf7a5c352f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:28:41 +0100 Subject: [PATCH 66/73] Add missing context --- test/components/views/settings/DevicesPanel-test.tsx | 6 +++++- .../settings/tabs/user/SecurityUserSettingsTab-test.tsx | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/components/views/settings/DevicesPanel-test.tsx b/test/components/views/settings/DevicesPanel-test.tsx index a7baf139af3..81f6fb328a6 100644 --- a/test/components/views/settings/DevicesPanel-test.tsx +++ b/test/components/views/settings/DevicesPanel-test.tsx @@ -28,6 +28,7 @@ import { mkPusher, mockClientMethodsUser, } from "../../../test-utils"; +import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; describe('', () => { const userId = '@alice:server.org'; @@ -46,7 +47,10 @@ describe('', () => { setPusher: jest.fn(), }); - const getComponent = () => ; + const getComponent = () => + + + ; beforeEach(() => { jest.clearAllMocks(); diff --git a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx index bddb493463f..3497f2f161f 100644 --- a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -17,6 +17,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab"; +import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext'; import SettingsStore from '../../../../../../src/settings/SettingsStore'; import { getMockClientWithEventEmitter, @@ -31,11 +32,10 @@ describe('', () => { const defaultProps = { closeSettingsFn: jest.fn(), }; - const getComponent = () => ; const userId = '@alice:server.org'; const deviceId = 'alices-device'; - getMockClientWithEventEmitter({ + const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsServer(), ...mockClientMethodsDevice(deviceId), @@ -44,6 +44,11 @@ describe('', () => { getIgnoredUsers: jest.fn(), }); + const getComponent = () => + + + ; + const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue'); beforeEach(() => { From d8c94f2d498ec4c9c125bedb516ce8619028ad1d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:29:17 +0100 Subject: [PATCH 67/73] Type updates to match js-sdk --- src/components/views/auth/LoginWithQR.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index b947044f578..90aeac87c93 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from 'react'; -import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; +import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; -import { MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; +import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels'; import { logger } from 'matrix-js-sdk/src/logger'; import { MatrixClient } from 'matrix-js-sdk/src/client'; @@ -149,12 +149,14 @@ export default class LoginWithQR extends React.Component { private generateCode = async () => { let rendezvous: MSC3906Rendezvous; try { - const transport = new MSC3886SimpleHttpRendezvousTransport({ + const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, client: this.props.client, }); - const channel = new MSC3903ECDHv1RendezvousChannel(transport, undefined, this.onFailure); + const channel = new MSC3903ECDHv1RendezvousChannel( + transport, undefined, this.onFailure, + ); rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); From 984bf7236abcbc0ebf4b25f102af6cd0c62d56d2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 17:34:47 +0100 Subject: [PATCH 68/73] Wrap click handlers in useCallback --- .../views/settings/tabs/user/SessionManagerTab.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 115d3a087f7..49ca1bdbf29 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -184,10 +184,18 @@ const SessionManagerTab: React.FC = () => { const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show"); + const onQrFinish = useCallback(() => { + setSignInWithQrMode(null); + }, [setSignInWithQrMode]); + + const onShowQrClicked = useCallback(() => { + setSignInWithQrMode(Mode.Show); + }, [setSignInWithQrMode]); + if (showQrCodeEnabled && signInWithQrMode) { return setSignInWithQrMode(null)} + onFinished={onQrFinish} client={matrixClient} />; } @@ -240,7 +248,7 @@ const SessionManagerTab: React.FC = () => { } { showQrCodeEnabled ? - setSignInWithQrMode(Mode.Show)} versions={clientVersions} /> + : null } ; From 4e417adfffaee8fb46ad25c2f88c64dee64f450c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 19 Oct 2022 09:18:14 +0100 Subject: [PATCH 69/73] Update src/components/views/settings/DevicesPanel.tsx Co-authored-by: Travis Ralston --- src/components/views/settings/DevicesPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index 45a50625052..1b06fa06fe6 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -41,8 +41,8 @@ interface IState { } export default class DevicesPanel extends React.Component { - static contextType = MatrixClientContext; - context!: React.ContextType; + public static contextType = MatrixClientContext; + public context!: React.ContextType; private unmounted = false; constructor(props: IProps) { From 6b682b5a96dc6187edb655adbbb6be0630e351ce Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 19 Oct 2022 10:00:38 +0100 Subject: [PATCH 70/73] Wait for DOM update instead of timeout --- src/components/views/auth/LoginWithQR.tsx | 2 +- .../settings/devices/LoginWithQR-test.tsx | 23 ++----------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 90aeac87c93..5ad4eca18a5 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -286,7 +286,7 @@ export default class LoginWithQR extends React.Component { centreTitle = true; titleIcon = ; backButton = false; - main =

{ cancellationMessage }

; + main =

{ cancellationMessage }

; buttons = <> ', () => { onFinished: () => {}, }; - beforeAll(() => { - global.Olm = { - SAS: jest.fn().mockImplementation(() => { - return { - get_pubkey: jest.fn().mockReturnValue('mock-public-key'), - free: jest.fn(), - } as unknown as SAS; - }), - } as unknown as typeof global.Olm; - }); - - beforeEach(() => { - }); - const getComponent = (props: { client: MatrixClient }) => (); it('no content in case of no support', async () => { const { container } = render(getComponent({ client })); - await act(async () => { - await sleep(1000); - }); + await waitFor(() => screen.getAllByTestId('cancellation-message').length === 1); expect(container).toMatchSnapshot(); }); }); From c22479cb76df2153d8afa33d3cd4a7127f97fdf9 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 19 Oct 2022 10:26:42 +0100 Subject: [PATCH 71/73] Add missing snapshot update from last commit --- .../settings/devices/__snapshots__/LoginWithQR-test.tsx.snap | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap index de4abcf2879..a0c5759ab4c 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -18,7 +18,9 @@ exports[` no content in case of no support 1`] = `
-

+

The homeserver doesn't support signing in another device.

From edbe5596877f8aab187c7abca2754188be3e4238 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 19 Oct 2022 10:27:53 +0100 Subject: [PATCH 72/73] Remove void keyword in favour of then() clauses --- src/components/views/auth/LoginWithQR.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 5ad4eca18a5..e11d80bfa56 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -82,12 +82,12 @@ export default class LoginWithQR extends React.Component { } public componentDidMount(): void { - void this.updateMode(this.props.mode); + this.updateMode(this.props.mode).then(() => {}); } public componentDidUpdate(prevProps: Readonly): void { if (prevProps.mode !== this.props.mode) { - void this.updateMode(this.props.mode); + this.updateMode(this.props.mode).then(() => {}); } } @@ -108,7 +108,7 @@ export default class LoginWithQR extends React.Component { // 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 - void this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); + this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {}); } } @@ -217,8 +217,8 @@ export default class LoginWithQR extends React.Component { await this.updateMode(this.props.mode); }; - private onBackClick = () => { - void this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); + private onBackClick = async () => { + await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); this.props.onFinished(false); }; From c7532d908a970a5bd6aae4fb4c7c299f6611121a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 19 Oct 2022 13:26:38 +0200 Subject: [PATCH 73/73] test main paths in LoginWithQR --- src/components/views/auth/LoginWithQR.tsx | 11 +- .../settings/devices/LoginWithQR-test.tsx | 245 +++++++++++++- .../__snapshots__/LoginWithQR-test.tsx.snap | 320 ++++++++++++++++++ 3 files changed, 572 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e11d80bfa56..f95e618cc52 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -316,12 +316,14 @@ export default class LoginWithQR extends React.Component { buttons = <> { _t("Cancel") } @@ -371,7 +373,14 @@ export default class LoginWithQR extends React.Component {
{ backButton ? - + + + : null }

{ titleIcon }{ title }

diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index d50b8862801..c106b2f9a86 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -14,12 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { mocked } from 'jest-mock'; import React from 'react'; +import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports'; +import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous'; import LoginWithQR, { Mode } from '../../../../../src/components/views/auth/LoginWithQR'; import type { MatrixClient } from 'matrix-js-sdk/src/matrix'; +import { flushPromisesWithFakeTimers } from '../../../../test-utils'; + +jest.useFakeTimers(); + +jest.mock('matrix-js-sdk/src/rendezvous'); +jest.mock('matrix-js-sdk/src/rendezvous/transports'); +jest.mock('matrix-js-sdk/src/rendezvous/channels'); function makeClient() { return mocked({ @@ -34,6 +43,7 @@ function makeClient() { mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), doesServerSupportUnstableFeature: jest.fn().mockReturnValue(true), removeListener: jest.fn(), + requestLoginToken: jest.fn(), currentState: { on: jest.fn(), }, @@ -44,15 +54,244 @@ describe('', () => { const client = makeClient(); const defaultProps = { mode: Mode.Show, - onFinished: () => {}, + onFinished: jest.fn(), }; + const mockConfirmationDigits = 'mock-confirmation-digits'; + const newDeviceId = 'new-device-id'; - const getComponent = (props: { client: MatrixClient }) => + const getComponent = (props: { client: MatrixClient, onFinished?: () => void }) => (); + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRestore(); + jest.spyOn(MSC3906Rendezvous.prototype, 'cancel').mockResolvedValue(); + jest.spyOn(MSC3906Rendezvous.prototype, 'declineLoginOnExistingDevice').mockResolvedValue(); + jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockResolvedValue(mockConfirmationDigits); + jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockResolvedValue(newDeviceId); + client.requestLoginToken.mockResolvedValue({ + login_token: 'token', + expires_in: 1000, + }); + // @ts-ignore + client.crypto = undefined; + }); + it('no content in case of no support', async () => { + // simulate no support + jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRejectedValue(''); const { container } = render(getComponent({ client })); await waitFor(() => screen.getAllByTestId('cancellation-message').length === 1); expect(container).toMatchSnapshot(); }); + + it('renders spinner while generating code', async () => { + const { container } = render(getComponent({ client })); + expect(container).toMatchSnapshot(); + }); + + it('cancels rendezvous after user goes back', async () => { + const { getByTestId } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('back-button')); + + // wait for cancel + await flushPromisesWithFakeTimers(); + + expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled); + }); + + it('displays qr code after it is created', async () => { + const { container, getByText } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + await flushPromisesWithFakeTimers(); + + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(getByText('Sign in with QR code')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + it('displays confirmation digits after connected to rendezvous', async () => { + const { container, getByText } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + expect(container).toMatchSnapshot(); + expect(getByText(mockConfirmationDigits)).toBeTruthy(); + }); + + it('displays unknown error if connection to rendezvous fails', async () => { + const { container } = render(getComponent({ client })); + expect(MSC3886SimpleHttpRendezvousTransport).toHaveBeenCalledWith({ + onFailure: expect.any(Function), + client, + }); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + mocked(rendezvous).startAfterShowingCode.mockRejectedValue('oups'); + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + expect(container).toMatchSnapshot(); + }); + + it('declines login', async () => { + const { getByTestId } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('decline-login-button')); + + expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled(); + }); + + it('displays error when approving login fails', async () => { + const { container, getByTestId } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + client.requestLoginToken.mockRejectedValue('oups'); + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('approve-login-button')); + + expect(client.requestLoginToken).toHaveBeenCalled(); + // flush token request promise + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(container).toMatchSnapshot(); + }); + + it('approves login and waits for new device', async () => { + const { container, getByTestId, getByText } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('approve-login-button')); + + expect(client.requestLoginToken).toHaveBeenCalled(); + // flush token request promise + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(getByText('Waiting for device to sign in')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + it('does not continue with verification when user denies login', async () => { + const onFinished = jest.fn(); + const { getByTestId } = render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + // no device id returned => user denied + mocked(rendezvous).approveLoginOnExistingDevice.mockReturnValue(undefined); + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('approve-login-button')); + + // flush token request promise + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled(); + + await flushPromisesWithFakeTimers(); + expect(onFinished).not.toHaveBeenCalled(); + expect(rendezvous.verifyNewDeviceOnExistingDevice).not.toHaveBeenCalled(); + }); + + it('waits for device approval on existing device and finishes when crypto is not setup', async () => { + const { getByTestId } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('approve-login-button')); + + // flush token request promise + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled(); + await flushPromisesWithFakeTimers(); + expect(defaultProps.onFinished).toHaveBeenCalledWith(true); + // didnt attempt verification + expect(rendezvous.verifyNewDeviceOnExistingDevice).not.toHaveBeenCalled(); + }); + + it('waits for device approval on existing device and verifies device', async () => { + const { getByTestId } = render(getComponent({ client })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + // @ts-ignore assign to private prop + rendezvous.code = 'rendezvous-code'; + // we just check for presence of crypto + // pretend it is set up + // @ts-ignore + client.crypto = {}; + + // flush generate code promise + await flushPromisesWithFakeTimers(); + // flush waiting for connection promise + await flushPromisesWithFakeTimers(); + + fireEvent.click(getByTestId('approve-login-button')); + + // flush token request promise + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled(); + // flush login approval + await flushPromisesWithFakeTimers(); + expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); + // flush verification + await flushPromisesWithFakeTimers(); + expect(defaultProps.onFinished).toHaveBeenCalledWith(true); + }); }); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap index a0c5759ab4c..91fe73abf40 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQR-test.tsx.snap @@ -1,5 +1,279 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` approves login and waits for new device 1`] = ` +
+
+
+
+
+
+

+

+
+
+
+
+
+
+

+ Waiting for device to sign in +

+
+
+
+
+
+ Cancel +
+
+
+
+`; + +exports[` displays confirmation digits after connected to rendezvous 1`] = ` +
+
+
+

+
+ Devices connected +

+
+
+

+ Check that the code below matches with your other device: +

+
+ mock-confirmation-digits +
+
+
+
+
+
+ By approving access for this device, it will have full access to your account. +
+
+
+
+
+ Cancel +
+
+ Approve +
+
+
+
+`; + +exports[` displays error when approving login fails 1`] = ` +
+
+
+

+
+ Connection failed +

+
+
+

+ An unexpected error occurred. +

+
+
+
+ Try again +
+
+ Cancel +
+
+
+
+`; + +exports[` displays qr code after it is created 1`] = ` +
+
+
+
+
+
+

+ Sign in with QR code +

+
+
+

+ Scan the QR code below with your device that's signed out. +

+
    +
  1. + Start at the sign in screen +
  2. +
  3. + Select 'Scan QR code' +
  4. +
  5. + Review and approve the sign in +
  6. +
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[` displays unknown error if connection to rendezvous fails 1`] = ` +
+
+
+

+
+ Connection failed +

+
+
+

+ An unexpected error occurred. +

+
+
+
+ Try again +
+
+ Cancel +
+
+
+
+`; + exports[` no content in case of no support 1`] = `
no content in case of no support 1`] = `
`; + +exports[` renders spinner while generating code 1`] = ` +
+
+
+
+
+
+

+

+
+
+
+
+
+
+
+
+
+
+
+
+`;