diff --git a/apps/ledger-live-mobile/src/components/BleDevicePairingFlow/index.tsx b/apps/ledger-live-mobile/src/components/BleDevicePairingFlow/index.tsx index a3e2d3d50f5b..9d52347f215f 100644 --- a/apps/ledger-live-mobile/src/components/BleDevicePairingFlow/index.tsx +++ b/apps/ledger-live-mobile/src/components/BleDevicePairingFlow/index.tsx @@ -11,6 +11,7 @@ import type { BleDevicePairingProps } from "./BleDevicePairing"; import { track } from "~/analytics"; import { NavigationHeaderBackButton } from "../NavigationHeaderBackButton"; import { NavigationHeaderCloseButton } from "../NavigationHeaderCloseButton"; +import { Text } from "@ledgerhq/native-ui"; const TIMEOUT_AFTER_PAIRED_MS = 2000; diff --git a/apps/ledger-live-mobile/src/components/SelectDevice2/Item.tsx b/apps/ledger-live-mobile/src/components/SelectDevice2/Item.tsx index b5a3a2182974..f95aa33015fc 100644 --- a/apps/ledger-live-mobile/src/components/SelectDevice2/Item.tsx +++ b/apps/ledger-live-mobile/src/components/SelectDevice2/Item.tsx @@ -43,6 +43,7 @@ const Item = ({ device, onPress }: Props) => { onPress(device)} touchableTestID={"device-item-" + device.deviceId} + testID={"device-item-" + device.deviceId} accessibilityRole="button" > { Animated.parallel([ Animated.timing(translateY, { + ...commonAnimatedProps, toValue: 0, - duration: ANIMATION_DURATION, - useNativeDriver: true, }), Animated.timing(opacity, { + ...commonAnimatedProps, toValue: 1, - duration: ANIMATION_DURATION, - useNativeDriver: true, }), Animated.timing(scale, { + ...commonAnimatedProps, toValue: 1, - duration: ANIMATION_DURATION, - useNativeDriver: true, }), ]).start(); - }, 125); + }, ANIMATION_DELAY); return () => { clearTimeout(animationTiemout); }; diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx index 01a35c79193d..d21a580b0c0d 100644 --- a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx @@ -3,16 +3,13 @@ import { Platform } from "react-native"; import { createStackNavigator } from "@react-navigation/stack"; import { useTheme } from "styled-components/native"; import { useTranslation } from "react-i18next"; -import { NavigationProp, useRoute } from "@react-navigation/native"; +import { useRoute } from "@react-navigation/native"; import { ScreenName } from "~/const"; import { getStackNavigatorConfig } from "~/navigation/navigatorConfig"; import { track } from "~/analytics"; import SelectDevice, { addAccountsSelectDeviceHeaderOptions, } from "LLM/features/DeviceSelection/screens/SelectDevice"; -import ConnectDevice, { - connectDeviceHeaderOptions, -} from "LLM/features/DeviceSelection/screens/ConnectDevice"; import StepHeader from "~/components/StepHeader"; import { NavigationHeaderCloseButtonAdvanced } from "~/components/NavigationHeaderCloseButton"; import { DeviceSelectionNavigatorParamsList } from "./types"; @@ -37,14 +34,6 @@ export default function Navigator() { [colors, onClose], ); - const onConnectDeviceBack = useCallback((navigation: NavigationProp>) => { - track("button_clicked", { - button: "Back arrow", - page: ScreenName.ConnectDevice, - }); - navigation.goBack(); - }, []); - return ( - {/* Select / Connect Device */} - ({ - headerTitle: () => ( - - ), - ...connectDeviceHeaderOptions(() => onConnectDeviceBack(navigation)), - })} - /> + {/*Connect Device : Only for receive flow context it will be re-added & adjusted in https://ledgerhq.atlassian.net/browse/LIVE-14726 */} ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/__integrations__/deviceSelection.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/__integrations__/deviceSelection.integration.test.tsx new file mode 100644 index 000000000000..ee3f9e74c536 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/__integrations__/deviceSelection.integration.test.tsx @@ -0,0 +1,91 @@ +import * as React from "react"; +import { render, screen } from "@tests/test-renderer"; +import DeviceSelectionNavigator from "../Navigator"; +import { useRoute, useNavigation } from "@react-navigation/native"; +import { discoverDevices } from "@ledgerhq/live-common/hw/index"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { of } from "rxjs"; + +const MockUseRoute = useRoute as jest.Mock; +const mockNavigate = jest.fn(); +const mockDiscoverDevices = discoverDevices as jest.Mock; + +(useNavigation as jest.Mock).mockReturnValue({ + navigate: mockNavigate, + addListener: jest.fn(), +}); + +jest.mock("@ledgerhq/live-common/deposit/index", () => ({ + useGroupedCurrenciesByProvider: jest.fn(), +})); + +jest.mock("@react-navigation/native", () => ({ + ...jest.requireActual("@react-navigation/native"), + useRoute: jest.fn(), + useNavigation: jest.fn(), +})); + +jest.mock("@ledgerhq/live-common/hw/index", () => ({ + ...jest.requireActual("@ledgerhq/live-common/hw/index"), + discoverDevices: jest.fn(), +})); + +describe("Device Selection feature integration test", () => { + beforeAll(() => { + MockUseRoute.mockReturnValue({ + params: { + context: "addAccounts", + currency: { + type: "CryptoCurrency", + id: "bitcoin", + ticker: "BTC", + name: "Bitcoin", + family: "bitcoin", + color: "#ffae35", + decimals: 8, + managerAppName: "Bitcoin", + }, + }, + }); + }); + it("should render a device connection screen when no device is installed", () => { + mockDiscoverDevices.mockReturnValue(of({})); + render(); + + const screenTitle = screen.getByText(/Connect device/i); + const listHeader = screen.getByText(/Devices/i); + const stepIndicator = screen.getByText(/Step 2 of 3/i); + const addDevuceCTA = screen.getByText(/Add a Ledger/i); + const bottomText = screen.getByText(/Need a new Ledger?/i); + const buyNowCTA = screen.getByText(/Buy now?/i); + + [listHeader, screenTitle, stepIndicator, addDevuceCTA, bottomText, buyNowCTA].forEach( + element => { + expect(element).toBeOnTheScreen(); + }, + ); + }); + + it("should render a device selection screen when a device is installed", () => { + mockDiscoverDevices.mockReturnValue( + of({ + type: "add", + id: "usb|1", + name: "Ledger Stax device", + deviceModel: { id: DeviceModelId.stax }, + wired: true, + }), + ); + render(); + const deviceCTA = screen.getByTestId("device-item-usb|1"); + const notConnectedText = screen.getByText(/connected/i); + const addDevuceCTA = screen.queryByText(/Add a Ledger/i); + const bottomText = screen.getByText(/Need a new Ledger?/i); + const buyNowCTA = screen.getByText(/Buy now?/i); + + [deviceCTA, notConnectedText, bottomText, buyNowCTA].forEach(element => { + expect(element).toBeOnTheScreen(); + }); + expect(addDevuceCTA).not.toBeOnTheScreen(); + }); +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/ConnectDevice/index.tsx b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/ConnectDevice/index.tsx deleted file mode 100644 index ee3400828799..000000000000 --- a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/ConnectDevice/index.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { View, StyleSheet } from "react-native"; -import { useSelector } from "react-redux"; - -import { Flex } from "@ledgerhq/native-ui"; -import { - getAccountCurrency, - getMainAccount, - getReceiveFlowError, -} from "@ledgerhq/live-common/account/index"; -import type { Device } from "@ledgerhq/live-common/hw/actions/types"; - -import { accountScreenSelector } from "~/reducers/accounts"; -import { ScreenName } from "~/const"; -import { TrackScreen, track } from "~/analytics"; -import SelectDevice2, { SetHeaderOptionsRequest } from "~/components/SelectDevice2"; -import { readOnlyModeEnabledSelector } from "~/reducers/settings"; -import GenericErrorView from "~/components/GenericErrorView"; -import DeviceActionModal from "~/components/DeviceActionModal"; -// TODO: use byFamily in the next feature for device connection (scope Add account v2) -//import byFamily from "~/generated/ConnectDevice"; - -import { - ReactNavigationHeaderOptions, - StackNavigatorProps, -} from "~/components/RootNavigator/types/helpers"; -import { NavigationHeaderCloseButton } from "~/components/NavigationHeaderCloseButton"; -import { NavigationHeaderBackButton } from "~/components/NavigationHeaderBackButton"; -import { useAppDeviceAction } from "~/hooks/deviceActions"; -import ReadOnlyWarning from "~/screens/ReceiveFunds/ReadOnlyWarning"; -import NotSyncedWarning from "~/screens/ReceiveFunds/NotSyncedWarning"; -import { DeviceSelectionNavigatorParamsList } from "../../types"; -// TODO: use SkipSelectDevice in the next feature for device connection if needed (scope Add account v2) -//import SkipSelectDevice from "~/screens/SkipSelectDevice"; - -// Defines some of the header options for this screen to be able to reset back to them. -export const connectDeviceHeaderOptions = ( - onHeaderBackButtonPress: () => void, -): ReactNavigationHeaderOptions => ({ - headerRight: () => , - headerLeft: () => , -}); - -export default function ConnectDevice({ - navigation, - route, -}: StackNavigatorProps) { - const { account, parentAccount } = useSelector(accountScreenSelector(route)); - const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector); - const [device, setDevice] = useState(); - const action = useAppDeviceAction(); - - useEffect(() => { - const readOnlyTitle = "transfer.receive.titleReadOnly"; - if (readOnlyModeEnabled && route.params?.title !== readOnlyTitle) { - navigation.setParams({ - title: readOnlyTitle, - }); - } - }, [navigation, readOnlyModeEnabled, route.params]); - - const error = useMemo( - () => (account ? getReceiveFlowError(account, parentAccount) : null), - [account, parentAccount], - ); - - const onResult = () => { - // TODO: implement business logic for both Add account v2 and Receive flow - }; - - const onSkipDevice = useCallback(() => { - if (!account) return; - // TODO: implement business logic for both Add account v2 and Receive flow - }, [account]); - - const onClose = useCallback(() => { - setDevice(undefined); - }, []); - - const onHeaderBackButtonPress = useCallback(() => { - track("button_clicked", { - button: "Back arrow", - page: ScreenName.ReceiveConnectDevice, - }); - navigation.goBack(); - }, [navigation]); - - // Reacts from request to update the screen header - const requestToSetHeaderOptions = useCallback( - (request: SetHeaderOptionsRequest) => { - if (request.type === "set") { - navigation.setOptions({ - headerLeft: request.options.headerLeft, - headerRight: request.options.headerRight, - }); - } else { - // Sets back the header to its initial values set for this screen - navigation.setOptions({ - ...connectDeviceHeaderOptions(onHeaderBackButtonPress), - }); - } - }, - [navigation, onHeaderBackButtonPress], - ); - - if (!account) return null; - - if (error) { - return ( - - - - ); - } - - const mainAccount = getMainAccount(account, parentAccount); - const currency = getAccountCurrency(mainAccount); - if (currency.type !== "CryptoCurrency") return null; // this should not happen: currency of main account is a crypto currency - const tokenCurrency = account && account.type === "TokenAccount" ? account.token : undefined; - - // check for coin specific UI - // TODO: implement business logic for both Add account v2 and Receive flow - //const CustomConnectDevice = byFamily[currency.family as keyof typeof byFamily]; - //if (CustomConnectDevice) return ; - - if (readOnlyModeEnabled) { - return ; - } - - if (!mainAccount.freshAddress) { - return ; - } - - return ( - <> - - {/* - * TODO: implement business logic for both Add account v2 and Receive flow - - */} - - - - setDevice(undefined)} - analyticsPropertyFlow="receive" - /> - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, - bodyError: { - flex: 1, - flexDirection: "column", - alignSelf: "center", - justifyContent: "center", - alignItems: "center", - paddingBottom: 16, - }, - scroll: { - flex: 1, - }, - scrollContainer: { - padding: 16, - }, -}); diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx index 94ce84697635..ae2c0b946f99 100644 --- a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx @@ -4,7 +4,7 @@ import { Flex } from "@ledgerhq/native-ui"; import { useTheme } from "@react-navigation/native"; import { ScreenName } from "~/const"; import { track } from "~/analytics"; -import SelectDevice2, { SetHeaderOptionsRequest } from "~/components/SelectDevice2"; +import DeviceSelector, { SetHeaderOptionsRequest } from "~/components/SelectDevice2"; import DeviceActionModal from "~/components/DeviceActionModal"; import { @@ -16,6 +16,7 @@ import { NavigationHeaderBackButton } from "~/components/NavigationHeaderBackBut import { DeviceSelectionNavigatorParamsList } from "../../types"; import { NetworkBasedAddAccountNavigator } from "LLM/features/Accounts/screens/AddAccount/types"; import useSelectDeviceViewModel from "./useSelectDeviceViewModel"; +import SkipSelectDevice from "~/screens/SkipSelectDevice"; // Defines some of the header options for this screen to be able to reset back to them. export const addAccountsSelectDeviceHeaderOptions = ( @@ -70,12 +71,9 @@ export default function SelectDevice({ }, ]} > - {/* - TODO: should be rendered only on receive flow context -> TO BE DONE After delivering the add account flow - - */} + - void; - }; [ScreenName.SelectDevice]: SelectDeviceRouteParams; [NavigatorName.AddAccounts]?: Partial>; }; diff --git a/apps/ledger-live-mobile/src/screens/SkipSelectDevice.tsx b/apps/ledger-live-mobile/src/screens/SkipSelectDevice.tsx index ab38a3d45a6f..3ab0a5c281f9 100644 --- a/apps/ledger-live-mobile/src/screens/SkipSelectDevice.tsx +++ b/apps/ledger-live-mobile/src/screens/SkipSelectDevice.tsx @@ -10,11 +10,13 @@ import { ReceiveFundsStackParamList } from "~/components/RootNavigator/types/Rec import { ScreenName } from "~/const"; import { useDebouncedRequireBluetooth } from "~/components/RequiresBLE/hooks/useRequireBluetooth"; import RequiresBluetoothDrawer from "~/components/RequiresBLE/RequiresBluetoothDrawer"; +import { DeviceSelectionNavigatorParamsList } from "~/newArch/features/DeviceSelection/types"; type Navigation = | StackNavigatorProps | StackNavigatorProps - | StackNavigatorProps; + | StackNavigatorProps + | StackNavigatorProps; type Props = { onResult: (device: Device) => void;