Repository: LedgerHQ/ledger-live
Pull request #9950: feat: Large Mover Footer Component
Author: login: RobinVncnt
Labels: name: mobile
description: Has changes in LLM
name: ui
description: Has changes in the design system library
Created: 2025-04-15T15:10:46Z | Merged: 2025-04-22T07:57:37Z

<!--
Thank you for your contribution! 👍
Please make sure to read CONTRIBUTING.md if you have not already. Pull Requests that do not comply with the rules will be arbitrarily closed.
-->

### ✅ Checklist

<!-- Pull Requests must pass the CI and be code reviewed. Set as Draft if the PR is not ready. -->

- [x] `npx changeset` was attached.
- [ ] **Covered by automatic tests.** <!-- if not, please explain. (Feature must be tested / Bug fix must bring non-regression) -->
- [ ] **Impact of the changes:** <!-- Please take some time to list the impact & what specific areas Quality Assurance (QA) should focus on -->
  - ...

### 📝 Description

feat: Large Mover Footer Component

### ❓ Context

- **JIRA or GitHub link**: [LIVE-18108]<!-- Attach the relevant ticket number if applicable. (e.g., [JIRA-123] for Jira or #123 for a Github issue) -->

<img width="396" alt="Screenshot 2025-04-15 at 17 06 57" src="https://github.com/user-attachments/assets/0a627de4-cffe-4b3d-bdea-fe4d1a77cd82" />


---

### 🧐 Checklist for the PR Reviewers

<!-- Please do not edit this if you are the PR author -->

- **The code aligns with the requirements** described in the linked JIRA or GitHub issue.
- **The PR description clearly documents the changes** made and explains any technical trade-offs or design decisions.
- **There are no undocumented trade-offs**, technical debt, or maintainability issues.
- **The PR has been tested** thoroughly, and any potential edge cases have been considered and handled.
- **Any new dependencies** have been justified and documented.
- **Performance** considerations have been taken into account. (changes have been profiled or benchmarked if necessary)


[LIVE-18108]: https://ledgerhq.atlassian.net/browse/LIVE-18108?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/.changeset/shaggy-knives-judge.md b/.changeset/shaggy-knives-judge.md
new file mode 100644
index 00000000000..7c0286041ad
--- /dev/null
+++ b/.changeset/shaggy-knives-judge.md
@@ -0,0 +1,6 @@
+---
+"live-mobile": minor
+"@ledgerhq/native-ui": minor
+---
+
+feat: Large Mover Footer Component
diff --git a/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx
new file mode 100644
index 00000000000..c95f05dd34d
--- /dev/null
+++ b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { Flex, QuickActionList } from "@ledgerhq/native-ui";
+import { QuickActionProps } from "~/hooks/useQuickActions";
+import { useFooterQuickActions } from "../hooks/useFooterQuickAction";
+
+export const Footer: React.FC<QuickActionProps> = props => {
+  const quickActionsData = useFooterQuickActions(props);
+  return (
+    <Flex>
+      <QuickActionList
+        data={quickActionsData}
+        numColumns={3}
+        id="asset_three_columns"
+        isActive={true}
+        testID="button"
+      />
+    </Flex>
+  );
+};
diff --git a/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/Footer.test.tsx b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/Footer.test.tsx
new file mode 100644
index 00000000000..77a5fcc1818
--- /dev/null
+++ b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/Footer.test.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { render } from "@tests/test-renderer";
+import { useRoute, useNavigation } from "@react-navigation/native";
+import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index";
+import { Footer } from "../Footer";
+
+const solana = getCryptoCurrencyById("solana");
+
+jest.mock("@react-navigation/native", () => ({
+  ...jest.requireActual("@react-navigation/native"),
+  useRoute: jest.fn(),
+  useNavigation: jest.fn(),
+}));
+
+jest.mock("~/hooks/useQuickActions", () => ({
+  __esModule: true,
+  default: () => ({
+    quickActionsList: {
+      BUY: {
+        icon: () => null,
+        route: ["Buy"],
+        disabled: false,
+        testID: "button-buy",
+      },
+      SWAP: {
+        icon: () => null,
+        route: ["Swap"],
+        disabled: false,
+        testID: "button-swap",
+      },
+      SELL: {
+        icon: () => null,
+        route: ["Sell"],
+        disabled: false,
+        testID: "button-sell",
+      },
+    },
+  }),
+}));
+
+describe("Footer", () => {
+  const mockNavigate = jest.fn();
+  const mockRoute = { name: "Portfolio" };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    (useNavigation as jest.Mock).mockReturnValue({ navigate: mockNavigate });
+    (useRoute as jest.Mock).mockReturnValue(mockRoute);
+  });
+
+  it("renders all available quick actions", () => {
+    const { getByTestId } = render(<Footer currency={solana} />);
+    expect(getByTestId("button-buy")).toBeTruthy();
+    expect(getByTestId("button-swap")).toBeTruthy();
+    expect(getByTestId("button-sell")).toBeTruthy();
+  });
+
+  it("calls track and navigate when a quick action is pressed", async () => {
+    const { getByTestId, user } = render(<Footer currency={solana} />);
+    await user.press(getByTestId("button-buy"));
+
+    expect(mockNavigate).toHaveBeenCalledWith("Buy");
+  });
+});
diff --git a/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/LargeMoverGraph.test.tsx b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/LargeMoverGraph.test.tsx
similarity index 97%
rename from apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/LargeMoverGraph.test.tsx
rename to apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/LargeMoverGraph.test.tsx
index c60e3e8a2f0..dd132f96635 100644
--- a/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/LargeMoverGraph.test.tsx
+++ b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/LargeMoverGraph.test.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 import { render } from "@tests/test-renderer";
-import { LargeMoverGraph } from "./LargeMoverGraph";
+import { LargeMoverGraph } from "../LargeMoverGraph";
 import { MarketCoinDataChart } from "@ledgerhq/live-common/market/utils/types";
 import Graph from "~/components/Graph";
 import { Flex } from "@ledgerhq/native-ui";
