Skip to content

Commit

Permalink
Merge pull request #9215 from LedgerHQ/feat/LIVE-16967
Browse files Browse the repository at this point in the history
💄 (llm) empty accounts/assets screen
  • Loading branch information
LucasWerey authored Feb 14, 2025
2 parents 15a8f00 + 964b884 commit 1a56e86
Show file tree
Hide file tree
Showing 12 changed files with 523 additions and 110 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-dolls-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Create reusable emptyList component and use it for accountsList and assetsList
8 changes: 8 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7235,5 +7235,13 @@
"detectedAccounts": "{{count}} account (detected)",
"detectedAccounts_plural": "{{count}} accounts (detected)"
}
},
"emptyList": {
"accounts": {
"title": "No accounts found",
"subTitle": "Looks like you haven’t added an account yet. Get started now.",
"cta": "Add an account",
"link": "Need help? Learn how to add an account to Ledger\u00a0Live."
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { useState } from "react";
import AddAccountDrawer from "LLM/features/Accounts/screens/AddAccount";
import { urls } from "~/utils/urls";
import EmptyList from "../components";
import { track } from "~/analytics";

type Props = {
sourceScreenName: string;
};

const AccountsEmptyList = ({ sourceScreenName }: Props) => {
const [isAddModalOpened, setIsAddModalOpened] = useState<boolean>(false);

const openAddModal = () => {
track("button_clicked", { button: "Add a new account", page: sourceScreenName });
setIsAddModalOpened(true);
};
const closeAddModal = () => setIsAddModalOpened(false);

return (
<>
<EmptyList
titleKey="emptyList.accounts.title"
subTitleKey="emptyList.accounts.subTitle"
buttonTextKey="emptyList.accounts.cta"
onButtonPress={openAddModal}
linkTextKey="emptyList.accounts.link"
urlLink={urls.addAccount}
/>
<AddAccountDrawer isOpened={isAddModalOpened} onClose={closeAddModal} />
</>
);
};

export default AccountsEmptyList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { ReactNode } from "react";
import { renderWithReactQuery } from "@tests/test-renderer";
import AccountsEmptyList from "../AccountsEmptyList/index";
import { track } from "~/analytics";
import { Linking } from "react-native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

const Stack = createNativeStackNavigator();

const MockNavigator = ({ children }: { children: ReactNode }) => (
<Stack.Navigator>
<Stack.Screen name="MockScreen">{() => children}</Stack.Screen>
</Stack.Navigator>
);

describe("AccountsEmptyList", () => {
it("should render the empty list screen", () => {
const { getByText } = renderWithReactQuery(
<MockNavigator>
<AccountsEmptyList sourceScreenName="AccountsEmptyList" />
</MockNavigator>,
);
expect(getByText(/no accounts found/i)).toBeVisible();
expect(
getByText(/looks like you havent added an account yet. get started now/i),
).toBeVisible();
expect(getByText("Add an account")).toBeVisible();
expect(getByText(/need help\? learn how to add an account to ledger live./i)).toBeVisible();
});

it("should trigger the track on the button and open the drawer", async () => {
const { getByText, user } = renderWithReactQuery(
<MockNavigator>
<AccountsEmptyList sourceScreenName="AccountsEmptyList" />
</MockNavigator>,
);
expect(getByText("Add an account")).toBeVisible();
await user.press(getByText("Add an account"));
expect(track).toHaveBeenCalledWith("button_clicked", {
button: "Add a new account",
page: "AccountsEmptyList",
});
expect(getByText(/add another account/i)).toBeVisible();
});

it("should trigger the openUrl with good url", async () => {
const { getByText, user } = renderWithReactQuery(
<MockNavigator>
<AccountsEmptyList sourceScreenName="AccountsEmptyList" />
</MockNavigator>,
);
const url = "https://support.ledger.com/article/4404389482641-zd";
const link = getByText(/need help\? learn how to add an account to ledger live./i);
expect(link).toBeVisible();
await user.press(link);
expect(Linking.openURL).toHaveBeenCalledWith(url);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react";
import { Flex, Text, Button, Link } from "@ledgerhq/native-ui";
import { useTranslation } from "react-i18next";
import { Linking } from "react-native";

type Props = {
titleKey: string;
subTitleKey?: string;
} & (
| {
buttonTextKey: string;
onButtonPress: () => void;
}
| {
buttonTextKey?: never;
onButtonPress?: never;
}
) &
(
| {
linkTextKey: string;
urlLink: string;
}
| {
linkTextKey?: never;
urlLink?: never;
}
);

const EmptyList: React.FC<Props> = ({
titleKey,
subTitleKey,
buttonTextKey,
linkTextKey,
urlLink,
onButtonPress,
}) => {
const { t } = useTranslation();

const onLinkPress = (url: string) => Linking.openURL(url);

return (
<Flex flex={1} alignItems="center" justifyContent="center" px={6}>
{!!titleKey && (
<Text
textAlign="center"
variant="h1Inter"
fontWeight="semiBold"
color="neutral.c100"
mb={6}
>
{t(titleKey)}
</Text>
)}
{!!subTitleKey && (
<Text
textAlign="center"
variant="bodyLineHeight"
fontWeight="semiBold"
color="neutral.c80"
mb={8}
>
{t(subTitleKey)}
</Text>
)}
{!!buttonTextKey && (
<Button onPress={onButtonPress} size="large" type="main" mb={8}>
{t(buttonTextKey)}
</Button>
)}
{!!linkTextKey && (
<Link onPress={() => onLinkPress(urlLink)} size="medium">
<Text
fontWeight="semiBold"
variant="paragraph"
textAlign="center"
style={{ textDecorationLine: "underline" }}
>
{t(linkTextKey)}
</Text>
</Link>
)}
</Flex>
);
};
export default EmptyList;
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ jest.mock("@ledgerhq/live-countervalues-react", () => ({
}));

describe("AccountsList Screen", () => {
const renderComponent = (params: AccountsListNavigator[ScreenName.AccountsList]) => {
const renderComponent = (
params: AccountsListNavigator[ScreenName.AccountsList],
withoutAccount: boolean = false,
) => {
const Stack = createStackNavigator<AccountsListNavigator>();

return renderWithReactQuery(
Expand All @@ -53,6 +56,15 @@ describe("AccountsList Screen", () => {
</Stack.Navigator>,
{
...INITIAL_STATE,
...(withoutAccount && {
overrideInitialState: (state: State) => ({
...state,
accounts: {
...state.accounts,
active: [],
},
}),
}),
},
);
};
Expand Down Expand Up @@ -139,4 +151,22 @@ describe("AccountsList Screen", () => {
expect(getByText(/use your ledger device/i)).toBeVisible();
expect(getByText(/use ledger sync/i)).toBeVisible();
});

it("should render the empty list screen", async () => {
const { getByText } = renderComponent(
{
sourceScreenName: ScreenName.AccountsList,
showHeader: true,
canAddAccount: true,
},
true,
);

expect(getByText(/no accounts found/i)).toBeVisible();
expect(
getByText(/looks like you havent added an account yet. get started now/i),
).toBeVisible();
expect(getByText("Add an account")).toBeVisible();
expect(getByText(/need help\? learn how to add an account to ledger live./i)).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useSelector } from "react-redux";
import { useGlobalSyncState } from "@ledgerhq/live-common/bridge/react/useGlobalSyncState";
import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
import { ScreenName } from "~/const";
import { hasNoAccountsSelector, isUpToDateSelector } from "~/reducers/accounts";
import { parseBoolean } from "LLM/utils/parseBoolean";
import { TrackingEvent } from "../../../enums";
import { AccountsListNavigator } from "../types";
export type Props = BaseComposite<
StackNavigatorProps<AccountsListNavigator, ScreenName.AccountsList>
>;

export type GenericAccountsType = ReturnType<typeof useGenericAccountsListViewModel>;

export default function useGenericAccountsListViewModel({ route }: Props) {
const { params } = route;

const hasNoAccount = useSelector(hasNoAccountsSelector);
const isUpToDate = useSelector(isUpToDateSelector);

const canAddAccount =
(params?.canAddAccount ? parseBoolean(params?.canAddAccount) : false) && !hasNoAccount;
const showHeader =
(params?.showHeader ? parseBoolean(params?.showHeader) : false) && !hasNoAccount;
const isSyncEnabled = params?.isSyncEnabled ? parseBoolean(params?.isSyncEnabled) : false;
const sourceScreenName = params?.sourceScreenName;
const specificAccounts = params?.specificAccounts;

const globalSyncState = useGlobalSyncState();
const syncPending = globalSyncState.pending && !isUpToDate;

const pageTrackingEvent = TrackingEvent.AccountsList;

return {
hasNoAccount,
isSyncEnabled,
canAddAccount,
showHeader,
pageTrackingEvent,
syncPending,
sourceScreenName,
specificAccounts,
};
}
Loading

0 comments on commit 1a56e86

Please sign in to comment.