/** * useOnlineStatus Hook * * React hook for monitoring device/sensor online status. * Provides automatic polling, loading states, and easy status access. */ import { useState, useEffect, useCallback, useRef } from 'react'; import { AppState, AppStateStatus } from 'react-native'; import { onlineStatusService, BeneficiaryOnlineStatus, OnlineStatus, DeviceStatusInfo, } from '@/services/onlineStatusService'; export interface UseOnlineStatusOptions { /** Enable automatic polling (default: false) */ enablePolling?: boolean; /** Polling interval in milliseconds (default: 60000 = 1 minute) */ pollingInterval?: number; /** Whether to fetch immediately on mount (default: true) */ fetchOnMount?: boolean; /** Pause polling when app is in background (default: true) */ pauseInBackground?: boolean; } export interface UseOnlineStatusResult { /** Loading state for initial fetch */ isLoading: boolean; /** Refreshing state for subsequent fetches */ isRefreshing: boolean; /** Error message if fetch failed */ error: string | null; /** Full status data */ status: BeneficiaryOnlineStatus | null; /** Overall status (online/warning/offline) */ overallStatus: OnlineStatus | null; /** Array of device status info */ devices: DeviceStatusInfo[]; /** Count of online devices */ onlineCount: number; /** Count of devices with warning */ warningCount: number; /** Count of offline devices */ offlineCount: number; /** Total number of devices */ totalDevices: number; /** Manually trigger a refresh */ refresh: () => Promise; /** Check if a specific device is online */ isDeviceOnline: (deviceId: string) => boolean; /** Get status for a specific device */ getDeviceStatus: (deviceId: string) => DeviceStatusInfo | undefined; } const DEFAULT_OPTIONS: UseOnlineStatusOptions = { enablePolling: false, pollingInterval: 60000, // 1 minute fetchOnMount: true, pauseInBackground: true, }; /** * Hook for monitoring online status of a beneficiary's devices * * @param beneficiaryId - The beneficiary ID to monitor * @param options - Configuration options * @returns Status data and control functions * * @example * ```tsx * function SensorList({ beneficiaryId }) { * const { * isLoading, * devices, * overallStatus, * refresh, * } = useOnlineStatus(beneficiaryId, { enablePolling: true }); * * if (isLoading) return ; * * return ( * * Status: {overallStatus} * {devices.map(device => ( * * ))} * * ); * } * ``` */ export function useOnlineStatus( beneficiaryId: string | null | undefined, options: UseOnlineStatusOptions = {} ): UseOnlineStatusResult { const opts = { ...DEFAULT_OPTIONS, ...options }; const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [status, setStatus] = useState(null); // Refs for cleanup and tracking const pollingIntervalRef = useRef | null>(null); const isBackgroundRef = useRef(false); const mountedRef = useRef(true); /** * Fetch status from service */ const fetchStatus = useCallback( async (isInitial = false) => { if (!beneficiaryId) { setIsLoading(false); setStatus(null); return; } try { if (isInitial) { setIsLoading(true); } else { setIsRefreshing(true); } setError(null); const result = await onlineStatusService.checkBeneficiaryStatus( beneficiaryId, true // Force refresh ); // Check if component is still mounted if (!mountedRef.current) return; if (result.ok && result.data) { setStatus(result.data); setError(null); } else { setError(result.error || 'Failed to fetch status'); } } catch (err) { if (!mountedRef.current) return; setError(err instanceof Error ? err.message : 'Unknown error'); } finally { if (mountedRef.current) { setIsLoading(false); setIsRefreshing(false); } } }, [beneficiaryId] ); /** * Manual refresh function */ const refresh = useCallback(async () => { await fetchStatus(false); }, [fetchStatus]); /** * Check if specific device is online */ const isDeviceOnline = useCallback( (deviceId: string): boolean => { if (!status) return false; const device = status.devices.find(d => d.deviceId === deviceId); return device?.status === 'online'; }, [status] ); /** * Get status for specific device */ const getDeviceStatus = useCallback( (deviceId: string): DeviceStatusInfo | undefined => { if (!status) return undefined; return status.devices.find(d => d.deviceId === deviceId); }, [status] ); // Initial fetch on mount useEffect(() => { mountedRef.current = true; if (opts.fetchOnMount && beneficiaryId) { fetchStatus(true); } else { setIsLoading(false); } return () => { mountedRef.current = false; }; }, [beneficiaryId, opts.fetchOnMount, fetchStatus]); // Setup polling useEffect(() => { if (!opts.enablePolling || !beneficiaryId) { return; } // Clear any existing interval if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } // Start polling pollingIntervalRef.current = setInterval(() => { // Skip if in background and pauseInBackground is enabled if (opts.pauseInBackground && isBackgroundRef.current) { return; } fetchStatus(false); }, opts.pollingInterval); return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }; }, [beneficiaryId, opts.enablePolling, opts.pollingInterval, opts.pauseInBackground, fetchStatus]); // Handle app state changes (background/foreground) useEffect(() => { if (!opts.pauseInBackground) { return; } const handleAppStateChange = (nextState: AppStateStatus) => { const wasBackground = isBackgroundRef.current; isBackgroundRef.current = nextState !== 'active'; // Refresh when coming back to foreground if (wasBackground && nextState === 'active' && beneficiaryId) { fetchStatus(false); } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription.remove(); }; }, [opts.pauseInBackground, beneficiaryId, fetchStatus]); // Computed values const devices = status?.devices || []; const onlineCount = status?.onlineCount || 0; const warningCount = status?.warningCount || 0; const offlineCount = status?.offlineCount || 0; const totalDevices = status?.totalDevices || 0; const overallStatus = status?.overallStatus || null; return { isLoading, isRefreshing, error, status, overallStatus, devices, onlineCount, warningCount, offlineCount, totalDevices, refresh, isDeviceOnline, getDeviceStatus, }; } /** * Hook for checking status of multiple beneficiaries * * @param beneficiaryIds - Array of beneficiary IDs to monitor * @param options - Configuration options * @returns Map of beneficiary statuses and control functions */ export function useMultipleOnlineStatus( beneficiaryIds: string[], options: UseOnlineStatusOptions = {} ): { isLoading: boolean; statuses: Map; errors: Map; refresh: () => Promise; getStatus: (beneficiaryId: string) => BeneficiaryOnlineStatus | undefined; } { const opts = { ...DEFAULT_OPTIONS, ...options }; const [isLoading, setIsLoading] = useState(true); const [statuses, setStatuses] = useState>(new Map()); const [errors, setErrors] = useState>(new Map()); const mountedRef = useRef(true); const pollingIntervalRef = useRef | null>(null); const isBackgroundRef = useRef(false); const fetchStatuses = useCallback( async (isInitial = false) => { if (beneficiaryIds.length === 0) { setIsLoading(false); return; } try { if (isInitial) { setIsLoading(true); } const results = await onlineStatusService.checkMultipleBeneficiaries( beneficiaryIds, true ); if (!mountedRef.current) return; const newStatuses = new Map(); const newErrors = new Map(); results.forEach((result, id) => { if (result.ok && result.data) { newStatuses.set(id, result.data); } else if (result.error) { newErrors.set(id, result.error); } }); setStatuses(newStatuses); setErrors(newErrors); } catch (err) { if (!mountedRef.current) return; // Set error for all beneficiaries const errorMsg = err instanceof Error ? err.message : 'Unknown error'; const newErrors = new Map(); beneficiaryIds.forEach(id => newErrors.set(id, errorMsg)); setErrors(newErrors); } finally { if (mountedRef.current) { setIsLoading(false); } } }, [beneficiaryIds] ); const refresh = useCallback(async () => { await fetchStatuses(false); }, [fetchStatuses]); const getStatus = useCallback( (beneficiaryId: string) => statuses.get(beneficiaryId), [statuses] ); // Initial fetch useEffect(() => { mountedRef.current = true; if (opts.fetchOnMount) { fetchStatuses(true); } else { setIsLoading(false); } return () => { mountedRef.current = false; }; }, [JSON.stringify(beneficiaryIds), opts.fetchOnMount]); // Polling useEffect(() => { if (!opts.enablePolling || beneficiaryIds.length === 0) { return; } if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } pollingIntervalRef.current = setInterval(() => { if (opts.pauseInBackground && isBackgroundRef.current) { return; } fetchStatuses(false); }, opts.pollingInterval); return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }; }, [beneficiaryIds, opts.enablePolling, opts.pollingInterval, opts.pauseInBackground]); // App state handling useEffect(() => { if (!opts.pauseInBackground) { return; } const handleAppStateChange = (nextState: AppStateStatus) => { const wasBackground = isBackgroundRef.current; isBackgroundRef.current = nextState !== 'active'; if (wasBackground && nextState === 'active' && beneficiaryIds.length > 0) { fetchStatuses(false); } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription.remove(); }; }, [opts.pauseInBackground, beneficiaryIds.length]); return { isLoading, statuses, errors, refresh, getStatus, }; } export type { OnlineStatus, DeviceStatusInfo, BeneficiaryOnlineStatus };