From 50e508074494409bca443262cc80f617255bf8fe Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Thu, 23 Jul 2026 13:46:34 +0100 Subject: [PATCH 1/2] fix(native): refetch stale queries on app resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TanStack Query's refetchOnWindowFocus keys off visibilitychange, which Android WebViews don't reliably fire on resume — an app restored from background kept rendering its pre-background query data, so the home Activity widget showed weeks-old transactions. Drive the focusManager from Capacitor's appStateChange so stale queries refetch on resume. Also send the history fetch with cache: 'no-store' (server now sets Cache-Control: no-store too) and drop stale service-worker-cache comments — the SW no longer intercepts API responses. --- src/config/wagmi.config.tsx | 1 - src/hooks/useNativePlugins.ts | 14 ++++++++++++++ src/hooks/useTransactionHistory.ts | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/config/wagmi.config.tsx b/src/config/wagmi.config.tsx index 7606b4ef81..ff3d0ccb2b 100644 --- a/src/config/wagmi.config.tsx +++ b/src/config/wagmi.config.tsx @@ -14,7 +14,6 @@ const queryClient = new QueryClient({ refetchOnWindowFocus: true, // Refetch stale data when user returns refetchOnReconnect: true, // Refetch when connectivity restored // Allow queries when offline to read from TanStack Query in-memory cache - // Service Worker provides additional HTTP API response caching (user data, history, prices) networkMode: 'always', // Run queries even when offline (reads from cache) }, mutations: { diff --git a/src/hooks/useNativePlugins.ts b/src/hooks/useNativePlugins.ts index 22edcaa048..955b087af3 100644 --- a/src/hooks/useNativePlugins.ts +++ b/src/hooks/useNativePlugins.ts @@ -2,6 +2,7 @@ import { useEffect } from 'react' import { useRouter } from 'next/navigation' +import { focusManager } from '@tanstack/react-query' import { captureMessage } from '@sentry/nextjs' import { isCapacitor, getPlatform } from '@/utils/capacitor' import { deepLinkToNativePath } from '@/utils/native-routes' @@ -36,6 +37,19 @@ export function useNativePlugins() { const init = async () => { try { const { App } = await import('@capacitor/app') + + // TanStack Query's refetchOnWindowFocus keys off visibilitychange, + // which Android WebViews don't reliably fire on app resume — a + // resumed app kept rendering its pre-background query data (stale + // home Activity). Drive the focusManager from the native lifecycle. + const stateListener = await App.addListener('appStateChange', ({ isActive }: { isActive: boolean }) => + focusManager.setFocused(isActive) + ) + cleanups.push(() => { + stateListener.remove() + focusManager.setFocused(undefined) + }) + const backListener = await App.addListener('backButton', ({ canGoBack }: { canGoBack: boolean }) => { if (canGoBack) { router.back() diff --git a/src/hooks/useTransactionHistory.ts b/src/hooks/useTransactionHistory.ts index 4806be5a20..075cf0bd13 100644 --- a/src/hooks/useTransactionHistory.ts +++ b/src/hooks/useTransactionHistory.ts @@ -72,8 +72,11 @@ export function useTransactionHistory({ // append targetUsername to the query params if filterMutualTxs is true and username is provided if (filterMutualTxs && username) queryParams.append('targetUsername', username) + // no-store: home Activity must never render a cached copy of history + // (server also sends Cache-Control: no-store; this covers the WebView path) const response = await serverFetch(`/users/history?${queryParams.toString()}`, { method: 'GET', + cache: 'no-store', }) if (!response.ok) { @@ -94,7 +97,7 @@ export function useTransactionHistory({ // that bites if a caller ever flips `mode` mid-life. // Latest transactions (home page). - // Two-tier caching: TQ in-memory (30s) → SW disk cache (1 week) → Network. + // Cached only in TQ memory (30s stale); the HTTP response is no-store end to end. const latestQuery = useQuery({ queryKey: [TRANSACTIONS, 'latest', { limit, targetUsername: filterMutualTxs ? username : undefined }], queryFn: () => fetchHistory({ limit }), From 9701b08a9a128fdc9c49a1b2ab026ca6226fcd8c Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Thu, 23 Jul 2026 14:37:34 +0100 Subject: [PATCH 2/2] fix(native): don't leak listeners registered after effect teardown If the effect cleans up while a listener registration is still in flight, the handle resolved after teardown and was never removed. Route every registration through a disposed-aware track() helper. --- src/hooks/useNativePlugins.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/hooks/useNativePlugins.ts b/src/hooks/useNativePlugins.ts index 955b087af3..c4e2aa6409 100644 --- a/src/hooks/useNativePlugins.ts +++ b/src/hooks/useNativePlugins.ts @@ -24,6 +24,13 @@ export function useNativePlugins() { if (!isCapacitor()) return const cleanups: Array<() => void> = [] + let disposed = false + // Registrations resolve async; if the effect tore down first, run the + // cleanup now instead of leaking the handle. + const track = (cleanup: () => void) => { + if (disposed) cleanup() + else cleanups.push(cleanup) + } const openDeepLink = (url?: string | null) => { if (!url) return @@ -45,7 +52,7 @@ export function useNativePlugins() { const stateListener = await App.addListener('appStateChange', ({ isActive }: { isActive: boolean }) => focusManager.setFocused(isActive) ) - cleanups.push(() => { + track(() => { stateListener.remove() focusManager.setFocused(undefined) }) @@ -57,13 +64,13 @@ export function useNativePlugins() { App.minimizeApp() } }) - cleanups.push(() => backListener.remove()) + track(() => backListener.remove()) // App Links: cold start (getLaunchUrl) + warm start (appUrlOpen). const launch = await App.getLaunchUrl() openDeepLink(launch?.url) const urlListener = await App.addListener('appUrlOpen', ({ url }: { url: string }) => openDeepLink(url)) - cleanups.push(() => urlListener.remove()) + track(() => urlListener.remove()) } catch (e) { console.warn('failed to init app listeners:', e) // without these listeners push-tap deep links never route, so surface the failure @@ -84,7 +91,7 @@ export function useNativePlugins() { // relative path the API sends; the launch URL is the fallback for // notifications sent before that field existed. const adapter = await getOneSignalAdapter() - cleanups.push( + track( adapter.onNotificationClick(({ deepLink, additionalData }) => { const target = additionalData.deepLink const link = typeof target === 'string' ? target : deepLink @@ -137,6 +144,7 @@ export function useNativePlugins() { init() return () => { + disposed = true cleanups.forEach((fn) => fn()) } }, [router])