Skip to content

Commit

Permalink
feat(web3hub): add app screen webview [LIVE-13179]
Browse files Browse the repository at this point in the history
Also add real fetch calls to new manifest-api v2 preview
Update tests for app screen with mocks
  • Loading branch information
Justkant committed Jul 17, 2024
1 parent 80ae817 commit 0c8f9da
Show file tree
Hide file tree
Showing 26 changed files with 830 additions and 113 deletions.
8 changes: 8 additions & 0 deletions .changeset/hot-stingrays-divide.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions apps/ledger-live-mobile/__tests__/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions apps/ledger-live-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined>;
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => (
<>
<Text>{manifest.id}</Text>
<Text>{manifest.name}</Text>
</>
),
);

describe("Web3Hub integration test", () => {
it("Should list manifests and navigate to app page", async () => {
Expand All @@ -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();
Expand All @@ -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 () => {
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<View
style={[
styles.container,
{
width: size,
height: size,
borderColor: colors.neutral.c30,
backgroundColor: "transparent",
},
]}
>
{!imageLoaded && firstLetter ? (
<Text
fontWeight="semiBold"
variant="h2"
style={{
lineHeight: size,
}}
>
{firstLetter}
</Text>
) : icon ? (
<>
<Image
source={{
uri: icon,
}}
style={[
styles.image,
...(isDisabled ? [styles.disabledTopLayer] : []),
{
width: size,
height: size,
},
]}
fadeDuration={200}
onLoad={handleImageLoad}
onError={handleImageError}
/>
{isDisabled ? (
<Image
source={{
uri: icon,
}}
style={[
styles.image,
styles.disabledBottomLayer,
{
width: size,
height: size,
tintColor: colors.fog,
},
]}
fadeDuration={200}
/>
) : null}
</>
) : (
<Text
fontWeight="semiBold"
variant="h2"
style={{
lineHeight: size,
}}
>
{firstLetter}
</Text>
)}
</View>
);
};

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,
},
});
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
<TouchableOpacity disabled={isDisabled} onPress={handlePress}>
<Flex
Expand All @@ -89,7 +94,7 @@ export default function ManifestItem({
paddingX={4}
paddingY={2}
>
<AppIcon isDisabled={isDisabled} size={48} name={manifest.name} icon={manifest.icon} />
<AppIcon isDisabled={isDisabled} size={48} name={manifest.name} icon={icon} />
<Flex marginX={16} height="100%" flexGrow={1} flexShrink={1} justifyContent={"center"}>
<Flex flexDirection="row" alignItems={"center"} mb={2}>
<Text variant="large" color={color} numberOfLines={1} fontWeight="semiBold">
Expand Down
Loading

0 comments on commit 0c8f9da

Please sign in to comment.