Skip to content

Commit

Permalink
feat(web3hub): add confirmation bottom modal before opening app [LIVE…
Browse files Browse the repository at this point in the history
…-13188]

Updated the base url used to get manifest in web3hub
Refactor a bit our flashlist usage
Updated tests
  • Loading branch information
Justkant committed Aug 1, 2024
1 parent 9759099 commit 027a6f6
Show file tree
Hide file tree
Showing 17 changed files with 467 additions and 134 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-forks-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

feat(web3hub): add confirmation bottom modal before opening app
7 changes: 3 additions & 4 deletions apps/ledger-live-mobile/__tests__/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ jest.mock("react-native-localize", () => ({
findBestAvailableLanguage: jest.fn(),
}));

jest.mock("@react-native-async-storage/async-storage", () => ({
getItem: jest.fn(),
setItem: jest.fn(),
}));
jest.mock("@react-native-async-storage/async-storage", () =>
require("@react-native-async-storage/async-storage/jest/async-storage-mock"),
);

jest.mock("react-native-version-number", () => ({
appVersion: "1.0.0",
Expand Down
5 changes: 5 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -6777,6 +6777,11 @@
}
},
"web3hub": {
"disclaimer": {
"clearSigningEnabled": "Clear signing enabled",
"checkbox": "Do not remind me again.",
"CTA": "Open {{app}}"
},
"manifestsList": {
"title": "Explore",
"description": "Discover the best of web3 curated by Ledger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ 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("Do not remind me again.")).toBeOnTheScreen();
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

Expand Down Expand Up @@ -65,6 +68,9 @@ 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("Do not remind me again.")).toBeOnTheScreen();
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

Expand Down Expand Up @@ -104,6 +110,9 @@ 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("Do not remind me again.")).toBeOnTheScreen();
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

Expand Down Expand Up @@ -167,6 +176,9 @@ 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("Do not remind me again.")).toBeOnTheScreen();
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

