Skip to content

feat(mobile): port activity feed store and create ActivityFeed component - #528

Merged
Miracle656 merged 8 commits into
Miracle656:mainfrom
Belzabeem:activity-feed-mobile
Jul 30, 2026
Merged

feat(mobile): port activity feed store and create ActivityFeed component#528
Miracle656 merged 8 commits into
Miracle656:mainfrom
Belzabeem:activity-feed-mobile

Conversation

@Belzabeem

Copy link
Copy Markdown
Contributor

feat(mobile): port activity feed store and create ActivityFeed component

  • Create frontend/mobile/lib/activityFeed.ts with hydrate/append lifecycle ported from frontend/wallet
  • Wire to Wraith GET /transfers/:address endpoint with 15s polling
  • Create ActivityFeed component with filter pills, skeleton loading, swap display
  • Update mobile dashboard to integrate the feed

Closes #461

- Create frontend/mobile/lib/activityFeed.ts with hydrate/append lifecycle ported from frontend/wallet
- Wire to Wraith GET /transfers/:address endpoint with 15s polling
- Create ActivityFeed component with filter pills, skeleton loading, swap display
- Update mobile dashboard to integrate the feed

Closes Miracle656#461
@Belzabeem
Belzabeem requested a review from Miracle656 as a code owner July 28, 2026 03:20
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Belzabeem is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Belzabeem Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The store design is sound. startPolling guarding against a redundant restart and calling stopPolling() before rebinding is the part people usually get wrong — duplicate intervals stacking on every remount is the classic bug here, and you closed it:

if (_pollInterval && _currentAddress === address && _wraithUrl === wraithUrl) {
  return; // already polling this address
}
stopPolling();

The hydrate / append split and the subscribeActivityFeed listener pattern are the right shape too.

Three things to sort out.

1. This takes over app/index.tsx, which is the entry route

app/index.tsx (114+/14-) becomes the activity-feed home screen. But that file is the app's routing entry point, and it's the most contended file in the mobile queue — four other open PRs touch it:

  • #507 makes it a redirect into the (tabs) group
  • #516 deletes it in favour of (tabs)/index.tsx
  • #520 makes it session-aware: wallet → dashboard, first run → welcome
  • this PR makes it the activity feed screen

Only one can win, and it needs to be the routing one — a user opening the app has to land on onboarding or the dashboard depending on whether they have a wallet, which is #520's logic.

The activity feed is dashboard content, not the entry route. Once #507 lands, its home is app/(tabs)/dashboard.tsx. Please move <ActivityFeed /> and the useInitActivityFeed call there and leave app/index.tsx alone. Landing order is #507#520 → this.

Your components/ActivityFeed.tsx and lib/activityFeed.ts are unaffected — this is purely about where they get mounted.

2. It polls a demo address, not the user's wallet

const DEMO_ADDRESS = process.env.EXPO_PUBLIC_DEMO_ADDRESS ?? null;
const { loading } = useInitActivityFeed(DEMO_ADDRESS, WRAITH_URL);

If EXPO_PUBLIC_DEMO_ADDRESS is set in a build, every user sees that account's transaction history presented as their activity feed. If it isn't set, the feed is permanently empty.

I've had to flag variations of this on #515 (fixture NFTs shown as holdings) and #521 (stubbed vault showing fake transaction hashes), so let me state the general rule: never render another account's financial data, or fabricated data, as the user's own.

The address should come from wallet state. #520 reads it as invisible_wallet_address from storage — that's the source to use. A demo address is fine as a dev-only override, but it must not be the primary path, and the feed should render its empty state when there's no wallet.

3. fetch has no timeout

const res = await fetch(url);

On a 15-second interval with no abort, a stalled request on a flaky mobile connection leaves a pending fetch while the next tick fires — they accumulate. Other modules in this tree already use AbortSignal.timeout(...) (see lib/sep24.ts in #522), so:

const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });

Two notes, not blocking

