diff --git a/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx b/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx index 343eaef5f867..c69221620230 100644 --- a/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx +++ b/app/components/UI/Assets/components/Balance/AccountGroupBalance.test.tsx @@ -45,11 +45,40 @@ describe('AccountGroupBalance', () => { }), ); - const { getByTestId } = renderWithProvider(, { - state: testState, - }); + const { getByTestId, queryByTestId } = renderWithProvider( + , + { + state: testState, + }, + ); + + // Should render balance text, not empty state + expect(getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeTruthy(); + expect(queryByTestId('account-group-balance-empty-state')).toBeNull(); + }); - const el = getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT); - expect(el).toBeTruthy(); + it('renders balance empty state when balance is zero', () => { + const { selectBalanceBySelectedAccountGroup } = jest.requireMock( + '../../../../../selectors/assets/balances', + ); + (selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation( + () => ({ + walletId: 'wallet-1', + groupId: 'wallet-1/group-1', + totalBalanceInUserCurrency: 0, // Zero balance + userCurrency: 'usd', + }), + ); + + const { getByTestId, queryByTestId } = renderWithProvider( + , + { + state: testState, + }, + ); + + // Should render BalanceEmptyState instead of balance text + expect(getByTestId('account-group-balance-empty-state')).toBeDefined(); + expect(queryByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeNull(); }); }); diff --git a/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx b/app/components/UI/Assets/components/Balance/AccountGroupBalance.tsx index eaec7978d5fa..3c2362d59d8f 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.test.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx index 92ef10f79df5..7a7072facddf 100644 --- a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx @@ -5,21 +5,33 @@ import { backgroundState } from '../../../../../util/test/initial-root-state'; import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/WalletView.selectors'; import { PortfolioBalance } from '.'; import Engine from '../../../../../core/Engine'; +import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMultichainBalances'; const { PreferencesController } = Engine.context; // Mock the useMultichainBalances hook const mockSelectedAccountMultichainBalance = { displayBalance: '$123.45', - totalFiatBalance: '123.45', + displayCurrency: 'USD', + totalFiatBalance: 123.45, + totalNativeTokenBalance: '0.1', + nativeTokenUnit: 'ETH', shouldShowAggregatedPercentage: true, + isPortfolioVieEnabled: false, + aggregatedBalance: { + ethFiat: 123.45, + tokenFiat: 0, + tokenFiat1dAgo: 0, + ethFiat1dAgo: 100.0, + }, + isLoadingAccount: false, tokenFiatBalancesCrossChains: [], }; jest.mock('../../../../hooks/useMultichainBalances', () => ({ - useSelectedAccountMultichainBalances: () => ({ + useSelectedAccountMultichainBalances: jest.fn(() => ({ selectedAccountMultichainBalance: mockSelectedAccountMultichainBalance, - }), + })), })); jest.mock('../../../../../core/Engine', () => ({ @@ -47,6 +59,30 @@ jest.mock('../../../../../core/Engine', () => ({ }, })); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + }), + }; +}); + +jest.mock('../../../../../components/hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: jest.fn(), + createEventBuilder: jest.fn(() => ({ + addProperties: jest.fn().mockReturnThis(), + build: jest.fn(), + })), + }), + MetaMetricsEvents: { + CARD_ADD_FUNDS_DEPOSIT_CLICKED: 'CARD_ADD_FUNDS_DEPOSIT_CLICKED', + RAMPS_BUTTON_CLICKED: 'RAMPS_BUTTON_CLICKED', + }, +})); + const initialState = { engine: { backgroundState: { @@ -146,6 +182,15 @@ const renderPortfolioBalance = (state: any = {}) => renderWithProvider(, { state }); describe('PortfolioBalance', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset to default mock before each test + const mockedHook = jest.mocked(useSelectedAccountMultichainBalances); + mockedHook.mockReturnValue({ + selectedAccountMultichainBalance: mockSelectedAccountMultichainBalance, + }); + }); + it('fiat balance must be defined', () => { const { getByTestId } = renderPortfolioBalance(initialState); expect( @@ -207,4 +252,51 @@ describe('PortfolioBalance', () => { expect(PreferencesController.setPrivacyMode).toHaveBeenCalledWith(true); }); + + it('displays BalanceEmptyState when balance is zero', () => { + // Mock zero balance + const mockSelectedAccountMultichainBalanceZero = { + displayBalance: '$0.00', + displayCurrency: 'USD', + totalFiatBalance: 0, + totalNativeTokenBalance: '0', + nativeTokenUnit: 'ETH', + shouldShowAggregatedPercentage: false, + isPortfolioVieEnabled: false, + aggregatedBalance: { + ethFiat: 123.45, + tokenFiat: 0, + tokenFiat1dAgo: 0, + ethFiat1dAgo: 100.0, + }, + isLoadingAccount: false, + tokenFiatBalancesCrossChains: [], + }; + + const mockedHook = jest.mocked(useSelectedAccountMultichainBalances); + mockedHook.mockReturnValue({ + selectedAccountMultichainBalance: + mockSelectedAccountMultichainBalanceZero, + }); + + const { getByTestId, queryByTestId } = renderPortfolioBalance(initialState); + + // Should render BalanceEmptyState instead of balance text + expect(getByTestId('portfolio-balance-empty-state')).toBeDefined(); + expect(queryByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeNull(); + }); + + it('displays loader when balance is not available', () => { + // Mock undefined balance + const mockedHook = jest.mocked(useSelectedAccountMultichainBalances); + mockedHook.mockReturnValue({ + selectedAccountMultichainBalance: undefined, + }); + + const { queryByTestId } = renderPortfolioBalance(initialState); + + // Should not render balance text or empty state + expect(queryByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeNull(); + expect(queryByTestId('portfolio-balance-empty-state')).toBeNull(); + }); }); diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx index 53366bdf0ca0..8343ef355a5a 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/index.tsx b/app/components/UI/Tokens/TokenList/index.tsx index b5c14752d91a..d9f3d0532bb9 100644 --- a/app/components/UI/Tokens/TokenList/index.tsx +++ b/app/components/UI/Tokens/TokenList/index.tsx @@ -13,7 +13,6 @@ import TextComponent, { } from '../../../../component-library/components/Texts/Text'; import { TokenI } from '../types'; import { strings } from '../../../../../locales/i18n'; -import { TokenListFooter } from './TokenListFooter'; import { TokenListItem, TokenListItemBip44 } from './TokenListItem'; import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors'; import { useNavigation } from '@react-navigation/native'; @@ -107,7 +106,6 @@ const TokenListComponent = ({ return `${item.address}-${item.chainId}-${staked}-${idx}`; }} decelerationRate="fast" - ListFooterComponent={} refreshControl={