diff --git a/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/hooks/useFooterQuickAction.tsx b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/hooks/useFooterQuickAction.tsx
new file mode 100644
index 00000000000..f2abaf9854a
--- /dev/null
+++ b/apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/hooks/useFooterQuickAction.tsx
@@ -0,0 +1,52 @@
+import { useMemo } from "react";
+import { QuickActionButtonProps } from "@ledgerhq/native-ui";
+import { EntryOf } from "~/types/helpers";
+import { useTranslation } from "react-i18next";
+import { track } from "~/analytics";
+import { useNavigation, useRoute } from "@react-navigation/native";
+import { StackNavigationProp } from "@react-navigation/stack";
+import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator";
+import useQuickActions, { QuickActionProps } from "~/hooks/useQuickActions";
+
+const QUICK_ACTIONS = {
+  BUY: {
+    name: "portfolio.quickActions.buy",
+    analytics: "quick_action_buy",
+  },
+  SWAP: {
+    name: "portfolio.quickActions.swap",
+    analytics: "quick_action_swap",
+  },
+  SELL: {
+    name: "portfolio.quickActions.sell",
+    analytics: "quick_action_sell",
+  },
+} as const;
+
+export const useFooterQuickActions = (quickActionsProps: QuickActionProps) => {
+  const { t } = useTranslation();
+  const navigation = useNavigation<StackNavigationProp<BaseNavigatorStackParamList>>();
+  const router = useRoute();
+  const { quickActionsList } = useQuickActions(quickActionsProps);
+  const quickActionsData: QuickActionButtonProps[] = useMemo(
+    () =>
+      (Object.entries(QUICK_ACTIONS) as EntryOf<typeof QUICK_ACTIONS>[]).flatMap(([key, prop]) => {
+        const quickActionsItem = quickActionsList[key];
+        if (!quickActionsItem) return [];
+
+        return {
+          variant: "small",
+          textVariant: "small",
+          Icon: quickActionsItem.icon,
+          children: t(prop.name),
+          onPress: () => {
+            track("button_clicked", { button: prop.analytics, page: router.name });
+            navigation.navigate<keyof BaseNavigatorStackParamList>(...quickActionsItem.route);
+          },
+          disabled: quickActionsItem.disabled,
+        };
+      }),
+    [quickActionsList, t, navigation, router.name],
+  );
+  return quickActionsData;
+};
diff --git a/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionButton/index.tsx b/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionButton/index.tsx
index 1afc1e94e14..1f30d1d884c 100644
--- a/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionButton/index.tsx
+++ b/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionButton/index.tsx
@@ -14,6 +14,7 @@ export type QuickActionButtonProps = TouchableOpacityProps &
     onPressWhenDisabled?: TouchableOpacityProps["onPress"];
     textVariant?: TextVariants;
     variant?: Variant;
