Skip to content

Commit

Permalink
feat: wallet sync synchronize via qr code from mobile to desktop
Browse files Browse the repository at this point in the history
  • Loading branch information
cgrellard-ledger committed Aug 22, 2024
1 parent efea3b0 commit 2dd87f5
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 110 deletions.
8 changes: 8 additions & 0 deletions .changeset/dirty-drinks-roll.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getEnv } from "@ledgerhq/live-env";
import os from "os";

const platformMap: Record<string, string | undefined> = {
darwin: "Mac",
win32: "Windows",
linux: "Linux",
};

export function useInstanceName(): string {
const platform = os.platform();
const hash = getEnv("USER_ID").slice(0, 5);
const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`;
return name;
}
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -24,13 +29,14 @@ export function useQRCode() {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(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);
Expand All @@ -44,9 +50,15 @@ 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) {
Expand All @@ -55,15 +67,18 @@ export function useQRCode() {
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);
dispatch(setQrCodePinCode(null));
setIsLoading(false);
setError(null);
});
}, [trustchain, memberCredentials, trustchainApiBaseUrl, dispatch, sdk, queryClient]);
}, [memberCredentials, trustchainApiBaseUrl, memberName, dispatch, trustchain, sdk, queryClient]);

return {
url,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os from "os";
import { useMemo } from "react";
import { getEnv } from "@ledgerhq/live-env";
import { getSdk } from "@ledgerhq/trustchain/index";
Expand All @@ -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<string, string | undefined> = {
darwin: "Mac",
win32: "Windows",
linux: "Linux",
};
import { useInstanceName } from "./useInstanceName";

let sdkInstance: TrustchainSDK | null = null;

Expand All @@ -24,14 +18,12 @@ 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 hash = getEnv("USER_ID").slice(0, 5);
const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`;
return { applicationId, name, apiBaseUrl: trustchainApiBaseUrl };
}, [trustchainApiBaseUrl]);
}, [trustchainApiBaseUrl, name]);
const store = useStore();
const lifecycle = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <OpenOrInstallTrustChainApp goNext={goNext} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,7 @@ export interface BackRef {
export interface BackProps {}

export const WalletSyncRouter = forwardRef<BackRef, BackProps>((_props, ref) => {
useInitMemberCredentials();
const walletSyncFlow = useSelector(walletSyncFlowSelector);

switch (walletSyncFlow) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const ActivationFlow = ({
onQrCodeScanned={handleQrCodeScanned}
currentOption={currentOption}
setSelectedOption={setOption}
qrCodeValue={qrProcess.url}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
};

const QrCode = ({ qrCodeValue }: Props) => {
Expand Down Expand Up @@ -79,12 +79,16 @@ const QrCode = ({ qrCodeValue }: Props) => {
justifyContent={"center"}
testID="ws-qr-code-displayed"
>
<QRCode
value={qrCodeValue}
logo={require("~/images/bigSquareLogo.png")}
logoSize={65}
size={QRCodeSize}
/>
{qrCodeValue ? (
<QRCode
value={qrCodeValue}
logo={require("~/images/bigSquareLogo.png")}
logoSize={65}
size={QRCodeSize}
/>
) : (
<InfiniteLoader size={65} />
)}
</Flex>
<BottomContainer steps={steps} />
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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<string | null>(null);
Expand All @@ -45,29 +52,26 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
trustchainApiBaseUrl,
onDisplayQRCode: 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,
Expand All @@ -79,16 +83,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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<number | null>(null);
const [input, setInput] = useState<string | null>(null);
const instanceName = useInstanceName();
const trustchain = useSelector(trustchainSelector);
const sdk = useTrustchainSdk();

const navigation = useNavigation();

Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 2dd87f5

Please sign in to comment.