From 1660676d1a25a18d0a17e0e00424852a8d13d14a Mon Sep 17 00:00:00 2001 From: Mounir Hamzaoui Date: Wed, 11 Dec 2024 18:15:03 +0100 Subject: [PATCH] feat: migrate all entry screen, ctas to new add account (uncompleted v2) flow --- .changeset/moody-mugs-grin.md | 6 + .../RootNavigator/SwapNavigator.tsx | 4 +- .../RootNavigator/types/BaseNavigator.ts | 8 +- .../types/RequestAccountNavigator.ts | 11 + .../RootNavigator/types/SwapNavigator.ts | 7 +- .../src/locales/en/common.json | 8 + .../newArch/features/Accounts/Navigator.tsx | 1 + .../useSelectAddAccountMethodViewModel.ts | 25 +- .../Accounts/screens/AddAccount/types.ts | 8 +- .../features/AssetSelection/Navigator.tsx | 19 +- .../assetSelection.integration.test.tsx | 124 ++++++ .../__integrations__/mockData.ts | 365 ++++++++++++++++++ .../components/NetworkBanner/index.tsx | 29 ++ .../screens/SelectCrypto/index.tsx | 154 +++----- .../SelectCrypto/useSelectCryptoViewModel.ts | 137 +++++++ .../screens/SelectNetwork/index.tsx | 240 +++--------- .../useSelectNetworkViewModel.ts | 217 +++++++++++ .../newArch/features/AssetSelection/types.ts | 50 +-- .../features/DeviceSelection/Navigator.tsx | 1 + .../screens/SelectDevice/index.tsx | 39 +- .../SelectDevice/useSelectDeviceViewModel.ts | 55 +++ .../newArch/features/DeviceSelection/types.ts | 37 +- .../Modals/InstalledAppModal.tsx | 12 +- .../screens/ReceiveFunds/01-SelectCrypto.tsx | 4 +- .../screens/ReceiveFunds/02-SelectAccount.tsx | 35 +- .../RequestAccount/02-SelectAccount.tsx | 33 +- .../src/screens/Swap/Form/Summary/index.tsx | 47 ++- .../screens/Swap/SubScreens/SelectAccount.tsx | 42 +- .../src/screens/WalletCentricAsset/index.tsx | 33 +- .../src/deposit/deposit.integration.test.ts | 32 +- libs/ledger-live-common/src/deposit/type.ts | 12 + .../useGroupedCurrenciesByProvider.hook.ts | 24 +- 32 files changed, 1358 insertions(+), 461 deletions(-) create mode 100644 .changeset/moody-mugs-grin.md create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/assetSelection.integration.test.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/mockData.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/components/NetworkBanner/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/useSelectCryptoViewModel.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/useSelectNetworkViewModel.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/useSelectDeviceViewModel.ts diff --git a/.changeset/moody-mugs-grin.md b/.changeset/moody-mugs-grin.md new file mode 100644 index 000000000000..fee9fc1c2457 --- /dev/null +++ b/.changeset/moody-mugs-grin.md @@ -0,0 +1,6 @@ +--- +"live-mobile": minor +"@ledgerhq/live-common": minor +--- + +When llmNetworkBasedAddAccount feature flag is enabled, all the ctas that open an add account process will be redirected to the new flow and support currency and related route params. diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/SwapNavigator.tsx b/apps/ledger-live-mobile/src/components/RootNavigator/SwapNavigator.tsx index a05b4ca37be8..c7b920345091 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/SwapNavigator.tsx +++ b/apps/ledger-live-mobile/src/components/RootNavigator/SwapNavigator.tsx @@ -41,9 +41,7 @@ export default function SwapNavigator( title: t("transfer.swap2.form.title"), headerLeft: () => null, }} - initialParams={{ - ...params, - }} + initialParams={params as Partial} /> >; [NavigatorName.AssetSelection]?: Partial< - NavigatorScreenParams - > & - CommonAddAccountNavigatorParamsList; + NavigatorScreenParams & { + context?: "addAccounts" | "receiveFunds"; + token?: TokenCurrency; + } // in some cases we need to pass directly the context to the navigator and let it handle the logic + >; [NavigatorName.Assets]?: Partial>; }; diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/RequestAccountNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/RequestAccountNavigator.ts index 6f246774dd1a..885c5e3efd06 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/RequestAccountNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/RequestAccountNavigator.ts @@ -6,6 +6,7 @@ import { Observable } from "rxjs"; import { NavigatorScreenParams } from "@react-navigation/native"; import { NavigatorName, ScreenName } from "~/const"; import { AddAccountsNavigatorParamList } from "./AddAccountsNavigator"; +import { DeviceSelectionNavigatorParamsList } from "LLM/features/DeviceSelection/types"; export type RequestAccountNavigatorParamList = { [ScreenName.RequestAccountsSelectCrypto]: { @@ -30,4 +31,14 @@ export type RequestAccountNavigatorParamList = { analyticsPropertyFlow?: string; onSuccess?: (account: AccountLike, parentAccount?: Account) => void; }>; + [NavigatorName.DeviceSelection]: Partial< + NavigatorScreenParams & + Partial<{ + token?: TokenCurrency; + inline?: boolean; + returnToSwap?: boolean; + analyticsPropertyFlow?: string; + onSuccess?: (account: AccountLike, parentAccount?: Account) => void; + }> + >; }; diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts index fabdb2249ade..658bf9fc1eb1 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts @@ -44,7 +44,9 @@ import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/fam import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types"; import BigNumber from "bignumber.js"; import { Account, Operation } from "@ledgerhq/types-live"; -import { ScreenName } from "~/const"; +import { NavigatorName, ScreenName } from "~/const"; +import { NavigatorScreenParams } from "@react-navigation/core"; +import { AssetSelectionNavigatorParamsList } from "LLM/features/AssetSelection/types"; type Target = "from" | "to"; @@ -329,4 +331,7 @@ export type SwapNavigatorParamList = { | ScreenName.SendSelectDevice | ScreenName.SwapForm; }; + [NavigatorName.AssetSelection]?: Partial< + NavigatorScreenParams + >; }; diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index 30f0df5aeb49..f71ba841699a 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -7123,5 +7123,13 @@ } } } + }, + "assetSelection": { + "selectCrypto": { + "title": "Select Asset" + }, + "selectNetwork": { + "title": "Select the blockchain network the asset belongs to" + } } } diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/Navigator.tsx index fe91bf4831d6..7323190f4406 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/Navigator.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/Navigator.tsx @@ -48,6 +48,7 @@ export default function Navigator() { options={{ headerTitle: "", }} + initialParams={route.params} /> {/* Scan accounts from device */} diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/SelectAddAccountMethod/useSelectAddAccountMethodViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/SelectAddAccountMethod/useSelectAddAccountMethodViewModel.ts index 6617519af156..08f5c3c673a4 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/SelectAddAccountMethod/useSelectAddAccountMethodViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/components/SelectAddAccountMethod/useSelectAddAccountMethodViewModel.ts @@ -27,12 +27,22 @@ const useSelectAddAccountMethodViewModel = ({ const hasCurrency = !!currency; const navigationParams = useMemo(() => { - return hasCurrency - ? currency.type === "TokenCurrency" - ? { token: currency } - : { currency } - : {}; - }, [hasCurrency, currency]); + if (hasCurrency) { + if (currency?.type === "TokenCurrency") { + return { + token: currency, + ...(llmNetworkBasedAddAccountFlow?.enabled && { context: "addAccounts" }), + }; + } else { + return { + currency, + ...(llmNetworkBasedAddAccountFlow?.enabled && { context: "addAccounts" }), + }; + } + } else { + return llmNetworkBasedAddAccountFlow?.enabled ? { context: "addAccounts" } : {}; + } + }, [hasCurrency, currency, llmNetworkBasedAddAccountFlow?.enabled]); const trackButtonClick = useCallback((button: string) => { track("button_clicked", { @@ -58,6 +68,9 @@ const useSelectAddAccountMethodViewModel = ({ const EntryNavigatorName = llmNetworkBasedAddAccountFlow?.enabled ? NavigatorName.AssetSelection : NavigatorName.AddAccounts; + // to delete after llmNetworkBasedAddAccountFlow is fully enabled (ts inference not working well based on navigationParams) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore navigation.navigate(EntryNavigatorName, navigationParams); }, [ navigation, diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/types.ts b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/types.ts index cff50a753d86..dc5f7686c794 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/types.ts @@ -2,12 +2,16 @@ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { ScreenName } from "~/const"; import { Device } from "@ledgerhq/types-devices"; +type CommonParams = { + context?: "addAccounts" | "receiveFunds"; + onSuccess?: () => void; +}; export type NetworkBasedAddAccountNavigator = { - [ScreenName.SelectAccounts]: { + [ScreenName.SelectAccounts]: CommonParams & { currency: CryptoCurrency | TokenCurrency; createTokenAccount?: boolean; }; - [ScreenName.ScanDeviceAccounts]: { + [ScreenName.ScanDeviceAccounts]: CommonParams & { currency: CryptoCurrency | TokenCurrency; device: Device; onSuccess?: (_?: unknown) => void; diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx index 1a6cd42457fc..d6aae06a7341 100644 --- a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx @@ -3,7 +3,7 @@ import { Platform } from "react-native"; import { createStackNavigator } from "@react-navigation/stack"; import { useTheme } from "styled-components/native"; import { useRoute } from "@react-navigation/native"; -import { ScreenName } from "~/const"; +import { NavigatorName, ScreenName } from "~/const"; import { getStackNavigatorConfig } from "~/navigation/navigatorConfig"; import { track } from "~/analytics"; import { Flex } from "@ledgerhq/native-ui"; @@ -16,13 +16,19 @@ import SelectNetwork from "LLM/features/AssetSelection/screens/SelectNetwork"; import { NavigationHeaderCloseButtonAdvanced } from "~/components/NavigationHeaderCloseButton"; import { NavigationHeaderBackButton } from "~/components/NavigationHeaderBackButton"; import { AssetSelectionNavigatorParamsList } from "./types"; +import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; + +type NavigationProps = BaseComposite< + StackNavigatorProps +>; export default function Navigator() { const { colors } = useTheme(); - const route = useRoute(); - + const route = useRoute(); const hasClosedNetworkBanner = useSelector(hasClosedNetworkBannerSelector); + const { token, currency } = route.params || {}; + const onClose = useCallback(() => { track("button_clicked", { button: "Close", @@ -44,15 +50,19 @@ export default function Navigator() { ...stackNavigationConfig, gestureEnabled: Platform.OS === "ios", }} + initialRouteName={ + token || currency ? ScreenName.SelectNetwork : ScreenName.AddAccountsSelectCrypto + } > , - headerTitle: "", + title: "", headerRight: () => , }} + initialParams={route.params} /> ), }} + initialParams={route.params} /> ); diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/assetSelection.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/assetSelection.integration.test.tsx new file mode 100644 index 000000000000..442cf001d3a3 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/assetSelection.integration.test.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { render, screen } from "@tests/test-renderer"; +import AssetSelectionNavigator from "../Navigator"; +import { useRoute, useNavigation } from "@react-navigation/native"; +import { useGroupedCurrenciesByProvider } from "@ledgerhq/live-common/deposit/index"; +import { + mockGroupedCurrenciesBySingleProviderData, + mockGroupedCurrenciesWithMultipleProviderData, +} from "./mockData"; + +const MockUseRoute = useRoute as jest.Mock; +const MockUseGroupedCurrenciesByProvider = useGroupedCurrenciesByProvider as jest.Mock; +const mockNavigate = jest.fn(); + +(useNavigation as jest.Mock).mockReturnValue({ + navigate: mockNavigate, +}); + +jest.mock("../components/NetworkBanner", () => { + return { + __esModule: true, + default: () =>
Mock Network Banner
, + }; +}); + +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.useFakeTimers(); + +describe("Asset Selection test suite", () => { + it("should render crypto selection screen when no currency is defined in the navigation route showing a loader", () => { + MockUseRoute.mockReturnValue({ + params: { + context: "addAccounts", + }, + }); + + MockUseGroupedCurrenciesByProvider.mockReturnValue({ + result: { currenciesByProvider: [], sortedCryptoCurrencies: [] }, + loadingStatus: "pending", + }); + + render(); + + const screenTitle = screen.getByText(/select asset/i); + const selectCryptoViewArea = screen.getByTestId("select-crypto-view-area"); + const loader = screen.getByTestId("loader"); + expect(screenTitle).toBeVisible(); + expect(screenTitle).toHaveProp("testID", "select-crypto-header-step1-title"); + expect(selectCryptoViewArea).toBeVisible(); + expect(loader).toBeVisible(); + }); + + it("should render crypto selection screen with empty list when useGroupedCurrenciesByProvider finish loading with empty result", () => { + MockUseRoute.mockReturnValue({ + params: { + context: "addAccounts", + }, + }); + + MockUseGroupedCurrenciesByProvider.mockReturnValue({ + result: { currenciesByProvider: [], sortedCryptoCurrencies: [] }, + loadingStatus: "success", + }); + + render(); + + const emptyList = screen.getByText(/no crypto assets found/i); + expect(emptyList).toBeVisible(); + }); + it("should display a list of cryptocurrencies when useGroupedCurrenciesByProvider successfully loads data", () => { + MockUseRoute.mockReturnValue({ + params: { + context: "addAccounts", + }, + }); + + MockUseGroupedCurrenciesByProvider.mockReturnValue({ + result: mockGroupedCurrenciesBySingleProviderData, + loadingStatus: "success", + }); + + render(); + + const selectCryptoViewArea = screen.getByTestId("select-crypto-view-area"); + const cryptoCurrencyRow = screen.getByText(/bitcoin/i); + expect(selectCryptoViewArea).toBeVisible(); + expect(cryptoCurrencyRow).toBeVisible(); + }); + it("should navigate to network selection when currency has more than one network provider", () => { + MockUseRoute.mockReturnValue({ + params: { + context: "addAccounts", + currency: "ethereum", + }, + }); + + MockUseGroupedCurrenciesByProvider.mockReturnValue({ + result: mockGroupedCurrenciesWithMultipleProviderData, + loadingStatus: "success", + }); + + render(); + + const selectNetwork = screen.getByTestId("select-network-view-area"); + const title = screen.getByText(/Select the blockchain network the asset belongs to/i); + const arbitrum = screen.getByText(/arbitrum/i); + const blast = screen.getByText(/blast/i); + const boba = screen.getByText(/boba/i); + expect(selectNetwork).toBeVisible(); + expect(title).toBeVisible(); + [arbitrum, blast, boba].forEach(network => { + expect(network).toBeVisible(); + }); + }); +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/mockData.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/mockData.ts new file mode 100644 index 000000000000..2210f4883bc2 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/__integrations__/mockData.ts @@ -0,0 +1,365 @@ +const mockGroupedCurrenciesBySingleProviderData = { + currenciesByProvider: [ + { + providerId: "bitcoin", + currenciesByNetwork: [ + { + type: "CryptoCurrency", + id: "bitcoin", + coinType: 0, + name: "Bitcoin", + managerAppName: "Bitcoin", + ticker: "BTC", + scheme: "bitcoin", + color: "#ffae35", + symbol: "Ƀ", + units: [ + { name: "bitcoin", code: "BTC", magnitude: 8 }, + { name: "mBTC", code: "mBTC", magnitude: 5 }, + { name: "bit", code: "bit", magnitude: 2 }, + { name: "satoshi", code: "sat", magnitude: 0 }, + ], + supportsSegwit: true, + supportsNativeSegwit: true, + family: "bitcoin", + blockAvgTime: 900, + bitcoinLikeInfo: { P2PKH: 0, P2SH: 5, XPUBVersion: 76067358 }, + explorerViews: [ + { + address: "https://blockstream.info/address/$address", + tx: "https://blockstream.info/tx/$hash", + }, + { + address: "https://www.blockchain.com/btc/address/$address", + tx: "https://blockchain.info/btc/tx/$hash", + }, + ], + keywords: ["btc", "bitcoin"], + explorerId: "btc", + }, + ], + }, + ], + sortedCryptoCurrencies: [ + { + type: "CryptoCurrency", + id: "bitcoin", + coinType: 0, + name: "Bitcoin", + managerAppName: "Bitcoin", + ticker: "BTC", + scheme: "bitcoin", + color: "#ffae35", + symbol: "Ƀ", + units: [ + { name: "bitcoin", code: "BTC", magnitude: 8 }, + { name: "mBTC", code: "mBTC", magnitude: 5 }, + { name: "bit", code: "bit", magnitude: 2 }, + { name: "satoshi", code: "sat", magnitude: 0 }, + ], + supportsSegwit: true, + supportsNativeSegwit: true, + family: "bitcoin", + blockAvgTime: 900, + bitcoinLikeInfo: { P2PKH: 0, P2SH: 5, XPUBVersion: 76067358 }, + explorerViews: [ + { + address: "https://blockstream.info/address/$address", + tx: "https://blockstream.info/tx/$hash", + }, + { + address: "https://www.blockchain.com/btc/address/$address", + tx: "https://blockchain.info/btc/tx/$hash", + }, + ], + keywords: ["btc", "bitcoin"], + explorerId: "btc", + }, + ], +}; + +const mockGroupedCurrenciesWithMultipleProviderData = { + currenciesByProvider: [ + { + providerId: "ethereum", + currenciesByNetwork: [ + { + type: "CryptoCurrency", + id: "ethereum", + coinType: 60, + name: "Ethereum", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "ethereum", + color: "#0ebdcd", + symbol: "Ξ", + family: "evm", + blockAvgTime: 15, + units: [ + { name: "ether", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 1 }, + explorerViews: [ + { + tx: "https://etherscan.io/tx/$hash", + address: "https://etherscan.io/address/$address", + token: "https://etherscan.io/token/$contractAddress?a=$address", + }, + ], + keywords: ["eth", "ethereum"], + explorerId: "eth", + }, + { + type: "CryptoCurrency", + id: "zksync", + coinType: 60, + name: "ZKsync", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "zksync", + color: "#000000", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 324 }, + explorerViews: [ + { + tx: "https://explorer.zksync.io/tx/$hash", + address: "https://explorer.zksync.io/address/$address", + token: "https://explorer.zksync.io/token/$contractAddress?a=$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "scroll", + coinType: 60, + name: "Scroll", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "scroll", + color: "#ebc28e", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + disableCountervalue: false, + ethereumLikeInfo: { chainId: 534352 }, + explorerViews: [ + { + tx: "https://scrollscan.com/tx/$hash", + address: "https://scrollscan.com/address/$address", + token: "https://scrollscan.com/token/$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "optimism", + coinType: 60, + name: "OP Mainnet", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "optimism", + color: "#FF0421", + family: "evm", + units: [ + { name: "ether", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 10 }, + explorerViews: [ + { + tx: "https://optimistic.etherscan.io/tx/$hash", + address: "https://optimistic.etherscan.io/address/$address", + token: "https://optimistic.etherscan.io/token/$contractAddress?a=$address", + }, + ], + keywords: ["optimism"], + }, + { + type: "CryptoCurrency", + id: "linea", + coinType: 60, + name: "Linea", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "linea", + color: "#000000", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + disableCountervalue: false, + ethereumLikeInfo: { chainId: 59144 }, + explorerViews: [ + { + tx: "https://lineascan.build/tx/$hash", + address: "https://lineascan.build/address/$address", + token: "https://lineascan.build/token/$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "base", + coinType: 60, + name: "Base", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "base", + color: "#1755FE", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 8453 }, + explorerViews: [ + { + tx: "https://basescan.org/tx/$hash", + address: "https://basescan.org/address/$address", + token: "https://basescan.org/token/$contractAddress?a=$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "arbitrum", + coinType: 60, + name: "Arbitrum", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "arbitrum", + color: "#28a0f0", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 42161 }, + explorerViews: [ + { + tx: "https://arbiscan.io/tx/$hash", + address: "https://arbiscan.io/address/$address", + token: "https://arbiscan.io/token/$contractAddress?a=$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "blast", + coinType: 60, + name: "Blast", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "blast", + color: "#FCFC06", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + disableCountervalue: false, + ethereumLikeInfo: { chainId: 81457 }, + explorerViews: [ + { + tx: "https://blastscan.io/tx/$hash", + address: "https://blastscan.io/address/$address", + token: "https://blastscan.io/token/$address", + }, + ], + }, + { + type: "CryptoCurrency", + id: "boba", + coinType: 60, + name: "Boba", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "boba", + color: "#CBFF00", + family: "evm", + units: [ + { name: "ETH", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 288 }, + explorerViews: [ + { + tx: "https://bobascan.com/tx/$hash", + address: "https://bobascan.com/address/$address", + token: "https://bobascan.com/token/$contractAddress?a=$address", + }, + ], + }, + ], + }, + ], + sortedCryptoCurrencies: [ + { + type: "CryptoCurrency", + id: "ethereum", + coinType: 60, + name: "Ethereum", + managerAppName: "Ethereum", + ticker: "ETH", + scheme: "ethereum", + color: "#0ebdcd", + symbol: "Ξ", + family: "evm", + blockAvgTime: 15, + units: [ + { name: "ether", code: "ETH", magnitude: 18 }, + { name: "Gwei", code: "Gwei", magnitude: 9 }, + { name: "Mwei", code: "Mwei", magnitude: 6 }, + { name: "Kwei", code: "Kwei", magnitude: 3 }, + { name: "wei", code: "wei", magnitude: 0 }, + ], + ethereumLikeInfo: { chainId: 1 }, + explorerViews: [ + { + tx: "https://etherscan.io/tx/$hash", + address: "https://etherscan.io/address/$address", + token: "https://etherscan.io/token/$contractAddress?a=$address", + }, + ], + keywords: ["eth", "ethereum"], + explorerId: "eth", + }, + ], +}; + +export { mockGroupedCurrenciesBySingleProviderData, mockGroupedCurrenciesWithMultipleProviderData }; diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/components/NetworkBanner/index.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/components/NetworkBanner/index.tsx new file mode 100644 index 000000000000..79bcc4faca50 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/components/NetworkBanner/index.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { BannerCard, Flex } from "@ledgerhq/native-ui"; + +import { ChartNetworkMedium } from "@ledgerhq/native-ui/assets/icons"; +import { useTranslation } from "react-i18next"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +type BannerProps = { + hideBanner: () => void; + onPress: () => void; +}; + +const NetworkBanner = ({ onPress, hideBanner }: BannerProps) => { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + return ( + + } + onPressDismiss={hideBanner} + onPress={onPress} + /> + + ); +}; + +export default NetworkBanner; diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx index 9e5bd36b0b15..dae63aa1ce15 100644 --- a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx @@ -1,28 +1,23 @@ -import React, { useCallback, useEffect, useMemo } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import React, { useCallback } from "react"; +import { Trans } from "react-i18next"; import { FlatList } from "react-native"; -import debounce from "lodash/debounce"; -import { useSelector } from "react-redux"; import type { CryptoCurrency, CryptoOrTokenCurrency, TokenCurrency, } from "@ledgerhq/types-cryptoassets"; -import { findCryptoCurrencyByKeyword } from "@ledgerhq/live-common/currencies/index"; import { getEnv } from "@ledgerhq/live-env"; -import { useGroupedCurrenciesByProvider } from "@ledgerhq/live-common/deposit/index"; import SafeAreaView from "~/components/SafeAreaView"; import { Flex, InfiniteLoader, Text } from "@ledgerhq/native-ui"; -import { NavigatorName, ScreenName } from "~/const"; -import { track, TrackScreen } from "~/analytics"; +import { ScreenName } from "~/const"; +import { TrackScreen } from "~/analytics"; import FilteredSearchBar from "~/components/FilteredSearchBar"; import BigCurrencyRow from "~/components/BigCurrencyRow"; -import { flattenAccountsSelector } from "~/reducers/accounts"; import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; -import { findAccountByCurrency } from "~/logic/deposit"; import { AssetSelectionNavigatorParamsList } from "../../types"; +import useSelectCryptoViewModel from "./useSelectCryptoViewModel"; const SEARCH_KEYS = getEnv("CRYPTO_ASSET_SEARCH_KEYS"); @@ -37,82 +32,17 @@ const renderEmptyList = () => ( ); export default function SelectCrypto({ - navigation, route, }: StackNavigatorProps) { - const paramsCurrency = route?.params?.currency; - const filterCurrencyIds = route?.params?.filterCurrencyIds; - const filterCurrencyIdsSet = useMemo( - () => (filterCurrencyIds ? new Set(filterCurrencyIds) : null), - [filterCurrencyIds], - ); - - const { t } = useTranslation(); - const accounts = useSelector(flattenAccountsSelector); - - const { currenciesByProvider, sortedCryptoCurrencies } = useGroupedCurrenciesByProvider(); - - const onPressItem = useCallback( - (curr: CryptoCurrency | TokenCurrency) => { - track("asset_clicked", { - asset: curr.name, - page: "Choose a crypto to secure", - }); - - const provider = currenciesByProvider.find(elem => - elem.currenciesByNetwork.some( - currencyByNetwork => (currencyByNetwork as CryptoCurrency | TokenCurrency).id === curr.id, - ), - ); - - // If the selected currency exists on multiple networks we redirect to the SelectNetwork screen - if (provider && provider?.currenciesByNetwork.length > 1) { - navigation.navigate(ScreenName.SelectNetwork, { - provider, - filterCurrencyIds, - }); - return; - } - - const isToken = curr.type === "TokenCurrency"; - const currency = isToken ? curr.parentCurrency : curr; - const currencyAccounts = findAccountByCurrency(accounts, currency); - - if (currencyAccounts.length > 0) { - // If we found one or more accounts of the currency then we select account - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.SelectAccounts, - params: { - currency, - }, - }); - } else { - // If we didn't find any account of the parent currency then we add one - navigation.navigate(NavigatorName.DeviceSelection, { - screen: ScreenName.SelectDevice, - params: { - currency, - createTokenAccount: isToken || undefined, - }, - }); - } - }, - [currenciesByProvider, accounts, navigation, filterCurrencyIds], - ); - - useEffect(() => { - if (paramsCurrency) { - const selectedCurrency = findCryptoCurrencyByKeyword(paramsCurrency.toUpperCase()); - - if (selectedCurrency) { - onPressItem(selectedCurrency); - } - } - }, [onPressItem, paramsCurrency]); - - const debounceTrackOnSearchChange = debounce((newQuery: string) => { - track("asset_searched", { page: "Choose a crypto to secure", asset: newQuery }); - }, 1500); + const { filterCurrencyIds, currency: paramsCurrency, context } = route?.params || {}; + const { + titleText, + titleTestId, + list, + onPressItem, + debounceTrackOnSearchChange, + providersLoadingStatus, + } = useSelectCryptoViewModel({ context, filterCurrencyIds, paramsCurrency }); const renderList = useCallback( (items: CryptoOrTokenCurrency[]) => ( @@ -129,35 +59,41 @@ export default function SelectCrypto({ [onPressItem], ); - const list = useMemo( - () => - filterCurrencyIdsSet - ? sortedCryptoCurrencies.filter(crypto => filterCurrencyIdsSet.has(crypto.id)) - : sortedCryptoCurrencies, - [filterCurrencyIdsSet, sortedCryptoCurrencies], - ); + const renderListView = useCallback(() => { + switch (providersLoadingStatus) { + case "success": + return list.length > 0 ? ( + + + + ) : ( + renderEmptyList() + ); + case "error": + // TODO: in an improvement feature, when the network fetch status is on error, implement a clean error message with a retry CTA + return renderEmptyList(); + default: + return ( + + + + ); + } + }, [providersLoadingStatus, list, renderList, debounceTrackOnSearchChange]); return ( - + - - {t("transfer.receive.selectCrypto.title")} + + {titleText} - {list.length > 0 ? ( - - - - ) : ( - - - - )} + {renderListView()} ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/useSelectCryptoViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/useSelectCryptoViewModel.ts new file mode 100644 index 000000000000..f82bb464f7a6 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/useSelectCryptoViewModel.ts @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import debounce from "lodash/debounce"; +import { useNavigation } from "@react-navigation/core"; + +import { findCryptoCurrencyByKeyword } from "@ledgerhq/live-common/currencies/index"; + +import { NavigatorName, ScreenName } from "~/const"; +import { track } from "~/analytics"; +import { flattenAccountsSelector } from "~/reducers/accounts"; +import { findAccountByCurrency } from "~/logic/deposit"; + +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { AssetSelectionNavigationProps } from "../../types"; +import { useGroupedCurrenciesByProvider } from "@ledgerhq/live-common/deposit/index"; +import { LoadingBasedGroupedCurrencies } from "@ledgerhq/live-common/deposit/type"; + +export default function useSelectCryptoViewModel({ + context, + filterCurrencyIds, + paramsCurrency, +}: { + context?: "addAccounts" | "receiveFunds"; + filterCurrencyIds: string[] | undefined; + paramsCurrency: string | undefined; +}) { + const { t } = useTranslation(); + const filterCurrencyIdsSet = useMemo( + () => (filterCurrencyIds ? new Set(filterCurrencyIds) : null), + [filterCurrencyIds], + ); + + const accounts = useSelector(flattenAccountsSelector); + const navigation = useNavigation(); + + const { result, loadingStatus: providersLoadingStatus } = useGroupedCurrenciesByProvider( + true, + ) as LoadingBasedGroupedCurrencies; + const { currenciesByProvider, sortedCryptoCurrencies } = result; + + const onPressItem = useCallback( + (curr: CryptoCurrency | TokenCurrency) => { + track("asset_clicked", { + asset: curr.name, + page: "Choose a crypto to secure", + }); + + const provider = currenciesByProvider.find(elem => + elem.currenciesByNetwork.some( + currencyByNetwork => (currencyByNetwork as CryptoCurrency | TokenCurrency).id === curr.id, + ), + ); + + // If the selected currency exists on multiple networks we redirect to the SelectNetwork screen + if (provider && provider?.currenciesByNetwork.length > 1) { + navigation.navigate(ScreenName.SelectNetwork, { + context, + currency: curr.id, + ...(context === "receiveFunds" && { filterCurrencyIds }), + }); + return; + } + + const isToken = curr.type === "TokenCurrency"; + const currency = isToken ? curr.parentCurrency : curr; + const currencyAccounts = findAccountByCurrency(accounts, currency); + + if (currencyAccounts.length > 0) { + // If we found one or more accounts of the currency then we select account + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.SelectAccounts, + params: { + currency, + }, + }); + } else { + // If we didn't find any account of the parent currency then we add one + navigation.navigate(NavigatorName.DeviceSelection, { + screen: ScreenName.SelectDevice, + params: { + currency, + createTokenAccount: isToken || undefined, + context, + }, + }); + } + }, + [currenciesByProvider, accounts, navigation, filterCurrencyIds, context], + ); + + useEffect(() => { + if (paramsCurrency) { + const selectedCurrency = findCryptoCurrencyByKeyword(paramsCurrency.toUpperCase()); + if (selectedCurrency && providersLoadingStatus === "success") { + onPressItem(selectedCurrency); + } + } + }, [onPressItem, paramsCurrency, providersLoadingStatus]); + + const debounceTrackOnSearchChange = debounce((newQuery: string) => { + track("asset_searched", { page: "Choose a crypto to secure", asset: newQuery }); + }, 1500); + + const list = useMemo( + () => + filterCurrencyIdsSet + ? sortedCryptoCurrencies.filter(crypto => filterCurrencyIdsSet.has(crypto.id)) + : sortedCryptoCurrencies, + [filterCurrencyIdsSet, sortedCryptoCurrencies], + ); + const { titleText, titleTestId } = useMemo(() => { + switch (context) { + case "addAccounts": + return { + titleText: t("assetSelection.selectCrypto.title"), + titleTestId: "select-crypto-header-step1-title", + }; + case "receiveFunds": + return { + titleText: t("transfer.receive.selectCrypto.title"), + titleTestId: "receive-header-step1-title", + }; + default: + return {}; + } + }, [context, t]); + + return { + titleText, + titleTestId, + list, + debounceTrackOnSearchChange, + onPressItem, + providersLoadingStatus, + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx index d4787a452db0..5d92f015da33 100644 --- a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx @@ -1,168 +1,39 @@ -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { StyleSheet, FlatList, Linking } from "react-native"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { findCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index"; -import { useCurrenciesByMarketcap } from "@ledgerhq/live-common/currencies/hooks"; - -import { BannerCard, Flex, Text } from "@ledgerhq/native-ui"; -import { useDispatch, useSelector } from "react-redux"; -import { NavigatorName, ScreenName } from "~/const"; -import { track, TrackScreen } from "~/analytics"; -import { flattenAccountsSelector } from "~/reducers/accounts"; +import { StyleSheet, FlatList } from "react-native"; +import { Flex, InfiniteLoader, Text } from "@ledgerhq/native-ui"; +import { ScreenName } from "~/const"; +import { TrackScreen } from "~/analytics"; import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; -import { ChartNetworkMedium } from "@ledgerhq/native-ui/assets/icons"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as Animatable from "react-native-animatable"; -import { setCloseNetworkBanner } from "~/actions/settings"; -import { hasClosedNetworkBannerSelector } from "~/reducers/settings"; import BigCurrencyRow from "~/components/BigCurrencyRow"; -import { findAccountByCurrency } from "~/logic/deposit"; -import { urls } from "~/utils/urls"; import { CryptoWithAccounts } from "./types"; import { AssetSelectionNavigatorParamsList } from "../../types"; +import useSelectNetworkViewModel from "./useSelectNetworkViewModel"; +import NetworkBanner from "../../components/NetworkBanner"; const keyExtractor = (elem: CryptoWithAccounts) => elem.crypto.id; const AnimatedView = Animatable.View; export default function SelectNetwork({ - navigation, route, }: StackNavigatorProps) { - const provider = route?.params?.provider; - const filterCurrencyIds = route?.params?.filterCurrencyIds; - - const networks = useMemo( - () => - provider?.currenciesByNetwork.map(elem => - elem.type === "TokenCurrency" ? elem.parentCurrency.id : elem.id, - ) || [], - [provider?.currenciesByNetwork], - ); - - const dispatch = useDispatch(); - - const hasClosedNetworkBanner = useSelector(hasClosedNetworkBannerSelector); - const [displayBanner, setBanner] = useState(!hasClosedNetworkBanner); - + const { filterCurrencyIds, context, currency } = route.params; const { t } = useTranslation(); - - const cryptoCurrencies = useMemo(() => { - if (!networks) { - return []; - } else { - const list = filterCurrencyIds - ? networks.filter(network => filterCurrencyIds.includes(network)) - : networks; - - return list.map(net => { - const selectedCurrency = findCryptoCurrencyById(net); - if (selectedCurrency) return selectedCurrency; - else return null; - }); - } - }, [filterCurrencyIds, networks]); - - const accounts = useSelector(flattenAccountsSelector); - - const sortedCryptoCurrencies = useCurrenciesByMarketcap( - cryptoCurrencies.filter(e => !!e) as CryptoCurrency[], - ); - - const sortedCryptoCurrenciesWithAccounts: CryptoWithAccounts[] = useMemo( - () => - sortedCryptoCurrencies - .map(crypto => { - const accs = findAccountByCurrency(accounts, crypto); - return { - crypto, - accounts: accs, - }; - }) - .sort((a, b) => b.accounts.length - a.accounts.length), - [accounts, sortedCryptoCurrencies], - ); - - const onPressItem = useCallback( - (currency: CryptoCurrency | TokenCurrency) => { - track("network_clicked", { - network: currency.name, - page: "Choose a network", - }); - - const cryptoToSend = provider?.currenciesByNetwork.find(curByNetwork => - curByNetwork.type === "TokenCurrency" - ? curByNetwork.parentCurrency.id === currency.id - : curByNetwork.id === currency.id, - ); - - if (!cryptoToSend) return; - - const accs = findAccountByCurrency(accounts, cryptoToSend); - - if (accs.length > 0) { - // if we found one or more accounts of the given currency we go to select account - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.SelectAccounts, - params: { - currency: cryptoToSend, - }, - }); - } else if (cryptoToSend.type === "TokenCurrency") { - // cases for token currencies - const parentAccounts = findAccountByCurrency(accounts, cryptoToSend.parentCurrency); - - if (parentAccounts.length > 0) { - // if we found one or more accounts of the parent currency we select account - - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.SelectAccounts, - params: { - currency: cryptoToSend, - createTokenAccount: true, - }, - }); - } else { - // if we didn't find any account of the parent currency we add and create one - navigation.navigate(NavigatorName.DeviceSelection, { - screen: ScreenName.SelectDevice, - params: { - currency: cryptoToSend.parentCurrency, - createTokenAccount: true, - }, - }); - } - } else { - // else we create a currency account - navigation.navigate(NavigatorName.DeviceSelection, { - screen: ScreenName.SelectDevice, - params: { - currency: cryptoToSend, - }, - }); - } - }, - [accounts, navigation, provider], - ); - - const hideBanner = useCallback(() => { - track("button_clicked", { - button: "Close network article", - page: "Choose a network", - }); - dispatch(setCloseNetworkBanner(true)); - setBanner(false); - }, [dispatch]); - - const clickLearn = () => { - track("button_clicked", { - button: "Choose a network article", - type: "card", - page: "Choose a network", - }); - Linking.openURL(urls.chooseNetwork); - }; + const { + hideBanner, + clickLearn, + sortedCryptoCurrenciesWithAccounts, + onPressItem, + displayBanner, + titleText, + subtitleText, + titleTestId, + subTitleTestId, + listTestId, + providersLoadingStatus, + } = useSelectNetworkViewModel({ filterCurrencyIds, context, currency }); const renderItem = useCallback( ({ item }: { item: CryptoWithAccounts }) => ( @@ -182,29 +53,37 @@ export default function SelectNetwork({ return ( <> - - - {t("transfer.receive.selectNetwork.title")} - - - {t("transfer.receive.selectNetwork.subtitle")} + + + {titleText} + {subtitleText && ( + + {subtitleText} + + )} - + {["success", "error"].includes(providersLoadingStatus) ? ( + + ) : ( + + + + )} {displayBanner ? ( @@ -219,27 +98,6 @@ export default function SelectNetwork({ ); } -type BannerProps = { - hideBanner: () => void; - onPress: () => void; -}; - -const NetworkBanner = ({ onPress, hideBanner }: BannerProps) => { - const { t } = useTranslation(); - const insets = useSafeAreaInsets(); - return ( - - } - onPressDismiss={hideBanner} - onPress={onPress} - /> - - ); -}; - const styles = StyleSheet.create({ list: { paddingBottom: 32, diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/useSelectNetworkViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/useSelectNetworkViewModel.ts new file mode 100644 index 000000000000..d35fd90da84f --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/useSelectNetworkViewModel.ts @@ -0,0 +1,217 @@ +import { useCallback, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigation } from "@react-navigation/core"; +import { useTranslation } from "react-i18next"; +import { Linking } from "react-native"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { findCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index"; +import { useCurrenciesByMarketcap } from "@ledgerhq/live-common/currencies/hooks"; +import { hasClosedNetworkBannerSelector } from "~/reducers/settings"; +import { flattenAccountsSelector } from "~/reducers/accounts"; +import { setCloseNetworkBanner } from "~/actions/settings"; +import { findAccountByCurrency } from "~/logic/deposit"; +import { track } from "~/analytics"; +import { NavigatorName, ScreenName } from "~/const"; +import { urls } from "~/utils/urls"; +import { AssetSelectionNavigationProps, SelectNetworkRouteParams } from "../../types"; +import { CryptoWithAccounts } from "./types"; +import { LoadingBasedGroupedCurrencies } from "@ledgerhq/live-common/deposit/type"; +import { useGroupedCurrenciesByProvider } from "@ledgerhq/live-common/deposit/index"; + +export default function useSelectNetworkViewModel({ + filterCurrencyIds, + context, + currency, +}: SelectNetworkRouteParams) { + const navigation = useNavigation(); + + const { result, loadingStatus: providersLoadingStatus } = useGroupedCurrenciesByProvider( + true, + ) as LoadingBasedGroupedCurrencies; + + const { currenciesByProvider } = result; + + const provider = useMemo( + () => + currenciesByProvider.find(elem => + elem.currenciesByNetwork.some( + currencyByNetwork => + (currencyByNetwork as CryptoCurrency | TokenCurrency).id === currency, + ), + ), + [currenciesByProvider, currency], + ); + + const networks = useMemo( + () => + provider?.currenciesByNetwork.map(elem => + elem.type === "TokenCurrency" ? elem?.parentCurrency?.id : elem.id, + ) || [], + [provider?.currenciesByNetwork], + ); + + const dispatch = useDispatch(); + + const hasClosedNetworkBanner = useSelector(hasClosedNetworkBannerSelector); + const [displayBanner, setBanner] = useState(!hasClosedNetworkBanner); + + const { t } = useTranslation(); + + const cryptoCurrencies = useMemo(() => { + if (!networks) { + return []; + } else { + const list = filterCurrencyIds + ? networks.filter(network => filterCurrencyIds.includes(network)) + : networks; + + return list.map(net => { + const selectedCurrency = findCryptoCurrencyById(net); + if (selectedCurrency) return selectedCurrency; + else return null; + }); + } + }, [filterCurrencyIds, networks]); + + const accounts = useSelector(flattenAccountsSelector); + + const sortedCryptoCurrencies = useCurrenciesByMarketcap( + cryptoCurrencies.filter(e => !!e) as CryptoCurrency[], + ); + + const sortedCryptoCurrenciesWithAccounts: CryptoWithAccounts[] = useMemo( + () => + sortedCryptoCurrencies + .map(crypto => { + const accs = findAccountByCurrency(accounts, crypto); + return { + crypto, + accounts: accs, + }; + }) + .sort((a, b) => b.accounts.length - a.accounts.length), + [accounts, sortedCryptoCurrencies], + ); + + const processNetworkSelection = useCallback( + (selectedCurrency: CryptoCurrency | TokenCurrency) => { + track("network_clicked", { + network: selectedCurrency.name, + page: "Choose a network", + }); + + const cryptoToSend = provider?.currenciesByNetwork.find(curByNetwork => + curByNetwork.type === "TokenCurrency" + ? curByNetwork.parentCurrency.id === selectedCurrency.id + : curByNetwork.id === selectedCurrency.id, + ); + + if (!cryptoToSend) return; + + const accs = findAccountByCurrency(accounts, cryptoToSend); + + if (accs.length > 0) { + // if we found one or more accounts of the given currency we go to select account + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.SelectAccounts, + params: { + currency: cryptoToSend, + context, + }, + }); + } else if (cryptoToSend.type === "TokenCurrency") { + // cases for token currencies + const parentAccounts = findAccountByCurrency(accounts, cryptoToSend.parentCurrency); + + if (parentAccounts.length > 0) { + // if we found one or more accounts of the parent currency we select account + + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.SelectAccounts, + params: { + currency: cryptoToSend, + createTokenAccount: true, + context, + }, + }); + } else { + // if we didn't find any account of the parent currency we add and create one + navigation.navigate(NavigatorName.DeviceSelection, { + screen: ScreenName.SelectDevice, + params: { + currency: cryptoToSend.parentCurrency, + createTokenAccount: true, + context, + }, + }); + } + } else { + // else we create a currency account + navigation.navigate(NavigatorName.DeviceSelection, { + screen: ScreenName.SelectDevice, + params: { + currency: cryptoToSend, + context, + }, + }); + } + }, + [accounts, navigation, provider, context], + ); + + const hideBanner = useCallback(() => { + track("button_clicked", { + button: "Close network article", + page: "Choose a network", + }); + dispatch(setCloseNetworkBanner(true)); + setBanner(false); + }, [dispatch]); + + const clickLearn = () => { + track("button_clicked", { + button: "Choose a network article", + type: "card", + page: "Choose a network", + }); + Linking.openURL(urls.chooseNetwork); + }; + + const { titleText, subtitleText, titleTestId, subTitleTestId, listTestId } = useMemo((): Record< + string, + string + > => { + switch (context) { + case "receiveFunds": + return { + titleText: t("selectNetwork.swap.title"), + titleTestId: "receive-header-step2-title", + subtitleText: t("selectNetwork.swap.subtitle"), + subTitleTestId: "transfer.receive.selectNetwork.subtitle", + listTestId: "receive-header-step2-networks", + }; + case "addAccounts": + return { + titleText: t("assetSelection.selectNetwork.title"), + titleTestId: "addAccounts-header-step2-title", + listTestId: "addAccounts-header-step2-networks", + }; + default: + return {}; + } + }, [context, t]); + + return { + hideBanner, + clickLearn, + sortedCryptoCurrenciesWithAccounts, + onPressItem: processNetworkSelection, + displayBanner, + titleText, + subtitleText, + titleTestId, + subTitleTestId, + listTestId, + providersLoadingStatus, + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts index 833b61018759..6ccac4886003 100644 --- a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts @@ -1,32 +1,32 @@ -import { CryptoCurrency, CryptoOrTokenCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { NavigatorScreenParams } from "@react-navigation/core"; import { NavigatorName, ScreenName } from "~/const"; +import { DeviceSelectionNavigatorParamsList } from "../DeviceSelection/types"; +import { NetworkBasedAddAccountNavigator } from "../Accounts/screens/AddAccount/types"; +import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +type CommonParams = { + context?: "addAccounts" | "receiveFunds"; + onSuccess?: () => void; + currency?: string; +}; +export type SelectNetworkRouteParams = CommonParams & { + filterCurrencyIds?: string[]; +}; export type AssetSelectionNavigatorParamsList = { - [ScreenName.AddAccountsSelectCrypto]: { + [ScreenName.AddAccountsSelectCrypto]: CommonParams & { filterCurrencyIds?: string[]; currency?: string; + returnToSwap?: boolean; + analyticsPropertyFlow?: string; }; - [ScreenName.SelectNetwork]: - | { - filterCurrencyIds?: string[]; - provider: { - currenciesByNetwork: CryptoOrTokenCurrency[]; - providerId: string; - }; - } - | undefined; - [NavigatorName.AddAccounts]: { - screen: ScreenName; - params: { - currency: CryptoCurrency | TokenCurrency; - createTokenAccount?: boolean; - }; - }; - [NavigatorName.DeviceSelection]: { - screen: ScreenName; - params: { - currency: CryptoCurrency; - createTokenAccount?: boolean; - }; - }; + [ScreenName.SelectNetwork]: SelectNetworkRouteParams; + [NavigatorName.DeviceSelection]?: Partial< + NavigatorScreenParams + >; + [NavigatorName.AddAccounts]?: Partial>; }; + +export type AssetSelectionNavigationProps = StackNavigatorProps< + AssetSelectionNavigatorParamsList, + ScreenName.AddAccountsSelectCrypto +>; 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 72b78053ed57..01a35c79193d 100644 --- a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx @@ -68,6 +68,7 @@ export default function Navigator() { ), ...addAccountsSelectDeviceHeaderOptions(onClose), }} + initialParams={route.params} /> {/* Select / Connect Device */} 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 91ee1e5311b4..94ce84697635 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 @@ -1,9 +1,7 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback } from "react"; import { StyleSheet, SafeAreaView } from "react-native"; import { Flex } from "@ledgerhq/native-ui"; -import type { Device } from "@ledgerhq/live-common/hw/actions/types"; -import { useIsFocused, useTheme } from "@react-navigation/native"; -import { prepareCurrency } from "~/bridge/cache"; +import { useTheme } from "@react-navigation/native"; import { ScreenName } from "~/const"; import { track } from "~/analytics"; import SelectDevice2, { SetHeaderOptionsRequest } from "~/components/SelectDevice2"; @@ -15,9 +13,9 @@ import { } from "~/components/RootNavigator/types/helpers"; import { NavigationHeaderCloseButtonAdvanced } from "~/components/NavigationHeaderCloseButton"; import { NavigationHeaderBackButton } from "~/components/NavigationHeaderBackButton"; -import { useAppDeviceAction } from "~/hooks/deviceActions"; import { DeviceSelectionNavigatorParamsList } from "../../types"; -import { NetworkBasedAddAccountNavigator } from "~/newArch/features/Accounts/screens/AddAccount/types"; +import { NetworkBasedAddAccountNavigator } from "LLM/features/Accounts/screens/AddAccount/types"; +import useSelectDeviceViewModel from "./useSelectDeviceViewModel"; // Defines some of the header options for this screen to be able to reset back to them. export const addAccountsSelectDeviceHeaderOptions = ( @@ -28,39 +26,17 @@ export const addAccountsSelectDeviceHeaderOptions = ( }); export default function SelectDevice({ - navigation, route, + navigation, }: StackNavigatorProps< DeviceSelectionNavigatorParamsList & Partial, ScreenName.SelectDevice >) { const { currency } = route.params; + const { onResult, device, action, isFocused, onClose, setDevice } = + useSelectDeviceViewModel(route); const { colors } = useTheme(); - const [device, setDevice] = useState(null); - const action = useAppDeviceAction(); - const isFocused = useIsFocused(); - - const onClose = useCallback(() => { - setDevice(null); - }, []); - - const onResult = useCallback( - // @ts-expect-error should be AppResult but navigation.navigate does not agree - meta => { - setDevice(null); - const arg = { ...route.params, ...meta }; - navigation.navigate(ScreenName.ScanDeviceAccounts, arg); - }, - [navigation, route], - ); - - useEffect(() => { - // load ahead of time - prepareCurrency(currency); - }, [currency]); - const analyticsPropertyFlow = route.params?.analyticsPropertyFlow; - const onHeaderCloseButton = useCallback(() => { track("button_clicked", { button: "Close 'x'", @@ -85,7 +61,6 @@ export default function SelectDevice({ }, [navigation, onHeaderCloseButton], ); - return ( , + ScreenName.SelectDevice + >, +) { + const { context, currency } = route.params; + const navigation = useNavigation(); + const [device, setDevice] = useState(null); + const action = useAppDeviceAction(); + const isFocused = useIsFocused(); + + const onClose = useCallback(() => { + setDevice(null); + }, []); + + const onResult = useCallback( + (meta: AppResult) => { + setDevice(null); + const arg = { ...route.params, ...meta, context }; + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.ScanDeviceAccounts, + params: { + ...arg, + }, + }); + }, + [navigation, route, context], + ); + + useEffect(() => { + // load ahead of time + prepareCurrency(currency); + }, [currency]); + + return { + onResult, + device, + action, + isFocused, + onClose, + setDevice, + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts index 6db86661a4b7..dc3315a2a2ad 100644 --- a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts @@ -1,24 +1,39 @@ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import { AccountLike } from "@ledgerhq/types-live"; -import { ScreenName } from "~/const"; +import { NavigatorScreenParams } from "@react-navigation/core"; +import { NavigatorName, ScreenName } from "~/const"; +import { NetworkBasedAddAccountNavigator } from "../Accounts/screens/AddAccount/types"; +import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; + +type CommonParams = { + context?: "addAccounts" | "receiveFunds"; + onSuccess?: () => void; +}; + +export type SelectDeviceRouteParams = CommonParams & { + accountId?: string; + parentId?: string; + currency: CryptoCurrency; + inline?: boolean; + analyticsPropertyFlow?: string; + createTokenAccount?: boolean; +}; export type DeviceSelectionNavigatorParamsList = { - [ScreenName.ConnectDevice]: { + [ScreenName.ConnectDevice]: CommonParams & { account?: AccountLike; accountId: string; parentId?: string; notSkippable?: boolean; title?: string; appName?: string; - onSuccess?: () => void; onError?: () => void; }; - [ScreenName.SelectDevice]: { - accountId?: string; - parentId?: string; - currency: CryptoCurrency; - inline?: boolean; - analyticsPropertyFlow?: string; - createTokenAccount?: boolean; - }; + [ScreenName.SelectDevice]: SelectDeviceRouteParams; + [NavigatorName.AddAccounts]?: Partial>; }; + +export type DeviceSelectionNavigationProps = StackNavigatorProps< + DeviceSelectionNavigatorParamsList, + ScreenName.AddAccountsSelectCrypto +>; diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/Modals/InstalledAppModal.tsx b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/Modals/InstalledAppModal.tsx index 5dc71343f745..617280b6dbd1 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerDevice/Modals/InstalledAppModal.tsx +++ b/apps/ledger-live-mobile/src/screens/MyLedgerDevice/Modals/InstalledAppModal.tsx @@ -17,6 +17,7 @@ import AppIcon from "../AppsList/AppIcon"; import QueuedDrawer from "~/components/QueuedDrawer"; import type { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; import { MyLedgerNavigatorStackParamList } from "~/components/RootNavigator/types/MyLedgerNavigator"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; type NavigationProps = BaseComposite< StackNavigatorProps @@ -49,11 +50,16 @@ const ButtonsContainer = styled(Flex).attrs({ const InstallSuccessBar = ({ state, navigation, disable }: Props) => { const [hasBeenShown, setHasBeenShown] = useState(disable); const { installQueue, uninstallQueue, recentlyInstalledApps, appByName, installed } = state; - + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const onAddAccount = useCallback(() => { - navigation.navigate(NavigatorName.AddAccounts); + if (llmNetworkBasedAddAccountFlow?.enabled) + navigation.navigate(NavigatorName.AssetSelection, { + context: "addAccounts", + }); + else navigation.navigate(NavigatorName.AddAccounts); + setHasBeenShown(true); - }, [navigation]); + }, [navigation, llmNetworkBasedAddAccountFlow?.enabled]); const successInstalls = useMemo( () => diff --git a/apps/ledger-live-mobile/src/screens/ReceiveFunds/01-SelectCrypto.tsx b/apps/ledger-live-mobile/src/screens/ReceiveFunds/01-SelectCrypto.tsx index d7efa4fc4f87..519fbf2766b7 100644 --- a/apps/ledger-live-mobile/src/screens/ReceiveFunds/01-SelectCrypto.tsx +++ b/apps/ledger-live-mobile/src/screens/ReceiveFunds/01-SelectCrypto.tsx @@ -22,6 +22,7 @@ import { getEnv } from "@ledgerhq/live-env"; import { findAccountByCurrency } from "~/logic/deposit"; import { useGroupedCurrenciesByProvider } from "@ledgerhq/live-common/deposit/index"; +import { GroupedCurrencies } from "@ledgerhq/live-common/deposit/type"; const SEARCH_KEYS = getEnv("CRYPTO_ASSET_SEARCH_KEYS"); @@ -50,7 +51,8 @@ export default function AddAccountsSelectCrypto({ navigation, route }: Props) { const { t } = useTranslation(); const accounts = useSelector(flattenAccountsSelector); - const { currenciesByProvider, sortedCryptoCurrencies } = useGroupedCurrenciesByProvider(); + const { currenciesByProvider, sortedCryptoCurrencies } = + useGroupedCurrenciesByProvider() as GroupedCurrencies; const onPressItem = useCallback( (curr: CryptoCurrency | TokenCurrency) => { diff --git a/apps/ledger-live-mobile/src/screens/ReceiveFunds/02-SelectAccount.tsx b/apps/ledger-live-mobile/src/screens/ReceiveFunds/02-SelectAccount.tsx index 4b7224be2c17..f23e63d0ae83 100644 --- a/apps/ledger-live-mobile/src/screens/ReceiveFunds/02-SelectAccount.tsx +++ b/apps/ledger-live-mobile/src/screens/ReceiveFunds/02-SelectAccount.tsx @@ -19,6 +19,7 @@ import { useNavigation } from "@react-navigation/core"; import { withDiscreetMode } from "~/context/DiscreetModeContext"; import { walletSelector } from "~/reducers/wallet"; import { accountNameWithDefaultSelector } from "@ledgerhq/live-wallet/store"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; type SubAccountEnhanced = SubAccount & { parentAccount: Account; @@ -39,9 +40,11 @@ function ReceiveSelectAccount({ ScreenName.ReceiveSelectAccount >) { const currency = route?.params?.currency; + const { t } = useTranslation(); const navigationAccount = useNavigation(); const insets = useSafeAreaInsets(); + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const accounts = useSelector( currency && currency.type === "CryptoCurrency" ? flattenAccountsByCryptoCurrencyScreenSelector(currency) @@ -123,20 +126,30 @@ function ReceiveSelectAccount({ button: "Create a new account", page: "Select account to deposit to", }); - if (currency && currency.type === "TokenCurrency") { - navigationAccount.navigate(NavigatorName.AddAccounts, { - screen: undefined, - params: { - token: currency, - }, + + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigationAccount.navigate(NavigatorName.AssetSelection, { + ...(currency && currency.type === "TokenCurrency" + ? { token: currency.id } + : { currency: currency.id }), + context: "addAccounts", }); } else { - navigationAccount.navigate(NavigatorName.AddAccounts, { - screen: undefined, - currency, - }); + if (currency && currency.type === "TokenCurrency") { + navigationAccount.navigate(NavigatorName.AddAccounts, { + screen: undefined, + params: { + token: currency, + }, + }); + } else { + navigationAccount.navigate(NavigatorName.AddAccounts, { + screen: undefined, + currency, + }); + } } - }, [currency, navigationAccount]); + }, [currency, navigationAccount, llmNetworkBasedAddAccountFlow?.enabled]); const keyExtractor = useCallback((item: AccountLikeEnhanced) => item?.id, []); diff --git a/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx b/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx index a487fca28e61..077923298754 100644 --- a/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx +++ b/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx @@ -4,7 +4,7 @@ import { Trans } from "react-i18next"; import type { Account, AccountLike, SubAccount } from "@ledgerhq/types-live"; import { useSelector } from "react-redux"; import { CompositeScreenProps, useTheme } from "@react-navigation/native"; -import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { CryptoCurrency, CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; import { useGetAccountIds } from "@ledgerhq/live-common/wallet-api/react"; import { accountsByCryptoCurrencyScreenSelector } from "~/reducers/accounts"; import { TrackScreen } from "~/analytics"; @@ -23,6 +23,7 @@ import type { import { RequestAccountNavigatorParamList } from "~/components/RootNavigator/types/RequestAccountNavigator"; import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator"; import { Flex } from "@ledgerhq/native-ui"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; const SEARCH_KEYS = [ "name", @@ -99,6 +100,7 @@ function SelectAccount({ navigation, route }: Props) { const { accounts$, currency, allowAddAccount, onSuccess } = route.params; const accountIds = useGetAccountIds(accounts$); const accounts = useSelector(accountsByCryptoCurrencyScreenSelector(currency, accountIds)); + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const onSelect = useCallback( (account: AccountLike, parentAccount?: Account) => { onSuccess && onSuccess(account, parentAccount); @@ -115,14 +117,27 @@ function SelectAccount({ navigation, route }: Props) { ); const onAddAccount = useCallback(() => { - navigation.navigate(NavigatorName.RequestAccountsAddAccounts, { - screen: ScreenName.AddAccountsSelectDevice, - params: { - currency: currency as CryptoOrTokenCurrency, - onSuccess: () => navigation.navigate(ScreenName.RequestAccountsSelectAccount, route.params), - }, - }); - }, [currency, navigation, route.params]); + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigation.navigate(NavigatorName.DeviceSelection, { + screen: ScreenName.SelectDevice, + params: { + currency: currency as CryptoCurrency, + context: "addAccounts", + onSuccess: () => + navigation.navigate(ScreenName.RequestAccountsSelectAccount, route.params), + }, + }); + } else { + navigation.navigate(NavigatorName.RequestAccountsAddAccounts, { + screen: ScreenName.AddAccountsSelectDevice, + params: { + currency: currency as CryptoOrTokenCurrency, + onSuccess: () => + navigation.navigate(ScreenName.RequestAccountsSelectAccount, route.params), + }, + }); + } + }, [currency, navigation, route.params, llmNetworkBasedAddAccountFlow?.enabled]); const renderFooter = useCallback( () => diff --git a/apps/ledger-live-mobile/src/screens/Swap/Form/Summary/index.tsx b/apps/ledger-live-mobile/src/screens/Swap/Form/Summary/index.tsx index 3f9b8f18f6d4..101f30e30825 100644 --- a/apps/ledger-live-mobile/src/screens/Swap/Form/Summary/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Swap/Form/Summary/index.tsx @@ -29,6 +29,7 @@ import { sharedSwapTracking } from "../../utils"; import { EDITABLE_FEE_FAMILIES } from "@ledgerhq/live-common/exchange/swap/const/blockchain"; import { useMaybeAccountName } from "~/reducers/wallet"; import { useMaybeAccountUnit } from "~/hooks/useAccountUnit"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; interface Props { provider?: string; @@ -49,6 +50,7 @@ export function Summary({ provider, swapTx: { swap, status, transaction } }: Pro const exchangeRate = useSelector(rateSelector); const ratesExpiration = useSelector(rateExpirationSelector); const rawCounterValueCurrency = useSelector(counterValueCurrencySelector); + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const name = useMemo(() => provider && getProviderName(provider), [provider]); @@ -84,23 +86,42 @@ export function Summary({ provider, swapTx: { swap, status, transaction } }: Pro }; if (to.currency.type === "TokenCurrency") { - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.AddAccountsTokenCurrencyDisclaimer, - params: { + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigation.navigate(NavigatorName.AssetSelection, { ...params, token: to.currency, - }, - }); + context: "addAccounts", + }); + } else { + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.AddAccountsTokenCurrencyDisclaimer, + params: { + ...params, + token: to.currency, + }, + }); + } } else { - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.AddAccountsSelectDevice, - params: { - ...params, - currency: to.currency, - }, - }); + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigation.navigate(NavigatorName.DeviceSelection, { + screen: ScreenName.SelectDevice, + params: { + ...params, + currency: to.currency, + context: "addAccounts", + }, + }); + } else { + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.AddAccountsSelectDevice, + params: { + ...params, + currency: to.currency, + }, + }); + } } - }, [navigation, to, track]); + }, [navigation, to, track, llmNetworkBasedAddAccountFlow?.enabled]); const counterValueCurrency = to.currency || rawCounterValueCurrency; const effectiveUnit = from.currency?.units[0]; diff --git a/apps/ledger-live-mobile/src/screens/Swap/SubScreens/SelectAccount.tsx b/apps/ledger-live-mobile/src/screens/Swap/SubScreens/SelectAccount.tsx index baf8e27d8436..03af2952c841 100644 --- a/apps/ledger-live-mobile/src/screens/Swap/SubScreens/SelectAccount.tsx +++ b/apps/ledger-live-mobile/src/screens/Swap/SubScreens/SelectAccount.tsx @@ -21,10 +21,11 @@ import { accountsSelector } from "~/reducers/accounts"; import { sharedSwapTracking } from "../utils"; import { walletSelector } from "~/reducers/wallet"; import { accountNameWithDefaultSelector } from "@ledgerhq/live-wallet/store"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; export function SelectAccount({ navigation, route: { params } }: SelectAccountParamList) { const { provider, target, selectableCurrencyIds, selectedCurrency } = params; - + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const { track } = useAnalytics(); const unfilteredAccounts = useSelector(accountsSelector); @@ -146,19 +147,34 @@ export function SelectAccount({ navigation, route: { params } }: SelectAccountPa account: "account", button: "new source account", }); - // @ts-expect-error navigation type is only partially declared - navigation.navigate(NavigatorName.AddAccounts, { - screen: ScreenName.AddAccountsSelectCrypto, - params: { - returnToSwap: true, - filterCurrencyIds: selectableCurrencyIds, - onSuccess: () => { - navigation.navigate(ScreenName.SwapSelectAccount, params); + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigation.navigate(NavigatorName.AssetSelection, { + screen: ScreenName.AddAccountsSelectCrypto, + params: { + returnToSwap: true, + filterCurrencyIds: selectableCurrencyIds, + onSuccess: () => { + navigation.navigate(ScreenName.SwapSelectAccount, params); + }, + analyticsPropertyFlow: "swap", + context: "addAccounts", }, - analyticsPropertyFlow: "swap", - }, - }); - }, [navigation, params, selectableCurrencyIds, track]); + }); + } else { + // @ts-expect-error navigation type is only partially declared + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.AddAccountsSelectCrypto, + params: { + returnToSwap: true, + filterCurrencyIds: selectableCurrencyIds, + onSuccess: () => { + navigation.navigate(ScreenName.SwapSelectAccount, params); + }, + analyticsPropertyFlow: "swap", + }, + }); + } + }, [navigation, params, selectableCurrencyIds, track, llmNetworkBasedAddAccountFlow?.enabled]); const renderList = useCallback( (items: typeof allAccounts) => { diff --git a/apps/ledger-live-mobile/src/screens/WalletCentricAsset/index.tsx b/apps/ledger-live-mobile/src/screens/WalletCentricAsset/index.tsx index cf50cfb9d7c4..55e5b3e6bcce 100644 --- a/apps/ledger-live-mobile/src/screens/WalletCentricAsset/index.tsx +++ b/apps/ledger-live-mobile/src/screens/WalletCentricAsset/index.tsx @@ -36,6 +36,7 @@ import { View } from "react-native-animatable"; import Alert from "~/components/Alert"; import { urls } from "~/utils/urls"; import { CurrencyConfig } from "@ledgerhq/coin-framework/config"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; const AnimatedFlatListWithRefreshControl = Animated.createAnimatedComponent( accountSyncRefreshControl(FlatList), @@ -48,6 +49,7 @@ type NavigationProps = BaseComposite< const AssetScreen = ({ route }: NavigationProps) => { const { t } = useTranslation(); const { colors } = useTheme(); + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); const navigation = useNavigation(); const { currency } = route?.params; const cryptoAccounts = useSelector( @@ -85,20 +87,29 @@ const AssetScreen = ({ route }: NavigationProps) => { track("button_clicked", { button: "Add new", }); - if (currency && currency.type === "TokenCurrency") { - navigation.navigate(NavigatorName.AddAccounts, { - screen: undefined, - params: { - token: currency, - }, + if (llmNetworkBasedAddAccountFlow?.enabled) { + navigation.navigate(NavigatorName.AssetSelection, { + ...(currency && currency.type === "TokenCurrency" + ? { token: currency.id } + : { currency: currency.id }), + context: "addAccounts", }); } else { - navigation.navigate(NavigatorName.AddAccounts, { - screen: undefined, - currency, - }); + if (currency && currency.type === "TokenCurrency") { + navigation.navigate(NavigatorName.AddAccounts, { + screen: undefined, + params: { + token: currency, + }, + }); + } else { + navigation.navigate(NavigatorName.AddAccounts, { + screen: undefined, + currency, + }); + } } - }, [currency, navigation]); + }, [currency, navigation, llmNetworkBasedAddAccountFlow]); let currencyConfig: CurrencyConfig | undefined = undefined; if (isCryptoCurrency(currency)) { diff --git a/libs/ledger-live-common/src/deposit/deposit.integration.test.ts b/libs/ledger-live-common/src/deposit/deposit.integration.test.ts index c1ce7f1ea469..384364aa7a58 100644 --- a/libs/ledger-live-common/src/deposit/deposit.integration.test.ts +++ b/libs/ledger-live-common/src/deposit/deposit.integration.test.ts @@ -4,13 +4,31 @@ import "../__tests__/test-helpers/setup"; import { renderHook, waitFor } from "@testing-library/react"; import { useGroupedCurrenciesByProvider } from "."; +import { GroupedCurrencies, LoadingBasedGroupedCurrencies } from "./type"; +// Explicitly mock the featureFlags module -test("list is starting with Bitcoin", async () => { - const { result } = renderHook(() => useGroupedCurrenciesByProvider()); +describe("useGroupedCurrenciesByProvider", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it("should list is starting with Bitcoin", async () => { + const { result } = renderHook(() => useGroupedCurrenciesByProvider()); + await waitFor(() => + expect( + (result.current as GroupedCurrencies).sortedCryptoCurrencies.slice(0, 1).map(o => o.id), + ).toMatchObject(["bitcoin"]), + ); + }); - await waitFor(() => - expect(result.current.sortedCryptoCurrencies.slice(0, 1).map(o => o.id)).toMatchObject([ - "bitcoin", - ]), - ); + it("should list is starting with Bitcoin when withLoading is activated", async () => { + const { result: hookRef } = renderHook(() => useGroupedCurrenciesByProvider(true)); + + await waitFor(() => + expect( + (hookRef.current as LoadingBasedGroupedCurrencies).result.sortedCryptoCurrencies + .slice(0, 1) + .map(o => o.id), + ).toMatchObject(["bitcoin"]), + ); + }); }); diff --git a/libs/ledger-live-common/src/deposit/type.ts b/libs/ledger-live-common/src/deposit/type.ts index 4edbf28e1de9..c248fd8ccfc1 100644 --- a/libs/ledger-live-common/src/deposit/type.ts +++ b/libs/ledger-live-common/src/deposit/type.ts @@ -31,3 +31,15 @@ export type GroupedCurrencies = { currenciesByProvider: CurrenciesByProviderId[]; sortedCryptoCurrencies: CryptoOrTokenCurrency[]; }; + +export enum LoadingStatus { + Idle = "idle", + Pending = "pending", + Success = "success", + Error = "error", +} + +export type LoadingBasedGroupedCurrencies = { + result: GroupedCurrencies; + loadingStatus: LoadingStatus; +}; diff --git a/libs/ledger-live-common/src/deposit/useGroupedCurrenciesByProvider.hook.ts b/libs/ledger-live-common/src/deposit/useGroupedCurrenciesByProvider.hook.ts index 32861b38b065..b7d49aac5ef9 100644 --- a/libs/ledger-live-common/src/deposit/useGroupedCurrenciesByProvider.hook.ts +++ b/libs/ledger-live-common/src/deposit/useGroupedCurrenciesByProvider.hook.ts @@ -1,4 +1,4 @@ -import { GroupedCurrencies } from "./type"; +import { GroupedCurrencies, LoadingBasedGroupedCurrencies, LoadingStatus } from "./type"; import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; import { useEffect, useMemo, useState } from "react"; import { isCurrencySupported, listSupportedCurrencies, listTokens } from "../currencies"; @@ -12,9 +12,12 @@ const initialResult: GroupedCurrencies = { currenciesByProvider: [], }; -export const useGroupedCurrenciesByProvider = () => { +export const useGroupedCurrenciesByProvider = ( + withLoading?: boolean, +): GroupedCurrencies | LoadingBasedGroupedCurrencies => { const [result, setResult] = useState(initialResult); + const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.Idle); const coinsAndTokensSupported = useMemo( () => (listSupportedCurrencies() as CryptoOrTokenCurrency[]).concat(listSupportedTokens()), [], @@ -22,8 +25,17 @@ export const useGroupedCurrenciesByProvider = () => { // Get mapped assets filtered by supported & sorted currencies, grouped by provider id useEffect(() => { - loadCurrenciesByProvider(coinsAndTokensSupported).then(setResult); - }, [coinsAndTokensSupported]); - - return result; + if (withLoading) { + setLoadingStatus(LoadingStatus.Idle); + loadCurrenciesByProvider(coinsAndTokensSupported) + .then(data => { + setResult(data); + setLoadingStatus(LoadingStatus.Success); + }) + .catch(() => setLoadingStatus(LoadingStatus.Error)); + } else { + loadCurrenciesByProvider(coinsAndTokensSupported).then(setResult); + } + }, [coinsAndTokensSupported, withLoading]); + return withLoading ? { result, loadingStatus } : result; };