Skip to content

Commit 3cef363

Browse files
authored
feat: hide zero-balance tokens on homepage and migrate setting for all users (#28527)
- Filter zero-balance tokens in selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance when hideZeroBalanceTokens is true, at selector level for memoized efficiency - Add migration 132 to set hideZeroBalanceTokens=true for all existing users <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: hide zero-balance tokens on homepage and migrate setting for all users ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> <img width="437" height="894" alt="Screenshot 2026-04-08 at 14 35 17" src="https://github.com/user-attachments/assets/5b9c0cbf-7b27-4255-9268-082a46aee42a" /> ### **After** <!-- [screenshots/recordings] --> <img width="434" height="902" alt="Screenshot 2026-04-08 at 14 36 59" src="https://github.com/user-attachments/assets/ceb965ab-217f-4fa7-b159-d8c4308996a3" /> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the default `hideZeroBalanceTokens` setting to enabled and applies it to the homepage “sorted by balance” asset selector, which could affect which assets users see and rely on correct balance parsing across asset types. > > **Overview** > **Homepage token list now respects the `hideZeroBalanceTokens` setting.** `selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance` pulls `selectHideZeroBalanceTokens` and filters out assets with a zero `balance` (while still excluding Tron special assets) before merging/sorting. > > **Default behavior changes for all users.** The settings reducer’s initial state flips `hideZeroBalanceTokens` from `false` to `true`, and selector unit tests are updated/expanded to cover both enabled/disabled filtering and mixed zero/non-zero balances. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f40d601. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 413327c commit 3cef363

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

app/reducers/settings/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const initialState = {
66
primaryCurrency: 'ETH',
77
lockTime: -1, // Disabled by default,
88
avatarAccountType: AvatarAccountType.Maskicon,
9-
hideZeroBalanceTokens: false,
9+
hideZeroBalanceTokens: true,
1010
basicFunctionalityEnabled: true,
1111
deepLinkModalDisabled: false,
1212
// Perps chart preferences

app/selectors/assets/assets-list.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ const mockState = ({
362362
},
363363
},
364364
},
365+
settings: {
366+
hideZeroBalanceTokens: false,
367+
},
365368
}) as unknown as RootState;
366369