No AppState handling. The interval runs regardless of whether the app is foregrounded. iOS suspends JS timers when backgrounded so the practical impact is limited, but the flip side matters more: on resume there's no immediate refetch, so the user sees stale data for up to 15 seconds. Subscribing to AppState to pause on background and refetch on foreground would fix both.

Network switching. #526 adds runtime testnet/mainnet switching, and its own docs note that a switch invalidates everything fetched from the previous chain. This feed caches records in module state with no network key, so after a switch it would show the old chain's transactions. Worth keying the cache on network once #526 lands.

No tests. A jest.config.js arrives with #524/#525. hydrate / append dedup and the startPolling guard are all pure logic and easy to cover.

Belzabeem and others added 7 commits July 30, 2026 14:14
…se wallet address from store, add fetch timeout
- Restore QuickActions on the dashboard. This branch predates Miracle656#517 and its
  dashboard rewrite dropped the component and its import, which would have
  removed the quick-actions row.
- fetchTransfers swallowed every failure and returned [], so an unreachable
  indexer, an HTTP error and a genuinely empty wallet all rendered as
  "No transactions yet." It now throws; useInitActivityFeed already catches
  into its error state, which was being returned but never used.
- ActivityFeed takes an error prop and renders a distinct message, and the
  dashboard passes it through.

tsc clean; jest 11 suites / 198 tests; expo lint clean.
The lockfile auto-merged into a state npm 10 rejected:

  npm error `npm ci` can only install packages when your package.json and
  package-lock.json ... are in sync
  npm error Missing: @jest/transform@29.7.0 from lock file

Regenerated with npm 10 so it matches what CI installs.
@Miracle656

Copy link
Copy Markdown
Owner

Merging. The store is well built — an external-store subscription with hydrate/append, dedup by txHash, and sorting by timestamp is the right shape for a feed that has to take both a historical batch and live additions, and it reads the address from walletStore rather than inventing one.

Three changes before merge:

Restored QuickActions on the dashboard. This branch predates #517, and its dashboard rewrite dropped both the <QuickActions /> element and its import — merging as-is would have removed the quick-actions row that just landed.

Failures were being reported as an empty wallet. fetchTransfers caught everything and returned []:

if (!res.ok) {
  console.warn(`[activityFeed] Wraith returned ${res.status}`);
  return [];
}
...
} catch (err) {
  console.warn('[activityFeed] fetchTransfers failed:', err);
  return [];
}

So an unreachable indexer, a 500, a malformed response and a genuinely empty wallet all rendered identically as "No transactions yet." On a wallet screen that's a meaningful lie — a user checking whether a payment arrived would be told it hadn't.

Interestingly useInitActivityFeed already had the machinery: it wraps the call in try/catch, sets an error string, and returns { loading, error }. That error just never fired, because nothing below it ever threw — and the dashboard destructured it without using it. So the fix was small: fetchTransfers now throws on HTTP failure or an unexpected shape, ActivityFeed takes an error prop and renders a distinct message, and the dashboard passes it through.

Lockfile. package-lock.json auto-merged into a state npm 10 rejected (Missing: @jest/transform@29.7.0 from lock file), which failed the mobile CI job. Regenerated with npm 10 to match what CI installs.

Verified: tsc --noEmit clean, jest 11 suites / 198 tests, expo lint clean, CI green.

Two follow-ups worth raising as issues rather than holding this up:

  • EXPO_PUBLIC_DEMO_ADDRESS in .env.example — worth being careful with. Pointing the feed at someone else's address renders their real transfers on a user's dashboard; fine for local development, but it shouldn't ever become a fallback when the real address is missing. Right now it isn't, and it's worth keeping it that way.
  • The polling interval keeps running after unmount by design ("keep feed alive for other screens"), with stopPolling() intended to be called on account switch. Nothing calls it yet, so switching networks via feat(mobile): switch between testnet and mainnet at runtime #526 will leave a poller pointed at the old network's indexer.

@Miracle656
Miracle656 merged commit 614fa4b into Miracle656:main Jul 30, 2026
10 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

33. Recent activity feed

3 participants