From fb3a7befa280a7fe8267a16dca49e83d4061b3f3 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Thu, 2 Oct 2025 12:39:14 -0700 Subject: [PATCH 01/14] chore: adding balance empty state component --- .../BalanceEmptyState.stories.tsx | 10 ++ .../BalanceEmptyState.test.tsx | 98 +++++++++++++++++++ .../BalanceEmptyState/BalanceEmptyState.tsx | 87 ++++++++++++++++ .../BalanceEmptyState.types.ts | 25 +++++ app/components/UI/BalanceEmptyState/index.ts | 2 + 5 files changed, 222 insertions(+) create mode 100644 app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx create mode 100644 app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx create mode 100644 app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx create mode 100644 app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts create mode 100644 app/components/UI/BalanceEmptyState/index.ts diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx new file mode 100644 index 000000000000..4abe5daf9d8e --- /dev/null +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx @@ -0,0 +1,10 @@ +import BalanceEmptyState from './BalanceEmptyState'; + +const BalanceEmptyStateMeta = { + title: 'Components / UI / BalanceEmptyState', + component: BalanceEmptyState, +}; + +export default BalanceEmptyStateMeta; + +export const Default = {}; diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx new file mode 100644 index 000000000000..b8335023f513 --- /dev/null +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import renderWithProvider from '../../../util/test/renderWithProvider'; +import BalanceEmptyState from './BalanceEmptyState'; + +describe('BalanceEmptyState', () => { + it('renders correctly with default props', () => { + const { getByTestId, getByText } = renderWithProvider( + , + ); + + expect(getByTestId('balance-empty-state')).toBeTruthy(); + expect(getByText('Fund your wallet')).toBeTruthy(); + expect(getByText('Buy tokens to get started')).toBeTruthy(); + expect(getByText('Buy crypto')).toBeTruthy(); + }); + + it('renders with custom props', () => { + const customTitle = 'Custom wallet title'; + const customSubtitle = 'Custom subtitle text'; + const customActionText = 'Custom action'; + const customTestID = 'custom-test-id'; + + const { getByTestId, getByText } = renderWithProvider( + , + ); + + expect(getByTestId(customTestID)).toBeTruthy(); + expect(getByText(customTitle)).toBeTruthy(); + expect(getByText(customSubtitle)).toBeTruthy(); + expect(getByText(customActionText)).toBeTruthy(); + }); + + it('calls onAction when button is pressed', () => { + const mockOnAction = jest.fn(); + const { getByTestId } = renderWithProvider( + , + ); + + const actionButton = getByTestId('balance-empty-state-action-button'); + fireEvent.press(actionButton); + + expect(mockOnAction).toHaveBeenCalledTimes(1); + }); + + it('renders without action button when onAction is not provided', () => { + const { getByTestId, queryByTestId } = renderWithProvider( + , + ); + + expect(getByTestId('balance-empty-state')).toBeTruthy(); + expect(queryByTestId('balance-empty-state-action-button')).toBeTruthy(); // Button should still render + }); + + it('has proper test IDs for all elements', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect(getByTestId('test-component')).toBeTruthy(); + expect(getByTestId('test-component-image')).toBeTruthy(); + expect(getByTestId('test-component-title')).toBeTruthy(); + expect(getByTestId('test-component-subtitle')).toBeTruthy(); + expect(getByTestId('test-component-action-button')).toBeTruthy(); + }); + + it('displays the bank transfer image', () => { + const { getByTestId } = renderWithProvider(); + + const image = getByTestId('balance-empty-state-image'); + expect(image).toBeTruthy(); + expect(image.props.source.uri).toBe( + 'http://localhost:3845/assets/380bd6dd5c4ed318751b45ce142a72e476987493.png', + ); + }); + + it('matches snapshot', () => { + const component = renderWithProvider(); + expect(component).toMatchSnapshot(); + }); + + it('matches snapshot with custom props', () => { + const component = renderWithProvider( + , + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx new file mode 100644 index 000000000000..58ebb92d09d9 --- /dev/null +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Image } from 'react-native'; +import { + Box, + Text, + TextVariant, + TextColor, + BoxBackgroundColor, + BoxFlexDirection, + BoxAlignItems, + BoxJustifyContent, +} from '@metamask/design-system-react-native'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import ButtonHero from '../../../component-library/components-temp/Buttons/ButtonHero'; +import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; + +// Bank transfer image from Figma +const bankTransferImage = + 'http://localhost:3845/assets/380bd6dd5c4ed318751b45ce142a72e476987493.png'; + +/** + * BalanceEmptyState component displays an empty state for wallet balance + * with an illustration, title, subtitle, and action button. + */ +const BalanceEmptyState: React.FC = ({ + onAction, + testID = 'balance-empty-state', + title = 'Fund your wallet', + subtitle = 'Buy tokens to get started', + actionText = 'Buy crypto', +}) => { + const tw = useTailwind(); + + return ( + + + {/* Content Container */} + + {/* Text Content */} + + + {title} + + + {subtitle} + + + + {actionText} + + + + ); +}; + +export default BalanceEmptyState; diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts new file mode 100644 index 000000000000..b181aabc79e1 --- /dev/null +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts @@ -0,0 +1,25 @@ +/** + * Props for the BalanceEmptyState component + */ +export interface BalanceEmptyStateProps { + /** + * Callback function triggered when the action button is pressed + */ + onAction?: () => void; + /** + * Test ID for component testing + */ + testID?: string; + /** + * Title text to display (defaults to "Fund your wallet") + */ + title?: string; + /** + * Subtitle text to display (defaults to "Buy tokens to get started") + */ + subtitle?: string; + /** + * Action button text (defaults to "Buy crypto") + */ + actionText?: string; +} diff --git a/app/components/UI/BalanceEmptyState/index.ts b/app/components/UI/BalanceEmptyState/index.ts new file mode 100644 index 000000000000..238606c1827b --- /dev/null +++ b/app/components/UI/BalanceEmptyState/index.ts @@ -0,0 +1,2 @@ +export { default } from './BalanceEmptyState'; +export type { BalanceEmptyStateProps } from './BalanceEmptyState.types'; From d10d51306d9b39cbd9d8b5a2db18f2196ff2d635 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Thu, 2 Oct 2025 16:02:47 -0700 Subject: [PATCH 02/14] chore: update --- .../Buttons/ButtonHero/ButtonHero.tsx | 4 ++ .../BalanceEmptyState.stories.tsx | 19 +++++- .../BalanceEmptyState.test.tsx | 32 +++------ .../BalanceEmptyState/BalanceEmptyState.tsx | 62 +++++++++--------- .../DefiEmptyState/DefiEmptyState.stories.tsx | 2 +- app/images/bank.transfer.png | Bin 0 -> 33509 bytes 6 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 app/images/bank.transfer.png diff --git a/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx b/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx index 3b58bce9c3ec..a6137ad7db14 100644 --- a/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx +++ b/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx @@ -3,6 +3,7 @@ import React, { useCallback } from 'react'; import { ButtonBase, ButtonBaseProps, + TextVariant, } from '@metamask/design-system-react-native'; import { useTailwind, @@ -43,6 +44,9 @@ const ButtonHeroInner = ({ isLoading={isLoading} loadingText={loadingText} textClassName={textClassName || getTextClassName} + textProps={{ + variant: TextVariant.BodyLg, + }} {...props} style={getStyle} > diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx index 4abe5daf9d8e..e104df8158b6 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx @@ -1,10 +1,27 @@ +import React from 'react'; import BalanceEmptyState from './BalanceEmptyState'; +import { Box, BoxBackgroundColor } from '@metamask/design-system-react-native'; const BalanceEmptyStateMeta = { title: 'Components / UI / BalanceEmptyState', component: BalanceEmptyState, + decorators: [ + (Story: React.FC) => ( + + + + ), + ], }; export default BalanceEmptyStateMeta; -export const Default = {}; +export const Default = { + args: { + title: 'Fund your wallet', + subtitle: 'Get your wallet ready to use web3', + actionText: 'Add funds', + // eslint-disable-next-line no-console + onAction: () => console.log('onAction'), + }, +}; diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index b8335023f513..31a05e69c149 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -5,35 +5,19 @@ import BalanceEmptyState from './BalanceEmptyState'; describe('BalanceEmptyState', () => { it('renders correctly with default props', () => { - const { getByTestId, getByText } = renderWithProvider( - , - ); - - expect(getByTestId('balance-empty-state')).toBeTruthy(); - expect(getByText('Fund your wallet')).toBeTruthy(); - expect(getByText('Buy tokens to get started')).toBeTruthy(); - expect(getByText('Buy crypto')).toBeTruthy(); - }); - - it('renders with custom props', () => { - const customTitle = 'Custom wallet title'; - const customSubtitle = 'Custom subtitle text'; - const customActionText = 'Custom action'; - const customTestID = 'custom-test-id'; - const { getByTestId, getByText } = renderWithProvider( , ); - expect(getByTestId(customTestID)).toBeTruthy(); - expect(getByText(customTitle)).toBeTruthy(); - expect(getByText(customSubtitle)).toBeTruthy(); - expect(getByText(customActionText)).toBeTruthy(); + expect(getByTestId('balance-empty-state')).toBeDefined(); + expect(getByText('Fund your wallet')).toBeDefined(); + expect(getByText('Buy tokens to get started')).toBeDefined(); + expect(getByText('Buy crypto')).toBeDefined(); }); it('calls onAction when button is pressed', () => { diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index 58ebb92d09d9..e5464ecd49a9 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -9,14 +9,14 @@ import { BoxFlexDirection, BoxAlignItems, BoxJustifyContent, + FontWeight, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import ButtonHero from '../../../component-library/components-temp/Buttons/ButtonHero'; import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; +import ButtonHero from '../../../component-library/components-temp/Buttons/ButtonHero'; // Bank transfer image from Figma -const bankTransferImage = - 'http://localhost:3845/assets/380bd6dd5c4ed318751b45ce142a72e476987493.png'; +const bankTransferImage = require('../../../images/bank.transfer.png'); /** * BalanceEmptyState component displays an empty state for wallet balance @@ -24,62 +24,60 @@ const bankTransferImage = */ const BalanceEmptyState: React.FC = ({ onAction, - testID = 'balance-empty-state', - title = 'Fund your wallet', - subtitle = 'Buy tokens to get started', - actionText = 'Buy crypto', + testID, + title, + subtitle, + actionText, }) => { const tw = useTailwind(); return ( - - {/* Content Container */} - - {/* Text Content */} - + + + {title && ( {title} + )} + {subtitle && ( {subtitle} - - + )} + + {actionText && onAction && ( + {actionText} - + )} ); }; diff --git a/app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx b/app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx index b96cff93930a..0d361c2e5356 100644 --- a/app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx +++ b/app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx @@ -1,7 +1,7 @@ import { DefiEmptyState } from './DefiEmptyState'; const DefiEmptyStateMeta = { - title: 'Components/UI/DefiEmptyState', + title: 'Components / UI / DefiEmptyState', component: DefiEmptyState, }; diff --git a/app/images/bank.transfer.png b/app/images/bank.transfer.png new file mode 100644 index 0000000000000000000000000000000000000000..df5e0c92c32c3599eca19d1cc5b974c28fada66c GIT binary patch literal 33509 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 ZuYS7I!>bMpfd_a4fv2mV%Q~loCIH@u89x93 literal 0 HcmV?d00001 From bfcba2bae8bf418a753d1ba27143ae211c0f7efc Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Thu, 2 Oct 2025 16:04:00 -0700 Subject: [PATCH 03/14] chore: update --- .storybook/storybook.requires.js | 1 + locales/languages/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index 007b50e36f98..b0bd44ada1f6 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -126,6 +126,7 @@ const getStories = () => { "./app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx": require("../app/component-library/components/Texts/TextWithPrefixIcon/TextWithPrefixIcon.stories.tsx"), "./app/component-library/components/Toast/Toast.stories.tsx": require("../app/component-library/components/Toast/Toast.stories.tsx"), "./app/components/Base/Keypad/Keypad.stories.tsx": require("../app/components/Base/Keypad/Keypad.stories.tsx"), + "./app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx": require("../app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx"), "./app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx": require("../app/components/UI/CollectiblesEmptyState/CollectiblesEmptyState.stories.tsx"), "./app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx": require("../app/components/UI/DefiEmptyState/DefiEmptyState.stories.tsx"), "./app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx": require("../app/components/UI/Perps/Views/PerpsEmptyState/PerpsEmptyState.stories.tsx"), diff --git a/locales/languages/en.json b/locales/languages/en.json index d2ece994be8d..ff6fce7ab3e0 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1789,6 +1789,7 @@ "alphabetically": "Alphabetically (A-Z)", "add_to_get_started": "Add crypto to get started", "token_is_needed_to_continue": "{{tokenSymbol}} is needed to continue", + "fund_your_wallet": "Fund your wallet", "fund_your_wallet_to_get_started": "Fund your wallet to get started in web3", "add_funds": "Add funds", "next": "Next", From 0ded7866a14cdbdb0d75be9bdfa20c25f23db16c Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 3 Oct 2025 10:41:08 -0700 Subject: [PATCH 04/14] chore: updates --- .../BalanceEmptyState.stories.tsx | 11 +- .../BalanceEmptyState.test.tsx | 170 ++++++++++++++---- .../BalanceEmptyState/BalanceEmptyState.tsx | 102 +++++++---- .../BalanceEmptyState.types.ts | 17 +- locales/languages/en.json | 1 + 5 files changed, 208 insertions(+), 93 deletions(-) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx index e104df8158b6..e019cf3d68cb 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import BalanceEmptyState from './BalanceEmptyState'; import { Box, BoxBackgroundColor } from '@metamask/design-system-react-native'; @@ -16,12 +17,4 @@ const BalanceEmptyStateMeta = { export default BalanceEmptyStateMeta; -export const Default = { - args: { - title: 'Fund your wallet', - subtitle: 'Get your wallet ready to use web3', - actionText: 'Add funds', - // eslint-disable-next-line no-console - onAction: () => console.log('onAction'), - }, -}; +export const Default = {}; diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index 31a05e69c149..2a7deca18a5b 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -2,48 +2,156 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import BalanceEmptyState from './BalanceEmptyState'; +import { MetaMetricsEvents } from '../../../hooks/useMetrics'; +import { TraceName } from '../../../../util/trace'; + +// Mock navigation +const mockNavigate = jest.fn(); +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + +// Mock metrics +const mockTrackEvent = jest.fn(); +const mockCreateEventBuilder = jest.fn(() => ({ + addProperties: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue({}), +})); + +jest.mock('../../../hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), + MetaMetricsEvents: { + CARD_ADD_FUNDS_DEPOSIT_CLICKED: 'CARD_ADD_FUNDS_DEPOSIT_CLICKED', + RAMPS_BUTTON_CLICKED: 'RAMPS_BUTTON_CLICKED', + }, +})); + +// Mock trace +const mockTrace = jest.fn(); +jest.mock('../../../../util/trace', () => ({ + trace: mockTrace, + TraceName: { + LoadDepositExperience: 'LoadDepositExperience', + }, +})); + +// Mock createDepositNavigationDetails +const mockCreateDepositNavigationDetails = jest.fn(() => ['DepositScreen', {}]); +jest.mock('../../Ramp/Deposit/routes/utils', () => ({ + createDepositNavigationDetails: mockCreateDepositNavigationDetails, +})); + +// Mock getDecimalChainId +jest.mock('../../../../util/networks', () => ({ + getDecimalChainId: jest.fn(() => 1), +})); + +// Mock strings +jest.mock('../../../../locales/i18n', () => ({ + strings: jest.fn((key: string) => { + const strings: { [key: string]: string } = { + 'wallet.fund_your_wallet': 'Fund your wallet', + 'wallet.get_ready_for_web3': 'Get your wallet ready to use web3', + 'wallet.add_funds': 'Add funds', + }; + return strings[key] || key; + }), +})); describe('BalanceEmptyState', () => { - it('renders correctly with default props', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + networkConfigurations: {}, + }, + }, + }, + }; + + it('renders correctly with localized content', () => { const { getByTestId, getByText } = renderWithProvider( - , + , + { state: mockState }, ); expect(getByTestId('balance-empty-state')).toBeDefined(); expect(getByText('Fund your wallet')).toBeDefined(); - expect(getByText('Buy tokens to get started')).toBeDefined(); - expect(getByText('Buy crypto')).toBeDefined(); + expect(getByText('Get your wallet ready to use web3')).toBeDefined(); + expect(getByText('Add funds')).toBeDefined(); }); - it('calls onAction when button is pressed', () => { - const mockOnAction = jest.fn(); - const { getByTestId } = renderWithProvider( - , + it('navigates to deposit screen when button is pressed', () => { + const { getByTestId } = renderWithProvider(, { + state: mockState, + }); + + const actionButton = getByTestId('balance-empty-state-action-button'); + fireEvent.press(actionButton); + + expect(mockCreateDepositNavigationDetails).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith('DepositScreen', {}); + }); + + it('tracks metrics when button is pressed', () => { + const { getByTestId } = renderWithProvider(, { + state: mockState, + }); + + const actionButton = getByTestId('balance-empty-state-action-button'); + fireEvent.press(actionButton); + + expect(mockTrackEvent).toHaveBeenCalledTimes(2); + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED, ); + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_BUTTON_CLICKED, + ); + }); + + it('traces deposit experience when button is pressed', () => { + const { getByTestId } = renderWithProvider(, { + state: mockState, + }); const actionButton = getByTestId('balance-empty-state-action-button'); fireEvent.press(actionButton); - expect(mockOnAction).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith({ + name: TraceName.LoadDepositExperience, + }); }); - it('renders without action button when onAction is not provided', () => { - const { getByTestId, queryByTestId } = renderWithProvider( - , + it('calls custom onAction when provided', () => { + const mockOnAction = jest.fn(); + const { getByTestId } = renderWithProvider( + , + { state: mockState }, ); - expect(getByTestId('balance-empty-state')).toBeTruthy(); - expect(queryByTestId('balance-empty-state-action-button')).toBeTruthy(); // Button should still render + const actionButton = getByTestId('balance-empty-state-action-button'); + fireEvent.press(actionButton); + + expect(mockOnAction).toHaveBeenCalledTimes(1); + expect(mockNavigate).not.toHaveBeenCalled(); }); it('has proper test IDs for all elements', () => { const { getByTestId } = renderWithProvider( , + { state: mockState }, ); expect(getByTestId('test-component')).toBeTruthy(); @@ -54,29 +162,19 @@ describe('BalanceEmptyState', () => { }); it('displays the bank transfer image', () => { - const { getByTestId } = renderWithProvider(); + const { getByTestId } = renderWithProvider(, { + state: mockState, + }); const image = getByTestId('balance-empty-state-image'); expect(image).toBeTruthy(); - expect(image.props.source.uri).toBe( - 'http://localhost:3845/assets/380bd6dd5c4ed318751b45ce142a72e476987493.png', - ); + expect(image.props.source).toBeDefined(); }); it('matches snapshot', () => { - const component = renderWithProvider(); - expect(component).toMatchSnapshot(); - }); - - it('matches snapshot with custom props', () => { - const component = renderWithProvider( - , - ); + const component = renderWithProvider(, { + state: mockState, + }); expect(component).toMatchSnapshot(); }); }); diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index e5464ecd49a9..ff574f81e00c 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Image } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; import { Box, Text, @@ -12,24 +14,55 @@ import { FontWeight, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; import ButtonHero from '../../../component-library/components-temp/Buttons/ButtonHero'; - -// Bank transfer image from Figma -const bankTransferImage = require('../../../images/bank.transfer.png'); +import { strings } from '../../../../locales/i18n'; +import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; +import { getDecimalChainId } from '../../../util/networks'; +import { selectChainId } from '../../../selectors/networkController'; +import { trace, TraceName } from '../../../util/trace'; +import { createDepositNavigationDetails } from '../Ramp/Deposit/routes/utils'; +import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; +import bankTransferImage from '../../../images/bank.transfer.png'; /** - * BalanceEmptyState component displays an empty state for wallet balance - * with an illustration, title, subtitle, and action button. + * BalanceEmptyState smart component displays an empty state for wallet balance + * with an illustration, title, subtitle, and action button that navigates to deposit flow. */ const BalanceEmptyState: React.FC = ({ onAction, - testID, - title, - subtitle, - actionText, + testID = 'balance-empty-state', }) => { const tw = useTailwind(); + const chainId = useSelector(selectChainId); + const navigation = useNavigation(); + const { trackEvent, createEventBuilder } = useMetrics(); + + const goToDeposit = () => { + navigation.navigate(...createDepositNavigationDetails()); + + trackEvent( + createEventBuilder( + MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED, + ).build(), + ); + + trackEvent( + createEventBuilder(MetaMetricsEvents.RAMPS_BUTTON_CLICKED) + .addProperties({ + text: 'Add funds', + location: 'BalanceEmptyState', + chain_id_destination: getDecimalChainId(chainId), + ramp_type: 'DEPOSIT', + }) + .build(), + ); + + trace({ + name: TraceName.LoadDepositExperience, + }); + }; + + const handleAction = onAction || goToDeposit; return ( = ({ source={bankTransferImage} style={tw.style('w-[100px] h-[100px]')} resizeMode="cover" + testID={`${testID}-image`} /> - {title && ( - - {title} - - )} - {subtitle && ( - - {subtitle} - - )} + + {strings('wallet.fund_your_wallet')} + + + {strings('wallet.get_ready_for_web3')} + - {actionText && onAction && ( - - {actionText} - - )} + + {strings('wallet.add_funds')} + ); }; diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts index b181aabc79e1..e8d06d9d7cf5 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts @@ -1,25 +1,14 @@ /** - * Props for the BalanceEmptyState component + * Props for the BalanceEmptyState smart component */ export interface BalanceEmptyStateProps { /** - * Callback function triggered when the action button is pressed + * Optional callback function to override the default deposit navigation. + * If not provided, will navigate to the deposit flow with tracking. */ onAction?: () => void; /** * Test ID for component testing */ testID?: string; - /** - * Title text to display (defaults to "Fund your wallet") - */ - title?: string; - /** - * Subtitle text to display (defaults to "Buy tokens to get started") - */ - subtitle?: string; - /** - * Action button text (defaults to "Buy crypto") - */ - actionText?: string; } diff --git a/locales/languages/en.json b/locales/languages/en.json index ff6fce7ab3e0..fbede326177c 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1790,6 +1790,7 @@ "add_to_get_started": "Add crypto to get started", "token_is_needed_to_continue": "{{tokenSymbol}} is needed to continue", "fund_your_wallet": "Fund your wallet", + "get_ready_for_web3": "Get your wallet ready to use web3", "fund_your_wallet_to_get_started": "Fund your wallet to get started in web3", "add_funds": "Add funds", "next": "Next", From 4e2be200c08f93604be88148d80ef60b1a0fbc9d Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 3 Oct 2025 14:22:25 -0700 Subject: [PATCH 05/14] chore: updating redux mock state for storybook --- .storybook/storybook-store.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.storybook/storybook-store.js b/.storybook/storybook-store.js index 34619192463e..a4c366e07096 100644 --- a/.storybook/storybook-store.js +++ b/.storybook/storybook-store.js @@ -1,5 +1,10 @@ import { AppThemeKey } from '../app/util/theme/models'; +import initialRootState from '../app/util/test/initial-root-state'; export const storybookStore = { - user: { appTheme: AppThemeKey.os }, + ...initialRootState, + user: { + ...initialRootState.user, + appTheme: AppThemeKey.os, + }, }; From 4a890b6212f98a6b19bf17e9799567cb4fb7ad59 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Thu, 16 Oct 2025 11:25:15 -0700 Subject: [PATCH 06/14] chore; lint fix and image rename --- .../UI/BalanceEmptyState/BalanceEmptyState.test.tsx | 4 ++-- .../UI/BalanceEmptyState/BalanceEmptyState.tsx | 2 +- app/images/{bank.transfer.png => bank-transfer.png} | Bin 3 files changed, 3 insertions(+), 3 deletions(-) rename app/images/{bank.transfer.png => bank-transfer.png} (100%) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index 2a7deca18a5b..4845911977ce 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import BalanceEmptyState from './BalanceEmptyState'; -import { MetaMetricsEvents } from '../../../hooks/useMetrics'; -import { TraceName } from '../../../../util/trace'; +import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events'; +import { TraceName } from '../../../util/trace'; // Mock navigation const mockNavigate = jest.fn(); diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index ff574f81e00c..8d7668e20089 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -22,7 +22,7 @@ import { selectChainId } from '../../../selectors/networkController'; import { trace, TraceName } from '../../../util/trace'; import { createDepositNavigationDetails } from '../Ramp/Deposit/routes/utils'; import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; -import bankTransferImage from '../../../images/bank.transfer.png'; +import bankTransferImage from '../../../images/bank-transfer.png'; /** * BalanceEmptyState smart component displays an empty state for wallet balance diff --git a/app/images/bank.transfer.png b/app/images/bank-transfer.png similarity index 100% rename from app/images/bank.transfer.png rename to app/images/bank-transfer.png From d772e0c2ba87ff4b399a9cbcd2d4f025e9eeda0b Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 17 Oct 2025 09:05:08 -0700 Subject: [PATCH 07/14] chore: updating tests --- .../BalanceEmptyState.test.tsx | 170 +++--------------- 1 file changed, 27 insertions(+), 143 deletions(-) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index 4845911977ce..d3c7bcdf5c4b 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -1,66 +1,14 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../util/test/initial-root-state'; import BalanceEmptyState from './BalanceEmptyState'; -import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events'; -import { TraceName } from '../../../util/trace'; -// Mock navigation -const mockNavigate = jest.fn(); +// Mock navigation (component requires it) jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useNavigation: () => ({ - navigate: mockNavigate, - }), -})); - -// Mock metrics -const mockTrackEvent = jest.fn(); -const mockCreateEventBuilder = jest.fn(() => ({ - addProperties: jest.fn().mockReturnThis(), - build: jest.fn().mockReturnValue({}), -})); - -jest.mock('../../../hooks/useMetrics', () => ({ - useMetrics: () => ({ - trackEvent: mockTrackEvent, - createEventBuilder: mockCreateEventBuilder, - }), - MetaMetricsEvents: { - CARD_ADD_FUNDS_DEPOSIT_CLICKED: 'CARD_ADD_FUNDS_DEPOSIT_CLICKED', - RAMPS_BUTTON_CLICKED: 'RAMPS_BUTTON_CLICKED', - }, -})); - -// Mock trace -const mockTrace = jest.fn(); -jest.mock('../../../../util/trace', () => ({ - trace: mockTrace, - TraceName: { - LoadDepositExperience: 'LoadDepositExperience', - }, -})); - -// Mock createDepositNavigationDetails -const mockCreateDepositNavigationDetails = jest.fn(() => ['DepositScreen', {}]); -jest.mock('../../Ramp/Deposit/routes/utils', () => ({ - createDepositNavigationDetails: mockCreateDepositNavigationDetails, -})); - -// Mock getDecimalChainId -jest.mock('../../../../util/networks', () => ({ - getDecimalChainId: jest.fn(() => 1), -})); - -// Mock strings -jest.mock('../../../../locales/i18n', () => ({ - strings: jest.fn((key: string) => { - const strings: { [key: string]: string } = { - 'wallet.fund_your_wallet': 'Fund your wallet', - 'wallet.get_ready_for_web3': 'Get your wallet ready to use web3', - 'wallet.add_funds': 'Add funds', - }; - return strings[key] || key; + navigate: jest.fn(), }), })); @@ -69,112 +17,48 @@ describe('BalanceEmptyState', () => { jest.clearAllMocks(); }); - const mockState = { - engine: { - backgroundState: { - NetworkController: { - selectedNetworkClientId: 'mainnet', - networkConfigurations: {}, + const renderComponent = () => + renderWithProvider(, { + state: { + engine: { + backgroundState, }, }, - }, - }; - - it('renders correctly with localized content', () => { - const { getByTestId, getByText } = renderWithProvider( - , - { state: mockState }, - ); - - expect(getByTestId('balance-empty-state')).toBeDefined(); - expect(getByText('Fund your wallet')).toBeDefined(); - expect(getByText('Get your wallet ready to use web3')).toBeDefined(); - expect(getByText('Add funds')).toBeDefined(); - }); - - it('navigates to deposit screen when button is pressed', () => { - const { getByTestId } = renderWithProvider(, { - state: mockState, }); - const actionButton = getByTestId('balance-empty-state-action-button'); - fireEvent.press(actionButton); - - expect(mockCreateDepositNavigationDetails).toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith('DepositScreen', {}); - }); - - it('tracks metrics when button is pressed', () => { - const { getByTestId } = renderWithProvider(, { - state: mockState, - }); - - const actionButton = getByTestId('balance-empty-state-action-button'); - fireEvent.press(actionButton); - - expect(mockTrackEvent).toHaveBeenCalledTimes(2); - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED, - ); - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.RAMPS_BUTTON_CLICKED, - ); + it('renders correctly', () => { + const { getByTestId } = renderComponent(); + expect(getByTestId('balance-empty-state')).toBeDefined(); }); - it('traces deposit experience when button is pressed', () => { - const { getByTestId } = renderWithProvider(, { - state: mockState, - }); - + it('has action button that can be pressed', () => { + const { getByTestId } = renderComponent(); const actionButton = getByTestId('balance-empty-state-action-button'); - fireEvent.press(actionButton); - expect(mockTrace).toHaveBeenCalledWith({ - name: TraceName.LoadDepositExperience, - }); + expect(actionButton).toBeDefined(); + // Test that button press doesn't throw an error + expect(() => fireEvent.press(actionButton)).not.toThrow(); }); it('calls custom onAction when provided', () => { const mockOnAction = jest.fn(); const { getByTestId } = renderWithProvider( - , - { state: mockState }, + , + { + state: { + engine: { + backgroundState, + }, + }, + }, ); const actionButton = getByTestId('balance-empty-state-action-button'); fireEvent.press(actionButton); expect(mockOnAction).toHaveBeenCalledTimes(1); - expect(mockNavigate).not.toHaveBeenCalled(); - }); - - it('has proper test IDs for all elements', () => { - const { getByTestId } = renderWithProvider( - , - { state: mockState }, - ); - - expect(getByTestId('test-component')).toBeTruthy(); - expect(getByTestId('test-component-image')).toBeTruthy(); - expect(getByTestId('test-component-title')).toBeTruthy(); - expect(getByTestId('test-component-subtitle')).toBeTruthy(); - expect(getByTestId('test-component-action-button')).toBeTruthy(); - }); - - it('displays the bank transfer image', () => { - const { getByTestId } = renderWithProvider(, { - state: mockState, - }); - - const image = getByTestId('balance-empty-state-image'); - expect(image).toBeTruthy(); - expect(image.props.source).toBeDefined(); - }); - - it('matches snapshot', () => { - const component = renderWithProvider(, { - state: mockState, - }); - expect(component).toMatchSnapshot(); }); }); From 5636930dc5a9d370e32bebf8f5b8660403c66576 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 17 Oct 2025 09:31:13 -0700 Subject: [PATCH 08/14] chore: extending box props for access to wrapper box component --- .../BalanceEmptyState.test.tsx | 50 ++++++++----------- .../BalanceEmptyState/BalanceEmptyState.tsx | 9 ++-- .../BalanceEmptyState.types.ts | 8 +-- 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index d3c7bcdf5c4b..31a2c0db1c82 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -3,12 +3,14 @@ import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { backgroundState } from '../../../util/test/initial-root-state'; import BalanceEmptyState from './BalanceEmptyState'; +import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; // Mock navigation (component requires it) +const mockNavigate = jest.fn(); jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useNavigation: () => ({ - navigate: jest.fn(), + navigate: mockNavigate, }), })); @@ -17,48 +19,40 @@ describe('BalanceEmptyState', () => { jest.clearAllMocks(); }); - const renderComponent = () => - renderWithProvider(, { - state: { - engine: { - backgroundState, + const renderComponent = (props: Partial = {}) => + renderWithProvider( + , + { + state: { + engine: { + backgroundState, + }, }, }, - }); + ); it('renders correctly', () => { const { getByTestId } = renderComponent(); expect(getByTestId('balance-empty-state')).toBeDefined(); }); + it('passes a twClassName to the Box component', () => { + const { getByTestId } = renderComponent({ twClassName: 'mt-4' }); + expect(getByTestId('balance-empty-state')).toHaveStyle({ + marginTop: 16, // mt-4 + }); + }); + it('has action button that can be pressed', () => { const { getByTestId } = renderComponent(); const actionButton = getByTestId('balance-empty-state-action-button'); expect(actionButton).toBeDefined(); - // Test that button press doesn't throw an error - expect(() => fireEvent.press(actionButton)).not.toThrow(); - }); - it('calls custom onAction when provided', () => { - const mockOnAction = jest.fn(); - const { getByTestId } = renderWithProvider( - , - { - state: { - engine: { - backgroundState, - }, - }, - }, - ); - - const actionButton = getByTestId('balance-empty-state-action-button'); + // Press the button fireEvent.press(actionButton); - expect(mockOnAction).toHaveBeenCalledTimes(1); + // Verify that navigation was triggered + expect(mockNavigate).toHaveBeenCalled(); }); }); diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index 8d7668e20089..659659a5ba2a 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -29,15 +29,15 @@ import bankTransferImage from '../../../images/bank-transfer.png'; * with an illustration, title, subtitle, and action button that navigates to deposit flow. */ const BalanceEmptyState: React.FC = ({ - onAction, testID = 'balance-empty-state', + ...props }) => { const tw = useTailwind(); const chainId = useSelector(selectChainId); const navigation = useNavigation(); const { trackEvent, createEventBuilder } = useMetrics(); - const goToDeposit = () => { + const handleAction = () => { navigation.navigate(...createDepositNavigationDetails()); trackEvent( @@ -62,11 +62,8 @@ const BalanceEmptyState: React.FC = ({ }); }; - const handleAction = onAction || goToDeposit; - return ( = ({ backgroundColor={BoxBackgroundColor.BackgroundSection} gap={5} testID={testID} + {...props} + twClassName={`rounded-2xl ${props.twClassName}`} > void; +export interface BalanceEmptyStateProps extends BoxProps { /** * Test ID for component testing */ From 418c95484c5a9a21072e8e1976eb9c4e3a35970e Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 17 Oct 2025 09:46:54 -0700 Subject: [PATCH 09/14] chore: updating types --- app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts index 6871cd7a044e..029ee44f6bad 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts @@ -2,9 +2,10 @@ import { BoxProps } from '@metamask/design-system-react-native'; /** * Props for the BalanceEmptyState smart component */ -export interface BalanceEmptyStateProps extends BoxProps { +export interface BalanceEmptyStateProps extends Omit { /** * Test ID for component testing + * @default 'balance-empty-state' */ testID?: string; } From 04ccd0e7f1b29c6fa450508d67dee2eeeba405df Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 17 Oct 2025 11:19:49 -0700 Subject: [PATCH 10/14] chore: update to twClassName condition --- app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index 659659a5ba2a..e31c34082499 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -73,7 +73,7 @@ const BalanceEmptyState: React.FC = ({ gap={5} testID={testID} {...props} - twClassName={`rounded-2xl ${props.twClassName}`} + twClassName={`rounded-2xl ${props?.twClassName ?? ''}`} > Date: Fri, 17 Oct 2025 14:44:57 -0700 Subject: [PATCH 11/14] chore: reverting button hero text variant update --- .../components-temp/Buttons/ButtonHero/ButtonHero.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx b/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx index a6137ad7db14..3b58bce9c3ec 100644 --- a/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx +++ b/app/component-library/components-temp/Buttons/ButtonHero/ButtonHero.tsx @@ -3,7 +3,6 @@ import React, { useCallback } from 'react'; import { ButtonBase, ButtonBaseProps, - TextVariant, } from '@metamask/design-system-react-native'; import { useTailwind, @@ -44,9 +43,6 @@ const ButtonHeroInner = ({ isLoading={isLoading} loadingText={loadingText} textClassName={textClassName || getTextClassName} - textProps={{ - variant: TextVariant.BodyLg, - }} {...props} style={getStyle} > From b665d2c61f244b5036017de4929086f4c9ed3b63 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Fri, 17 Oct 2025 16:34:36 -0700 Subject: [PATCH 12/14] chore: initial commit --- .../Balance/AccountGroupBalance.styles.ts | 5 +- .../Balance/AccountGroupBalance.test.tsx | 9 ++ .../Balance/AccountGroupBalance.tsx | 21 ++- .../TokenList/PortfolioBalance/index.tsx | 18 ++- .../__snapshots__/index.test.tsx.snap | 67 --------- .../TokenList/TokenListFooter/index.test.tsx | 135 ------------------ .../TokenList/TokenListFooter/index.tsx | 86 ----------- app/components/UI/Tokens/TokenList/index.tsx | 2 - app/components/UI/Tokens/styles.ts | 3 - locales/languages/de.json | 1 - locales/languages/el.json | 1 - locales/languages/en.json | 1 - locales/languages/es.json | 1 - locales/languages/fr.json | 1 - locales/languages/hi.json | 1 - locales/languages/id.json | 1 - locales/languages/ja.json | 1 - locales/languages/ko.json | 1 - locales/languages/pt.json | 1 - locales/languages/ru.json | 1 - locales/languages/tl.json | 1 - locales/languages/tr.json | 1 - locales/languages/vi.json | 1 - locales/languages/zh.json | 1 - 24 files changed, 38 insertions(+), 323 deletions(-) delete mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/__snapshots__/index.test.tsx.snap delete mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx delete mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx diff --git a/app/components/UI/Assets/components/Balance/AccountGroupBalance.styles.ts b/app/components/UI/Assets/components/Balance/AccountGroupBalance.styles.ts index d45c874f0578..4c0ffcc1f772 100644 --- a/app/components/UI/Assets/components/Balance/AccountGroupBalance.styles.ts +++ b/app/components/UI/Assets/components/Balance/AccountGroupBalance.styles.ts @@ -2,13 +2,10 @@ import { StyleSheet } from 'react-native'; const createStyles = () => StyleSheet.create({ accountGroupBalance: { - flexDirection: 'row', - alignItems: 'center', marginHorizontal: 16, - justifyContent: 'space-between', - paddingTop: 24, }, balanceContainer: { + paddingTop: 24, flexDirection: 'row', alignItems: 'center', }, diff --git a/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx b/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx index 343eaef5f867..21f5119a5650 100644 --- a/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx +++ b/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx @@ -52,4 +52,13 @@ describe('AccountGroupBalance', () => { const el = getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT); expect(el).toBeTruthy(); }); + + it('renders balance empty state when balance is zero', () => { + const { getByTestId } = renderWithProvider(, { + state: testState, + }); + + const el = getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT); + expect(el).toBeTruthy(); + }); }); diff --git a/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx b/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx index eaec7978d5fa..5284b9f85c15 100644 --- a/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx +++ b/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx @@ -16,6 +16,7 @@ import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/W import { Skeleton } from '../../../../../component-library/components/Skeleton'; import { useFormatters } from '../../../../hooks/useFormatters'; import AccountGroupBalanceChange from '../../components/BalanceChange/AccountGroupBalanceChange'; +import BalanceEmptyState from '../../../BalanceEmptyState'; const AccountGroupBalance = () => { const { PreferencesController } = Engine.context; @@ -38,10 +39,23 @@ const AccountGroupBalance = () => { const userCurrency = groupBalance?.userCurrency ?? ''; const displayBalance = formatCurrency(totalBalance, userCurrency); + // Check if balance is zero (empty state) - only check when we have balance data + const hasZeroBalance = + groupBalance && groupBalance.totalBalanceInUserCurrency === 0; + return ( - {groupBalance ? ( + {!groupBalance ? ( + + + + + ) : hasZeroBalance ? ( + <> + + + ) : ( togglePrivacy(!privacyMode)} testID="balance-container" @@ -66,11 +80,6 @@ const AccountGroupBalance = () => { /> )} - ) : ( - - - - )} diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx index 53366bdf0ca0..6692357252c2 100644 --- a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx @@ -15,6 +15,7 @@ import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMulti import Loader from '../../../../../component-library/components-temp/Loader/Loader'; import NonEvmAggregatedPercentage from '../../../../../component-library/components-temp/Price/AggregatedPercentage/NonEvmAggregatedPercentage'; import { selectIsEvmNetworkSelected } from '../../../../../selectors/multichainNetworkController'; +import BalanceEmptyState from '../../../BalanceEmptyState'; export const PortfolioBalance = React.memo(() => { const { PreferencesController } = Engine.context; @@ -57,10 +58,21 @@ export const PortfolioBalance = React.memo(() => { [PreferencesController], ); + // Check if balance is zero (empty state) - only check when we have balance data + const hasZeroBalance = + selectedAccountMultichainBalance && + selectedAccountMultichainBalance.totalFiatBalance === 0; + return ( - {selectedAccountMultichainBalance?.displayBalance ? ( + {!selectedAccountMultichainBalance ? ( + + + + ) : hasZeroBalance ? ( + + ) : ( toggleIsBalanceAndAssetsHidden(!privacyMode)} testID="balance-container" @@ -78,10 +90,6 @@ export const PortfolioBalance = React.memo(() => { {renderAggregatedPercentage()} - ) : ( - - - )} diff --git a/app/components/UI/Tokens/TokenList/TokenListFooter/__snapshots__/index.test.tsx.snap b/app/components/UI/Tokens/TokenList/TokenListFooter/__snapshots__/index.test.tsx.snap deleted file mode 100644 index c11dbca7a630..000000000000 --- a/app/components/UI/Tokens/TokenList/TokenListFooter/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TokenListFooter renders correctly 1`] = ` - - - Fund your wallet to get started in web3 - - - - Add funds - - - -`; diff --git a/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx b/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx deleted file mode 100644 index addb199517b9..000000000000 --- a/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react-native'; -import { TokenListFooter } from '.'; -import configureMockStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import { strings } from '../../../../../../locales/i18n'; -import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork'; -import { MetaMetricsEvents } from '../../../../../components/hooks/useMetrics'; -import { mockNetworkState } from '../../../../../util/test/network'; -import { backgroundState } from '../../../../../util/test/initial-root-state'; -import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMultichainBalances'; - -jest.mock('../../../Ramp/Aggregator/hooks/useRampNetwork', () => jest.fn()); -jest.mock('../../../../hooks/useMultichainBalances', () => ({ - useSelectedAccountMultichainBalances: jest.fn(), -})); -jest.mock('../../../../../core/Engine', () => ({ - context: { - PreferencesController: { - setPrivacyMode: jest.fn(), - }, - }, - getTotalEvmFiatAccountBalance: jest.fn(() => ({ - ethFiat: 0, - tokenFiat: 0, - tokenFiat1dAgo: 0, - ethFiat1dAgo: 0, - totalNativeTokenBalance: '0', - ticker: 'ETH', - })), -})); -jest.mock('../../../../../components/hooks/useMetrics', () => ({ - MetaMetricsEvents: { - CARD_ADD_FUNDS_DEPOSIT_CLICKED: 'CARD_ADD_FUNDS_DEPOSIT_CLICKED', - RAMPS_BUTTON_CLICKED: 'RAMPS_BUTTON_CLICKED', - }, - useMetrics: jest.fn(() => ({ - trackEvent: jest.fn(), - createEventBuilder: jest.fn(() => ({ - addProperties: jest.fn().mockReturnThis(), - build: jest.fn(), - })), - })), -})); - -jest.mock('../../../../../util/trace', () => ({ - trace: jest.fn(), - TraceName: { - LoadDepositExperience: 'Load Deposit Experience', - }, -})); - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useNavigation: () => ({ - navigate: jest.fn(), - }), - }; -}); - -const mockStore = configureMockStore(); - -const initialState = { - engine: { - backgroundState: { - ...backgroundState, - NetworkController: { - ...mockNetworkState({ - chainId: '0x1', - id: 'mainnet', - nickname: 'Ethereum Mainnet', - ticker: 'ETH', - }), - }, - }, - }, - settings: { - primaryCurrency: 'usd', - }, -}; - -const store = mockStore(initialState); - -describe('TokenListFooter', () => { - beforeEach(() => { - (useRampNetwork as jest.Mock).mockReturnValue([true, true]); - (useSelectedAccountMultichainBalances as jest.Mock).mockReturnValue({ - selectedAccountMultichainBalance: { - totalFiatBalance: 0, - }, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const renderComponent = (initialStore = store) => - render( - - - , - ); - - it('renders correctly', () => { - const { toJSON } = renderComponent(); - expect(toJSON()).toMatchSnapshot(); - }); - - it('does not render the deposit button when total account balance is greater than zero', () => { - (useSelectedAccountMultichainBalances as jest.Mock).mockReturnValue({ - selectedAccountMultichainBalance: { - totalFiatBalance: 100, - }, - }); - const { queryByText } = renderComponent(); - expect(queryByText(strings('wallet.add_funds'))).toBeNull(); - expect( - queryByText(strings('wallet.fund_your_wallet_to_get_started')), - ).toBeNull(); - }); - - it('tracks the CARD_ADD_FUNDS_DEPOSIT_CLICKED and RAMPS_BUTTON_CLICKED events when the deposit button is pressed', async () => { - const { getByText } = renderComponent(); - - fireEvent.press(getByText(strings('wallet.add_funds'))); - - await waitFor(() => { - expect(MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED).toBeDefined(); - expect(MetaMetricsEvents.RAMPS_BUTTON_CLICKED).toBeDefined(); - }); - }); -}); diff --git a/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx b/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx deleted file mode 100644 index 6c936386bf47..000000000000 --- a/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import createStyles from '../../styles'; -import { useTheme } from '../../../../../util/theme'; -import { View } from 'react-native'; -import TextComponent, { - TextVariant, -} from '../../../../../component-library/components/Texts/Text'; -import { strings } from '../../../../../../locales/i18n'; -import { useSelector } from 'react-redux'; -import Button, { - ButtonVariants, - ButtonSize, - ButtonWidthTypes, -} from '../../../../../component-library/components/Buttons/Button'; -import { useNavigation } from '@react-navigation/native'; -import { - MetaMetricsEvents, - useMetrics, -} from '../../../../../components/hooks/useMetrics'; -import { getDecimalChainId } from '../../../../../util/networks'; -import { selectChainId } from '../../../../../selectors/networkController'; -import { trace, TraceName } from '../../../../../util/trace'; -import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMultichainBalances'; -import { createDepositNavigationDetails } from '../../../Ramp/Deposit/routes/utils'; - -export const TokenListFooter = () => { - const chainId = useSelector(selectChainId); - const navigation = useNavigation(); - const { colors } = useTheme(); - const styles = createStyles(colors); - const { trackEvent, createEventBuilder } = useMetrics(); - const { selectedAccountMultichainBalance } = - useSelectedAccountMultichainBalances(); - - const shouldShowFooter = - selectedAccountMultichainBalance?.totalFiatBalance === 0; - - const goToDeposit = () => { - navigation.navigate(...createDepositNavigationDetails()); - - trackEvent( - createEventBuilder( - MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED, - ).build(), - ); - - trackEvent( - createEventBuilder(MetaMetricsEvents.RAMPS_BUTTON_CLICKED) - .addProperties({ - text: 'Deposit', - location: 'TokenListFooter', - chain_id_destination: getDecimalChainId(chainId), - ramp_type: 'DEPOSIT', - }) - .build(), - ); - - trace({ - name: TraceName.LoadDepositExperience, - }); - }; - - return ( - <> - {/* render buy button */} - {shouldShowFooter && ( - - - {strings('wallet.fund_your_wallet_to_get_started')} - -