Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@ jest.mock('../../../../../selectors/assets/balances', () => ({
selectBalanceBySelectedAccountGroup: jest.fn(() => null),
// This one is a factory: selectBalanceChangeBySelectedAccountGroup(period) -> (state) => value
selectBalanceChangeBySelectedAccountGroup: jest.fn(() => () => null),
// This selector is used to display the BalanceEmptyState
selectAccountGroupBalanceForEmptyState: jest.fn(() => null),
}));

// Mock homepage redesign feature flag for BalanceEmptyState
jest.mock('../../../../../selectors/featureFlagController/homepage', () => ({
selectHomepageRedesignV1Enabled: jest.fn(() => true),
}));

// This selector is used to determine if the current network is a testnet for BalanceEmptyState display logic
jest.mock('../../../../../selectors/networkController', () => ({
...jest.requireActual('../../../../../selectors/networkController'),
selectEvmChainId: jest.fn(() => '0x1'), // Ethereum mainnet (not a testnet)
selectChainId: jest.fn(() => '0x1'), // BalanceEmptyState also needs this
}));

// Mock navigation hooks used by BalanceEmptyState
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({
navigate: jest.fn(),
goBack: jest.fn(),
reset: jest.fn(),
}),
}));

// Mock metrics hook used by BalanceEmptyState
jest.mock('../../../../../components/hooks/useMetrics', () => ({
useMetrics: () => ({
trackEvent: jest.fn(),
createEventBuilder: jest.fn(() => ({ record: jest.fn() })),
}),
}));
Comment on lines +12 to 44
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding necessary mocks for BalanceEmptyState component


const testState = {
Expand Down Expand Up @@ -52,4 +84,36 @@ describe('AccountGroupBalance', () => {
const el = getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT);
expect(el).toBeTruthy();
});

it('renders empty state when account group balance is zero', () => {
const {
selectAccountGroupBalanceForEmptyState,
selectBalanceBySelectedAccountGroup,
} = jest.requireMock('../../../../../selectors/assets/balances');

// Mock the regular balance selector to return data (prevents skeleton loader)
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
totalBalanceInUserCurrency: 100, // Some non-zero amount for current network
userCurrency: 'usd',
}),
);

// Mock the empty state selector to return zero balance across all mainnet networks
(selectAccountGroupBalanceForEmptyState as jest.Mock).mockImplementation(
() => ({
totalBalanceInUserCurrency: 0, // Zero across all mainnet networks
userCurrency: 'usd',
}),
);

const { getByTestId } = renderWithProvider(<AccountGroupBalance />, {
state: testState,
});

const el = getByTestId(
WalletViewSelectorsIDs.BALANCE_EMPTY_STATE_CONTAINER,
);
expect(el).toBeOnTheScreen();
});
Comment on lines +88 to +118
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding test to check for balance emtpy state

});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { selectPrivacyMode } from '../../../../../selectors/preferencesControlle
import {
selectBalanceBySelectedAccountGroup,
selectBalanceChangeBySelectedAccountGroup,
selectAccountGroupBalanceForEmptyState,
} from '../../../../../selectors/assets/balances';
import { selectHomepageRedesignV1Enabled } from '../../../../../selectors/featureFlagController/homepage';
import { selectEvmChainId } from '../../../../../selectors/networkController';
import { TEST_NETWORK_IDS } from '../../../../../constants/network';
Comment on lines +10 to +14
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing new selector to calculate account group balance across all main nets, the feature flag for the new homepage redesign which includes full page scroll, the selector for current evm network and test networks ids for empty state display logic

import SensitiveText, {
SensitiveTextLength,
} from '../../../../../component-library/components/Texts/SensitiveText';
Expand All @@ -16,16 +20,24 @@ 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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing BalanceEmptyState ui component


