From d0e252a46ba8a754327fba3f1980ae767fca3193 Mon Sep 17 00:00:00 2001 From: Clawdbot Date: Sun, 8 Feb 2026 02:03:14 -0800 Subject: [PATCH] Mobile: Offline support and network monitoring --- package-lock.json | 10 ++++++++++ package.json | 1 + src/hooks/useOffline.tsx | 27 +++++++++------------------ src/lib/network.ts | 34 ++++++++++++++++++++++++++++++++++ src/main.tsx | 4 ++++ 5 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 src/lib/network.ts diff --git a/package-lock.json b/package-lock.json index 5aeed8e..f7ba114 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@capacitor/haptics": "^8.0.0", "@capacitor/ios": "^8.0.2", "@capacitor/keyboard": "^8.0.0", + "@capacitor/network": "^8.0.0", "@capacitor/status-bar": "^8.0.0", "@originals/auth": "^1.8.2", "@originals/sdk": "^1.8.2", @@ -443,6 +444,15 @@ "@capacitor/core": ">=8.0.0" } }, + "node_modules/@capacitor/network": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-8.0.0.tgz", + "integrity": "sha512-fgvB7pNKn8pKavuzys218j4YuA5euNfavp7nS3NuwWKWNupZAlbucfnl75lazxCyVF/ZRjzYVTb4vtTEfFrK1A==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/status-bar": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.0.tgz", diff --git a/package.json b/package.json index 8618fa5..65846f7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@capacitor/haptics": "^8.0.0", "@capacitor/ios": "^8.0.2", "@capacitor/keyboard": "^8.0.0", + "@capacitor/network": "^8.0.0", "@capacitor/status-bar": "^8.0.0", "@originals/auth": "^1.8.2", "@originals/sdk": "^1.8.2", diff --git a/src/hooks/useOffline.tsx b/src/hooks/useOffline.tsx index e67348e..35eac71 100644 --- a/src/hooks/useOffline.tsx +++ b/src/hooks/useOffline.tsx @@ -9,6 +9,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useConvex } from "convex/react"; import { syncManager, type SyncStatus } from "../lib/sync"; import { getQueuedMutations } from "../lib/offline"; +import { getNetworkStatus, onNetworkChange } from "../lib/network"; /** * Hook return type for useOffline @@ -55,9 +56,7 @@ export interface UseOfflineResult { * ``` */ export function useOffline(): UseOfflineResult { - const [isOnline, setIsOnline] = useState( - typeof navigator !== "undefined" ? navigator.onLine : true - ); + const [isOnline, setIsOnline] = useState(getNetworkStatus()); const [syncStatus, setSyncStatus] = useState({ status: "idle" }); const [pendingCount, setPendingCount] = useState(0); const convex = useConvex(); @@ -71,24 +70,16 @@ export function useOffline(): UseOfflineResult { }; }, []); - // Track online/offline events and trigger sync on reconnect + // Track online/offline events using Capacitor network service and trigger sync on reconnect useEffect(() => { - const handleOnline = () => { - setIsOnline(true); - syncManager.sync(convex); - }; - - const handleOffline = () => { - setIsOnline(false); + const handleNetworkChange = (connected: boolean) => { + setIsOnline(connected); + if (connected) { + syncManager.sync(convex); + } }; - window.addEventListener("online", handleOnline); - window.addEventListener("offline", handleOffline); - - return () => { - window.removeEventListener("online", handleOnline); - window.removeEventListener("offline", handleOffline); - }; + return onNetworkChange(handleNetworkChange); }, [convex]); // Subscribe to sync status updates from SyncManager diff --git a/src/lib/network.ts b/src/lib/network.ts new file mode 100644 index 0000000..2799c73 --- /dev/null +++ b/src/lib/network.ts @@ -0,0 +1,34 @@ +import { Capacitor } from '@capacitor/core'; + +type NetworkStatusListener = (connected: boolean) => void; +const listeners: NetworkStatusListener[] = []; +let isConnected = true; + +export function getNetworkStatus() { return isConnected; } + +export function onNetworkChange(listener: NetworkStatusListener) { + listeners.push(listener); + return () => { + const idx = listeners.indexOf(listener); + if (idx >= 0) listeners.splice(idx, 1); + }; +} + +export async function initNetworkMonitoring() { + if (!Capacitor.isNativePlatform()) { + // Web fallback + isConnected = navigator.onLine; + window.addEventListener('online', () => { isConnected = true; listeners.forEach(l => l(true)); }); + window.addEventListener('offline', () => { isConnected = false; listeners.forEach(l => l(false)); }); + return; + } + + const { Network } = await import('@capacitor/network'); + const status = await Network.getStatus(); + isConnected = status.connected; + + Network.addListener('networkStatusChange', (status: { connected: boolean }) => { + isConnected = status.connected; + listeners.forEach(l => l(status.connected)); + }); +} diff --git a/src/main.tsx b/src/main.tsx index cdfd45d..01c3204 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,6 +9,7 @@ import { SettingsProvider } from './hooks/useSettings' import { registerServiceWorker } from './lib/sw-registration' import { initDarkMode } from './lib/storage' import { initNativePlatform } from './lib/native' +import { initNetworkMonitoring } from './lib/network' import './index.css' import App from './App.tsx' @@ -20,6 +21,9 @@ initDarkMode(); // Initialize native platform features (status bar, keyboard) initNativePlatform(); +// Initialize network monitoring for offline support +initNetworkMonitoring(); + // Register service worker for offline support (web only) if (!Capacitor.isNativePlatform()) { registerServiceWorker({