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>
193 lines
4.6 KiB
TypeScript
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]
|
|
);
|
|
}
|