From e381081aa94d629ca1bee0802b0c95676aee7bdc Mon Sep 17 00:00:00 2001 From: Mounir Hamzaoui Date: Fri, 29 Nov 2024 16:12:26 +0100 Subject: [PATCH] refactor: copy & rename Receive's reusable step screens for new add account flow (#8377) --- .changeset/eight-ducks-rescue.md | 5 + .../RootNavigator/BaseNavigator.tsx | 33 +- .../RootNavigator/types/BaseNavigator.ts | 27 +- .../src/const/navigation.ts | 9 + .../src/navigation/DeeplinksProvider.tsx | 49 ++- .../newArch/features/Accounts/Navigator.tsx | 60 +++ .../useSelectAddAccountMethodViewModel.ts | 15 +- .../Accounts/screens/AddAccount/types.ts | 15 + .../screens/ScanDeviceAccounts/index.tsx | 367 ++++++++++++++++++ .../Accounts/screens/SelectAccounts/index.tsx | 171 ++++++++ .../features/AssetSelection/Navigator.tsx | 78 ++++ .../screens/SelectCrypto/index.tsx | 163 ++++++++ .../screens/SelectNetwork/index.tsx | 247 ++++++++++++ .../screens/SelectNetwork/types.ts | 4 + .../newArch/features/AssetSelection/types.ts | 32 ++ .../features/DeviceSelection/Navigator.tsx | 94 +++++ .../screens/ConnectDevice/index.tsx | 179 +++++++++ .../screens/SelectDevice/index.tsx | 133 +++++++ .../newArch/features/DeviceSelection/types.ts | 24 ++ 19 files changed, 1673 insertions(+), 32 deletions(-) create mode 100644 .changeset/eight-ducks-rescue.md create mode 100644 apps/ledger-live-mobile/src/newArch/features/Accounts/Navigator.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Accounts/screens/AddAccount/types.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/Accounts/screens/ScanDeviceAccounts/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Accounts/screens/SelectAccounts/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/types.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/ConnectDevice/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts diff --git a/.changeset/eight-ducks-rescue.md b/.changeset/eight-ducks-rescue.md new file mode 100644 index 000000000000..fcccba8e9e4b --- /dev/null +++ b/.changeset/eight-ducks-rescue.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +Prepare add account v2 reusable screens diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx b/apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx index 904848ec7ee2..0d8c92420121 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx +++ b/apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx @@ -91,6 +91,9 @@ import CustomErrorNavigator from "./CustomErrorNavigator"; import WalletSyncNavigator from "LLM/features/WalletSync/WalletSyncNavigator"; import Web3HubNavigator from "LLM/features/Web3Hub/Navigator"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import AddAccountsV2Navigator from "LLM/features/Accounts/Navigator"; +import DeviceSelectionNavigator from "LLM/features/DeviceSelection/Navigator"; +import AssetSelectionNavigator from "LLM/features/AssetSelection/Navigator"; const Stack = createStackNavigator(); @@ -109,6 +112,7 @@ export default function BaseNavigator() { const isAccountsEmpty = useSelector(hasNoAccountsSelector); const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector) && isAccountsEmpty; const web3hub = useFeature("web3hub"); + const llmNetworkBasedAddAccountFlow = useFeature("llmNetworkBasedAddAccountFlow"); return ( <> @@ -267,11 +271,7 @@ export default function BaseNavigator() { component={ClaimRewardsNavigator} options={{ headerShown: false }} /> - + + + ) : null} + + + + ); +} + +function AddingAccountLoading({ currency }: { currency: Currency }) { + const { t } = useTranslation(); + + return ( + + ); +} + +function Loading({ + children, + title, + subtitle, +}: { + children?: React.ReactNode; + title: string; + subtitle?: string; +}) { + const { colors } = useTheme(); + + return ( + <> + + + + + + + {title} + + + {subtitle} + + + {children} + + + ); +} + +export default memo(ScanDeviceAccounts); diff --git a/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/SelectAccounts/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/SelectAccounts/index.tsx new file mode 100644 index 000000000000..431110a4f06c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Accounts/screens/SelectAccounts/index.tsx @@ -0,0 +1,171 @@ +import React, { useCallback, useMemo } from "react"; +import { FlatList, ListRenderItemInfo } from "react-native"; +import { useSelector } from "react-redux"; + +import { Button, Flex, Text } from "@ledgerhq/native-ui"; +import { useTranslation } from "react-i18next"; +import { Account, SubAccount, TokenAccount } from "@ledgerhq/types-live"; +import { makeEmptyTokenAccount } from "@ledgerhq/live-common/account/index"; +import { flattenAccountsByCryptoCurrencyScreenSelector } from "~/reducers/accounts"; +import { ScreenName } from "~/const"; +import { track, TrackScreen } from "~/analytics"; + +import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +import AccountCard from "~/components/AccountCard"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { withDiscreetMode } from "~/context/DiscreetModeContext"; +import { walletSelector } from "~/reducers/wallet"; +import { accountNameWithDefaultSelector } from "@ledgerhq/live-wallet/store"; +import { NetworkBasedAddAccountNavigator } from "LLM/features/Accounts/screens/AddAccount/types"; + +type SubAccountEnhanced = SubAccount & { + parentAccount: Account; + triggerCreateAccount: boolean; +}; + +type AccountLikeEnhanced = SubAccountEnhanced | Account | TokenAccount; + +//TODO: (receive flow only) implement this in this ticket https://ledgerhq.atlassian.net/browse/LIVE-14640 +/*type NavigationProps = BaseComposite< + StackNavigatorProps +>;*/ + +function SelectAccount({ + route, +}: StackNavigatorProps) { + const currency = route?.params?.currency; + const { t } = useTranslation(); + //TODO: (receive flow only) implement this in this ticket https://ledgerhq.atlassian.net/browse/LIVE-14640 + //const navigationAccount = useNavigation(); + const insets = useSafeAreaInsets(); + const accounts = useSelector( + currency && currency.type === "CryptoCurrency" + ? flattenAccountsByCryptoCurrencyScreenSelector(currency) + : () => null, + ); + const parentAccounts = useSelector( + currency && currency.type === "TokenCurrency" + ? flattenAccountsByCryptoCurrencyScreenSelector(currency?.parentCurrency) + : () => null, + ); + + const aggregatedAccounts = useMemo( + () => + currency && currency.type === "TokenCurrency" + ? parentAccounts!.reduce((accs, pa) => { + const tokenAccounts = + pa.type === "Account" && pa.subAccounts + ? pa.subAccounts?.filter( + acc => acc.type === "TokenAccount" && acc.token.id === currency.id, + ) + : []; + + if (tokenAccounts && tokenAccounts.length > 0) { + accs.push(...tokenAccounts); + } else if (pa.type === "Account") { + const tokenAcc = makeEmptyTokenAccount(pa, currency); + + const tokenA: SubAccountEnhanced = { + ...tokenAcc, + parentAccount: pa, + triggerCreateAccount: true, + }; + + accs.push(tokenA); + } + + return accs; + }, []) + : accounts, + [accounts, currency, parentAccounts], + ); + + const selectAccount = useCallback((account: AccountLikeEnhanced) => { + // TODO: implement this to support multiple and single selection of accounts (add account / receive) + console.warn("selected account ", account); // TODO: remove this after the implementation in the next ticket + }, []); + + const walletState = useSelector(walletSelector); + + const renderItem = useCallback( + ({ item }: ListRenderItemInfo) => ( + + {accountNameWithDefaultSelector(walletState, item)} + } + onPress={() => selectAccount(item)} + /> + + ), + [walletState, selectAccount], + ); + + const createNewAccount = useCallback(() => { + track("button_clicked", { + button: "Create a new account", + page: "Select account to deposit to", + }); + /** + * TODO: (receive flow only) implement this in this ticket https://ledgerhq.atlassian.net/browse/LIVE-14645 + * if (currency && currency.type === "TokenCurrency") { + navigationAccount.navigate(NavigatorName.AddAccounts, { + screen: undefined, + params: { + token: currency, + }, + }); + } else { + navigationAccount.navigate(NavigatorName.AddAccounts, { + screen: undefined, + currency, + }); + } + */ + }, []); + + const keyExtractor = useCallback((item: AccountLikeEnhanced) => item?.id, []); + + return currency && aggregatedAccounts && aggregatedAccounts.length > 0 ? ( + <> + + + + {t("transfer.receive.selectAccount.title")} + + + {t("transfer.receive.selectAccount.subtitle", { + currencyTicker: currency.ticker, + })} + + + + + + + + + ) : null; +} +export default withDiscreetMode(SelectAccount); diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx new file mode 100644 index 000000000000..1a6cd42457fc --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/Navigator.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useMemo } from "react"; +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 { getStackNavigatorConfig } from "~/navigation/navigatorConfig"; +import { track } from "~/analytics"; +import { Flex } from "@ledgerhq/native-ui"; +import HelpButton from "~/screens/ReceiveFunds/HelpButton"; +import { useSelector } from "react-redux"; +import { hasClosedNetworkBannerSelector } from "~/reducers/settings"; +import { urls } from "~/utils/urls"; +import SelectCrypto from "LLM/features/AssetSelection/screens/SelectCrypto"; +import SelectNetwork from "LLM/features/AssetSelection/screens/SelectNetwork"; +import { NavigationHeaderCloseButtonAdvanced } from "~/components/NavigationHeaderCloseButton"; +import { NavigationHeaderBackButton } from "~/components/NavigationHeaderBackButton"; +import { AssetSelectionNavigatorParamsList } from "./types"; + +export default function Navigator() { + const { colors } = useTheme(); + const route = useRoute(); + + const hasClosedNetworkBanner = useSelector(hasClosedNetworkBannerSelector); + + const onClose = useCallback(() => { + track("button_clicked", { + button: "Close", + screen: route.name, + }); + }, [route]); + + const stackNavigationConfig = useMemo( + () => ({ + ...getStackNavigatorConfig(colors, true), + headerRight: () => , + }), + [colors, onClose], + ); + + return ( + + , + headerTitle: "", + headerRight: () => , + }} + /> + + , + headerTitle: "", + headerRight: () => ( + + {hasClosedNetworkBanner && ( + + )} + + + ), + }} + /> + + ); +} + +const Stack = createStackNavigator(); 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 new file mode 100644 index 000000000000..9e5bd36b0b15 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectCrypto/index.tsx @@ -0,0 +1,163 @@ +import React, { useCallback, useEffect, useMemo } from "react"; +import { Trans, useTranslation } 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 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"; + +const SEARCH_KEYS = getEnv("CRYPTO_ASSET_SEARCH_KEYS"); + +const keyExtractor = (currency: CryptoCurrency | TokenCurrency) => currency.id; + +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 renderList = useCallback( + (items: CryptoOrTokenCurrency[]) => ( + ( + + )} + keyExtractor={keyExtractor} + showsVerticalScrollIndicator={false} + keyboardDismissMode="on-drag" + /> + ), + [onPressItem], + ); + + const list = useMemo( + () => + filterCurrencyIdsSet + ? sortedCryptoCurrencies.filter(crypto => filterCurrencyIdsSet.has(crypto.id)) + : sortedCryptoCurrencies, + [filterCurrencyIdsSet, sortedCryptoCurrencies], + ); + + return ( + + + + {t("transfer.receive.selectCrypto.title")} + + {list.length > 0 ? ( + + + + ) : ( + + + + )} + + ); +} 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 new file mode 100644 index 000000000000..d4787a452db0 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/index.tsx @@ -0,0 +1,247 @@ +import React, { useCallback, useMemo, useState } 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 { 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"; + +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 { 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 renderItem = useCallback( + ({ item }: { item: CryptoWithAccounts }) => ( + 0 + ? t("transfer.receive.selectNetwork.account", { count: item.accounts.length }) + : "" + } + /> + ), + [onPressItem, t], + ); + + return ( + <> + + + + {t("transfer.receive.selectNetwork.title")} + + + {t("transfer.receive.selectNetwork.subtitle")} + + + + + + {displayBanner ? ( + + + + ) : ( + + + + )} + + ); +} + +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/types.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/types.ts new file mode 100644 index 000000000000..19d68b86f759 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/screens/SelectNetwork/types.ts @@ -0,0 +1,4 @@ +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { AccountLike } from "@ledgerhq/types-live"; + +export type CryptoWithAccounts = { crypto: CryptoCurrency; accounts: AccountLike[] }; diff --git a/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts new file mode 100644 index 000000000000..833b61018759 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/AssetSelection/types.ts @@ -0,0 +1,32 @@ +import { CryptoCurrency, CryptoOrTokenCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { NavigatorName, ScreenName } from "~/const"; + +export type AssetSelectionNavigatorParamsList = { + [ScreenName.AddAccountsSelectCrypto]: { + filterCurrencyIds?: string[]; + currency?: 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; + }; + }; +}; diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx new file mode 100644 index 000000000000..72b78053ed57 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/Navigator.tsx @@ -0,0 +1,94 @@ +import React, { useCallback, useMemo } from "react"; +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 { 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"; + +export default function Navigator() { + const { colors } = useTheme(); + const { t } = useTranslation(); + const route = useRoute(); + + const onClose = useCallback(() => { + track("button_clicked", { + button: "Close", + screen: route.name, + }); + }, [route]); + + const stackNavigationConfig = useMemo( + () => ({ + ...getStackNavigatorConfig(colors, true), + headerRight: () => , + }), + [colors, onClose], + ); + + const onConnectDeviceBack = useCallback((navigation: NavigationProp>) => { + track("button_clicked", { + button: "Back arrow", + page: ScreenName.ConnectDevice, + }); + navigation.goBack(); + }, []); + + return ( + + {/* Select Device */} + ( + + ), + ...addAccountsSelectDeviceHeaderOptions(onClose), + }} + /> + + {/* Select / Connect Device */} + ({ + headerTitle: () => ( + + ), + ...connectDeviceHeaderOptions(() => onConnectDeviceBack(navigation)), + })} + /> + + ); +} + +const Stack = createStackNavigator(); 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 new file mode 100644 index 000000000000..ee3400828799 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/ConnectDevice/index.tsx @@ -0,0 +1,179 @@ +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 new file mode 100644 index 000000000000..91ee1e5311b4 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/screens/SelectDevice/index.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useEffect, useState } 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 { ScreenName } from "~/const"; +import { track } from "~/analytics"; +import SelectDevice2, { SetHeaderOptionsRequest } from "~/components/SelectDevice2"; +import DeviceActionModal from "~/components/DeviceActionModal"; + +import { + ReactNavigationHeaderOptions, + StackNavigatorProps, +} 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"; + +// Defines some of the header options for this screen to be able to reset back to them. +export const addAccountsSelectDeviceHeaderOptions = ( + onClose: () => void, +): ReactNavigationHeaderOptions => ({ + headerRight: () => , + headerLeft: () => , +}); + +export default function SelectDevice({ + navigation, + route, +}: StackNavigatorProps< + DeviceSelectionNavigatorParamsList & Partial, + ScreenName.SelectDevice +>) { + const { currency } = route.params; + 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'", + page: route.name, + }); + }, [route]); + + const requestToSetHeaderOptions = useCallback( + (request: SetHeaderOptionsRequest) => { + if (request.type === "set") { + navigation.setOptions({ + headerShown: true, + headerLeft: request.options.headerLeft, + headerRight: request.options.headerRight, + }); + } else { + // Sets back the header to its initial values set for this screen + navigation.setOptions({ + ...addAccountsSelectDeviceHeaderOptions(onHeaderCloseButton), + }); + } + }, + [navigation, onHeaderCloseButton], + ); + + return ( + + {/* + TODO: should be rendered only on receive flow context -> TO BE DONE After delivering the add account flow + + */} + + + + setDevice(null)} + analyticsPropertyFlow={analyticsPropertyFlow || "add account"} + /> + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + scroll: { + flex: 1, + backgroundColor: "transparent", + }, + scrollContainer: { + padding: 16, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts new file mode 100644 index 000000000000..6db86661a4b7 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/DeviceSelection/types.ts @@ -0,0 +1,24 @@ +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { AccountLike } from "@ledgerhq/types-live"; +import { ScreenName } from "~/const"; + +export type DeviceSelectionNavigatorParamsList = { + [ScreenName.ConnectDevice]: { + 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; + }; +};