WellNuo/utils/networkStatus.ts
Sergei 91e677178e Add offline mode graceful degradation
Implements comprehensive offline handling for API-first architecture:

Network Detection:
- Real-time connectivity monitoring via @react-native-community/netinfo
- useNetworkStatus hook for React components
- Utility functions: getNetworkStatus(), isOnline()
- Retry logic with exponential backoff

Offline-Aware API Layer:
- Wraps all API methods with network detection
- User-friendly error messages for offline states
- Automatic retries for read operations
- Custom offline messages for write operations

UI Components:
- OfflineBanner: Animated banner at top/bottom
- InlineOfflineBanner: Non-animated inline version
- Auto-shows/hides based on network status

Data Fetching Hooks:
- useOfflineAwareData: Hook for data fetching with offline handling
- useOfflineAwareMutation: Hook for create/update/delete operations
- Auto-refetch when network returns
- Optional polling support

Error Handling:
- Consistent error messages across app
- Network error detection
- Retry functionality with user feedback

Tests:
- Network status detection tests
- Offline-aware API wrapper tests
- 23 passing tests with full coverage

Documentation:
- Complete offline mode guide (docs/OFFLINE_MODE.md)
- Usage examples (components/examples/OfflineAwareExample.tsx)
- Best practices and troubleshooting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 16:49:15 -08:00

193 lines
4.6 KiB
TypeScript

/**
* Network Status Detection and Monitoring
*
* Provides utilities for detecting and responding to network connectivity changes.
* Used throughout the app for graceful offline mode degradation.
*/
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
import { useEffect, useState, useCallback } from 'react';
/**
* Network status type
*/
export type NetworkStatus = 'online' | 'offline' | 'unknown';
/**
* Get current network status (sync)
* Use this for one-time checks
*/
export async function getNetworkStatus(): Promise<NetworkStatus> {
try {
const state = await NetInfo.fetch();
return state.isConnected ? 'online' : 'offline';
} catch {
return 'unknown';
}
}
/**
* Check if device is currently online
* Use this before making API calls
*/
export async function isOnline(): Promise<boolean> {
const status = await getNetworkStatus();
return status === 'online';
}
/**
* React hook for network status monitoring
*
* @example
* function MyComponent() {
* const { isOnline, isOffline, status } = useNetworkStatus();
*
* if (isOffline) {
* return <OfflineBanner />;
* }
* // ...
* }
*/
export function useNetworkStatus() {
const [status, setStatus] = useState<NetworkStatus>('unknown');
useEffect(() => {
// Get initial status
getNetworkStatus().then(setStatus);
// Subscribe to network changes
const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => {
setStatus(state.isConnected ? 'online' : 'offline');
});
return () => {
unsubscribe();
};
}, []);
return {
status,
isOnline: status === 'online',
isOffline: status === 'offline',
isUnknown: status === 'unknown',
};
}
/**
* React hook for online-only callback
* Executes callback only when online, otherwise shows error
*
* @example
* function MyComponent() {
* const executeOnline = useOnlineOnly();
*
* const handleSave = () => {
* executeOnline(() => {
* // This only runs when online
* api.saveBeneficiary(data);
* }, 'Cannot save while offline');
* };
* }
*/
export function useOnlineOnly() {
const { isOnline } = useNetworkStatus();
return useCallback(
async (callback: () => void | Promise<void>, offlineMessage?: string) => {
if (!isOnline) {
throw new Error(offlineMessage || 'This action requires an internet connection');
}
return await callback();
},
[isOnline]
);
}
/**
* Retry configuration for API calls
*/
export interface RetryConfig {
maxAttempts: number;
delayMs: number;
backoffMultiplier: number;
}
/**
* Default retry configuration
*/
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxAttempts: 3,
delayMs: 1000,
backoffMultiplier: 2,
};
/**
* Retry an async operation with exponential backoff
* Only retries if network is available
*
* @param operation - Async function to retry
* @param config - Retry configuration
* @returns Promise with operation result
*
* @example
* const data = await retryWithBackoff(
* () => api.getBeneficiaries(),
* { maxAttempts: 3, delayMs: 1000, backoffMultiplier: 2 }
* );
*/
export async function retryWithBackoff<T>(
operation: () => Promise<T>,
config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<T> {
let lastError: Error | undefined;
let delay = config.delayMs;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
// Check network before retry (after first attempt)
if (attempt > 1) {
const online = await isOnline();
if (!online) {
throw new Error('Network unavailable');
}
}
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
// Don't retry on last attempt
if (attempt === config.maxAttempts) {
break;
}
// Wait before retry with exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
delay *= config.backoffMultiplier;
}
}
throw lastError || new Error('Operation failed after retries');
}
/**
* React hook for retrying async operations
*
* @example
* function MyComponent() {
* const retry = useRetry();
*
* const loadData = async () => {
* const data = await retry(() => api.getBeneficiaries());
* setData(data);
* };
* }
*/
export function useRetry(config: RetryConfig = DEFAULT_RETRY_CONFIG) {
return useCallback(
<T>(operation: () => Promise<T>) => retryWithBackoff(operation, config),
// eslint-disable-next-line react-hooks/exhaustive-deps
[config.maxAttempts, config.delayMs, config.backoffMultiplier]
);
}