diff --git a/.changeset/loud-donkeys-glow.md b/.changeset/loud-donkeys-glow.md
new file mode 100644
index 00000000000..3a2070d5cf6
--- /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 78196b972dd..9bae1018da4 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 3b6aa7cc300..5f0b869bf3a 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 32a1ff05604..bc672dcad49 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 1d9dff133e0..93c3f14b10e 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 08ed51acac1..e8b7c1da63b 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 00000000000..f294f04116f
--- /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 00000000000..c16fd04be0f
--- /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 1e09b07d492..84d33a73f01 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 cd5359f8f3c..0f9efb663e5 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 e4ebd009d9e..16eb680e149 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 afb0e087ea4..4e7d6e2f4a0 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 008f1369fdd..d4a6135a2aa 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+/=]*$/);
+}