const AccountGroupBalance = () => {
const { PreferencesController } = Engine.context;
const styles = createStyles();
const { formatCurrency } = useFormatters();
const privacyMode = useSelector(selectPrivacyMode);
const groupBalance = useSelector(selectBalanceBySelectedAccountGroup);
const accountGroupBalance = useSelector(
selectAccountGroupBalanceForEmptyState,
);
Comment on lines +31 to +33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting account group balance across all chains using new selector

const balanceChange1d = useSelector(
selectBalanceChangeBySelectedAccountGroup('1d'),
);
const isHomepageRedesignV1Enabled = useSelector(
selectHomepageRedesignV1Enabled,
);
Comment on lines +37 to +39
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using new feature flag to conditionally render the balance empty state

const selectedChainId = useSelector(selectEvmChainId);

const togglePrivacy = useCallback(
(value: boolean) => {
Expand All @@ -38,10 +50,30 @@ const AccountGroupBalance = () => {
const userCurrency = groupBalance?.userCurrency ?? '';
const displayBalance = formatCurrency(totalBalance, userCurrency);

// Check if account group balance (across all mainnet networks) is zero for empty state
const hasZeroAccountGroupBalance =
accountGroupBalance && accountGroupBalance.totalBalanceInUserCurrency === 0;

// Check if current network is a testnet
const isCurrentNetworkTestnet = TEST_NETWORK_IDS.includes(selectedChainId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Bug

The testnet detection logic in AccountGroupBalance uses EVM-specific selectors (selectEvmChainId and TEST_NETWORK_IDS). This incorrectly identifies non-EVM testnets (e.g., Solana) as mainnet, causing the empty state to display instead of the numerical balance ($0.00).

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently only show EVM test networks but this is a good point. If we were to add solana test net we may see the empty state. Do we have a non EVM test network constant that we can use here. Do you know @salimtb?


// Show empty state on accounts with an aggregated mainnet balance of zero
const shouldShowEmptyState =
hasZeroAccountGroupBalance &&
isHomepageRedesignV1Enabled &&
!isCurrentNetworkTestnet;
Comment on lines +53 to +64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic for displaying Balance empty state.

  • Show if the account group has a zero balance across all main nets and isHomepageRedesignV1Enabled feature flag is enabled
  • Do not show if it's a test network regardless of balance - show $0.00


return (
<View style={styles.accountGroupBalance}>
<View>
{groupBalance ? (
{!groupBalance ? (
<View style={styles.skeletonContainer}>
<Skeleton width={100} height={40} />
<Skeleton width={100} height={20} />
</View>
) : shouldShowEmptyState ? (
<BalanceEmptyState testID="account-group-balance-empty-state" />
) : (
<TouchableOpacity
onPress={() => togglePrivacy(!privacyMode)}
testID="balance-container"
Expand All @@ -66,11 +98,6 @@ const AccountGroupBalance = () => {
/>
)}
</TouchableOpacity>
) : (
<View style={styles.skeletonContainer}>
<Skeleton width={100} height={40} />
<Skeleton width={100} height={20} />
</View>
)}
</View>
</View>
Expand Down
113 changes: 113 additions & 0 deletions app/selectors/assets/balances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import {
selectBalancePercentChangeByAccountGroup,
selectBalanceBySelectedAccountGroup,
selectBalanceChangeBySelectedAccountGroup,
selectAccountGroupBalanceForEmptyState,
} from './balances';

// Enhanced state factory with realistic data
Expand Down Expand Up @@ -484,4 +485,116 @@ describe('assets balance and balance change selectors (mobile)', () => {
expect(selector(state)).toBeNull();
});
});

describe('selectAccountGroupBalanceForEmptyState', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding tests for new selector

it('excludes testnet chains and includes only mainnet chains in balance calculation', () => {
const mockCalculateBalanceForAllWallets = jest.requireMock(
'@metamask/assets-controllers',
).calculateBalanceForAllWallets;

const state = makeState({
engine: {
backgroundState: {
...makeState().engine.backgroundState,
NetworkController: {
networkConfigurationsByChainId: {
// Mainnet networks - should be included
'0x1': { chainId: '0x1', name: 'Ethereum Mainnet' },
'0x89': { chainId: '0x89', name: 'Polygon Mainnet' },
// Testnet networks - should be excluded
'0xaa36a7': { chainId: '0xaa36a7', name: 'Sepolia' },
'0x5': { chainId: '0x5', name: 'Goerli' },
},
},
MultichainNetworkController: {
multichainNetworkConfigurationsByChainId: {
// Mainnet networks - should be included
'eip155:1': { chainId: '0x1', name: 'Ethereum Mainnet' },
'eip155:137': { chainId: '0x89', name: 'Polygon Mainnet' },
// Testnet networks - should be excluded
'eip155:11155111': { chainId: '0xaa36a7', name: 'Sepolia' },
'eip155:5': { chainId: '0x5', name: 'Goerli' },
// Non-EVM testnet - should be excluded
'solana:103': { chainId: 'solana:103', name: 'Solana Testnet' },
},
},
},
},
}) as unknown as RootState;

// Clear previous calls
mockCalculateBalanceForAllWallets.mockClear();

selectAccountGroupBalanceForEmptyState(state);

// Verify calculateBalanceForAllWallets was called with proper enabledNetworkMap
expect(mockCalculateBalanceForAllWallets).toHaveBeenCalledTimes(1);
const enabledNetworkMap =
mockCalculateBalanceForAllWallets.mock.calls[0][8];

// Should include mainnet networks only
expect(enabledNetworkMap).toEqual({
eip155: {
'0x1': true, // Ethereum mainnet
'0x89': true, // Polygon mainnet
},
// No testnet networks should be present
});
});

it('returns null when no account group is selected', () => {
const state = makeState({
engine: {
backgroundState: {
...makeState().engine.backgroundState,
NetworkController: {
networkConfigurationsByChainId: {
'0x1': { chainId: '0x1', name: 'Ethereum Mainnet' },
},
},
MultichainNetworkController: {
multichainNetworkConfigurationsByChainId: {
'eip155:1': { chainId: '0x1', name: 'Ethereum Mainnet' },
},
},
},
},
}) as unknown as RootState;
state.engine.backgroundState.AccountTreeController.accountTree.selectedAccountGroup =
'';

const result = selectAccountGroupBalanceForEmptyState(state);
expect(result).toBeNull();
});

it('returns zeroed fallback when selected group does not exist', () => {
const state = makeState({
engine: {
backgroundState: {
...makeState().engine.backgroundState,
NetworkController: {
networkConfigurationsByChainId: {
'0x1': { chainId: '0x1', name: 'Ethereum Mainnet' },
},
},
MultichainNetworkController: {
multichainNetworkConfigurationsByChainId: {
'eip155:1': { chainId: '0x1', name: 'Ethereum Mainnet' },
},
},
},
},
}) as unknown as RootState;
state.engine.backgroundState.AccountTreeController.accountTree.selectedAccountGroup =
'keyring:wallet-1/group-999';

const result = selectAccountGroupBalanceForEmptyState(state);
expect(result).toEqual({
walletId: 'keyring:wallet-1',
groupId: 'keyring:wallet-1/group-999',
totalBalanceInUserCurrency: 0,
userCurrency: 'usd',
});
});
});
});
Loading
Loading