Skip to content

Commit bf38619

Browse files
authored
Merge pull request #334 from Jessepriase/fix/175-notification-feed-wallet-switch
fix(#175): clear notification feed immediately on wallet switch
2 parents 3cc510c + 99f57ff commit bf38619

4 files changed

Lines changed: 158 additions & 2 deletions

File tree

dashboard/src/__tests__/wallet-integration.test.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,99 @@ describe('Wallet integration report', () => {
191191
expect(fs.existsSync(REPORT_PATH)).toBe(true);
192192
});
193193
});
194+
195+
// ─── Regression tests for issue #175 ─────────────────────────────────────────
196+
// Verify that switching wallets updates walletStore address immediately and
197+
// leaves no stale address behind, which is the precondition that
198+
// useWalletAccountSync relies on to trigger a feed refresh.
199+
200+
describe('Notification feed clears on wallet switch (issue #175)', () => {
201+
it('walletStore address updates immediately when switching to a different wallet', async () => {
202+
const { wallet, store, kit } = await load();
203+
204+
// Connect first wallet
205+
kit.__control.authModalImpl = async () => {
206+
kit.__emit('WALLET_SELECTED', { id: 'freighter' });
207+
kit.__emit('STATE_UPDATED', { address: SUPPORTED_WALLETS[0].address });
208+
};
209+
await wallet.connectWallet();
210+
expect(store.useWalletStore.getState().address).toBe(SUPPORTED_WALLETS[0].address);
211+
212+
// Switch to second wallet — address in the store must change synchronously
213+
kit.__control.authModalImpl = async () => {
214+
kit.__emit('WALLET_SELECTED', { id: 'albedo' });
215+
kit.__emit('STATE_UPDATED', { address: SUPPORTED_WALLETS[1].address });
216+
};
217+
await wallet.connectWallet();
218+
219+
expect(store.useWalletStore.getState().address).toBe(SUPPORTED_WALLETS[1].address);
220+
expect(store.useWalletStore.getState().address).not.toBe(SUPPORTED_WALLETS[0].address);
221+
});
222+
223+
it('walletStore address is null after disconnect, clearing any previous account', async () => {
224+
const { wallet, store, kit } = await load();
225+
226+
kit.__control.authModalImpl = async () => {
227+
kit.__emit('WALLET_SELECTED', { id: 'freighter' });
228+
kit.__emit('STATE_UPDATED', { address: SUPPORTED_WALLETS[0].address });
229+
};
230+
await wallet.connectWallet();
231+
expect(store.useWalletStore.getState().address).toBe(SUPPORTED_WALLETS[0].address);
232+
233+
kit.__control.disconnectImpl = async () => {
234+
kit.__emit('DISCONNECT', {});
235+
};
236+
await wallet.disconnectWallet();
237+
238+
expect(store.useWalletStore.getState().address).toBeNull();
239+
expect(localStorage.getItem(WALLET_ADDRESS_KEY)).toBeNull();
240+
});
241+
242+
it('no stale address remains in localStorage after switching wallets', async () => {
243+
const { wallet, store, kit } = await load();
244+
245+
kit.__control.authModalImpl = async () => {
246+
kit.__emit('WALLET_SELECTED', { id: 'freighter' });
247+
kit.__emit('STATE_UPDATED', { address: SUPPORTED_WALLETS[0].address });
248+
};
249+
await wallet.connectWallet();
250+
251+
kit.__control.authModalImpl = async () => {
252+
kit.__emit('WALLET_SELECTED', { id: 'xbull' });
253+
kit.__emit('STATE_UPDATED', { address: SUPPORTED_WALLETS[2].address });
254+
};
255+
await wallet.connectWallet();
256+
257+
// localStorage must reflect the new account only
258+
expect(localStorage.getItem(WALLET_ADDRESS_KEY)).toBe(SUPPORTED_WALLETS[2].address);
259+
expect(localStorage.getItem(WALLET_ADDRESS_KEY)).not.toBe(SUPPORTED_WALLETS[0].address);
260+
expect(store.useWalletStore.getState().address).toBe(SUPPORTED_WALLETS[2].address);
261+
});
262+
263+
it('switching wallets across all supported providers leaves only the current address', async () => {
264+
for (const [index, provider] of SUPPORTED_WALLETS.entries()) {
265+
const { wallet, store, kit } = await load();
266+
267+
// First connect one of the other wallets
268+
const previous = SUPPORTED_WALLETS[(index + 1) % SUPPORTED_WALLETS.length];
269+
kit.__control.authModalImpl = async () => {
270+
kit.__emit('WALLET_SELECTED', { id: previous.id });
271+
kit.__emit('STATE_UPDATED', { address: previous.address });
272+
};
273+
await wallet.connectWallet();
274+
275+
// Now switch to the target provider
276+
kit.__control.authModalImpl = async () => {
277+
kit.__emit('WALLET_SELECTED', { id: provider.id });
278+
kit.__emit('STATE_UPDATED', { address: provider.address });
279+
};
280+
await wallet.connectWallet();
281+
282+
expect(store.useWalletStore.getState().address).toBe(provider.address);
283+
expect(localStorage.getItem(WALLET_ADDRESS_KEY)).toBe(provider.address);
284+
expect(localStorage.getItem(WALLET_ID_KEY)).toBe(provider.id);
285+
286+
localStorage.clear();
287+
}
288+
});
289+
});

