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 (
-
- )
- }
- iconPosition={"left"}
- type="primary"
- onPress={onSelectAccount}
- isNewIcon
- >
- {!currentAccount ? (
-
-
-
- ) : (
- {currentAccountName}
- )}
-
+ <>
+
+
+ )
+ }
+ iconPosition={"left"}
+ type="primary"
+ onPress={onSelectAccount}
+ isNewIcon
+ >
+ {!currentAccount ? (
+
+
+
+ ) : (
+ {currentAccountName}
+ )}
+
+ >
);
}
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);