diff --git a/src/pages/MatrixPage.cache.test.tsx b/src/pages/MatrixPage.cache.test.tsx index 2fd4e44..7ddf365 100644 --- a/src/pages/MatrixPage.cache.test.tsx +++ b/src/pages/MatrixPage.cache.test.tsx @@ -60,4 +60,31 @@ describe('MatrixPage — scan cache', () => { expect(fetchSpy).toHaveBeenCalled(); }); }); + + it('refresh preserves user-visible chains when balances are zero', async () => { + storage.setScanCache(ADDR, { balances: {}, prices: {} }); + // hiddenChains defaults to empty in storage, so all chains start visible. + globalThis.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, result: '0x0' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ) as unknown as typeof fetch; + + const user = userEvent.setup(); + renderWithProviders(routes, [`/address/${ADDR}`]); + + // Ethereum (chain id 1) is visible after hydrating from cache. + expect(await screen.findByText('Ethereum Mainnet')).toBeInTheDocument(); + + const btn = await screen.findByRole('button', { name: /Refresh/ }); + await user.click(btn); + + // After a refresh with zero balances everywhere, the Ethereum column + // must still be visible — the old auto-hide pass used to remove it on + // every refresh. + await waitFor(() => { + expect(screen.getByText('Ethereum Mainnet')).toBeInTheDocument(); + }); + }); }); diff --git a/src/state/WalletContext.tsx b/src/state/WalletContext.tsx index d38c0a8..7b66789 100644 --- a/src/state/WalletContext.tsx +++ b/src/state/WalletContext.tsx @@ -156,7 +156,8 @@ export function WalletProvider({ children }: { children: ReactNode }) { })); }, []); - const startScan = useCallback(async (address: string) => { + const startScan = useCallback(async (address: string, opts: { autoHide?: boolean } = {}) => { + const autoHide = opts.autoHide ?? true; setState((s) => ({ ...s, address, demo: false, scanning: true, activeChain: null, scanProgress: {} })); // Scan chains sequentially, ordered by numeric chain ID (Ethereum first). @@ -221,11 +222,16 @@ export function WalletProvider({ children }: { children: ReactNode }) { // Persist so repeat visits don't need to re-scan all chains. storage.setScanCache(address, { balances, prices }); - // Auto-hide chains that have no tokens of value. - const autoHidden = new Set(); - CHAINS.forEach((c) => { - if (!chainsWithValue.has(c.id)) autoHidden.add(c.id); - }); + // On first connect, hide empty chains so the matrix isn't mostly blank. + // On refresh, keep whatever the user has since chosen to show or hide. + let nextHidden: Set | null = null; + if (autoHide) { + nextHidden = new Set(); + CHAINS.forEach((c) => { + if (!chainsWithValue.has(c.id)) nextHidden!.add(c.id); + }); + storage.setHiddenChains(nextHidden); + } setState((s) => ({ ...s, @@ -233,7 +239,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { prices, scanning: false, activeChain: null, - hiddenChains: autoHidden, + hiddenChains: nextHidden ?? s.hiddenChains, lastRefreshedAt: Date.now(), fromCache: false, })); @@ -427,7 +433,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { const refreshBalances = useCallback(async (): Promise => { const addr = loadedKeyRef.current; if (!addr || addr === 'demo') return; - await startScan(addr); + await startScan(addr, { autoHide: false }); }, [startScan]); const disconnect = useCallback(() => {