367370
describe('selectAssetsBySelectedAccountGroup', () => {
@@ -1062,6 +1065,126 @@ describe('selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance', () => {
10621065
expect(byCaip).toEqual(byHex);
10631066
});
10641067

1068+
it('includes all tokens when hideZeroBalanceTokens is false', () => {
1069+
const state = {
1070+
...mockState(),
1071+
settings: { hideZeroBalanceTokens: false },
1072+
} as unknown as RootState;
1073+
1074+
const result = selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance(
1075+
state,
1076+
['eip155:1', '0xa'],
1077+
);
1078+
1079+
// All tokens from both chains should be present (all have non-zero balances in mockState)
1080+
const chainIds = [...new Set(result.map((r) => r.chainId))];
1081+
expect(chainIds.sort()).toEqual(['0x1', '0xa']);
1082+
expect(result.length).toBeGreaterThan(0);
1083+
});
1084+
1085+
it('filters out zero-balance tokens when hideZeroBalanceTokens is true', () => {
1086+
const stateWithZeroBalances = {
1087+
...mockState(),
1088+
settings: { hideZeroBalanceTokens: true },
1089+
engine: {
1090+
...mockState().engine,
1091+
backgroundState: {
1092+
...mockState().engine.backgroundState,
1093+
TokenBalancesController: {
1094+
tokenBalances: {
1095+
'0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': {
1096+
'0x1': {
1097+
// stETH has zero balance
1098+
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84': '0x0',
1099+
// DAI has non-zero balance
1100+
'0x6B175474E89094C44Da98b954EedeAC495271d0F':
1101+
'0xAD78EBC5AC6200000',
1102+
},
1103+
'0xa': {
1104+
// USDC has non-zero balance
1105+
'0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85': '0x3B9ACA00',
1106+
},
1107+
},
1108+
},
1109+
},
1110+
// Native ETH on 0x1 has zero balance
1111+
AccountTrackerController: {
1112+
accountsByChainId: {
1113+
'0x1': {
1114+
'0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': {
1115+
balance: '0x0',
1116+
stakedBalance: '0x0',
1117+
},
1118+
},
1119+
'0xa': {
1120+
'0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': {
1121+
balance: '0xDE0B6B3A7640000',
1122+
},
1123+
},
1124+
},
1125+
},
1126+
},
1127+
},
1128+
} as unknown as RootState;
1129+
1130+
const result = selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance(
1131+
stateWithZeroBalances,
1132+
['eip155:1', '0xa'],
1133+
);
1134+
1135+
const addresses = result.map((r) => r.address);
1136+
1137+
// stETH (zero balance) should be filtered out
1138+
expect(addresses).not.toContain(
1139+
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
1140+
);
1141+
// ETH on 0x1 (zero balance) should be filtered out
1142+
expect(
1143+
result.find(
1144+
(r) =>
1145+
r.address === '0x0000000000000000000000000000000000000000' &&
1146+
r.chainId === '0x1' &&
1147+
!r.isStaked,
1148+
),
1149+
).toBeUndefined();
1150+
// DAI (non-zero balance) should remain
1151+
expect(addresses).toContain('0x6B175474E89094C44Da98b954EedeAC495271d0F');
1152+
// USDC (non-zero balance) should remain
1153+
expect(addresses).toContain('0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85');
1154+
// ETH on 0xa (non-zero balance) should remain
1155+
expect(
1156+
result.find(
1157+
(r) =>
1158+
r.address === '0x0000000000000000000000000000000000000000' &&
1159+
r.chainId === '0xa',
1160+
),
1161+
).toBeDefined();
1162+
});
1163+
1164+
it('keeps all tokens when hideZeroBalanceTokens is true but all have non-zero balances', () => {
1165+
const state = {
1166+
...mockState(),
1167+
settings: { hideZeroBalanceTokens: true },
1168+
} as unknown as RootState;
1169+
1170+
const withFilter =
1171+
selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance(state, [
1172+
'eip155:1',
1173+
'0xa',
1174+
]);
1175+
const withoutFilter =
1176+
selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance(
1177+
{
1178+
...mockState(),
1179+
settings: { hideZeroBalanceTokens: false },
1180+
} as unknown as RootState,
1181+
['eip155:1', '0xa'],
1182+
);
1183+
1184+
// All tokens in mockState have non-zero balances so counts should match
1185+
expect(withFilter.length).toBe(withoutFilter.length);
1186+
});
1187+
10651188
it('always sorts by balance and ignores PreferencesController tokenSortConfig', () => {
10661189
const stateWithNameSort = {
10671190
...mockState(),

app/selectors/assets/assets-list.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { formatWithThreshold } from '../../util/assets';
2020
import { selectEvmNetworkConfigurationsByChainId } from '../networkController';
2121
import { selectEnabledNetworksByNamespace } from '../networkEnablementController';
2222
import { selectTokenSortConfig } from '../preferencesController';
23+
import { selectHideZeroBalanceTokens } from '../settings';
2324
import { createDeepEqualSelector } from '../util';
2425
import { fromWei, hexToBN, weiToFiatNumber } from '../../util/number';
2526
import {
@@ -427,15 +428,19 @@ export const selectSortedAssetsBySelectedAccountGroupForChainIdsByBalance =
427428
selectAssetsBySelectedAccountGroup,
428429
(_state: RootState, chainIds: string[]) => chainIds,
429430
selectStakedAssets,
431+
selectHideZeroBalanceTokens,
430432
],
431-
(bip44Assets, chainIds, stakedAssets) => {
433+
(bip44Assets, chainIds, stakedAssets, hideZeroBalance) => {
432434
const allowedIds = buildAllowedNetworkIdSet(chainIds);
433435
const filteredAssets = Object.entries(bip44Assets)
434436
.filter(([networkId]) => allowedIds.has(networkId))
435437
.flatMap(([_, chainAssets]) =>
436-
chainAssets.filter(
437-
(asset) => !isTronSpecialAsset(asset.chainId, asset.symbol),
438-
),
438+
chainAssets.filter((asset) => {
439+
if (isTronSpecialAsset(asset.chainId, asset.symbol)) return false;
440+
if (hideZeroBalance && parseFloat(asset.balance ?? '0') === 0)
441+
return false;
442+
return true;
443+
}),
439444
);
440445
return mergeStakedSortAndDedupeAssets(
441446
filteredAssets,

0 commit comments

Comments
 (0)