import { App as capacitorApp } from '@capacitor/app' import { Capacitor } from '@capacitor/core' import { useQueryClient } from '@tanstack/react-query' import { useEffect, useRef } from 'react' import { commandQueue } from '../utils/CommandQueue' import { offlineDB } from '../utils/OfflineDB' import { isOAuthExchangeInProgress } from '../utils/OAuthExchangeState' import { isOfflineFeatureEnabled } from '../utils/OfflineFeatureToggle' import { syncEngine } from '../utils/SyncEngine' import { networkManager } from './NetworkManager' export const PENDING_POLL_MS = 30_000 // retry pending commands every 30s export const SERVER_PROBE_MS = 15_000 // probe server when marked unreachable but device has network const CACHE_REFRESH_MS = 5 * 60_000 // refresh IDB cache every 5 min while online export function useSyncOnReconnect() { const queryClient = useQueryClient() const initialized = useRef(false) useEffect(() => { let pendingPollInterval let cacheRefreshInterval let serverProbeInterval let resumeListener let networkListener const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { runSync() } } const handleOnline = () => runSync() const init = async () => { if (initialized.current) return initialized.current = true if (isOfflineFeatureEnabled()) { await offlineDB.init() } // 1. Device network change (works on native + real network drops) networkListener = async isOnline => { if (isOnline) { await runSync() } } networkManager.registerNetworkListener(networkListener) // 2. Tab becomes visible (user switches back to the tab after reconnecting backend) document.addEventListener('visibilitychange', handleVisibilityChange) // 3. Browser online event (fires when device network is restored) window.addEventListener('online', handleOnline) // 3.5 Native app resume (fires when returning to the foreground) if (Capacitor.isNativePlatform()) { resumeListener = await capacitorApp.addListener( 'appStateChange', ({ isActive }) => { if (isActive) { console.log( 'App resumed, checking connectivity and syncing if online...', ) runSync() } }, ) } // 4. Retry pending commands every 30s (catches backend restart) pendingPollInterval = setInterval(async () => { const pending = await commandQueue.getPending() if (pending.length > 0) { runSync() } }, PENDING_POLL_MS) // 5. Keep IDB cache fresh every 5 min while online (so offline reads are current) cacheRefreshInterval = setInterval(() => { runSync() }, CACHE_REFRESH_MS) // 6. Probe server every 15s when server is unreachable but device has network serverProbeInterval = setInterval(async () => { if (!networkManager.isOnline && networkManager.deviceOnline) { await runSync() } }, SERVER_PROBE_MS) } const runSync = async () => { if (!isOfflineFeatureEnabled()) return // Skip while the OAuth code exchange is in flight — there's no session // yet, so a sync here just 401s. Note the app-resume listener fires in // the same tick as the deep link, before the route changes, so this has // to test the shared flag rather than the pathname. if (isOAuthExchangeInProgress()) return // No session, nothing to sync — and a 401 here would force a logout that // hard-navigates signed-out visitors (invite links) away to /login. if (!localStorage.getItem('token')) return const wasOffline = !networkManager.isOnline const didSync = await syncEngine.sync() if (didSync) { queryClient.invalidateQueries() // After recovery from server-unreachable, run a second pass to flush // any commands that were skipped while offline if (wasOffline && networkManager.isOnline) { const didSync2 = await syncEngine.sync() if (didSync2) queryClient.invalidateQueries() } } } init() return () => { if (pendingPollInterval) { clearInterval(pendingPollInterval) } if (cacheRefreshInterval) { clearInterval(cacheRefreshInterval) } if (serverProbeInterval) { clearInterval(serverProbeInterval) } if (networkListener) { networkManager.unregisterNetworkListener(networkListener) } document.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('online', handleOnline) resumeListener?.remove() } }, [queryClient]) }