Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💄 (llm) empty accounts/assets screen #9215

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 haven’t 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 haven’t 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
Loading