From 52ae4d3ea2ae52306e868923e48f4a5807a78d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Grellard?= Date: Fri, 13 Sep 2024 10:01:57 +0200 Subject: [PATCH] Ledger Sync - Display error when scanning invalid QR Code (#7800) fix(llm): ledger sync display error when scanning invalid qr code --- .changeset/loud-donkeys-glow.md | 6 +++ .../src/locales/en/common.json | 14 +++++ .../AddAccount/components/StepFlow.tsx | 8 +++ .../components/Activation/ActivationFlow.tsx | 8 +++ .../hooks/useLedgerSyncAnalytics.ts | 2 + .../WalletSync/hooks/useSyncWithQrCode.ts | 8 ++- .../Synchronize/ScannedInvalidQrCode.tsx | 34 ++++++++++++ .../Synchronize/ScannedOldImportQrCode.tsx | 34 ++++++++++++ .../features/WalletSync/types/Activation.ts | 2 + .../src/screens/ImportAccounts/Scan.tsx | 11 +++- libs/trustchain/src/errors.ts | 3 ++ libs/trustchain/src/qrcode/index.test.ts | 52 +++++++++++++++++++ libs/trustchain/src/qrcode/index.ts | 9 +++- 13 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 .changeset/loud-donkeys-glow.md create mode 100644 apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode.tsx diff --git a/.changeset/loud-donkeys-glow.md b/.changeset/loud-donkeys-glow.md new file mode 100644 index 000000000000..3a2070d5cf69 --- /dev/null +++ b/.changeset/loud-donkeys-glow.md @@ -0,0 +1,6 @@ +--- +"live-mobile": patch +"@ledgerhq/trustchain": patch +--- + +Ledger Sync - Display relevant error when scanning old accounts export qr code or an invalid one diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index 78196b972dd8..9bae1018da45 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -977,6 +977,10 @@ }, "DeleteAppDataError": { "title": "Error deleting app data" + }, + "ScannedNewImportQrCode": { + "title": "Update required", + "description": "To sync your apps, please make sure both are updated to the latest version." } }, "crash": { @@ -6875,6 +6879,16 @@ "tryAgain": "Try again" } }, + "scannedInvalidQrCode": { + "title": "Invalid QR Code", + "desc": "It looks like the QR code you scanned isn't valid. Please try again with a Ledger Sync valid QR code.", + "tryAgain": "Try again" + }, + "scannedOldQrCode": { + "title": "Update required", + "desc": "To sync your apps, please make sure both are updated to the latest version.", + "tryAgain": "Try again" + }, "unbacked": { "title": "You need to create your encryption key first", "description": "Please make sure you’ve created an encryption key on one of your Ledger Live apps before continuing your synchronization.", diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx index 3b6aa7cc300b..5f0b869bf3ae 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/StepFlow.tsx @@ -14,6 +14,8 @@ import { useSyncWithQrCode } from "LLM/features/WalletSync/hooks/useSyncWithQrCo import { SpecificError } from "LLM/features/WalletSync/components/Error/SpecificError"; import { ErrorReason } from "LLM/features/WalletSync/hooks/useSpecificError"; import { useCurrentStep } from "LLM/features/WalletSync/hooks/useCurrentStep"; +import ScannedInvalidQrCode from "~/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode"; +import ScannedOldImportQrCode from "~/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode"; type Props = { currency?: CryptoCurrency | TokenCurrency | null; @@ -97,6 +99,12 @@ const StepFlow = ({ case Steps.SyncError: return ; + case Steps.ScannedInvalidQrCode: + return ; + + case Steps.ScannedOldImportQrCode: + return ; + case Steps.UnbackedError: return ; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx index 32a1ff056042..bc672dcad497 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Activation/ActivationFlow.tsx @@ -17,6 +17,8 @@ import { AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; import PinCodeDisplay from "../../screens/Synchronize/PinCodeDisplay"; import PinCodeInput from "../../screens/Synchronize/PinCodeInput"; import SyncError from "../../screens/Synchronize/SyncError"; +import ScannedInvalidQrCode from "../../screens/Synchronize/ScannedInvalidQrCode"; +import ScannedOldImportQrCode from "../../screens/Synchronize/ScannedOldImportQrCode"; import { useInitMemberCredentials } from "../../hooks/useInitMemberCredentials"; import { useSyncWithQrCode } from "../../hooks/useSyncWithQrCode"; import { SpecificError } from "../Error/SpecificError"; @@ -102,6 +104,12 @@ const ActivationFlow = ({ case Steps.SyncError: return ; + case Steps.ScannedInvalidQrCode: + return ; + + case Steps.ScannedOldImportQrCode: + return ; + case Steps.UnbackedError: if (!hasCompletedOnboarding) { return ( diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts index 1d9dff133e0a..93c3f14b10e8 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics.ts @@ -10,6 +10,8 @@ export enum AnalyticsPage { SyncWithQrCode = "Sync with QR code", PinCode = "Pin code", PinCodesDoNotMatch = "Pin codes don't match", + ScannedInvalidQrCode = "Scanned invalid QR code", + ScannedIncompatibleApps = "Scans incompatible apps", Loading = "Loading", SettingsGeneral = "Settings General", LedgerSyncSettings = "Ledger Sync Settings", diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useSyncWithQrCode.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useSyncWithQrCode.ts index 08ed51acac13..e8b7c1da63be 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useSyncWithQrCode.ts +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useSyncWithQrCode.ts @@ -2,6 +2,8 @@ import { useCallback, useState } from "react"; import { MemberCredentials, TrustchainMember } from "@ledgerhq/trustchain/types"; import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index"; import { + ScannedOldImportQrCode, + ScannedInvalidQrCode, InvalidDigitsError, NoTrustchainInitialized, TrustchainAlreadyInitialized, @@ -72,7 +74,11 @@ export const useSyncWithQrCode = () => { onSyncFinished(); return true; } catch (e) { - if (e instanceof InvalidDigitsError) { + if (e instanceof ScannedOldImportQrCode) { + setCurrentStep(Steps.ScannedOldImportQrCode); + } else if (e instanceof ScannedInvalidQrCode) { + setCurrentStep(Steps.ScannedInvalidQrCode); + } else if (e instanceof InvalidDigitsError) { setCurrentStep(Steps.SyncError); return; } else if (e instanceof NoTrustchainInitialized) { diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode.tsx new file mode 100644 index 000000000000..f294f04116f9 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { ErrorComponent } from "../../components/Error/Simple"; +import { AnalyticsButton, AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; +import { track } from "~/analytics"; + +interface Props { + tryAgain: () => void; +} + +export default function ScannedInvalidQrCode({ tryAgain }: Props) { + const { t } = useTranslation(); + + const onTryAgain = () => { + tryAgain(); + track("button_clicked", { + button: AnalyticsButton.TryAgain, + page: AnalyticsPage.ScannedInvalidQrCode, + }); + }; + + return ( + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode.tsx new file mode 100644 index 000000000000..c16fd04be0fa --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { ErrorComponent } from "../../components/Error/Simple"; +import { AnalyticsButton, AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics"; +import { track } from "~/analytics"; + +interface Props { + tryAgain: () => void; +} + +export default function ScannedOldImportQrCode({ tryAgain }: Props) { + const { t } = useTranslation(); + + const onTryAgain = () => { + tryAgain(); + track("button_clicked", { + button: AnalyticsButton.TryAgain, + page: AnalyticsPage.ScannedIncompatibleApps, + }); + }; + + return ( + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts index 1e09b07d4929..84d33a73f012 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/types/Activation.ts @@ -14,6 +14,8 @@ export enum Steps { PinDisplay = "PinDisplay", PinInput = "PinInput", SyncError = "SyncError", + ScannedInvalidQrCode = "ScannedInvalidQrCode", + ScannedOldImportQrCode = "ScannedOldImportQrCode", UnbackedError = "UnbackedError", AlreadyBacked = "AlreadyBacked", BackedWithDifferentSeeds = "BackedWithDifferentSeeds", diff --git a/apps/ledger-live-mobile/src/screens/ImportAccounts/Scan.tsx b/apps/ledger-live-mobile/src/screens/ImportAccounts/Scan.tsx index cd5359f8f3ce..0f9efb663e5b 100644 --- a/apps/ledger-live-mobile/src/screens/ImportAccounts/Scan.tsx +++ b/apps/ledger-live-mobile/src/screens/ImportAccounts/Scan.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from "react"; import { StyleSheet, View } from "react-native"; import { parseFramesReducer, framesToData, areFramesComplete, progressOfFrames } from "qrloop"; import { Result as ImportAccountsResult, decode } from "@ledgerhq/live-wallet/liveqr/cross"; -import { TrackScreen } from "~/analytics"; +import { screen, TrackScreen } from "~/analytics"; import { ScreenName } from "~/const"; import Scanner from "~/components/Scanner"; import GenericErrorBottomModal from "~/components/GenericErrorBottomModal"; @@ -11,6 +11,8 @@ import { withTheme } from "../../colors"; import type { Theme } from "../../colors"; import type { ImportAccountsNavigatorParamList } from "~/components/RootNavigator/types/ImportAccountsNavigator"; import type { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +import { ScannedNewImportQrCode } from "@ledgerhq/trustchain/errors"; +import { AnalyticsPage } from "~/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics"; type NavigationProps = StackNavigatorProps< ImportAccountsNavigatorParamList, @@ -75,6 +77,13 @@ class Scan extends PureComponent< } } } catch (e) { + if (data.match(/host=([0-9A-Fa-f]+)/)) { + this.setState({ + error: new ScannedNewImportQrCode(), + progress: 0, + }); + screen("", AnalyticsPage.ScannedIncompatibleApps, { source: "Account Import Sync" }); + } console.warn(e); } } diff --git a/libs/trustchain/src/errors.ts b/libs/trustchain/src/errors.ts index e4ebd009d9ec..16eb680e1493 100644 --- a/libs/trustchain/src/errors.ts +++ b/libs/trustchain/src/errors.ts @@ -1,5 +1,8 @@ import { createCustomErrorClass } from "@ledgerhq/errors"; +export const ScannedOldImportQrCode = createCustomErrorClass("ScannedOldImportQrCode"); +export const ScannedNewImportQrCode = createCustomErrorClass("ScannedNewImportQrCode"); +export const ScannedInvalidQrCode = createCustomErrorClass("ScannedInvalidQrCode"); export const InvalidDigitsError = createCustomErrorClass("InvalidDigitsError"); export const InvalidEncryptionKeyError = createCustomErrorClass("InvalidEncryptionKeyError"); export const TrustchainEjected = createCustomErrorClass("TrustchainEjected"); diff --git a/libs/trustchain/src/qrcode/index.test.ts b/libs/trustchain/src/qrcode/index.test.ts index afb0e087ea49..4e7d6e2f4a06 100644 --- a/libs/trustchain/src/qrcode/index.test.ts +++ b/libs/trustchain/src/qrcode/index.test.ts @@ -2,6 +2,7 @@ import { createQRCodeHostInstance, createQRCodeCandidateInstance } from "."; import WebSocket from "ws"; import { convertKeyPairToLiveCredentials } from "../sdk"; import { crypto } from "@ledgerhq/hw-trustchain"; +import { ScannedInvalidQrCode, ScannedOldImportQrCode } from "../errors"; describe("Trustchain QR Code", () => { let server; @@ -83,4 +84,55 @@ describe("Trustchain QR Code", () => { ); expect(res).toEqual(trustchain); }); + test("invalid qr code scanned", async () => { + const trustchain = { + rootId: "test-root-id", + walletSyncEncryptionKey: "test-wallet-sync-encryption-key", + applicationPath: "m/0'/16'/0'", + }; + const addMember = jest.fn(() => Promise.resolve(trustchain)); + const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair()); + const memberName = "foo"; + + const onRequestQRCodeInput = jest.fn(); + + const scannedUrl = "https://example.com"; + + const candidateP = createQRCodeCandidateInstance({ + memberCredentials, + memberName, + initialTrustchainId: undefined, + addMember, + scannedUrl, + onRequestQRCodeInput, + }); + + await expect(candidateP).rejects.toThrow(new ScannedInvalidQrCode()); + }); + test("old accounts export qr code scanned", async () => { + const trustchain = { + rootId: "test-root-id", + walletSyncEncryptionKey: "test-wallet-sync-encryption-key", + applicationPath: "m/0'/16'/0'", + }; + const addMember = jest.fn(() => Promise.resolve(trustchain)); + const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair()); + const memberName = "foo"; + + const onRequestQRCodeInput = jest.fn(); + + const scannedUrl = + "ZAADAAIAAAAEd2JXMpuoYdzvkNzFTlmQLPcGf2LSjDOgqaB3nQoZqlimcCX6HNkescWKyT1DCGuwO7IesD7oYg+fdZPkiIfFL3V9swfZRePkaNN09IjXsWLsim9hK/qi/RC1/ofX3hYNKUxUAgYVVG82WKXIk47siWfUlRZsCYSAARQ6ASpUgidPjMHaOMK6w53wTZplwo7Zjv1HrIyKwr3Ci8OmrFye5g=="; + + const candidateP = createQRCodeCandidateInstance({ + memberCredentials, + memberName, + initialTrustchainId: undefined, + addMember, + scannedUrl, + onRequestQRCodeInput, + }); + + await expect(candidateP).rejects.toThrow(new ScannedOldImportQrCode()); + }); }); diff --git a/libs/trustchain/src/qrcode/index.ts b/libs/trustchain/src/qrcode/index.ts index 008f1369fddf..d4a6135a2aa0 100644 --- a/libs/trustchain/src/qrcode/index.ts +++ b/libs/trustchain/src/qrcode/index.ts @@ -7,6 +7,8 @@ import { InvalidDigitsError, NoTrustchainInitialized, QRCodeWSClosed, + ScannedInvalidQrCode, + ScannedOldImportQrCode, TrustchainAlreadyInitialized, } from "../errors"; import { log } from "@ledgerhq/logs"; @@ -267,7 +269,8 @@ export async function createQRCodeCandidateInstance({ }): Promise { const m = scannedUrl.match(/host=([0-9A-Fa-f]+)/); if (!m) { - throw new Error("invalid scannedUrl"); + if (isFromOldAccountsImport(scannedUrl)) throw new ScannedOldImportQrCode(); + throw new ScannedInvalidQrCode(); } const hostPublicKey = crypto.from_hex(m[1]); const ephemeralKey = await crypto.randomKeypair(); @@ -386,3 +389,7 @@ function fromErrorMessage(payload: { message: string; type: string }): Error { error.name = "TrustchainQRCode-" + payload.type; return error; } + +function isFromOldAccountsImport(scannedUrl: string): boolean { + return !!scannedUrl.match(/^[A-Za-z0-9+/=]*$/); +}