From 0c8f9da0cbbaf95f25478cf3ba882d6b4ac3f241 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 17 Jul 2024 10:11:49 +0200 Subject: [PATCH] feat(web3hub): add app screen webview [LIVE-13179] Also add real fetch calls to new manifest-api v2 preview Update tests for app screen with mocks --- .changeset/hot-stingrays-divide.md | 8 ++ .../__tests__/jest-setup.js | 2 + apps/ledger-live-mobile/package.json | 1 + .../newArch/features/Web3Hub/Navigator.tsx | 4 + .../web3hub.integration.test.tsx | 58 +++++--- .../Web3Hub/components/AppIcon/index.tsx | 122 ++++++++++++++++ .../useCategoriesListViewModel.ts | 11 +- .../ManifestsList/ManifestItem/index.tsx | 9 +- .../components/ManifestsList/index.tsx | 86 ++++++----- .../useManifestsListViewModel.ts | 8 +- .../components/Web3Player/BottomBar.tsx | 123 ++++++++++++++++ .../components/Web3Player/InfoPanel.tsx | 136 ++++++++++++++++++ .../components/Web3Player/RightHeader.tsx | 40 ++++++ .../components/Web3Player/index.tsx | 134 +++++++++++++++++ .../Web3Hub/screens/Web3HubApp/index.tsx | 44 ++++-- .../Web3HubApp/useWeb3HubAppViewModel.ts | 19 +++ .../Web3HubMain/components/Header/index.tsx | 17 ++- .../Web3Hub/screens/Web3HubMain/index.tsx | 19 ++- .../SearchList/SearchItem/index.tsx | 2 +- .../components/SearchList/index.tsx | 11 +- .../SearchList/useSearchListViewModel.ts | 8 +- .../Web3Hub/screens/Web3HubSearch/index.tsx | 6 +- .../features/Web3Hub/utils/api/categories.ts | 8 ++ .../features/Web3Hub/utils/api/manifests.ts | 45 +++--- .../Web3Hub/utils/api/mocks/manifests.ts | 19 +++ pnpm-lock.yaml | 3 + 26 files changed, 830 insertions(+), 113 deletions(-) create mode 100644 .changeset/hot-stingrays-divide.md create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/AppIcon/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/BottomBar.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/InfoPanel.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/RightHeader.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/useWeb3HubAppViewModel.ts diff --git a/.changeset/hot-stingrays-divide.md b/.changeset/hot-stingrays-divide.md new file mode 100644 index 000000000000..c52781e55418 --- /dev/null +++ b/.changeset/hot-stingrays-divide.md @@ -0,0 +1,8 @@ +--- +"live-mobile": minor +--- + +feat(web3hub): add app screen webview + +Also add real fetch calls to new manifest-api v2 preview +Update tests for app screen with mocks diff --git a/apps/ledger-live-mobile/__tests__/jest-setup.js b/apps/ledger-live-mobile/__tests__/jest-setup.js index 50fbd66b1df3..ed7c62a13348 100644 --- a/apps/ledger-live-mobile/__tests__/jest-setup.js +++ b/apps/ledger-live-mobile/__tests__/jest-setup.js @@ -117,6 +117,8 @@ jest.mock("@react-native-firebase/messaging", () => ({ jest.mock("@braze/react-native-sdk", () => ({})); +jest.mock("react-native-webview", () => jest.fn()); + const originalError = console.error; const originalWarn = console.warn; diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index 1469c6d68c1d..e22768b5d6be 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -89,6 +89,7 @@ "@ledgerhq/live-countervalues": "workspace:^", "@ledgerhq/live-countervalues-react": "workspace:^", "@ledgerhq/live-env": "workspace:^", + "@ledgerhq/live-network": "workspace:^", "@ledgerhq/live-nft": "workspace:^", "@ledgerhq/live-nft-react": "workspace:^", "@ledgerhq/live-wallet": "workspace:^", diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx index 2cac16e5fe45..7264c4135ff1 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx @@ -10,11 +10,15 @@ import Web3HubTabsHeader from "./screens/Web3HubTabs/components/Header"; import Web3HubApp from "./screens/Web3HubApp"; import Web3HubAppHeader from "./screens/Web3HubApp/components/Header"; +// Uncomment to use mocks +// process.env.MOCK_WEB3HUB = "1"; + export type Web3HubStackParamList = { [ScreenName.Web3HubSearch]: undefined; [ScreenName.Web3HubTabs]: undefined; [ScreenName.Web3HubApp]: { manifestId: string; + queryParams?: Record; }; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/__integrations__/web3hub.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/__integrations__/web3hub.integration.test.tsx index 4b120ce8cc91..f3e1285d61ca 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/__integrations__/web3hub.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/__integrations__/web3hub.integration.test.tsx @@ -1,7 +1,21 @@ import * as React from "react"; import { screen, waitForElementToBeRemoved } from "@testing-library/react-native"; import { render } from "@tests/test-renderer"; +import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import { Web3HubTest } from "./shared"; +import { Text } from "@ledgerhq/native-ui"; + +// Need to fix some stuff if we want to test the player too +jest.mock( + "LLM/features/Web3Hub/screens/Web3HubApp/components/Web3Player", + () => + ({ manifest }: { manifest: AppManifest }) => ( + <> + {manifest.id} + {manifest.name} + + ), +); describe("Web3Hub integration test", () => { it("Should list manifests and navigate to app page", async () => { @@ -16,16 +30,16 @@ describe("Web3Hub integration test", () => { expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Dummy Wallet App")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("dummy")).toBeOnTheScreen(); + expect(await screen.findByText("dummy-0")).toBeOnTheScreen(); + expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Wallet API Tools")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("wallet-api-tools")).toBeOnTheScreen(); + expect(await screen.findByText("wallet-api-tools-0")).toBeOnTheScreen(); + expect(await screen.findByText("Wallet API Tools")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); @@ -42,27 +56,33 @@ describe("Web3Hub integration test", () => { }); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeDisabled(); await user.press(screen.getByRole("searchbox")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Dummy Wallet App")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("dummy")).toBeOnTheScreen(); + expect(await screen.findByText("dummy-0")).toBeOnTheScreen(); + expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Wallet API Tools")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("wallet-api-tools")).toBeOnTheScreen(); + expect(await screen.findByText("wallet-api-tools-0")).toBeOnTheScreen(); + expect(await screen.findByText("Wallet API Tools")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); + expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeDisabled(); }); it("Should list manifests, select a category and navigate to app page", async () => { @@ -88,8 +108,8 @@ describe("Web3Hub integration test", () => { expect(screen.queryByText("Wallet API Tools")).not.toBeOnTheScreen(); expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Dummy Wallet App")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("dummy")).toBeOnTheScreen(); + expect(await screen.findByText("dummy-0")).toBeOnTheScreen(); + expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); @@ -107,8 +127,8 @@ describe("Web3Hub integration test", () => { expect(screen.queryByText("Dummy Wallet App")).not.toBeOnTheScreen(); expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Wallet API Tools")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("wallet-api-tools")).toBeOnTheScreen(); + expect(await screen.findByText("wallet-api-tools-0")).toBeOnTheScreen(); + expect(await screen.findByText("Wallet API Tools")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); @@ -125,8 +145,10 @@ describe("Web3Hub integration test", () => { }); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeDisabled(); await user.press(screen.getByRole("searchbox")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen(); @@ -140,11 +162,12 @@ describe("Web3Hub integration test", () => { expect(screen.queryByText("Dummy Wallet App")).not.toBeOnTheScreen(); expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Wallet API Tools")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("wallet-api-tools")).toBeOnTheScreen(); + expect(await screen.findByText("wallet-api-tools-0")).toBeOnTheScreen(); + expect(await screen.findByText("Wallet API Tools")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen(); @@ -160,13 +183,16 @@ describe("Web3Hub integration test", () => { expect(screen.queryByText("Wallet API Tools")).not.toBeOnTheScreen(); expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen(); await user.press(screen.getAllByText("Dummy Wallet App")[0]); - expect(await screen.findByText("Web3HubApp")).toBeOnTheScreen(); - expect(await screen.findByText("dummy")).toBeOnTheScreen(); + expect(await screen.findByText("dummy-0")).toBeOnTheScreen(); + expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeEnabled(); await user.press(screen.getByTestId("navigation-header-back-button")); expect(await screen.findByText("Explore web3")).toBeOnTheScreen(); + expect(await screen.findByRole("searchbox")).toBeOnTheScreen(); + expect(screen.getByRole("searchbox")).toBeDisabled(); }); }); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/AppIcon/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/AppIcon/index.tsx new file mode 100644 index 000000000000..4b926d7add8c --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/AppIcon/index.tsx @@ -0,0 +1,122 @@ +import React, { memo, useState, useCallback, useEffect } from "react"; +import { Image, View, StyleSheet } from "react-native"; +import { Text } from "@ledgerhq/native-ui"; +import { DefaultTheme, useTheme } from "styled-components/native"; +import { Theme } from "~/colors"; + +type Props = { + name?: string | null; + icon?: string | null; + size?: number; + isDisabled?: boolean; +}; + +const AppIcon = ({ size = 48, name, icon, isDisabled }: Props) => { + const { colors } = useTheme() as DefaultTheme & Theme; + const [imageLoaded, setImageLoaded] = useState(true); + const handleImageLoad = useCallback(() => setImageLoaded(true), []); + const handleImageError = useCallback(() => setImageLoaded(false), []); + const firstLetter = typeof name === "string" && name[0] ? name[0].toUpperCase() : ""; + + // Reset state if icon changed with recycled view + useEffect(() => { + setImageLoaded(true); + }, [icon]); + + return ( + + {!imageLoaded && firstLetter ? ( + + {firstLetter} + + ) : icon ? ( + <> + + {isDisabled ? ( + + ) : null} + + ) : ( + + {firstLetter} + + )} + + ); +}; + +export default memo(AppIcon); + +const styles = StyleSheet.create({ + container: { + alignItems: "center", + justifyContent: "center", + borderRadius: 8, + borderWidth: 1, + position: "relative", + }, + image: { + borderRadius: 8, + overflow: "hidden", + }, + disabledTopLayer: { + position: "absolute", + opacity: 0.3, + zIndex: 1, + }, + disabledBottomLayer: { + position: "absolute", + zIndex: 0, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/CategoriesList/useCategoriesListViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/CategoriesList/useCategoriesListViewModel.ts index 38e98c8a1b83..f31d7aa99895 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/CategoriesList/useCategoriesListViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/CategoriesList/useCategoriesListViewModel.ts @@ -1,6 +1,10 @@ import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { fetchCategoriesMock, selectCategories } from "LLM/features/Web3Hub/utils/api/categories"; +import { + fetchCategories, + fetchCategoriesMock, + selectCategories, +} from "LLM/features/Web3Hub/utils/api/categories"; export type useCategoriesListViewModelProps = { selectedCategory: string; @@ -9,13 +13,16 @@ export type useCategoriesListViewModelProps = { export const queryKey = ["web3hub/categories"]; +const isInTest = process.env.NODE_ENV === "test" || !!process.env.MOCK_WEB3HUB; +const queryFn = isInTest ? fetchCategoriesMock : fetchCategories; + export default function useCategoriesListViewModel({ selectedCategory, selectCategory, }: useCategoriesListViewModelProps) { const categoriesQuery = useQuery({ queryKey, - queryFn: fetchCategoriesMock, + queryFn: queryFn, select: selectCategories, }); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/ManifestItem/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/ManifestItem/index.tsx index eff7dc195dc4..1a39ab4119ee 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/ManifestItem/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/ManifestItem/index.tsx @@ -5,7 +5,7 @@ import { useTheme } from "@react-navigation/native"; import { Flex, Text } from "@ledgerhq/native-ui"; import { AppBranch, AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import type { MainProps, SearchProps } from "LLM/features/Web3Hub/types"; -import { AppIcon } from "~/screens/Platform/v2/AppIcon"; +import AppIcon from "LLM/features/Web3Hub/components/AppIcon"; import { NavigatorName, ScreenName } from "~/const"; import { Theme } from "~/colors"; @@ -79,6 +79,11 @@ export default function ManifestItem({ return new URL(manifest.url).origin; }, [manifest.url]); + const icon = useMemo(() => { + // RN tries to load file locally if there is a space in front of the file url + return manifest.icon?.trim(); + }, [manifest.icon]); + return ( - + diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/index.tsx index 586b73a3f446..295a4515493d 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/index.tsx @@ -1,57 +1,67 @@ import React, { useState } from "react"; import { View } from "react-native"; +import Animated from "react-native-reanimated"; import { useTranslation } from "react-i18next"; -import { FlashList } from "@shopify/flash-list"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { FlashList, FlashListProps } from "@shopify/flash-list"; import { Box, Text } from "@ledgerhq/native-ui"; -import { MAIN_BUTTON_BOTTOM, MAIN_BUTTON_SIZE } from "~/components/TabBar/shared"; +import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import ManifestItem, { type NavigationProp } from "./ManifestItem"; import CategoriesList from "./CategoriesList"; import LoadingIndicator from "./LoadingIndicator"; import useManifestsListViewModel from "./useManifestsListViewModel"; -export default function ManifestsList({ navigation }: { navigation: NavigationProp }) { +type Props = { + navigation: NavigationProp; + onScroll?: FlashListProps["onScroll"]; + pb?: number; +}; + +const AnimatedFlashList = Animated.createAnimatedComponent>(FlashList); + +export default function ManifestsList({ navigation, onScroll, pb = 0 }: Props) { const { t } = useTranslation(); const insets = useSafeAreaInsets(); const [selectedCategory, selectCategory] = useState("all"); const { data, isLoading, onEndReached } = useManifestsListViewModel(selectedCategory); return ( - <> - - {t("web3hub.manifestsList.title")} - - - {t("web3hub.manifestsList.description")} - - - - - + + + {t("web3hub.manifestsList.title")} + + + {t("web3hub.manifestsList.description")} + - item.id} - renderItem={({ item }) => { - return ; - }} - ListFooterComponent={isLoading ? : null} - ListEmptyComponent={ - isLoading ? null : ( // TODO handle empty case - - {t("common.retry")} - - ) - } - estimatedItemSize={128} - data={data} - onEndReached={onEndReached} - /> - + + + + + } + testID="web3hub-manifests-scroll" + keyExtractor={item => item.id} + renderItem={({ item }) => { + return ; + }} + ListFooterComponent={isLoading ? : null} + ListEmptyComponent={ + isLoading ? null : ( // TODO handle empty case + + {t("common.retry")} + + ) + } + estimatedItemSize={128} + data={data} + onScroll={onScroll} + scrollEventThrottle={16} + onEndReached={onEndReached} + /> ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/useManifestsListViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/useManifestsListViewModel.ts index 13ebd6f524b5..80e306507ddb 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/useManifestsListViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/ManifestsList/useManifestsListViewModel.ts @@ -1,16 +1,20 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { - fetchManifestsMock, + fetchManifests, selectManifests, getNextPageParam, + fetchManifestsMock, } from "LLM/features/Web3Hub/utils/api/manifests"; export const queryKey = (selectedCategory: string) => ["web3hub/manifests", selectedCategory]; +const isInTest = process.env.NODE_ENV === "test" || !!process.env.MOCK_WEB3HUB; +const queryFn = isInTest ? fetchManifestsMock : fetchManifests; + export default function useManifestListViewModel(selectedCategory: string) { const manifestsQuery = useInfiniteQuery({ queryKey: queryKey(selectedCategory), - queryFn: fetchManifestsMock(selectedCategory, ""), + queryFn: queryFn(selectedCategory, ""), initialPageParam: 1, getNextPageParam, select: selectManifests, diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/BottomBar.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/BottomBar.tsx new file mode 100644 index 000000000000..59c19ac48fc5 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/BottomBar.tsx @@ -0,0 +1,123 @@ +import React, { RefObject, useCallback } from "react"; +import { TouchableOpacity } from "react-native"; +import { useTheme } from "styled-components/native"; +import { Trans } from "react-i18next"; +import { Flex, Text } from "@ledgerhq/native-ui"; +import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; +import { ArrowLeftMedium, ArrowRightMedium, ReverseMedium } from "@ledgerhq/native-ui/assets/icons"; +import { safeGetRefValue, CurrentAccountHistDB } from "@ledgerhq/live-common/wallet-api/react"; +import { useDappCurrentAccount } from "@ledgerhq/live-common/wallet-api/useDappLogic"; +import { WebviewAPI, WebviewState } from "~/components/Web3AppWebview/types"; +import Button from "~/components/Button"; +import CircleCurrencyIcon from "~/components/CircleCurrencyIcon"; +import { useSelectAccount } from "~/components/Web3AppWebview/helpers"; +import { useMaybeAccountName } from "~/reducers/wallet"; + +type BottomBarProps = { + manifest: AppManifest; + webviewAPIRef: RefObject; + webviewState: WebviewState; + currentAccountHistDb: CurrentAccountHistDB; +}; + +function IconButton({ + children, + onPress, + disabled, + ...flexProps +}: React.PropsWithChildren< + { + children: React.ReactNode; + disabled?: boolean; + onPress: () => void; + } & React.ComponentProps +>) { + return ( + + + {children} + + + ); +} + +export function BottomBar({ + manifest, + webviewAPIRef, + webviewState, + currentAccountHistDb, +}: BottomBarProps) { + const { colors } = useTheme(); + const { currentAccount } = useDappCurrentAccount(currentAccountHistDb); + const shouldDisplaySelectAccount = !!manifest.dapp; + + const handleForward = useCallback(() => { + const webview = safeGetRefValue(webviewAPIRef); + + webview.goForward(); + }, [webviewAPIRef]); + + const handleBack = useCallback(() => { + const webview = safeGetRefValue(webviewAPIRef); + + webview.goBack(); + }, [webviewAPIRef]); + + const handleReload = useCallback(() => { + const webview = safeGetRefValue(webviewAPIRef); + + webview.reload(); + }, [webviewAPIRef]); + + const { onSelectAccount } = useSelectAccount({ manifest, currentAccountHistDb }); + + const currentAccountName = useMaybeAccountName(currentAccount); + + return ( + + + + + + + + + + + + {shouldDisplaySelectAccount ? ( + + ) : null} + + + + + + ); +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/InfoPanel.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/InfoPanel.tsx new file mode 100644 index 000000000000..e31bb54ab8ad --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/InfoPanel.tsx @@ -0,0 +1,136 @@ +import React, { useCallback } from "react"; +import { StyleSheet, View, TouchableOpacity, Linking, ScrollView } from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { Text } from "@ledgerhq/native-ui"; +import { Trans } from "react-i18next"; +import { translateContent } from "@ledgerhq/live-common/platform/logic"; +import type { TranslatableString } from "@ledgerhq/live-common/platform/types"; +import ExternalLinkIcon from "~/icons/ExternalLink"; +import AppIcon from "LLM/features/Web3Hub/components/AppIcon"; +import QueuedDrawer from "~/components/QueuedDrawer"; +import { useSettings } from "~/hooks"; + +type Props = { + name: string; + icon?: string | null; + description: TranslatableString; + url?: string | null; + uri?: string | null; + isOpened: boolean; + setIsOpened: (_: boolean) => void; +}; + +export function InfoPanel({ name, icon, description, url, uri, isOpened, setIsOpened }: Props) { + const { language } = useSettings(); + const { colors } = useTheme(); + const onClose = useCallback(() => { + setIsOpened(false); + }, [setIsOpened]); + const onLinkPress = useCallback((url: string) => { + Linking.openURL(url); + }, []); + return ( + + + + {icon ? ( + + + + ) : null} + + {name} + + + + {translateContent(description, language)} + + {url ? ( + <> + + + + + onLinkPress(url)}> + + {url} + + + + + + + ) : null} + {__DEV__ && uri ? ( + <> + + + URI: + + onLinkPress(uri)}> + + {uri} + + + + + + + ) : null} + + + ); +} + +const styles = StyleSheet.create({ + root: { + paddingHorizontal: 24, + paddingVertical: 30, + position: "relative", + }, + flexRow: { + display: "flex", + flexDirection: "row", + alignItems: "center", + }, + basicFontStyle: { + fontSize: 14, + lineHeight: 21, + }, + titleContainer: { + marginBottom: 16, + }, + appIcon: { + paddingRight: 12, + }, + title: { + fontSize: 22, + }, + description: { + opacity: 0.5, + }, + hr: { + borderBottomColor: "rgba(20, 37, 51, 0.1)", + borderBottomWidth: 1, + paddingTop: 32, + marginBottom: 32, + }, + subSectionTitle: { + textTransform: "capitalize", + fontWeight: "600", + marginBottom: 4, + }, + externalLinkIcon: { + paddingLeft: 6, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/RightHeader.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/RightHeader.tsx new file mode 100644 index 000000000000..3cb922d10b14 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/RightHeader.tsx @@ -0,0 +1,40 @@ +import React, { RefObject } from "react"; +import { View, TouchableOpacity, StyleSheet } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { Flex, Icon } from "@ledgerhq/native-ui"; +import { WebviewAPI, WebviewState } from "~/components/Web3AppWebview/types"; + +type RightHeaderProps = { + webviewAPIRef: RefObject; + webviewState: WebviewState; + handlePressInfo?: () => void; +}; + +export function RightHeader({ handlePressInfo }: RightHeaderProps) { + const navigation = useNavigation(); + + return ( + + {handlePressInfo ? ( + + + + + + ) : null} + + + + + + + ); +} + +const styles = StyleSheet.create({ + headerRight: { + display: "flex", + flexDirection: "row", + paddingRight: 8, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/index.tsx new file mode 100644 index 000000000000..51eb7ff6ed66 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/components/Web3Player/index.tsx @@ -0,0 +1,134 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { StyleSheet, SafeAreaView, BackHandler, Platform } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { Flex } from "@ledgerhq/native-ui"; +import { CurrentAccountHistDB, safeGetRefValue } from "@ledgerhq/live-common/wallet-api/react"; +import { handlers as loggerHandlers } from "@ledgerhq/live-common/wallet-api/CustomLogger/server"; +import { AppManifest, WalletAPICustomHandlers } from "@ledgerhq/live-common/wallet-api/types"; +import { WebviewAPI, WebviewState } from "~/components/Web3AppWebview/types"; +import { Web3AppWebview } from "~/components/Web3AppWebview"; +import { + RootNavigationComposite, + StackNavigatorNavigation, +} from "~/components/RootNavigator/types/helpers"; +import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator"; +import HeaderTitle from "~/components/HeaderTitle"; +import { initialWebviewState } from "~/components/Web3AppWebview/helpers"; +import { usePTXCustomHandlers } from "~/components/WebPTXPlayer/CustomHandlers"; +import { useCurrentAccountHistDB } from "~/screens/Platform/v2/hooks"; +import { RightHeader } from "./RightHeader"; +import { BottomBar } from "./BottomBar"; +import { InfoPanel } from "./InfoPanel"; + +type Props = { + manifest: AppManifest; + inputs?: Record; +}; + +const WebPlatformPlayer = ({ manifest, inputs }: Props) => { + const webviewAPIRef = useRef(null); + const [webviewState, setWebviewState] = useState(initialWebviewState); + const [isInfoPanelOpened, setIsInfoPanelOpened] = useState(false); + + const navigation = + useNavigation>>(); + + const currentAccountHistDb: CurrentAccountHistDB = useCurrentAccountHistDB(); + + const handleHardwareBackPress = useCallback(() => { + const webview = safeGetRefValue(webviewAPIRef); + + if (webviewState.canGoBack) { + webview.goBack(); + return true; // prevent default behavior (native navigation) + } + + return false; + }, [webviewState.canGoBack, webviewAPIRef]); + + // eslint-disable-next-line consistent-return + useEffect(() => { + if (Platform.OS === "android") { + BackHandler.addEventListener("hardwareBackPress", handleHardwareBackPress); + + return () => { + BackHandler.removeEventListener("hardwareBackPress", handleHardwareBackPress); + }; + } + }, [handleHardwareBackPress]); + + useEffect(() => { + navigation.setOptions({ + headerTitleAlign: "left", + headerLeft: () => null, + headerTitleContainerStyle: { marginHorizontal: 0 }, + headerTitle: () => ( + + {manifest.homepageUrl} + + ), + headerRight: () => ( + setIsInfoPanelOpened(true)} + /> + ), + headerShown: true, + }); + }, [manifest, navigation, webviewState]); + + const customPTXHandlers = usePTXCustomHandlers(manifest); + + const customHandlers = useMemo(() => { + return { + ...loggerHandlers, + ...customPTXHandlers, + }; + }, [customPTXHandlers]); + + return ( + + + + + + ); +}; + +export default WebPlatformPlayer; + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + headerRight: { + display: "flex", + flexDirection: "row", + paddingRight: 8, + }, + buttons: { + paddingVertical: 8, + paddingHorizontal: 8, + }, +}); diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/index.tsx index 680f4cd68d02..ca7663c38efe 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/index.tsx @@ -1,19 +1,35 @@ -import { Text } from "@ledgerhq/native-ui"; -import React from "react"; -import { View } from "react-native"; +import React, { useMemo } from "react"; +import { useTheme } from "styled-components/native"; +import { Flex, InfiniteLoader } from "@ledgerhq/native-ui"; import type { AppProps } from "LLM/features/Web3Hub/types"; -import { ScreenName } from "~/const"; +import WebPlatformPlayer from "./components/Web3Player"; +import GenericErrorView from "~/components/GenericErrorView"; +import { useLocale } from "~/context/Locale"; +import useWeb3HubAppViewModel from "./useWeb3HubAppViewModel"; +const appManifestNotFoundError = new Error("App not found"); + +// TODO local manifests ? export default function Web3HubApp({ route }: AppProps) { - const { manifestId } = route.params; - return ( - - {ScreenName.Web3HubApp} - {manifestId} - + const { manifestId, queryParams } = route.params; + const { theme } = useTheme(); + const { locale } = useLocale(); + + const { manifest, isLoading } = useWeb3HubAppViewModel(manifestId); + + const inputs = useMemo(() => { + return { + theme, + lang: locale, + ...queryParams, + }; + }, [locale, queryParams, theme]); + + return manifest ? ( + + ) : ( + + {isLoading ? : } + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/useWeb3HubAppViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/useWeb3HubAppViewModel.ts new file mode 100644 index 000000000000..f28685da5575 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubApp/useWeb3HubAppViewModel.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchManifestById, fetchManifestByIdMock } from "LLM/features/Web3Hub/utils/api/manifests"; + +export const queryKey = (manifestId: string) => ["web3hub/manifest", manifestId]; + +const isInTest = process.env.NODE_ENV === "test" || !!process.env.MOCK_WEB3HUB; +const queryFn = isInTest ? fetchManifestByIdMock : fetchManifestById; + +export default function useWeb3HubAppViewModel(manifestId: string) { + const manifestQuery = useQuery({ + queryKey: queryKey(manifestId), + queryFn: queryFn(manifestId), + }); + + return { + manifest: manifestQuery.data, + isLoading: manifestQuery.isLoading, + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/components/Header/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/components/Header/index.tsx index f8fa78757aba..d5ba0b37e629 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/components/Header/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/components/Header/index.tsx @@ -5,9 +5,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTheme } from "@react-navigation/native"; import Animated, { useAnimatedStyle, interpolate, Extrapolation } from "react-native-reanimated"; import { Flex, Text } from "@ledgerhq/native-ui"; +import { useQueryClient } from "@tanstack/react-query"; import type { SearchProps } from "LLM/features/Web3Hub/types"; import TextInput from "~/components/TextInput"; import { HeaderContext } from "LLM/features/Web3Hub/HeaderContext"; +import { queryKey } from "LLM/features/Web3Hub/components/ManifestsList/useManifestsListViewModel"; import { NavigatorName, ScreenName } from "~/const"; const TITLE_HEIGHT = 50; @@ -75,13 +77,22 @@ export default function Web3HubMainHeader({ title, navigation }: Props) { }); }, [navigation]); + // TODO remove later + // Useful for testing the infinite loading and onEndReach working correctly + const queryClient = useQueryClient(); + const clearCache = useCallback(() => { + queryClient.resetQueries({ queryKey: queryKey("all") }); + }, [queryClient]); + return ( - - {title} - + + + {title} + + diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/index.tsx index d48fd3b1407a..922c14d89ba1 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubMain/index.tsx @@ -1,8 +1,12 @@ import React, { useContext } from "react"; -import Animated, { useAnimatedScrollHandler } from "react-native-reanimated"; +import { View } from "react-native"; +import { useAnimatedScrollHandler } from "react-native-reanimated"; import type { MainProps } from "LLM/features/Web3Hub/types"; import { HeaderContext } from "LLM/features/Web3Hub/HeaderContext"; import ManifestsList from "LLM/features/Web3Hub/components/ManifestsList"; +import { MAIN_BUTTON_BOTTOM, MAIN_BUTTON_SIZE } from "~/components/TabBar/shared"; + +const PADDING_BOTTOM = MAIN_BUTTON_SIZE + MAIN_BUTTON_BOTTOM; export default function Web3HubMain({ navigation }: MainProps) { const { layoutY } = useContext(HeaderContext); @@ -13,14 +17,17 @@ export default function Web3HubMain({ navigation }: MainProps) { }); return ( - - - + + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/SearchItem/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/SearchItem/index.tsx index 4c7dc8ce5b4c..36ad4d271c1b 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/SearchItem/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/SearchItem/index.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@react-navigation/native"; import { Flex, Text } from "@ledgerhq/native-ui"; import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import type { SearchProps } from "LLM/features/Web3Hub/types"; -import { AppIcon } from "~/screens/Platform/v2/AppIcon"; +import AppIcon from "LLM/features/Web3Hub/components/AppIcon"; import { ScreenName } from "~/const"; import CurrencyIconList from "./CurrencyIconList"; diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/index.tsx index d6dfbc7f9de4..d5b52d44c626 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/index.tsx @@ -1,9 +1,8 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { FlashList } from "@shopify/flash-list"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Box, Text } from "@ledgerhq/native-ui"; -import { MAIN_BUTTON_BOTTOM, MAIN_BUTTON_SIZE } from "~/components/TabBar/shared"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import SearchItem, { type NavigationProp } from "./SearchItem"; import LoadingIndicator from "LLM/features/Web3Hub/components/ManifestsList/LoadingIndicator"; import useSearchListViewModel from "./useSearchListViewModel"; @@ -21,13 +20,11 @@ export default function SearchList({ return ( item.id} + testID="web3hub-manifests-search-scroll" + keyExtractor={item => item.id} renderItem={({ item }) => { return ; }} diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/useSearchListViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/useSearchListViewModel.ts index 571fc0750faa..84d0691e9a9c 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/useSearchListViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/components/SearchList/useSearchListViewModel.ts @@ -1,16 +1,20 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { - fetchManifestsMock, + fetchManifests, selectManifests, getNextPageParam, + fetchManifestsMock, } from "LLM/features/Web3Hub/utils/api/manifests"; export const queryKey = (search: string) => ["web3hub/manifests/search", search]; +const isInTest = process.env.NODE_ENV === "test" || !!process.env.MOCK_WEB3HUB; +const queryFn = isInTest ? fetchManifestsMock : fetchManifests; + export default function useSearchListViewModel(search: string) { const manifestsQuery = useInfiniteQuery({ queryKey: queryKey(search), - queryFn: fetchManifestsMock("", search), + queryFn: queryFn("", search), initialPageParam: 1, getNextPageParam, select: selectManifests, diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/index.tsx index ca02d05d6258..2355fb3c8ab2 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/screens/Web3HubSearch/index.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { ScrollView } from "react-native"; +import { View } from "react-native"; import type { SearchProps } from "LLM/features/Web3Hub/types"; import { HeaderContext } from "LLM/features/Web3Hub/HeaderContext"; import ManifestsList from "LLM/features/Web3Hub/components/ManifestsList"; @@ -9,7 +9,7 @@ export default function Web3HubSearch({ navigation }: SearchProps) { const { search } = useContext(HeaderContext); return ( - )} - + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/categories.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/categories.ts index bc407a01ece1..da3518ae9a54 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/categories.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/categories.ts @@ -1,3 +1,4 @@ +import network from "@ledgerhq/live-network/network"; import { mocks } from "./mocks/categories"; const MOCK_DELAY = 500; @@ -7,6 +8,13 @@ export const fetchCategoriesMock = async () => { return mocks; }; +export const fetchCategories = async () => { + const res = await network<{ categories: string[] }>({ + url: "https://manifest-api-git-feat-v2-search-ledgerhq.vercel.app/api/v2/categories", + }); + return res.data.categories; +}; + export const selectCategories = (data: string[]) => { return ["all", ...data]; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/manifests.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/manifests.ts index d157c7a551da..fb05e3b2bea1 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/manifests.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/manifests.ts @@ -1,24 +1,10 @@ +import network from "@ledgerhq/live-network/network"; import { GetNextPageParamFunction, InfiniteData, QueryFunction } from "@tanstack/react-query"; import { LiveAppManifest } from "@ledgerhq/live-common/platform/types"; -import { mocks } from "./mocks/manifests"; - -const manifests = [ - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, - ...mocks, -]; +import { manifests } from "./mocks/manifests"; const MOCK_DELAY = 1000; +const MOCK_BY_ID_DELAY = 300; const PAGE_SIZE = 10; @@ -44,6 +30,18 @@ export const fetchManifestsMock: ( return list.slice((pageParam - 1) * PAGE_SIZE, pageParam * PAGE_SIZE); }; +export const fetchManifests: ( + category: string, + search: string, +) => QueryFunction = + (category, search) => + async ({ pageParam }) => { + const res = await network({ + url: `https://manifest-api-git-feat-v2-search-ledgerhq.vercel.app/api/v2/apps?resultsPerPage=${PAGE_SIZE}&page=${pageParam}&categories=${category === "all" ? "" : category}&search=${search}`, + }); + return res.data; + }; + export const selectManifests = (data: InfiniteData) => { return data.pages.flat(1); }; @@ -61,3 +59,16 @@ export const getNextPageParam: GetNextPageParamFunction async () => { + await new Promise(resolve => setTimeout(resolve, MOCK_BY_ID_DELAY)); + + return manifests.find(mock => mock.id === manifestId); +}; + +export const fetchManifestById = (manifestId: string) => async () => { + const res = await network({ + url: `https://manifest-api-git-feat-v2-search-ledgerhq.vercel.app/api/v2/apps/${manifestId}`, + }); + return res.data; +}; diff --git a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/mocks/manifests.ts b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/mocks/manifests.ts index ab617189f058..4d5356930336 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/mocks/manifests.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Web3Hub/utils/api/mocks/manifests.ts @@ -216,3 +216,22 @@ export const mocks: AppManifest[] = [ visibility: "complete", }, ]; + +function getManifests() { + let manifests: AppManifest[] = []; + + for (let i = 0; i < 14; i++) { + manifests = manifests.concat( + mocks.map(manifest => { + return { + ...manifest, + id: `${manifest.id}-${i}`, + }; + }), + ); + } + + return manifests; +} + +export const manifests = getManifests(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e8fffae275b..8cd065f7fb3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -829,6 +829,9 @@ importers: '@ledgerhq/live-env': specifier: workspace:^ version: link:../../libs/env + '@ledgerhq/live-network': + specifier: workspace:^ + version: link:../../libs/live-network '@ledgerhq/live-nft': specifier: workspace:^ version: link:../../libs/live-nft