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..c4e2aa6409 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' @@ -23,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 @@ -36,6 +44,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) + ) + track(() => { + stateListener.remove() + focusManager.setFocused(undefined) + }) + const backListener = await App.addListener('backButton', ({ canGoBack }: { canGoBack: boolean }) => { if (canGoBack) { router.back() @@ -43,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 @@ -70,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 @@ -123,6 +144,7 @@ export function useNativePlugins() { init() return () => { + disposed = true cleanups.forEach((fn) => fn()) } }, [router]) 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 }),