diff --git a/.changeset/dirty-drinks-roll.md b/.changeset/dirty-drinks-roll.md
new file mode 100644
index 000000000000..05facbb142bc
--- /dev/null
+++ b/.changeset/dirty-drinks-roll.md
@@ -0,0 +1,8 @@
+---
+"ledger-live-desktop": minor
+"live-mobile": minor
+"@ledgerhq/trustchain": minor
+"@ledgerhq/web-tools": minor
+---
+
+Ledger Sync - Added the synchronization of a trustchain from mobile to desktop by scanning the QR code
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageSynchronizedInstances.test.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageSynchronizedInstances.test.tsx
index d00d6457455e..eb49135b4b45 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageSynchronizedInstances.test.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageSynchronizedInstances.test.tsx
@@ -7,6 +7,7 @@ jest.mock("../hooks/useTrustchainSdk", () => ({
useTrustchainSdk: () => ({
getMembers: (mockedSdk.getMembers = jest.fn()),
removeMember: (mockedSdk.removeMember = jest.fn()),
+ initMemberCredentials: (mockedSdk.initMemberCredentials = jest.fn()),
}),
}));
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageYourBackup.test.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageYourBackup.test.tsx
index cac8684a51ab..aaa2342cf87f 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageYourBackup.test.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/manageYourBackup.test.tsx
@@ -8,6 +8,7 @@ jest.mock("../hooks/useTrustchainSdk", () => ({
useTrustchainSdk: () => ({
destroyTrustchain: (mockedSdk.destroyTrustchain = jest.fn()),
getMembers: (mockedSdk.getMembers = jest.fn()),
+ initMemberCredentials: (mockedSdk.initMemberCredentials = jest.fn()),
}),
}));
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/useWatchWalletSync.test.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/useWatchWalletSync.test.ts
index f1f804126d93..6999cd88b901 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/useWatchWalletSync.test.ts
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/useWatchWalletSync.test.ts
@@ -31,6 +31,7 @@ jest.mock("../hooks/useTrustchainSdk", () => ({
useTrustchainSdk: () => ({
getMembers: (mockedSdk.getMembers = jest.fn()),
removeMember: (mockedSdk.removeMember = jest.fn()),
+ initMemberCredentials: (mockedSdk.initMemberCredentials = jest.fn()),
}),
}));
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/walletSync.test.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/walletSync.test.tsx
index db3c6386149c..2ab81b31cc00 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/walletSync.test.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/__tests__/walletSync.test.tsx
@@ -4,7 +4,15 @@
import React from "react";
import { render, screen, waitFor } from "tests/testUtils";
import { initialStateWalletSync } from "~/renderer/reducers/walletSync";
-import { WalletSyncTestApp } from "./shared";
+import { WalletSyncTestApp, mockedSdk } from "./shared";
+
+jest.mock("../hooks/useTrustchainSdk", () => ({
+ useTrustchainSdk: () => ({
+ getMembers: (mockedSdk.getMembers = jest.fn()),
+ removeMember: (mockedSdk.removeMember = jest.fn()),
+ initMemberCredentials: (mockedSdk.initMemberCredentials = jest.fn()),
+ }),
+}));
describe("Rendering", () => {
it("should loads and displays WalletSync Row", async () => {
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/Error.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/Error.tsx
index 4480ddc37f57..df196401be73 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/Error.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/Error.tsx
@@ -11,6 +11,7 @@ type Props = {
cta?: string;
onClick?: () => void;
analyticsPage?: AnalyticsPage;
+ outline?: boolean;
};
const Container = styled(Box)`
@@ -23,7 +24,14 @@ const Container = styled(Box)`
justify-content: center;
`;
-export const Error = ({ title, description, cta, onClick, analyticsPage }: Props) => {
+export const Error = ({
+ title,
+ description,
+ cta,
+ onClick,
+ analyticsPage,
+ outline = true,
+}: Props) => {
const { colors } = useTheme();
return (
@@ -38,7 +46,7 @@ export const Error = ({ title, description, cta, onClick, analyticsPage }: Props
{description}
{cta && onClick && (
-
+
{cta}
)}
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useInstanceName.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useInstanceName.ts
new file mode 100644
index 000000000000..e0bf04771251
--- /dev/null
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useInstanceName.ts
@@ -0,0 +1,14 @@
+import os from "os";
+
+const platformMap: Record = {
+ darwin: "Mac",
+ win32: "Windows",
+ linux: "Linux",
+};
+
+export function useInstanceName(): string {
+ const platform = os.platform();
+ const hostname = os.hostname();
+ const name = `${platformMap[platform] || platform}${hostname ? " " + hostname : ""}`;
+ return name;
+}
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useQRCode.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useQRCode.ts
index 447d34f8daa3..255190e7ed8f 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useQRCode.ts
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useQRCode.ts
@@ -1,15 +1,20 @@
import { useCallback, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
+import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { useDispatch, useSelector } from "react-redux";
import { setFlow, setQrCodePinCode } from "~/renderer/actions/walletSync";
import { Flow, Step } from "~/renderer/reducers/walletSync";
-import { trustchainSelector, memberCredentialsSelector } from "@ledgerhq/trustchain/store";
+import {
+ trustchainSelector,
+ memberCredentialsSelector,
+ setTrustchain,
+} from "@ledgerhq/trustchain/store";
import { useTrustchainSdk } from "./useTrustchainSdk";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useQueryClient } from "@tanstack/react-query";
import { QueryKey } from "./type.hooks";
+import { useInstanceName } from "./useInstanceName";
export function useQRCode() {
const queryClient = useQueryClient();
@@ -24,13 +29,14 @@ export function useQRCode() {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
const [error, setError] = useState(null);
+ const memberName = useInstanceName();
const goToActivation = useCallback(() => {
dispatch(setFlow({ flow: Flow.Activation, step: Step.DeviceAction }));
}, [dispatch]);
const startQRCodeProcessing = useCallback(() => {
- if (!trustchain || !memberCredentials) return;
+ if (!memberCredentials) return;
setError(null);
setIsLoading(true);
@@ -44,18 +50,29 @@ export function useQRCode() {
dispatch(setFlow({ flow: Flow.Synchronize, step: Step.PinCode }));
},
addMember: async member => {
- await sdk.addMember(trustchain, memberCredentials, member);
- return trustchain;
+ if (trustchain) {
+ await sdk.addMember(trustchain, memberCredentials, member);
+ return trustchain;
+ }
+ throw new NoTrustchainInitialized();
},
+ memberCredentials,
+ memberName,
+ alreadyHasATrustchain: !!trustchain,
})
.catch(e => {
if (e instanceof InvalidDigitsError) {
dispatch(setFlow({ flow: Flow.Synchronize, step: Step.PinCodeError }));
+ } else if (e instanceof NoTrustchainInitialized) {
+ dispatch(setFlow({ flow: Flow.Synchronize, step: Step.UnbackedError }));
}
setError(e);
throw e;
})
- .then(() => {
+ .then(newTrustchain => {
+ if (newTrustchain) {
+ dispatch(setTrustchain(newTrustchain));
+ }
dispatch(setFlow({ flow: Flow.Synchronize, step: Step.Synchronized }));
queryClient.invalidateQueries({ queryKey: [QueryKey.getMembers] });
setUrl(null);
@@ -63,7 +80,7 @@ export function useQRCode() {
setIsLoading(false);
setError(null);
});
- }, [trustchain, memberCredentials, trustchainApiBaseUrl, dispatch, sdk, queryClient]);
+ }, [memberCredentials, trustchainApiBaseUrl, memberName, dispatch, trustchain, sdk, queryClient]);
return {
url,
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts
index 25f806aeefe1..005b22b69803 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts
@@ -1,4 +1,3 @@
-import os from "os";
import { useMemo } from "react";
import { getEnv } from "@ledgerhq/live-env";
import { getSdk } from "@ledgerhq/trustchain/index";
@@ -10,12 +9,7 @@ import { walletSyncStateSelector } from "@ledgerhq/live-wallet/store";
import { TrustchainSDK } from "@ledgerhq/trustchain/types";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
-
-const platformMap: Record = {
- darwin: "Mac",
- win32: "Windows",
- linux: "Linux",
-};
+import { useInstanceName } from "./useInstanceName";
let sdkInstance: TrustchainSDK | null = null;
@@ -24,15 +18,13 @@ export function useTrustchainSdk() {
const { trustchainApiBaseUrl, cloudSyncApiBaseUrl } = getWalletSyncEnvironmentParams(
featureWalletSync?.params?.environment,
);
+ const name = useInstanceName();
const isMockEnv = !!getEnv("MOCK");
const defaultContext = useMemo(() => {
const applicationId = 16;
- const platform = os.platform();
- const hostname = os.hostname();
- const name = `${platformMap[platform] || platform}${hostname ? " " + hostname : ""}`;
return { applicationId, name, apiBaseUrl: trustchainApiBaseUrl };
- }, [trustchainApiBaseUrl]);
+ }, [trustchainApiBaseUrl, name]);
const store = useStore();
const lifecycle = useMemo(
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts
index d4264c94af85..7316d49e27bd 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWalletSyncAnalytics.ts
@@ -27,6 +27,7 @@ export enum AnalyticsPage {
SyncWithQR = "Sync with QR code",
PinCode = "Pin code",
PinCodeError = "Pin code error",
+ UnbackedError = "No trustchain initialized error",
SettingsGeneral = "Settings General",
WalletSyncSettings = "Wallet Sync Settings",
@@ -66,6 +67,7 @@ export const StepMappedToAnalytics: Record = {
[Step.PinCode]: AnalyticsPage.PinCode,
[Step.PinCodeError]: AnalyticsPage.PinCodeError,
[Step.Synchronized]: AnalyticsPage.KeyUpdated,
+ [Step.UnbackedError]: AnalyticsPage.UnbackedError,
//ManageInstances
[Step.SynchronizedInstances]: AnalyticsPage.ManageInstances,
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Activation/02-DeviceActionStep.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Activation/02-DeviceActionStep.tsx
index c86afe2cb7a4..82b66a720e40 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Activation/02-DeviceActionStep.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Activation/02-DeviceActionStep.tsx
@@ -1,14 +1,11 @@
import React from "react";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import OpenOrInstallTrustChainApp from "../DeviceActions/openOrInstall";
-import { useInitMemberCredentials } from "../../hooks/useInitMemberCredentials";
type Props = {
goNext: (device: Device) => void;
};
export default function DeviceActionStep({ goNext }: Props) {
- useInitMemberCredentials();
-
return ;
}
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/05-UnbackedError.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/05-UnbackedError.tsx
new file mode 100644
index 000000000000..a58a3df5acb8
--- /dev/null
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/05-UnbackedError.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { Error } from "../../components/Error";
+import { useTranslation } from "react-i18next";
+import { AnalyticsPage } from "../../hooks/useWalletSyncAnalytics";
+import { useDispatch } from "react-redux";
+import { setFlow } from "~/renderer/actions/walletSync";
+import { Flow, Step } from "~/renderer/reducers/walletSync";
+
+export default function UnbackedError() {
+ const { t } = useTranslation();
+
+ const dispatch = useDispatch();
+
+ return (
+ dispatch(setFlow({ flow: Flow.Activation, step: Step.DeviceAction }))}
+ outline={false}
+ />
+ );
+}
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/index.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/index.tsx
index 9b7659005c4e..ecfdb8319442 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/index.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/Synchronize/index.tsx
@@ -10,6 +10,7 @@ import PinCodeStep from "./03-PinCodeStep";
import SyncFinalStep from "./04-SyncFinalStep";
import { AnalyticsPage, useWalletSyncAnalytics } from "../../hooks/useWalletSyncAnalytics";
import PinCodeErrorStep from "./05-PinCodeError";
+import UnbackedErrorStep from "./05-UnbackedError";
import { BackProps, BackRef } from "../router";
const SynchronizeWallet = forwardRef((_props, ref) => {
@@ -66,23 +67,24 @@ const SynchronizeWallet = forwardRef((_props, ref) => {
case Step.PinCodeError:
return ;
+ case Step.UnbackedError:
+ return ;
+
case Step.Synchronized:
return ;
}
};
+ const centeredItems = [Step.Synchronized, Step.PinCodeError, Step.UnbackedError];
+
return (
{getStep()}
diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/router.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/router.tsx
index 418bc4715a9f..8f969bdd4aae 100644
--- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/router.tsx
+++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/screens/router.tsx
@@ -6,6 +6,7 @@ import SynchronizeWallet from "./Synchronize";
import WalletSyncManageInstances from "./ManageInstances";
import WalletSyncActivation from "./Activation";
import WalletSyncManage from "./Manage";
+import { useInitMemberCredentials } from "../hooks/useInitMemberCredentials";
export interface BackRef {
goBack: () => void;
@@ -14,6 +15,7 @@ export interface BackRef {
export interface BackProps {}
export const WalletSyncRouter = forwardRef((_props, ref) => {
+ useInitMemberCredentials();
const walletSyncFlow = useSelector(walletSyncFlowSelector);
switch (walletSyncFlow) {
diff --git a/apps/ledger-live-desktop/src/renderer/reducers/walletSync.ts b/apps/ledger-live-desktop/src/renderer/reducers/walletSync.ts
index 372e67813ed0..1c2143e2d47b 100644
--- a/apps/ledger-live-desktop/src/renderer/reducers/walletSync.ts
+++ b/apps/ledger-live-desktop/src/renderer/reducers/walletSync.ts
@@ -30,6 +30,7 @@ export enum Step {
SynchronizeWithQRCode = "SynchronizeWithQRCode",
PinCode = "PinCode",
PinCodeError = "PinCodeError",
+ UnbackedError = "UnbackedError",
Synchronized = "Synchronized",
//ManageInstances
diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json
index 2423d073c931..7807c0941e7f 100644
--- a/apps/ledger-live-desktop/static/i18n/en/app.json
+++ b/apps/ledger-live-desktop/static/i18n/en/app.json
@@ -6508,6 +6508,11 @@
}
}
},
+ "unbackedError": {
+ "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.",
+ "cta": "Create your encryption key"
+ },
"pinCode": {
"title": "Enter the code",
"description": "Type the code displayed on the other Ledger Live you want to sync with.",
diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/useWatchWalletSync.test.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/useWatchWalletSync.test.ts
index ac89b636896e..9a1d1ce9c74d 100644
--- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/useWatchWalletSync.test.ts
+++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/useWatchWalletSync.test.ts
@@ -31,6 +31,7 @@ jest.mock("../hooks/useTrustchainSdk", () => ({
useTrustchainSdk: () => ({
getMembers: (mockedSdk.getMembers = jest.fn()),
removeMember: (mockedSdk.removeMember = jest.fn()),
+ initMemberCredentials: (mockedSdk.initMemberCredentials = jest.fn()),
}),
}));
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 ad36ffd4a52e..59cf1eaa9ece 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
@@ -75,6 +75,7 @@ const ActivationFlow = ({
onQrCodeScanned={handleQrCodeScanned}
currentOption={currentOption}
setSelectedOption={setOption}
+ qrCodeValue={qrProcess.url}
/>
);
diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx
index 86a46ace77d3..2c8fcc7bd687 100644
--- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx
+++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/components/Synchronize/QrCode.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Flex, Text } from "@ledgerhq/native-ui";
+import { Flex, InfiniteLoader, Text } from "@ledgerhq/native-ui";
import styled, { useTheme } from "styled-components/native";
import QRCode from "react-native-qrcode-svg";
import getWindowDimensions from "~/logic/getWindowDimensions";
@@ -12,7 +12,7 @@ const Italic = styled(Text)`
// Won't work since we don't have inter italic font
type Props = {
- qrCodeValue: string;
+ qrCodeValue?: string | null;
};
const QrCode = ({ qrCodeValue }: Props) => {
@@ -79,12 +79,16 @@ const QrCode = ({ qrCodeValue }: Props) => {
justifyContent={"center"}
testID="ws-qr-code-displayed"
>
-
+ {qrCodeValue ? (
+
+ ) : (
+
+ )}
diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useQRCodeHost.ts b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useQRCodeHost.ts
index 4d39827ad4eb..e4cc8c127af7 100644
--- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useQRCodeHost.ts
+++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/hooks/useQRCodeHost.ts
@@ -1,8 +1,12 @@
import { useCallback, useEffect, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
-import { useSelector } from "react-redux";
-import { trustchainSelector, memberCredentialsSelector } from "@ledgerhq/trustchain/store";
+import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
+import { useDispatch, useSelector } from "react-redux";
+import {
+ trustchainSelector,
+ memberCredentialsSelector,
+ setTrustchain,
+} from "@ledgerhq/trustchain/store";
import { useTrustchainSdk } from "./useTrustchainSdk";
import { Options, Steps } from "../types/Activation";
import { useNavigation } from "@react-navigation/native";
@@ -11,6 +15,7 @@ import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useQueryClient } from "@tanstack/react-query";
import { QueryKey } from "./type.hooks";
+import { useInstanceName } from "./useInstanceName";
interface Props {
setCurrentStep: (step: Steps) => void;
@@ -23,11 +28,13 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
const trustchain = useSelector(trustchainSelector);
const memberCredentials = useSelector(memberCredentialsSelector);
const sdk = useTrustchainSdk();
+ const dispatch = useDispatch();
const featureWalletSync = useFeature("llmWalletSync");
const { trustchainApiBaseUrl } = getWalletSyncEnvironmentParams(
featureWalletSync?.params?.environment,
);
+ const memberName = useInstanceName();
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
@@ -37,37 +44,35 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
const navigation = useNavigation();
const startQRCodeProcessing = useCallback(() => {
- if (!trustchain || !memberCredentials || isLoading) return;
+ if (!memberCredentials || isLoading) return;
setError(null);
setIsLoading(true);
createQRCodeHostInstance({
trustchainApiBaseUrl,
onDisplayQRCode: url => {
+ console.log("url", url);
setUrl(url);
-
- //TODO-remove when clearing code, used to test behavior with webTool
- // eslint-disable-next-line no-console
- console.log("onDisplayQRCode", url);
},
onDisplayDigits: digits => {
setPinCode(digits);
setCurrentStep(Steps.PinDisplay);
},
addMember: async member => {
- await sdk.addMember(trustchain, memberCredentials, member);
- return trustchain;
+ if (trustchain) {
+ await sdk.addMember(trustchain, memberCredentials, member);
+ return trustchain;
+ }
+ throw new NoTrustchainInitialized();
},
+ memberCredentials,
+ memberName,
+ alreadyHasATrustchain: !!trustchain,
})
- .catch(e => {
- if (e instanceof InvalidDigitsError) {
- setCurrentStep(Steps.SyncError);
- return;
+ .then(newTrustchain => {
+ if (newTrustchain) {
+ dispatch(setTrustchain(newTrustchain));
}
- setError(e);
- throw e;
- })
- .then(() => {
queryClient.invalidateQueries({ queryKey: [QueryKey.getMembers] });
navigation.navigate(NavigatorName.WalletSync, {
screen: ScreenName.WalletSyncSuccess,
@@ -79,16 +84,29 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
setUrl(null);
setPinCode(null);
setIsLoading(false);
+ })
+ .catch(e => {
+ if (e instanceof InvalidDigitsError) {
+ setCurrentStep(Steps.SyncError);
+ return;
+ } else if (e instanceof NoTrustchainInitialized) {
+ setCurrentStep(Steps.UnbackedError);
+ return;
+ }
+ setError(e);
+ throw e;
});
}, [
trustchain,
memberCredentials,
isLoading,
trustchainApiBaseUrl,
+ memberName,
setCurrentStep,
sdk,
queryClient,
navigation,
+ dispatch,
]);
useEffect(() => {
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 74bb3df9af31..66a613cde89e 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
@@ -1,18 +1,21 @@
import { useCallback, useState } from "react";
-import { MemberCredentials } from "@ledgerhq/trustchain/types";
+import { MemberCredentials, TrustchainMember } from "@ledgerhq/trustchain/types";
import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
-import { setTrustchain } from "@ledgerhq/trustchain/store";
-import { useDispatch } from "react-redux";
+import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
+import { setTrustchain, trustchainSelector } from "@ledgerhq/trustchain/store";
+import { useDispatch, useSelector } from "react-redux";
import { useNavigation } from "@react-navigation/native";
import { Steps } from "../types/Activation";
import { NavigatorName, ScreenName } from "~/const";
import { useInstanceName } from "./useInstanceName";
+import { useTrustchainSdk } from "./useTrustchainSdk";
export const useSyncWithQrCode = () => {
const [nbDigits, setDigits] = useState(null);
const [input, setInput] = useState(null);
const instanceName = useInstanceName();
+ const trustchain = useSelector(trustchainSelector);
+ const sdk = useTrustchainSdk();
const navigation = useNavigation();
@@ -46,27 +49,37 @@ export const useSyncWithQrCode = () => {
setCurrentStep: (step: Steps) => void,
) => {
try {
- const trustchain = await createQRCodeCandidateInstance({
+ const newTrustchain = await createQRCodeCandidateInstance({
memberCredentials,
scannedUrl: url,
memberName: instanceName,
onRequestQRCodeInput,
+ addMember: async (member: TrustchainMember) => {
+ if (trustchain) {
+ await sdk.addMember(trustchain, memberCredentials, member);
+ return trustchain;
+ }
+ throw new NoTrustchainInitialized();
+ },
+ alreadyHasATrustchain: !!trustchain,
});
- dispatch(setTrustchain(trustchain));
+ if (newTrustchain) {
+ dispatch(setTrustchain(newTrustchain));
+ }
onSyncFinished();
return true;
} catch (e) {
if (e instanceof InvalidDigitsError) {
setCurrentStep(Steps.SyncError);
return;
- } else {
+ } else if (e instanceof NoTrustchainInitialized) {
setCurrentStep(Steps.UnbackedError);
return;
}
throw e;
}
},
- [instanceName, onRequestQRCodeInput, onSyncFinished, dispatch],
+ [instanceName, onRequestQRCodeInput, trustchain, dispatch, onSyncFinished, sdk],
);
const handleSendDigits = useCallback(
diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx
index 4c76f7bf62fd..03b083db11b3 100644
--- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx
+++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Synchronize/QrCodeMethod.tsx
@@ -15,9 +15,15 @@ interface Props {
setSelectedOption: (option: OptionsType) => void;
onQrCodeScanned: (data: string) => void;
currentOption: Options;
+ qrCodeValue?: string | null;
}
-const QrCodeMethod = ({ setSelectedOption, onQrCodeScanned, currentOption }: Props) => {
+const QrCodeMethod = ({
+ setSelectedOption,
+ onQrCodeScanned,
+ currentOption,
+ qrCodeValue,
+}: Props) => {
const { onClickTrack } = useLedgerSyncAnalytics();
const { t } = useTranslation();
@@ -44,7 +50,7 @@ const QrCodeMethod = ({ setSelectedOption, onQrCodeScanned, currentOption }: Pro
return (
<>
-
+
>
);
}
diff --git a/apps/web-tools/trustchain/components/App.tsx b/apps/web-tools/trustchain/components/App.tsx
index e9a93eb2c6c3..fc601875203c 100644
--- a/apps/web-tools/trustchain/components/App.tsx
+++ b/apps/web-tools/trustchain/components/App.tsx
@@ -267,6 +267,7 @@ const App = () => {
diff --git a/apps/web-tools/trustchain/components/AppQRCodeCandidate.tsx b/apps/web-tools/trustchain/components/AppQRCodeCandidate.tsx
index ff1b68a382c4..2e749fed5dce 100644
--- a/apps/web-tools/trustchain/components/AppQRCodeCandidate.tsx
+++ b/apps/web-tools/trustchain/components/AppQRCodeCandidate.tsx
@@ -1,18 +1,22 @@
import React, { useCallback, useState } from "react";
import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
+import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
import { memberNameForPubKey } from "./IdentityManager";
import { Input } from "./Input";
+import { useTrustchainSDK } from "../context";
export function AppQRCodeCandidate({
memberCredentials,
setTrustchain,
+ trustchain,
}: {
memberCredentials: MemberCredentials | null;
setTrustchain: (trustchain: Trustchain | null) => void;
+ trustchain: Trustchain | null;
}) {
+ const sdk = useTrustchainSDK();
const [scannedUrl, setScannedUrl] = useState(null);
const [input, setInput] = useState(null);
const [digits, setDigits] = useState(null);
@@ -33,9 +37,19 @@ export function AppQRCodeCandidate({
scannedUrl,
memberName: memberNameForPubKey(memberCredentials.pubkey),
onRequestQRCodeInput,
+ addMember: async member => {
+ if (trustchain) {
+ await sdk.addMember(trustchain, memberCredentials, member);
+ return trustchain;
+ }
+ throw new NoTrustchainInitialized();
+ },
+ alreadyHasATrustchain: !!trustchain,
})
.then(trustchain => {
- setTrustchain(trustchain);
+ if (trustchain) {
+ setTrustchain(trustchain);
+ }
return true;
})
.catch(e => {
@@ -54,7 +68,7 @@ export function AppQRCodeCandidate({
setInputCallback(null);
});
},
- [onRequestQRCodeInput, setTrustchain],
+ [onRequestQRCodeInput, sdk, setTrustchain, trustchain],
);
const handleSendDigits = useCallback(
diff --git a/apps/web-tools/trustchain/components/AppQRCodeHost.tsx b/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
index 55cad8bb0855..29fa9747c355 100644
--- a/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
+++ b/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
+import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { RenderActionable } from "./Actionable";
import QRCode from "./QRCode";
@@ -30,9 +30,15 @@ export function AppQRCodeHost({
setDigits(digits);
},
addMember: async member => {
- await sdk.addMember(trustchain, memberCredentials, member);
- return trustchain;
+ if (trustchain) {
+ await sdk.addMember(trustchain, memberCredentials, member);
+ return trustchain;
+ }
+ throw new NoTrustchainInitialized();
},
+ memberCredentials,
+ memberName: "WebTools",
+ alreadyHasATrustchain: !!trustchain,
})
.catch(e => {
if (e instanceof InvalidDigitsError) {
diff --git a/libs/trustchain/src/errors.ts b/libs/trustchain/src/errors.ts
index 9bb8eac49168..fe9757d53aa1 100644
--- a/libs/trustchain/src/errors.ts
+++ b/libs/trustchain/src/errors.ts
@@ -6,3 +6,7 @@ export const TrustchainEjected = createCustomErrorClass("TrustchainEjected");
export const TrustchainNotAllowed = createCustomErrorClass("TrustchainNotAllowed");
export const TrustchainOutdated = createCustomErrorClass("TrustchainOutdated");
export const TrustchainNotFound = createCustomErrorClass("TrustchainNotFound");
+export const NoTrustchainInitialized = createCustomErrorClass("NoTrustchainInitialized");
+export const TrustchainsAlreadyInitialized = createCustomErrorClass(
+ "TrustchainsAlreadyInitialized",
+);
diff --git a/libs/trustchain/src/qrcode/index.test.ts b/libs/trustchain/src/qrcode/index.test.ts
index 21798a907581..2861b0e3bcf9 100644
--- a/libs/trustchain/src/qrcode/index.test.ts
+++ b/libs/trustchain/src/qrcode/index.test.ts
@@ -39,6 +39,7 @@ describe("Trustchain QR Code", () => {
};
const addMember = jest.fn(() => Promise.resolve(trustchain));
const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
+ const memberName = "foo";
let scannedUrlResolve: (url: string) => void;
const scannedUrlPromise = new Promise(resolve => {
@@ -56,14 +57,18 @@ describe("Trustchain QR Code", () => {
onDisplayQRCode,
onDisplayDigits,
addMember,
+ memberCredentials,
+ memberName,
+ alreadyHasATrustchain: !!trustchain,
});
const scannedUrl = await scannedUrlPromise;
- const memberName = "foo";
const candidateP = createQRCodeCandidateInstance({
memberCredentials,
memberName,
+ alreadyHasATrustchain: false,
+ addMember,
scannedUrl,
onRequestQRCodeInput,
});
diff --git a/libs/trustchain/src/qrcode/index.ts b/libs/trustchain/src/qrcode/index.ts
index d866ac4f46cb..7b4f041ae272 100644
--- a/libs/trustchain/src/qrcode/index.ts
+++ b/libs/trustchain/src/qrcode/index.ts
@@ -3,13 +3,94 @@ import WebSocket from "isomorphic-ws";
import { MemberCredentials, Trustchain, TrustchainMember } from "../types";
import { makeCipher, makeMessageCipher } from "./cipher";
import { Message } from "./types";
-import { InvalidDigitsError } from "../errors";
+import {
+ InvalidDigitsError,
+ NoTrustchainInitialized,
+ TrustchainsAlreadyInitialized,
+} from "../errors";
import { log } from "@ledgerhq/logs";
const version = 1;
const CLOSE_TIMEOUT = 100; // just enough time for the onerror to appear before onclose
+const commonSwitch = async ({
+ data,
+ cipher,
+ addMember,
+ send,
+ publisher,
+ resolve,
+ memberCredentials,
+ memberName,
+ reject,
+ ws,
+ setFinished,
+ alreadyHasATrustchain,
+}) => {
+ switch (data.message) {
+ case "TrustchainShareCredential": {
+ if (!alreadyHasATrustchain) {
+ const payload = {
+ type: "UNEXPECTED_SHARE_CREDENTIAL",
+ message: "unexpected share credential",
+ };
+ send({ version, publisher, message: "Failure", payload });
+ throw new NoTrustchainInitialized("unexpected share credential");
+ }
+ setFinished(true);
+ if (!cipher) {
+ throw new Error("sessionEncryptionKey not set");
+ }
+ const { id, name } = await cipher.decryptMessage(data);
+ const trustchain = await addMember({ id, name, permissions: Permissions.OWNER });
+ const payload = await cipher.encryptMessagePayload({ trustchain });
+ send({ version, publisher, message: "TrustchainAddedMember", payload });
+ resolve();
+ break;
+ }
+
+ case "TrustchainRequestCredential": {
+ if (alreadyHasATrustchain) {
+ const payload = {
+ type: "UNEXPECTED_REQUEST_CREDENTIAL",
+ message: "unexpected request credential",
+ };
+ send({ version, publisher, message: "Failure", payload });
+ throw new TrustchainsAlreadyInitialized("unexpected request credential");
+ }
+ const payload = await cipher.encryptMessagePayload({
+ id: memberCredentials.pubkey,
+ name: memberName,
+ });
+ send({ version, publisher, message: "TrustchainShareCredential", payload });
+ break;
+ }
+ case "TrustchainAddedMember": {
+ setFinished(true);
+ const { trustchain } = await cipher.decryptMessage(data);
+ resolve(trustchain);
+ ws.close();
+ break;
+ }
+ case "Failure": {
+ setFinished(true);
+ log("trustchain/qrcode", "Failure", { data });
+ const error = fromErrorMessage(data.payload);
+ reject(error);
+ ws.close();
+ break;
+ }
+ case "HandshakeChallenge":
+ case "HandshakeCompletionSucceeded":
+ case "InitiateHandshake":
+ case "CompleteHandshakeChallenge":
+ break;
+ default:
+ throw new Error("unexpected message");
+ }
+};
+
/**
* establish a channel to be able to add a member to the trustchain after displaying the QR Code
* @returns a promise that resolves when this is done
@@ -19,6 +100,9 @@ export async function createQRCodeHostInstance({
onDisplayQRCode,
onDisplayDigits,
addMember,
+ memberCredentials,
+ memberName,
+ alreadyHasATrustchain,
}: {
/**
* the base URL of the trustchain API
@@ -36,7 +120,19 @@ export async function createQRCodeHostInstance({
* this function will need to using the TrustchainSDK (and use sdk.addMember)
*/
addMember: (member: TrustchainMember) => Promise;
-}): Promise {
+ /**
+ * the client credentials of the instance (given by TrustchainSDK)
+ */
+ memberCredentials: MemberCredentials;
+ /**
+ * the name of the member
+ */
+ memberName: string;
+ /**
+ * if the member already has a trustchain, this will be true
+ */
+ alreadyHasATrustchain: boolean;
+}): Promise {
const ephemeralKey = await crypto.randomKeypair();
const publisher = crypto.to_hex(ephemeralKey.publicKey);
const url = `${trustchainApiBaseUrl.replace("http", "ws")}/v1/qr?host=${publisher}`;
@@ -49,6 +145,7 @@ export async function createQRCodeHostInstance({
let cipher: ReturnType | undefined;
let expectedDigits: string | undefined;
let finished = false;
+ const setFinished = newValue => (finished = newValue);
onDisplayQRCode(url);
return new Promise((resolve, reject) => {
@@ -96,30 +193,23 @@ export async function createQRCodeHostInstance({
send({ version, publisher, message: "HandshakeCompletionSucceeded", payload });
break;
}
- case "TrustchainShareCredential": {
- finished = true;
- if (!cipher) {
- throw new Error("sessionEncryptionKey not set");
- }
- const { id, name } = await cipher.decryptMessage(data);
- const trustchain = await addMember({ id, name, permissions: Permissions.OWNER });
- const payload = await cipher.encryptMessagePayload({ trustchain });
- send({ version, publisher, message: "TrustchainAddedMember", payload });
- resolve();
- break;
- }
- case "Failure": {
- finished = true;
- const error = fromErrorMessage(data.payload);
- reject(error);
- ws.close();
- break;
- }
- default: {
- throw new Error("unexpected message");
- }
}
+ await commonSwitch({
+ data,
+ cipher,
+ addMember,
+ send,
+ publisher,
+ resolve,
+ memberCredentials,
+ memberName,
+ reject,
+ ws,
+ setFinished,
+ alreadyHasATrustchain,
+ });
} catch (e) {
+ console.error("socket error", e);
ws.close();
reject(e);
}
@@ -134,6 +224,8 @@ export async function createQRCodeHostInstance({
export async function createQRCodeCandidateInstance({
memberCredentials,
memberName,
+ addMember,
+ alreadyHasATrustchain,
scannedUrl,
onRequestQRCodeInput,
}: {
@@ -145,6 +237,14 @@ export async function createQRCodeCandidateInstance({
* the name of the member
*/
memberName: string;
+ /**
+ * if the member already has a trustchain, this will be true
+ */
+ alreadyHasATrustchain: boolean;
+ /**
+ * this function will need to using the TrustchainSDK (and use sdk.addMember)
+ */
+ addMember: (member: TrustchainMember) => Promise;
/**
* the scanned URL that contains the host public key
*/
@@ -160,7 +260,7 @@ export async function createQRCodeCandidateInstance({
},
callback: (digits: string) => void,
) => void;
-}): Promise {
+}): Promise {
const m = scannedUrl.match(/host=([0-9A-Fa-f]+)/);
if (!m) {
throw new Error("invalid scannedUrl");
@@ -175,6 +275,7 @@ export async function createQRCodeCandidateInstance({
ws.send(JSON.stringify(message));
}
let finished = false;
+ const setFinished = newValue => (finished = newValue);
return new Promise((resolve, reject) => {
ws.addEventListener("close", () => {
@@ -197,32 +298,35 @@ export async function createQRCodeCandidateInstance({
break;
}
case "HandshakeCompletionSucceeded": {
- const payload = await cipher.encryptMessagePayload({
- id: memberCredentials.pubkey,
- name: memberName,
- });
- send({ version, publisher, message: "TrustchainShareCredential", payload });
- break;
- }
- case "TrustchainAddedMember": {
- finished = true;
- const { trustchain } = await cipher.decryptMessage(data);
- resolve(trustchain);
- ws.close();
- break;
- }
- case "Failure": {
- finished = true;
- log("trustchain/qrcode", "Failure", { data });
- const error = fromErrorMessage(data.payload);
- reject(error);
- ws.close();
+ if (alreadyHasATrustchain) {
+ const payload = await cipher.encryptMessagePayload({});
+ send({ version, publisher, message: "TrustchainRequestCredential", payload });
+ } else {
+ const payload = await cipher.encryptMessagePayload({
+ id: memberCredentials.pubkey,
+ name: memberName,
+ });
+ send({ version, publisher, message: "TrustchainShareCredential", payload });
+ }
break;
}
- default:
- throw new Error("unexpected message");
}
+ await commonSwitch({
+ data,
+ cipher,
+ addMember,
+ send,
+ publisher,
+ resolve,
+ memberCredentials,
+ memberName,
+ reject,
+ ws,
+ setFinished,
+ alreadyHasATrustchain,
+ });
} catch (e) {
+ console.error("socket error", e);
ws.close();
reject(e);
}
@@ -268,6 +372,12 @@ function fromErrorMessage(payload: { message: string; type: string }): Error {
if (payload.type === "HANDSHAKE_COMPLETION_FAILED") {
throw new InvalidDigitsError(payload.message);
}
+ if (payload.type === "UNEXPECTED_SHARE_CREDENTIAL") {
+ throw new NoTrustchainInitialized(payload.message);
+ }
+ if (payload.type === "UNEXPECTED_REQUEST_CREDENTIAL") {
+ throw new TrustchainsAlreadyInitialized(payload.message);
+ }
const error = new Error(payload.message);
error.name = "TrustchainQRCode-" + payload.type;
return error;
diff --git a/libs/trustchain/src/qrcode/types.ts b/libs/trustchain/src/qrcode/types.ts
index 1e633c1c4311..84a37e573f1e 100644
--- a/libs/trustchain/src/qrcode/types.ts
+++ b/libs/trustchain/src/qrcode/types.ts
@@ -35,6 +35,12 @@ export type Message =
message: "HandshakeCompletionSucceeded";
payload: Encrypted>;
}
+ | {
+ version: number;
+ publisher: string;
+ message: "TrustchainRequestCredential";
+ payload: Encrypted>;
+ }
| {
version: number;
publisher: string;