+    isActive?: boolean;
   };
 
 export const StyledText = baseStyled(Text)`
@@ -22,20 +23,25 @@ export const StyledText = baseStyled(Text)`
 `;
 
 export const Base = baseStyled(TouchableOpacity)<
-  TouchableOpacityProps & { visuallyDisabled?: boolean; variant: Variant }
+  TouchableOpacityProps & { visuallyDisabled?: boolean; variant: Variant; isActive?: boolean }
 >`
   height: ${({ variant }) => (variant === "small" ? 59 : 80)}px;
   flex-direction: column;
   text-align: center;
   align-items: center;
   justify-content: center;
-  border-radius: ${(p) => p.theme.radii[2]}px;
+  border-radius: ${({ isActive, theme }) => (isActive ? 14 : theme.radii[2])}px;
   padding: ${({ theme, variant }) =>
     variant === "small" ? `${theme.space[3]}px ${theme.space[2]}px` : `0 ${theme.space[6]}px`};
-  ${({ visuallyDisabled, theme }) =>
-    visuallyDisabled
-      ? `border: 1px solid ${theme.colors.neutral.c30};`
-      : `background-color: ${theme.colors.neutral.c20};`}
+  ${({ visuallyDisabled, isActive, theme }) => {
+    if (visuallyDisabled) {
+      return `border: 1px solid ${theme.colors.neutral.c30};`;
+    } else if (isActive) {
+      return `background-color: ${theme.colors.neutral.c30};`;
+    } else {
+      return `background-color: ${theme.colors.neutral.c20};`;
+    }
+  }}
 `;
 
 const QuickActionButton = ({
@@ -47,6 +53,7 @@ const QuickActionButton = ({
   textVariant = "body",
   variant = "large",
   testID,
+  isActive = true,
   ...otherProps
 }: QuickActionButtonProps): React.ReactElement => {
   const text = // Extract the text to use it as a testID
@@ -59,6 +66,7 @@ const QuickActionButton = ({
       onPress={disabled ? onPressWhenDisabled : onPress}
       visuallyDisabled={disabled}
       variant={variant}
+      isActive={!disabled && isActive}
       {...otherProps}
       testID={`${testID}-${text}`}
     >
diff --git a/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionList/index.tsx b/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionList/index.tsx
index 244de8ce12d..263e413da07 100644
--- a/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionList/index.tsx
+++ b/libs/ui/packages/native/src/components/cta/QuickAction/QuickActionList/index.tsx
@@ -6,6 +6,7 @@ import QuickActionButton, { QuickActionButtonProps } from "../QuickActionButton"
 export type QuickActionListProps = Omit<FlatListProps<QuickActionButtonProps>, "renderItem"> & {
   id: string;
   testID?: string;
+  isActive?: boolean;
 };
 
 const QuickActionList = ({
@@ -13,6 +14,7 @@ const QuickActionList = ({
   data,
   id,
   testID,
+  isActive = false,
   ...otherProps
 }: QuickActionListProps): React.ReactElement => {
   const renderItem = useCallback(
@@ -20,6 +22,7 @@ const QuickActionList = ({
       return (
         <QuickActionButton
           {...item}
+          isActive={isActive}
           flex={1}
           mr={(index + 1) % numColumns > 0 && data && index !== data.length - 1 ? 4 : 0}
           mb={data?.length && index + numColumns < data.length ? 4 : 0}


Reviews:
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:16:29Z
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:17:08Z
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T08:05:42Z
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:16:31Z
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:47:19Z
user: login: mcayuelas-ledger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:49:22Z
user: login: mcayuelas-ledger
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T15:02:32Z

Review comments:
diff_hunk: @@ -6,20 +6,23 @@ import QuickActionButton, { QuickActionButtonProps } from "../QuickActionButton"
 export type QuickActionListProps = Omit<FlatListProps<QuickActionButtonProps>, "renderItem"> & {
   id: string;
   testID?: string;
+  isActif?: boolean;
path: libs/ui/packages/native/src/components/cta/QuickAction/QuickActionList/index.tsx
user: login: mcayuelas-ledger
body: ```suggestion
  isActive?: boolean;