dashboard/src/components/ActivityFeed.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { fetchActivityFeed, generateMockActivityEvents } from '../services/activ
33
import type { ActivityEvent, ActivityType } from '../types/activity';
44
import { formatTimestamp } from '../utils/formatTime';
55
import { PaginationControls } from './PaginationControls';
6+
import { useWalletAccountSync } from '../hooks/useWalletAccountSync';
67

78
// Helper to get icon/color based on activity type
89
const getActivityTypeStyle = (type: ActivityType) => {
@@ -148,6 +149,16 @@ export function ActivityFeed() {
148149
setLiveEvents([]);
149150
};
150151

152+
// Clear stale activity and re-fetch from page 1 whenever the connected
153+
// wallet address changes (switch or disconnect). This is the fix for issue #175.
154+
useWalletAccountSync((_nextAddress) => {
155+
setEvents([]);
156+
setLiveEvents([]);
157+
setTotal(0);
158+
setPage(1);
159+
loadEvents(1, pageSize);
160+
});
161+
151162
// Events shown: live prepended events (only on page 1) + paginated events
152163
const displayedEvents = page === 1 ? [...liveEvents, ...events] : events;
153164

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect, useRef } from 'react';
2+
import { useWalletStore } from '../store/walletStore';
3+
4+
/**
5+
* Calls `onAccountChange` whenever the connected wallet address changes during
6+
* an active session.
7+
*
8+
* The callback is skipped on the initial mount (address going from undefined to
9+
* its initial value) — it fires only for subsequent transitions, i.e. a real
10+
* wallet switch or disconnect while the page is open.
11+
*/
12+
export function useWalletAccountSync(onAccountChange: (address: string | null) => void): void {
13+
const address = useWalletStore((state) => state.address);
14+
15+
// Track whether this is the very first render so we can skip it.
16+
const isFirstRender = useRef(true);
17+
// Hold a stable ref to the callback so the effect doesn't re-subscribe on
18+
// every render if the caller passes an inline function.
19+
const callbackRef = useRef(onAccountChange);
20+
callbackRef.current = onAccountChange;
21+
22+
useEffect(() => {
23+
if (isFirstRender.current) {
24+
isFirstRender.current = false;
25+
return;
26+
}
27+
callbackRef.current(address);
28+
}, [address]);
29+
}

dashboard/src/pages/EventExplorerPage.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { IndexingHealthPanel } from '../components/IndexingHealthPanel';
99
import { useEventFilters, useEventLoadingState, useFilteredEvents } from '../hooks/useEventSelectors';
1010
import { useEventStore } from '../store/eventStore';
1111
import { fetchEvents, fetchStatus, type ContractStatus } from '../services/eventsApi';
12-
import { fetchEvents } from '../services/eventsApi';
1312
import { resolveIndexingHealthUrl } from '../services/indexingHealthApi';
1413
import { generateMockEvents } from '../utils/eventData';
1514
import { restoreWalletSession } from '../services/wallet';
15+
import { useWalletAccountSync } from '../hooks/useWalletAccountSync';
1616

1717
const DEFAULT_EVENT_COUNT = 5000;
1818
const DEFAULT_LIMIT = 12;
@@ -87,13 +87,33 @@ export function EventExplorerPage() {
8787

8888
loadEvents();
8989
loadStatus();
90-
loadEvents();
9190

9291
return () => {
9392
cancelled = true;
9493
};
9594
}, [setEvents, setError, setLoading]);
9695

96+
// Clear stale events and re-fetch whenever the connected wallet address
97+
// changes (switch or disconnect). This is the fix for issue #175.
98+
useWalletAccountSync((_nextAddress) => {
99+
setEvents([]);
100+
setError(null);
101+
setPage(1);
102+
103+
setLoading(true);
104+
fetchEvents(API_URL)
105+
.then((remoteEvents) => {
106+
setEvents(remoteEvents);
107+
})
108+
.catch(() => {
109+
setEvents(generateMockEvents(DEFAULT_EVENT_COUNT));
110+
setError('Listener API unavailable — showing mock events for demo.');
111+
})
112+
.finally(() => {
113+
setLoading(false);
114+
});
115+
});
116+
97117
const pageCount = useMemo(
98118
() => Math.max(1, Math.ceil(filteredEvents.length / limit)),
99119
[filteredEvents.length, limit]

0 commit comments

Comments
 (0)