Expand Down Expand Up @@ -208,7 +220,7 @@ describe("Web3Hub integration test", () => {

await user.press(screen.getByRole("button", { name: /back/i }));

expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen();
expect((await screen.findAllByText("Wallet API Tools"))[0]).toBeOnTheScreen();
await user.press(screen.getAllByText("Wallet API Tools")[0]);
expect(await screen.findByText("wallet-api-tools-0")).toBeOnTheScreen();
expect(await screen.findByText("Wallet API Tools")).toBeOnTheScreen();
Expand All @@ -232,4 +244,50 @@ describe("Web3Hub integration test", () => {
expect(await screen.findByRole("searchbox")).toBeOnTheScreen();
expect(screen.getByRole("searchbox")).toBeDisabled();
});

it("Should only show the confirmation bottom modal if not dismissed previously", async () => {
const { user } = render(<Web3HubTest />);

expect(await screen.findByText("Explore web3")).toBeOnTheScreen();

await waitForLoader();

// TODO: Would be nice to test the close on bottom modal by tapping the close icon or the backdrop
expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen();
await user.press(screen.getAllByText("Dummy Wallet App")[0]);
expect(await screen.findByText("Do not remind me again.")).toBeOnTheScreen();
// await user.press(screen.getByText("Do not remind me again."));
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

expect(await screen.findByRole("button", { name: /back/i })).toBeOnTheScreen();
await user.press(screen.getByRole("button", { name: /back/i }));
expect(await screen.findByText("Explore web3")).toBeOnTheScreen();

expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen();
await user.press(screen.getAllByText("Dummy Wallet App")[0]);
expect(await screen.findByText("Do not remind me again.")).toBeOnTheScreen();
await user.press(screen.getByText("Do not remind me again."));
expect(await screen.findByText("Open Dummy Wallet App")).toBeOnTheScreen();
await user.press(screen.getByText("Open Dummy Wallet App"));
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

expect(await screen.findByRole("button", { name: /back/i })).toBeOnTheScreen();
await user.press(screen.getByRole("button", { name: /back/i }));
expect(await screen.findByText("Explore web3")).toBeOnTheScreen();

expect((await screen.findAllByText("Dummy Wallet App"))[0]).toBeOnTheScreen();
// Strange bug where I need to press on something else (that will not receive the press) to be able to press again
await user.press(screen.getAllByText("Dummy Wallet App")[1]);
await user.press(screen.getAllByText("Dummy Wallet App")[0]);
expect(await screen.findByText("dummy-0")).toBeOnTheScreen();
expect(await screen.findByText("Dummy Wallet App")).toBeOnTheScreen();

expect(await screen.findByRole("button", { name: /back/i })).toBeOnTheScreen();
await user.press(screen.getByRole("button", { name: /back/i }));
expect(await screen.findByText("Explore web3")).toBeOnTheScreen();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Flex, Text, Checkbox, Button, Icons, Box } from "@ledgerhq/native-ui";
import { translateContent } from "@ledgerhq/live-common/wallet-api/logic";
import ManifestRow from "LLM/features/Web3Hub/components/ManifestRow";
import QueuedDrawer from "~/components/QueuedDrawer";
import { useLocale } from "~/context/Locale";
import useDisclaimerViewModel from "./useDisclaimerViewModel";

type Props = {
disclaimer: ReturnType<typeof useDisclaimerViewModel>;
};

export { useDisclaimerViewModel };

export default function Disclaimer({
disclaimer: { isOpened, isChecked, toggleCheck, onClose, onConfirm, manifest },
}: Props) {
const { t } = useTranslation();
const { locale } = useLocale();

const description = useMemo(() => {
return manifest?.content.description
? translateContent(manifest.content.description, locale)
: undefined;
}, [locale, manifest?.content.description]);

return (
<QueuedDrawer isRequestingToBeOpened={isOpened} onClose={onClose}>
{manifest ? (
<Flex flexDirection="row" alignItems="center" height={72}>
<ManifestRow manifest={manifest} />
</Flex>
) : null}

{description ? (
<Flex mt={6}>
<Text fontSize={14} lineHeight={"22px"} color="smoke">
{description}
</Text>
</Flex>
) : null}

<Box mt={6} height="1px" width="100%" backgroundColor={"translucentGrey"} />

<Flex mt={6} flexDirection={"row"} alignItems={"center"}>
<Box mr={2}>
<Icons.Eye color={"smoke"} />
</Box>
<Text fontSize={14} color="smoke">
{t("web3hub.disclaimer.clearSigningEnabled")}
</Text>
</Flex>

<Flex mt={6}>
<Checkbox
label={" " + t("web3hub.disclaimer.checkbox")}
checked={isChecked}
onChange={toggleCheck}
/>
</Flex>

<Flex mt={6}>
<Button type="main" onPress={onConfirm}>
{t("web3hub.disclaimer.CTA", { app: manifest?.name })}
</Button>
</Flex>
</QueuedDrawer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useCallback, useEffect, useState } from "react";
import { AppManifest } from "@ledgerhq/live-common/wallet-api/types";
import { INITIAL_WEB3HUB_STATE, WEB3HUB_STORE_KEY } from "LLM/features/Web3Hub/constants";
import { Web3HubDB } from "LLM/features/Web3Hub/types";
import { useDB } from "~/db";

const dismissedManifestsSelector = (state: Web3HubDB) => state.dismissedManifests;

export function useDismissedManifests() {
return useDB<Web3HubDB, Web3HubDB["dismissedManifests"]>(
WEB3HUB_STORE_KEY,
INITIAL_WEB3HUB_STATE,
dismissedManifestsSelector,
);
}

export default function useDisclaimerViewModel(goToApp: (manifestId: string) => void) {
const [isChecked, setIsChecked] = useState(false);
const [disclaimerOpened, setDisclaimerOpened] = useState(false);
const [disclaimerManifest, setDisclaimerManifest] = useState<AppManifest>();
const [dismissedManifests, setWeb3HubDB] = useDismissedManifests();

useEffect(() => {
if (disclaimerManifest && !!dismissedManifests[disclaimerManifest.id]) {
setIsChecked(true);
} else {
setIsChecked(false);
}
}, [disclaimerManifest, dismissedManifests]);

useEffect(() => {
setWeb3HubDB(INITIAL_WEB3HUB_STATE);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onPressItem = useCallback(
(manifest: AppManifest) => {
if (manifest.branch === "soon") {
return;
}

if (!dismissedManifests[manifest.id] && manifest.author !== "ledger") {
setDisclaimerManifest(manifest);
setDisclaimerOpened(true);
} else {
// TODO append recently used
goToApp(manifest.id);
}
},
[dismissedManifests, goToApp],
);

const toggleCheck = useCallback(() => {
setIsChecked(value => !value);
}, []);

const onConfirm = useCallback(() => {
if (disclaimerManifest) {
if (isChecked) {
setWeb3HubDB(state => ({
...state,
dismissedManifests: {
...state.dismissedManifests,
[disclaimerManifest.id]: !state.dismissedManifests[disclaimerManifest.id],
},
}));
}

goToApp(disclaimerManifest.id);
}
}, [disclaimerManifest, goToApp, isChecked, setWeb3HubDB]);

const onClose = useCallback(() => {
setDisclaimerOpened(false);
}, []);

return {
manifest: disclaimerManifest,
isOpened: disclaimerOpened,
isChecked,
toggleCheck,
onConfirm,
onClose,
onPressItem,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { Flex, Text } from "@ledgerhq/native-ui";
import { AppManifest } from "@ledgerhq/live-common/wallet-api/types";
import AppIcon from "LLM/features/Web3Hub/components/AppIcon";
import CurrencyIconList from "./CurrencyIconList";

export default function ManifestRow({ manifest }: { manifest: AppManifest }) {
const isDisabled = manifest.branch === "soon";

return (
<>
<AppIcon isDisabled={isDisabled} size={48} name={manifest.name} icon={manifest.icon} />
<Flex marginX={16} height="100%" flexGrow={1} flexShrink={1} justifyContent={"center"}>
<Flex flexDirection="row" alignItems={"center"} mb={2}>
<Text variant="large" numberOfLines={1} fontWeight="semiBold">
{manifest.name}
</Text>
</Flex>
<CurrencyIconList currencies={manifest.currencies} />
{/* TODO add badges on certain categories */}
</Flex>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { StyleSheet } from "react-native";
import { FlashList } from "@shopify/flash-list";
import { Box } from "@ledgerhq/native-ui";
import useCategoriesListViewModel, {
Expand All @@ -10,26 +11,32 @@ const identityFn = (item: string) => item;

type Props = useCategoriesListViewModelProps;

const renderItem = ({
item,
extraData,
}: {
item: string;
extraData?: useCategoriesListViewModelProps;
}) => {
return (
<Badge
onPress={() => extraData?.selectCategory(item)}
label={item}
selected={extraData?.selectedCategory === item}
/>
);
};

export default function CategoriesList({ selectedCategory, selectCategory }: Props) {
const { data, extraData } = useCategoriesListViewModel({ selectedCategory, selectCategory });

return (
<FlashList
testID="web3hub-categories-scroll"
horizontal
contentContainerStyle={{
paddingHorizontal: 5,
}}
contentContainerStyle={styles.container}
keyExtractor={identityFn}
renderItem={({ item, extraData }) => {
return (
<Badge
onPress={() => extraData.selectCategory(item)}
label={item}
selected={extraData.selectedCategory === item}
/>
);
}}
renderItem={renderItem}
ListEmptyComponent={<Box height={32} />} // Empty box for first height calculation, could be improved
estimatedItemSize={50}
data={data}
Expand All @@ -38,3 +45,9 @@ export default function CategoriesList({ selectedCategory, selectCategory }: Pro
/>
);
}

const styles = StyleSheet.create({
container: {
paddingHorizontal: 5,
},
});
Loading

0 comments on commit 027a6f6

Please sign in to comment.