diff --git a/.changeset/gold-bats-perform.md b/.changeset/gold-bats-perform.md new file mode 100644 index 000000000000..eb1e6ecbed90 --- /dev/null +++ b/.changeset/gold-bats-perform.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +Improve QRCode flow for Ledger Sync allowing use to automate tests 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 7cd643d8da4e..7804c74675ee 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 @@ -51,7 +51,7 @@ const StepFlow = ({ const { memberCredentials } = useInitMemberCredentials(); const trustchain = useSelector(trustchainSelector); - const { handleStart, handleSendDigits, inputCallback, nbDigits } = useSyncWithQrCode(); + const { handleStart, handleSendDigits, nbDigits } = useSyncWithQrCode(); const handleQrCodeScanned = (data: string) => { onQrCodeScanned(); @@ -59,7 +59,7 @@ const StepFlow = ({ }; const handlePinCodeSubmit = (input: string) => { - if (input && inputCallback && nbDigits === input.length) handleSendDigits(inputCallback, input); + if (input && nbDigits === input.length) handleSendDigits(input); }; const getScene = () => { diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/synchronizeWithQrCode.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/synchronizeWithQrCode.integration.test.tsx index 054b87796b81..034129674acd 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/synchronizeWithQrCode.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/__integrations__/synchronizeWithQrCode.integration.test.tsx @@ -1,30 +1,52 @@ import React from "react"; -import { render, screen } from "@tests/test-renderer"; - +import { render, screen, act } from "@tests/test-renderer"; import { INITIAL_TEST, WalletSyncSettingsNavigator } from "./shared"; -import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams"; +import { createQRCodeCandidateInstance } from "@ledgerhq/ledger-key-ring-protocol/qrcode/index"; +import { BarcodeScanningResult } from "expo-camera"; + +const MOCK_BARCODE: BarcodeScanningResult = { + cornerPoints: [], + bounds: { + origin: { + x: 0, + y: 0, + }, + size: { + height: 0, + width: 0, + }, + }, + type: "", + data: "", +}; -jest.mock("../hooks/useQRCodeHost", () => ({ - useQRCodeHost: () => ({ - currentOption: jest.fn(), - url: "ledger.com", +const mockSimulateBarcodeScanned = jest.fn(); +jest.mock("expo-camera", () => ({ + CameraView: jest.fn(({ onBarcodeScanned }) => { + mockSimulateBarcodeScanned.mockImplementation(onBarcodeScanned); + return null; }), })); jest.mock("@ledgerhq/ledger-key-ring-protocol/qrcode/index", () => ({ - createQRCodeHostInstance: () => - Promise.resolve({ - trustchainApiBaseUrl: getWalletSyncEnvironmentParams("STAGING").trustchainApiBaseUrl, - onDisplayQRCode: jest.fn().mockImplementation(url => url), - onDisplayDigits: jest.fn().mockImplementation(digits => digits), - addMember: jest.fn(), - }), + createQRCodeCandidateInstance: jest.fn(), })); jest.useFakeTimers(); describe("SynchronizeWithQrCode", () => { it("Should display the QR code when 'show qr' toggle is pressed and add a new member through the flow", async () => { + let resolveQRCodeFlowPromise: unknown = null; + let requestQRCodeInput: unknown = null; + let pinCodeSet = false; + const mockPromiseQRCodeCandidate = new Promise(resolve => { + resolveQRCodeFlowPromise = resolve; + }); + (createQRCodeCandidateInstance as jest.Mock).mockImplementation(({ onRequestQRCodeInput }) => { + requestQRCodeInput = onRequestQRCodeInput; + return mockPromiseQRCodeCandidate; + }); + const { user } = render(, { overrideInitialState: INITIAL_TEST, }); @@ -34,16 +56,39 @@ describe("SynchronizeWithQrCode", () => { await user.press(screen.queryAllByText(/show qr/i)[0]); expect(screen.getByTestId("ws-qr-code-displayed")).toBeVisible(); - // TODO: We need to simulate the QR code scanning process - // //PinCode Page after scanning QRCode - // // Need to wait 3 seconds to simulate the time taken to scan the QR code - // expect(await screen.findByText("Enter Ledger Sync code")).toBeDefined(); - - // //Succes Page after PinCode - // expect( - // await screen.findByText( - // "Changes in your accounts will now automatically appear across all apps and platforms.", - // ), - // ).toBeDefined(); + await act(() => { + mockSimulateBarcodeScanned(MOCK_BARCODE); + }); + + // Call programmatically the requestQRCodeInput function to display the pin code input + await act(() => { + if (typeof requestQRCodeInput === "function") { + requestQRCodeInput({ digits: 3 }, (code: string) => { + if (code === "123") { + pinCodeSet = true; + } + }); + } + }); + + expect(await screen.findByText("Enter Ledger Sync code")).toBeDefined(); + const inputs = screen.getAllByDisplayValue(""); + await user.type(inputs[0], "1"); + await user.type(inputs[1], "2"); + await user.type(inputs[2], "3"); + + // Resolve the QR code flow promise only if pin code is set + if (pinCodeSet) { + if (typeof resolveQRCodeFlowPromise === "function") { + resolveQRCodeFlowPromise({ member: { name: "test" } }); + } + } + + expect(await screen.findByText("We are updating the synched instances...")).toBeDefined(); + //Simulate the sync process + await act(() => { + jest.advanceTimersByTime(3 * 1000); + }); + expect(await screen.findByText("Sync successful")).toBeDefined(); }); }); 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 bc672dcad497..7441995f2b33 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 @@ -1,5 +1,5 @@ import { useNavigation } from "@react-navigation/native"; -import React from "react"; +import React, { useCallback } from "react"; import { useSelector } from "react-redux"; import Activation from "."; import { TrackScreen } from "~/analytics"; @@ -52,16 +52,21 @@ const ActivationFlow = ({ const { currentStep, setCurrentStep } = useCurrentStep(); const { memberCredentials } = useInitMemberCredentials(); - const { handleStart, handleSendDigits, inputCallback, nbDigits } = useSyncWithQrCode(); + const { handleStart, handleSendDigits, nbDigits } = useSyncWithQrCode(); const handleQrCodeScanned = (data: string) => { onQrCodeScanned(); if (memberCredentials) handleStart(data, memberCredentials); }; - const handlePinCodeSubmit = (input: string) => { - if (input && inputCallback && nbDigits === input.length) handleSendDigits(inputCallback, input); - }; + const handlePinCodeSubmit = useCallback( + (input: string) => { + if (input && nbDigits === input.length) { + handleSendDigits(input); + } + }, + [handleSendDigits, nbDigits], + ); const hasCompletedOnboarding = useSelector(hasCompletedOnboardingSelector); const { navigate } = 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 c074790782be..bfe4dd777ca9 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,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useState, useRef } from "react"; import { MemberCredentials, TrustchainMember } from "@ledgerhq/ledger-key-ring-protocol/types"; import { createQRCodeCandidateInstance } from "@ledgerhq/ledger-key-ring-protocol/qrcode/index"; import { @@ -28,13 +28,13 @@ export const useSyncWithQrCode = () => { const navigation = useNavigation(); - const [inputCallback, setInputCallback] = useState<((input: string) => void) | null>(null); + const inputCallbackRef = useRef<((input: string) => void) | null>(null); const dispatch = useDispatch(); const onRequestQRCodeInput = useCallback( (config: { digits: number }, callback: (input: string) => void) => { setDigits(config.digits); - setInputCallback(() => callback); + inputCallbackRef.current = callback; }, [], ); @@ -42,7 +42,7 @@ export const useSyncWithQrCode = () => { const onSyncFinished = useCallback(() => { setDigits(null); setInput(null); - setInputCallback(null); + inputCallbackRef.current = null; navigation.navigate(NavigatorName.WalletSync, { screen: ScreenName.WalletSyncLoading, params: { @@ -102,9 +102,9 @@ export const useSyncWithQrCode = () => { ); const handleSendDigits = useCallback( - (inputCallback: (_: string) => void, input: string) => (inputCallback(input), true), + (input: string) => (inputCallbackRef.current?.(input), true), [], ); - return { nbDigits, input, handleStart, handleSendDigits, setInput, inputCallback }; + return { nbDigits, input, handleStart, handleSendDigits, setInput }; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationLoading.tsx b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationLoading.tsx index 75efdc3e99cd..9ea8535f7633 100644 --- a/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationLoading.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/WalletSync/screens/Activation/ActivationLoading.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { WalletSyncNavigatorStackParamList } from "~/components/RootNavigator/types/WalletSyncNavigator"; import { ScreenName } from "~/const"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; @@ -31,9 +31,11 @@ export function ActivationLoading({ route }: Props) { useLoadingStep(created); const hasCompletedOnboarding = useSelector(hasCompletedOnboardingSelector); - if (!hasCompletedOnboarding) { - dispatch(completeOnboarding()); - } + useEffect(() => { + if (!hasCompletedOnboarding) { + dispatch(completeOnboarding()); + } + }, [dispatch, hasCompletedOnboarding]); return ( <>