```
created_at: 2025-04-15T15:16:29Z
author_association: CONTRIBUTOR
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx
user: login: mcayuelas-ledger
body: you can add some tests here
created_at: 2025-04-15T15:17:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+import React from "react";
+import { render } from "@tests/test-renderer";
+import { useRoute, useNavigation } from "@react-navigation/native";
+import { Footer } from "./Footer";
+import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
+
+const solana: CryptoCurrency = {
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.test.tsx
user: login: mcayuelas-ledger
body: you can use `getCryptoCurrencyById("solana")`
created_at: 2025-04-17T08:04:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+import React from "react";
+import { render } from "@tests/test-renderer";
+import { useRoute, useNavigation } from "@react-navigation/native";
+import { Footer } from "./Footer";
+import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
+
+const solana: CryptoCurrency = {
+  type: "CryptoCurrency",
+  id: "solana",
+  name: "Solana",
+  managerAppName: "Solana",
+  coinType: 501,
+  scheme: "solana",
+  color: "#9945FF",
+  family: "solana",
+  blockAvgTime: 0.4,
+  supportsSegwit: false,
+  supportsNativeSegwit: false,
+  explorerViews: [
+    {
+      tx: "https://explorer.solana.com/tx/$hash",
+      address: "https://explorer.solana.com/address/$address",
+      token: "https://explorer.solana.com/address/$contractAddress",
+    },
+  ],
+  deviceTicker: "SOL",
+  ticker: "SOL",
+  units: [
+    {
+      name: "Solana",
+      code: "SOL",
+      magnitude: 9,
+    },
+  ],
+};
+
+jest.mock("@react-navigation/native", () => ({
+  ...jest.requireActual("@react-navigation/native"),
+  useRoute: jest.fn(),
+  useNavigation: jest.fn(),
+}));
+
+jest.mock("~/hooks/useQuickActions", () => ({
+  __esModule: true,
+  default: () => ({
+    quickActionsList: {
+      BUY: {
+        icon: () => null,
+        route: ["Buy"],
+        disabled: false,
+        testID: "button-buy",
+      },
+      SWAP: {
+        icon: () => null,
+        route: ["Swap"],
+        disabled: false,
+        testID: "button-swap",
+      },
+      SELL: {
+        icon: () => null,
+        route: ["Sell"],
+        disabled: false,
+        testID: "button-sell",
+      },
+    },
+  }),
+}));
+
+describe("Footer", () => {
+  const mockNavigate = jest.fn();
+  const mockRoute = { name: "Portfolio" };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    (useNavigation as jest.Mock).mockReturnValue({ navigate: mockNavigate });
+    (useRoute as jest.Mock).mockReturnValue(mockRoute);
+  });
+
+  it("renders all available quick actions", () => {
+    const { getByTestId } = render(<Footer accounts={[]} currency={solana} />);
+    expect(getByTestId("button-buy")).toBeTruthy();
+    expect(getByTestId("button-swap")).toBeTruthy();
+    expect(getByTestId("button-sell")).toBeTruthy();
+  });
+
+  it("calls track and navigate when a quick action is pressed", async () => {
+    const { getByTestId, user } = render(<Footer accounts={[]} currency={solana} />);
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.test.tsx
user: login: mcayuelas-ledger
body: why do we need `accounts` property ? 
created_at: 2025-04-17T08:05:40Z
author_association: CONTRIBUTOR
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx
user: login: mcayuelas-ledger
body: this one should be a good candidate to use the MVVM approach.

by creating your ViewModel you will pass all needed props, each button will have is testId and then you can test the click

created_at: 2025-04-17T13:14:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import React from "react";
+import { render } from "@tests/test-renderer";
+import { useRoute, useNavigation } from "@react-navigation/native";
+import { Footer } from "./Footer";
+import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index";
+
+const solana = getCryptoCurrencyById("solana");
+
+jest.mock("@react-navigation/native", () => ({
+  ...jest.requireActual("@react-navigation/native"),
+  useRoute: jest.fn(),
+  useNavigation: jest.fn(),
+}));
+
+jest.mock("~/hooks/useQuickActions", () => ({
+  __esModule: true,
+  default: () => ({
+    quickActionsList: {
+      BUY: {
+        icon: () => null,
+        route: ["Buy"],
+        disabled: false,
+        testID: "button-buy",
+      },
+      SWAP: {
+        icon: () => null,
+        route: ["Swap"],
+        disabled: false,
+        testID: "button-swap",
+      },
+      SELL: {
+        icon: () => null,
+        route: ["Sell"],
+        disabled: false,
+        testID: "button-sell",
+      },
+    },
+  }),
+}));
+
+describe("Footer", () => {
+  const mockNavigate = jest.fn();
+  const mockRoute = { name: "Portfolio" };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    (useNavigation as jest.Mock).mockReturnValue({ navigate: mockNavigate });
+    (useRoute as jest.Mock).mockReturnValue(mockRoute);
+  });
+
+  it("renders all available quick actions", () => {
+    const { getByTestId } = render(<Footer currency={solana} />);
+    expect(getByTestId("button-buy")).toBeTruthy();
+    expect(getByTestId("button-swap")).toBeTruthy();
+    expect(getByTestId("button-sell")).toBeTruthy();
+  });
+
+  it("calls track and navigate when a quick action is pressed", async () => {
+    const { getByTestId, user } = render(<Footer currency={solana} />);
+    await user.press(getByTestId("button-buy"));
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.test.tsx
user: login: mcayuelas-ledger
body: it's better to use getByRole : https://testing-library.com/docs/queries/about/#priority
created_at: 2025-04-17T13:16:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import React from "react";
+import { Flex, QuickActionList } from "@ledgerhq/native-ui";
+import { QuickActionProps } from "~/hooks/useQuickActions";
+import { useFooter } from "../hooks/useQuickAction";
+
+export const Footer: React.FC<QuickActionProps> = props => {
+  const quickActionsData = useFooter(props);
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/Footer.tsx
user: login: mcayuelas-ledger
body: should be named `useFooterQuickActions` I think
created_at: 2025-04-18T08:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import React from "react";
+import { render } from "@tests/test-renderer";
+import { useRoute, useNavigation } from "@react-navigation/native";
+import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index";
+import { Footer } from "../Footer";
+
+const solana = getCryptoCurrencyById("solana");
+
+jest.mock("@react-navigation/native", () => ({
+  ...jest.requireActual("@react-navigation/native"),
+  useRoute: jest.fn(),
+  useNavigation: jest.fn(),
+}));
+
+jest.mock("~/hooks/useQuickActions", () => ({
+  __esModule: true,
+  default: () => ({
+    quickActionsList: {
+      BUY: {
+        icon: () => null,
+        route: ["Buy"],
+        disabled: false,
+        testID: "button-buy",
+      },
+      SWAP: {
+        icon: () => null,
+        route: ["Swap"],
+        disabled: false,
+        testID: "button-swap",
+      },
+      SELL: {
+        icon: () => null,
+        route: ["Sell"],
+        disabled: false,
+        testID: "button-sell",
+      },
+    },
+  }),
+}));
+
+describe("Footer", () => {
+  const mockNavigate = jest.fn();
+  const mockRoute = { name: "Portfolio" };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    (useNavigation as jest.Mock).mockReturnValue({ navigate: mockNavigate });
+    (useRoute as jest.Mock).mockReturnValue(mockRoute);
+  });
+
+  it("renders all available quick actions", () => {
+    const { getByTestId } = render(<Footer currency={solana} />);
+    expect(getByTestId("button-buy")).toBeTruthy();
+    expect(getByTestId("button-swap")).toBeTruthy();
+    expect(getByTestId("button-sell")).toBeTruthy();
+  });
+
+  it("calls track and navigate when a quick action is pressed", async () => {
+    const { getByTestId, user } = render(<Footer currency={solana} />);
+    await user.press(getByTestId("button-buy"));
path: apps/ledger-live-mobile/src/newArch/features/LandingPages/screens/LargeMoverLandingPage/Components/__tests__/Footer.test.tsx
user: login: mcayuelas-ledger
body: use getByRole => https://testing-library.com/docs/queries/about/#priority

`getByRole("button", {
        name: /buy/i,
      })`
created_at: 2025-04-18T08:48:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-15T15:10:51Z
body: [vc]: #JCizfV77PLRMkcb9R8zz9KGIeLcj7i84T2ESQ0tZEAo=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZWFjdC11aS1zdG9yeWJvb2siLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbGVkZ2VyaHEvcmVhY3QtdWktc3Rvcnlib29rL0VUOG5iNjRmbWdVa1Rha29YREViVUhhYkM1ZFkiLCJwcmV2aWV3VXJsIjoicmVhY3QtdWktc3Rvcnlib29rLWdpdC1mZWF0LWxhcmdlLW1vdmVyLWZvb3Rlci1lMTI2NzktbGVkZ2VyaHEudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJuYXRpdmUtdWktc3Rvcnlib29rIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2xlZGdlcmhxL25hdGl2ZS11aS1zdG9yeWJvb2svbXh6aVdSQ3REeHFvWUtEdEhpOHRTM3hCajZhMSIsInByZXZpZXdVcmwiOiJuYXRpdmUtdWktc3Rvcnlib29rLWdpdC1mZWF0LWxhcmdlLW1vdmVyLWZvb3Rlci0wMTA0YmItbGVkZ2VyaHEudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJuYXRpdmUtdWktc3Rvcnlib29rLWdpdC1mZWF0LWxhcmdlLW1vdmVyLWZvb3Rlci0wMTA0YmItbGVkZ2VyaHEudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJ3ZWItdG9vbHMiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93ZWItdG9vbHMiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbGVkZ2VyaHEvd2ViLXRvb2xzLzd2dHhqM3RDM0NScmNzOVduTGJtaDh6QkZUeVkiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IkZBSUxFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX0seyJuYW1lIjoibGVkZ2VyLWxpdmUtZ2l0aHViLWJvdCIsInJvb3REaXJlY3RvcnkiOiJ0b29scy9naXRodWItYm90IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2xlZGdlcmhxL2xlZGdlci1saXZlLWdpdGh1Yi1ib3QvNTFSWjlHdEhmOUd1SE1nekR5bkhmQXk5NFV3VyIsInByZXZpZXdVcmwiOiJsZWRnZXItbGl2ZS1naXRodWItYm90LWdpdC1mZWF0LWxhcmdlLW1vdmVyLWZvby0yNmJiMzQtbGVkZ2VyaHEudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **native-ui-storybook** | ✅ Ready ([Inspect](https://vercel.com/ledgerhq/native-ui-storybook/mxziWRCtDxqoYKDtHi8tS3xBj6a1)) | [Visit Preview](https://native-ui-storybook-git-feat-large-mover-footer-0104bb-ledgerhq.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/native-ui-storybook-git-feat-large-mover-footer-0104bb-ledgerhq.vercel.app?via=pr-comment-feedback-link) | Apr 18, 2025 10:05am |
| **web-tools** | ❌ Failed ([Inspect](https://vercel.com/ledgerhq/web-tools/7vtxj3tC3CRrcs9WnLbmh8zBFTyY)) |  |  | Apr 18, 2025 10:05am |

<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **ledger-live-github-bot** | ⬜️ Ignored ([Inspect](https://vercel.com/ledgerhq/ledger-live-github-bot/51RZ9GtHf9GuHMgzDynHfAy94UwW)) | [Visit Preview](https://ledger-live-github-bot-git-feat-large-mover-foo-26bb34-ledgerhq.vercel.app) |  | Apr 18, 2025 10:05am |
| **react-ui-storybook** | ⬜️ Ignored ([Inspect](https://vercel.com/ledgerhq/react-ui-storybook/ET8nb64fmgUkTakoXDEbUHabC5dY)) | [Visit Preview](https://react-ui-storybook-git-feat-large-mover-footer-e12679-ledgerhq.vercel.app) |  | Apr 18, 2025 10:05am |
</details>





author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-18T10:17:28Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=LedgerHQ_ledger-live&pullRequest=9950) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [26.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=LedgerHQ_ledger-live&pullRequest=9950&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=LedgerHQ_ledger-live&pullRequest=9950)


author_association: NONE