diff --git a/.changeset/hip-plants-impress.md b/.changeset/hip-plants-impress.md new file mode 100644 index 000000000000..7ee6778318c0 --- /dev/null +++ b/.changeset/hip-plants-impress.md @@ -0,0 +1,8 @@ +--- +"live-mobile": minor +--- + +feat(web3hub): new bottom modal to select network and account with dapp browser v3 +Small perf improvement for dapp browser v3 dapps initial load +Lots of small visual improvements, polish and bugfixes +Update app screen header url diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/AddAccountsNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/AddAccountsNavigator.ts index bda002c19d75..1e30400175a8 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/AddAccountsNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/AddAccountsNavigator.ts @@ -15,14 +15,14 @@ export type AddAccountsNavigatorParamList = { inline?: boolean; returnToSwap?: boolean; analyticsPropertyFlow?: string; - onSuccess?: () => void; + onSuccess?: (res: { scannedAccounts: Account[]; selected: Account[] }) => void; }; [ScreenName.AddAccountsAccounts]: { currency: CryptoOrTokenCurrency; device: Device; inline?: boolean; returnToSwap?: boolean; - onSuccess?: (_?: unknown) => void; + onSuccess?: (res: { scannedAccounts: Account[]; selected: Account[] }) => void; }; [ScreenName.AddAccountsSuccess]?: { currency: CryptoOrTokenCurrency; diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index fecfc7b7c453..200a9804a707 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -6880,6 +6880,19 @@ "title": "Clear signing", "description": "Clear signing allows you to sign a message without revealing the content of the message to the app. This is useful for privacy and security reasons." } + }, + "app": { + "selectAccountModal": { + "networkHeader": { + "title": "Choose a network" + }, + "accountHeader": { + "title": "Choose an account" + }, + "addAccountItem": { + "name": "Add an account" + } + } } } } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountButton.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountButton.tsx index 5244e936605d..5bf98a64269e 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountButton.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountButton.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; import { Trans } from "react-i18next"; import { Text } from "@ledgerhq/native-ui"; import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import { CurrentAccountHistDB } from "@ledgerhq/live-common/wallet-api/react"; +import { useDappCurrentAccount } from "@ledgerhq/live-common/wallet-api/useDappLogic"; import Button from "~/components/Button"; import CircleCurrencyIcon from "~/components/CircleCurrencyIcon"; -import { useSelectAccount } from "~/components/Web3AppWebview/helpers"; import { useMaybeAccountName } from "~/reducers/wallet"; +import SelectAccountModal from "./SelectAccountModal"; type SelectAccountButtonProps = { manifest: AppManifest; @@ -17,36 +18,55 @@ export default function SelectAccountButton({ manifest, currentAccountHistDb, }: SelectAccountButtonProps) { - const { onSelectAccount, currentAccount } = useSelectAccount({ manifest, currentAccountHistDb }); + const { currentAccount } = useDappCurrentAccount(currentAccountHistDb); const currentAccountName = useMaybeAccountName(currentAccount); + const [modalOpened, setModalOpened] = useState(false); + + const onSelectAccount = useCallback(() => { + setModalOpened(true); + }, []); + + const onClose = useCallback(() => { + setModalOpened(false); + }, []); + return ( - + <> + + + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/BackButton/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/BackButton/index.tsx new file mode 100644 index 000000000000..6a4e89b2dd22 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/BackButton/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { BorderlessButton } from "react-native-gesture-handler"; +import { Box, Icons } from "@ledgerhq/native-ui"; + +export default function BackButton({ onPress }: { onPress: () => void }) { + return ( + + + + + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/index.tsx new file mode 100644 index 000000000000..a19d34ad3687 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountHeader/index.tsx @@ -0,0 +1,23 @@ +import React, { PropsWithChildren } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, Flex, Text } from "@ledgerhq/native-ui"; +import BackButton from "./BackButton"; + +export default function AccountHeaderWrapper(onBackPress: () => void) { + return function AccountHeader({ children }: PropsWithChildren) { + const { t } = useTranslation(); + + return ( + <> + + + + {t("web3hub.app.selectAccountModal.accountHeader.title")} + + {children} + + + + ); + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/AddAccountItem/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/AddAccountItem/index.tsx new file mode 100644 index 000000000000..93f5555c8b76 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/AddAccountItem/index.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { TouchableOpacity } from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { useTranslation } from "react-i18next"; +import { Flex, Text, Icons } from "@ledgerhq/native-ui"; + +export default function AddAccountItem({ onPress }: { onPress: () => void }) { + const { t } = useTranslation(); + const { colors } = useTheme(); + + return ( + <> + + + + + + + + {t("web3hub.app.selectAccountModal.addAccountItem.name")} + + + + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/index.tsx new file mode 100644 index 000000000000..e2cd98285d99 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/AccountsList/index.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { StyleSheet } from "react-native"; +import { useSelector } from "react-redux"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { AccountLike, SubAccount } from "@ledgerhq/types-live"; +import { isAccount } from "@ledgerhq/live-common/account/index"; +import { FlashList } from "@shopify/flash-list"; +import { accountsByCryptoCurrencyScreenSelector } from "~/reducers/accounts"; +import AccountCard from "~/components/AccountCard"; +import AddAccountItem from "./AddAccountItem"; + +type AccountTuple = { + account: AccountLike; + subAccount: SubAccount | null; + name: string; +}; + +const accountKeyExtractor = (item: AccountTuple) => item.subAccount?.id || item.account.id; + +const noop = () => {}; + +const renderAccountItem = ({ + item, + extraData = noop, +}: { + item: AccountTuple; + extraData?: (account: AccountLike) => void; +}) => { + const account = item.subAccount || item.account; + const parentAccount = item.subAccount && isAccount(item.account) ? item.account : undefined; + + return ( + { + extraData(account); + }} + /> + ); +}; + +export default function AccountsList({ + currency, + onPressItem, + onAddAccount, +}: { + currency: CryptoCurrency; + onPressItem: (account: AccountLike) => void; + onAddAccount: () => void; +}) { + const accounts = useSelector(accountsByCryptoCurrencyScreenSelector(currency)); + + return ( + } + keyExtractor={accountKeyExtractor} + renderItem={renderAccountItem} + estimatedItemSize={60} + data={accounts} + extraData={onPressItem} + /> + ); +} + +const styles = StyleSheet.create({ + accountCard: { + paddingHorizontal: 16, + }, + list: { + paddingBottom: 16, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkHeader/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkHeader/index.tsx new file mode 100644 index 000000000000..23ebb54f471c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkHeader/index.tsx @@ -0,0 +1,19 @@ +import React, { PropsWithChildren } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, Flex, Text } from "@ledgerhq/native-ui"; + +export default function NetworkHeader({ children }: PropsWithChildren) { + const { t } = useTranslation(); + + return ( + <> + + + {t("web3hub.app.selectAccountModal.networkHeader.title")} + + {children} + + + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkItem/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkItem/index.tsx new file mode 100644 index 000000000000..d3254eeb9145 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/NetworkItem/index.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from "react"; +import { TouchableOpacity } from "react-native"; +import { Flex, Text } from "@ledgerhq/native-ui"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import CircleCurrencyIcon from "~/components/CircleCurrencyIcon"; + +type Props = { + currency: CryptoCurrency; + onPress: (currency: CryptoCurrency) => void; +}; + +export default function NetworkItem({ currency, onPress }: Props) { + const handlePress = useCallback(() => { + onPress(currency); + }, [currency, onPress]); + + return ( + + + + + + {currency.name} + + + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/index.tsx new file mode 100644 index 000000000000..a631d6b4f51c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/index.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { Dimensions, StyleSheet } from "react-native"; +import { Flex } from "@ledgerhq/native-ui"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { FlashList } from "@shopify/flash-list"; +import QueuedDrawer from "~/components/QueuedDrawer"; +import AccountsList from "./AccountsList"; +import NetworkHeader from "./NetworkHeader"; +import AccountHeader from "./AccountHeader"; +import NetworkItem from "./NetworkItem"; +import useSelectAccountModalViewModel, { Params } from "./useSelectAccountModalViewModel"; + +type Props = { + isOpened: boolean; +} & Params; + +const currencyKeyExtractor = (item: CryptoCurrency) => item.id; + +const noop = () => {}; + +const renderCurrencyItem = ({ + item, + extraData = noop, +}: { + item: CryptoCurrency; + extraData?: (currency: CryptoCurrency) => void; +}) => { + return ; +}; + +const MODAL_HEIGHT = Dimensions.get("screen").height * 0.5; + +export default function SelectAccountModal({ isOpened, ...params }: Props) { + const { + selectedCurrency, + sortedCurrencies, + resetSelectedCurrency, + onPressCurrencyItem, + handleClose, + setSelectedAccount, + onAddAccount, + } = useSelectAccountModalViewModel(params); + + return ( + + + {selectedCurrency ? ( + + ) : ( + + )} + + + ); +} + +const styles = StyleSheet.create({ + drawerContainer: { + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + }, + list: { + paddingBottom: 16, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/useSelectAccountModalViewModel.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/useSelectAccountModalViewModel.tsx new file mode 100644 index 000000000000..196b195d178a --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/SelectAccountModal/useSelectAccountModalViewModel.tsx @@ -0,0 +1,94 @@ +import { useCallback, useMemo, useState } from "react"; +import { useNavigation } from "@react-navigation/native"; +import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; +import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index"; +import { CurrentAccountHistDB } from "@ledgerhq/live-common/wallet-api/react"; +import { useDappCurrentAccount } from "@ledgerhq/live-common/wallet-api/useDappLogic"; +import { useCurrenciesByMarketcap } from "@ledgerhq/live-common/currencies/hooks"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { AccountLike } from "@ledgerhq/types-live"; +import { AppProps } from "LLM/features/Web3Hub/types"; +import { NavigatorName, ScreenName } from "~/const"; + +export type Params = { + manifest: AppManifest; + currentAccountHistDb: CurrentAccountHistDB; + onSelectAccount: () => void; + onClose: () => void; +}; + +export default function useSelectAccountModalViewModel({ + manifest, + currentAccountHistDb, + onSelectAccount, + onClose, +}: Params) { + const [selectedCurrency, setSelectedCurrency] = useState(); + + const currencies = useMemo(() => { + return ( + manifest.dapp?.networks.map(network => { + return getCryptoCurrencyById(network.currency); + }) ?? [] + ); + }, [manifest.dapp?.networks]); + const sortedCurrencies = useCurrenciesByMarketcap(currencies); + + const { setCurrentAccountHist } = useDappCurrentAccount(currentAccountHistDb); + + const onPressCurrencyItem = useCallback((currency: CryptoCurrency) => { + setSelectedCurrency(currency); + }, []); + + const resetSelectedCurrency = useCallback(() => { + setSelectedCurrency(undefined); + }, []); + + const handleClose = useCallback(() => { + onClose(); + resetSelectedCurrency(); + }, [onClose, resetSelectedCurrency]); + + const setSelectedAccount = useCallback( + (account: AccountLike) => { + setCurrentAccountHist(manifest.id, account); + onClose(); + }, + [manifest.id, onClose, setCurrentAccountHist], + ); + + const navigation = useNavigation(); + const onAddAccount = useCallback(() => { + navigation.navigate(NavigatorName.RequestAccount, { + screen: NavigatorName.RequestAccountsAddAccounts, + params: { + screen: ScreenName.AddAccountsSelectDevice, + params: { + currency: selectedCurrency, + onSuccess: ({ selected }) => { + navigation.goBack(); + const account = selected[0]; + if (account) { + if (selected.length === 1) { + setSelectedAccount(account); + } else { + setSelectedCurrency(account.currency); + onSelectAccount(); + } + } + }, + }, + }, + }); + }, [navigation, onSelectAccount, selectedCurrency, setSelectedAccount]); + + return { + selectedCurrency, + sortedCurrencies, + resetSelectedCurrency, + onPressCurrencyItem, + handleClose, + setSelectedAccount, + onAddAccount, + }; +} diff --git a/apps/ledger-live-mobile/src/reducers/accounts.ts b/apps/ledger-live-mobile/src/reducers/accounts.ts index 55280e8512d8..dbe2665f30e7 100644 --- a/apps/ledger-live-mobile/src/reducers/accounts.ts +++ b/apps/ledger-live-mobile/src/reducers/accounts.ts @@ -248,13 +248,16 @@ export const flattenAccountsByCryptoCurrencySelector = createSelector( : accounts; }, ); -const emptyArray: AccountLike[] = []; + +const emptyTuples: ReturnType = []; export const accountsByCryptoCurrencyScreenSelector = (currency: CryptoOrTokenCurrency, accountIds?: Map) => (state: State) => { - if (!currency) return emptyArray; + // TODO look if we can remove this check as the types should already protect here + if (!currency) return emptyTuples; return accountsTuplesByCurrencySelector(state, currency, accountIds); }; +const emptyArray: AccountLike[] = []; export const flattenAccountsByCryptoCurrencyScreenSelector = (currency?: CryptoCurrency | TokenCurrency) => (state: State) => { if (!currency) return emptyArray; 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 c39c1cd35a60..a487fca28e61 100644 --- a/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx +++ b/apps/ledger-live-mobile/src/screens/RequestAccount/02-SelectAccount.tsx @@ -98,11 +98,7 @@ function SelectAccount({ navigation, route }: Props) { const { colors } = useTheme(); const { accounts$, currency, allowAddAccount, onSuccess } = route.params; const accountIds = useGetAccountIds(accounts$); - const accounts = useSelector(accountsByCryptoCurrencyScreenSelector(currency, accountIds)) as { - account: AccountLike; - subAccount: SubAccount | null; - name: string; - }[]; + const accounts = useSelector(accountsByCryptoCurrencyScreenSelector(currency, accountIds)); const onSelect = useCallback( (account: AccountLike, parentAccount?: Account) => { onSuccess && onSuccess(account, parentAccount);