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>
435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
# Offline Mode Graceful Degradation
|
|
|
|
This document describes the offline mode implementation for the WellNuo app.
|
|
|
|
## Overview
|
|
|
|
The WellNuo app uses an **API-first architecture** with no local storage for business data. However, it provides graceful degradation when the network is unavailable through:
|
|
|
|
1. **Network detection** - Real-time monitoring of connectivity status
|
|
2. **Offline-aware API wrapper** - Automatic detection and user-friendly error messages
|
|
3. **UI components** - Visual feedback when offline
|
|
4. **Retry logic** - Automatic retries with exponential backoff
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Application Layer │
|
|
│ │
|
|
│ Components use: │
|
|
│ - useOfflineAwareData hook │
|
|
│ - useOfflineAwareMutation hook │
|
|
│ - OfflineBanner component │
|
|
└──────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────▼───────────────────────────────────────┐
|
|
│ Offline-Aware API Layer │
|
|
│ │
|
|
│ offlineAwareApi wraps all API methods with: │
|
|
│ - Network status checks │
|
|
│ - Retry logic │
|
|
│ - User-friendly error messages │
|
|
└──────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────▼───────────────────────────────────────┐
|
|
│ Network Detection Layer │
|
|
│ │
|
|
│ - useNetworkStatus hook (React) │
|
|
│ - getNetworkStatus() (async function) │
|
|
│ - isOnline() (async function) │
|
|
│ - retryWithBackoff() (utility) │
|
|
└──────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────▼───────────────────────────────────────┐
|
|
│ @react-native-community/netinfo │
|
|
│ │
|
|
│ - Native network connectivity detection │
|
|
│ - Real-time change notifications │
|
|
└──────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Key Components
|
|
|
|
### 1. Network Detection (`utils/networkStatus.ts`)
|
|
|
|
**Functions:**
|
|
- `getNetworkStatus(): Promise<NetworkStatus>` - Get current network status
|
|
- `isOnline(): Promise<boolean>` - Check if device is online
|
|
- `retryWithBackoff<T>(operation, config): Promise<T>` - Retry with exponential backoff
|
|
|
|
**Hooks:**
|
|
- `useNetworkStatus()` - React hook for real-time network status
|
|
```typescript
|
|
const { isOnline, isOffline, status } = useNetworkStatus();
|
|
```
|
|
|
|
- `useOnlineOnly()` - Execute callback only when online
|
|
```typescript
|
|
const executeOnline = useOnlineOnly();
|
|
executeOnline(() => api.saveBeneficiary(data));
|
|
```
|
|
|
|
- `useRetry(config)` - Hook for retrying operations
|
|
```typescript
|
|
const retry = useRetry({ maxAttempts: 3, delayMs: 1000 });
|
|
const data = await retry(() => api.getAllBeneficiaries());
|
|
```
|
|
|
|
### 2. Offline-Aware API (`services/offlineAwareApi.ts`)
|
|
|
|
Wraps the main API service with offline detection and graceful error handling.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { offlineAwareApi } from '@/services/offlineAwareApi';
|
|
|
|
// Instead of:
|
|
// const response = await api.getAllBeneficiaries();
|
|
|
|
// Use:
|
|
const response = await offlineAwareApi.getAllBeneficiaries();
|
|
|
|
if (!response.ok) {
|
|
// response.error.message contains user-friendly message
|
|
Alert.alert('Error', response.error.message);
|
|
}
|
|
```
|
|
|
|
**Features:**
|
|
- Automatic network detection before API calls
|
|
- Custom offline messages for write operations
|
|
- Retry logic for read operations
|
|
- Consistent error format
|
|
|
|
**Available Methods:**
|
|
All methods from `api.ts` are wrapped, including:
|
|
- `getAllBeneficiaries()` - with retry
|
|
- `getWellNuoBeneficiary(id)` - with retry
|
|
- `createBeneficiary(data)` - with offline message
|
|
- `updateWellNuoBeneficiary(id, updates)` - with offline message
|
|
- `deleteBeneficiary(id)` - with offline message
|
|
- And all other API methods...
|
|
|
|
### 3. UI Components
|
|
|
|
#### OfflineBanner (`components/OfflineBanner.tsx`)
|
|
|
|
Displays a banner at the top/bottom of the screen when offline.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { OfflineBanner } from '@/components/OfflineBanner';
|
|
|
|
function MyScreen() {
|
|
return (
|
|
<SafeAreaView>
|
|
<OfflineBanner />
|
|
{/* Rest of content */}
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Props:**
|
|
- `message?: string` - Custom message (default: "No internet connection")
|
|
- `position?: 'top' | 'bottom'` - Banner position (default: "top")
|
|
- `backgroundColor?: string` - Background color (default: "#FF3B30")
|
|
- `textColor?: string` - Text color (default: "#FFFFFF")
|
|
- `height?: number` - Banner height (default: 40)
|
|
|
|
#### InlineOfflineBanner
|
|
|
|
Non-animated inline version for use within component trees.
|
|
|
|
```typescript
|
|
import { InlineOfflineBanner } from '@/components/OfflineBanner';
|
|
|
|
function MyComponent() {
|
|
return (
|
|
<View>
|
|
<InlineOfflineBanner />
|
|
{/* Content */}
|
|
</View>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 4. Data Fetching Hooks
|
|
|
|
#### useOfflineAwareData
|
|
|
|
Custom hook for data fetching with offline handling.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { useOfflineAwareData } from '@/hooks/useOfflineAwareData';
|
|
import { offlineAwareApi } from '@/services/offlineAwareApi';
|
|
|
|
function BeneficiariesList() {
|
|
const {
|
|
data: beneficiaries,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
refetching,
|
|
errorMessage,
|
|
isOfflineError,
|
|
} = useOfflineAwareData(
|
|
() => offlineAwareApi.getAllBeneficiaries(),
|
|
[], // dependencies
|
|
{
|
|
refetchOnReconnect: true, // Auto-refetch when back online
|
|
pollInterval: 0, // Poll interval in ms (0 = disabled)
|
|
}
|
|
);
|
|
|
|
if (loading) return <LoadingSpinner />;
|
|
if (error) return <ErrorMessage message={errorMessage} onRetry={refetch} />;
|
|
|
|
return (
|
|
<FlatList
|
|
data={beneficiaries}
|
|
refreshControl={<RefreshControl refreshing={refetching} onRefresh={refetch} />}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### useOfflineAwareMutation
|
|
|
|
Hook for mutations (create, update, delete) with offline handling.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { useOfflineAwareMutation } from '@/hooks/useOfflineAwareData';
|
|
|
|
function CreateBeneficiaryForm() {
|
|
const { mutate, loading, errorMessage } = useOfflineAwareMutation(
|
|
(data) => offlineAwareApi.createBeneficiary(data),
|
|
{
|
|
onSuccess: (beneficiary) => {
|
|
Alert.alert('Success', `Added ${beneficiary.name}`);
|
|
},
|
|
onError: (error) => {
|
|
Alert.alert('Error', errorMessage);
|
|
},
|
|
}
|
|
);
|
|
|
|
const handleSubmit = async () => {
|
|
await mutate({ name: 'John Doe' });
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity onPress={handleSubmit} disabled={loading}>
|
|
<Text>Add Beneficiary</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Error Messages
|
|
|
|
The offline-aware API provides user-friendly error messages:
|
|
|
|
| Error Type | User Message |
|
|
|------------|-------------|
|
|
| Offline | "No internet connection. Please check your network and try again." |
|
|
| Timeout | "Request timed out. Please try again." |
|
|
| Unreachable | "Unable to reach the server. Please try again later." |
|
|
| Generic Network | "Network error occurred. Please check your connection." |
|
|
|
|
Custom messages can be provided for specific operations:
|
|
```typescript
|
|
const response = await withOfflineCheck(
|
|
() => api.createBeneficiary(data),
|
|
{ offlineMessage: 'Cannot add beneficiary while offline' }
|
|
);
|
|
```
|
|
|
|
## Retry Logic
|
|
|
|
Operations can be retried automatically with exponential backoff:
|
|
|
|
```typescript
|
|
import { retryWithBackoff, DEFAULT_RETRY_CONFIG } from '@/utils/networkStatus';
|
|
|
|
const data = await retryWithBackoff(
|
|
() => api.getAllBeneficiaries(),
|
|
{
|
|
maxAttempts: 3, // Max retry attempts
|
|
delayMs: 1000, // Initial delay (ms)
|
|
backoffMultiplier: 2, // Exponential backoff factor
|
|
}
|
|
);
|
|
```
|
|
|
|
**Default config:**
|
|
- Max attempts: 3
|
|
- Initial delay: 1000ms
|
|
- Backoff multiplier: 2
|
|
- Retry delays: 1s → 2s → 4s
|
|
|
|
**Smart retry:**
|
|
- Only retries if network is available
|
|
- Stops immediately if network goes offline
|
|
- Uses exponential backoff to avoid overwhelming the server
|
|
|
|
## Testing
|
|
|
|
Tests are located in `__tests__/offline/`:
|
|
|
|
```bash
|
|
# Run offline mode tests
|
|
npm test -- __tests__/offline
|
|
|
|
# Run all tests
|
|
npm test
|
|
```
|
|
|
|
**Test coverage:**
|
|
- Network status detection
|
|
- Offline-aware API wrapper
|
|
- Error message generation
|
|
- Retry logic with backoff
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Use Offline-Aware API
|
|
|
|
❌ **Don't:**
|
|
```typescript
|
|
const response = await api.getAllBeneficiaries();
|
|
if (!response.ok) {
|
|
Alert.alert('Error', response.error?.message);
|
|
}
|
|
```
|
|
|
|
✅ **Do:**
|
|
```typescript
|
|
const response = await offlineAwareApi.getAllBeneficiaries();
|
|
if (!response.ok) {
|
|
Alert.alert('Error', response.error?.message); // User-friendly message
|
|
}
|
|
```
|
|
|
|
### 2. Use Hooks for Data Fetching
|
|
|
|
❌ **Don't:**
|
|
```typescript
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
api.getAllBeneficiaries().then(response => {
|
|
if (response.ok) setData(response.data);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
```
|
|
|
|
✅ **Do:**
|
|
```typescript
|
|
const { data, loading, error, refetch } = useOfflineAwareData(
|
|
() => offlineAwareApi.getAllBeneficiaries(),
|
|
[]
|
|
);
|
|
```
|
|
|
|
### 3. Show Offline Banner
|
|
|
|
```typescript
|
|
function MyScreen() {
|
|
return (
|
|
<SafeAreaView>
|
|
<OfflineBanner />
|
|
{/* Content */}
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 4. Handle Errors Gracefully
|
|
|
|
```typescript
|
|
const { data, loading, error, errorMessage, isOfflineError } = useOfflineAwareData(...);
|
|
|
|
if (error) {
|
|
return (
|
|
<View>
|
|
<Text>{isOfflineError ? 'You\'re Offline' : 'Error'}</Text>
|
|
<Text>{errorMessage}</Text>
|
|
<Button title="Retry" onPress={refetch} />
|
|
</View>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 5. Provide Pull-to-Refresh
|
|
|
|
```typescript
|
|
<FlatList
|
|
data={beneficiaries}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refetching}
|
|
onRefresh={refetch}
|
|
/>
|
|
}
|
|
/>
|
|
```
|
|
|
|
## Examples
|
|
|
|
See `components/examples/OfflineAwareExample.tsx` for complete examples of:
|
|
- Data fetching with offline handling
|
|
- Form mutations with offline detection
|
|
- Error states and retry logic
|
|
- Pull-to-refresh implementation
|
|
|
|
## Limitations
|
|
|
|
Due to the API-first architecture, the app cannot:
|
|
- ❌ Cache beneficiary data for offline use
|
|
- ❌ Queue operations for later sync
|
|
- ❌ Work completely offline
|
|
|
|
However, it provides:
|
|
- ✅ Clear offline state indication
|
|
- ✅ User-friendly error messages
|
|
- ✅ Automatic retry when back online
|
|
- ✅ Graceful degradation
|
|
|
|
## Future Enhancements
|
|
|
|
Potential improvements:
|
|
1. **Optimistic updates** - Show UI changes immediately, sync later
|
|
2. **Offline queue** - Queue write operations when offline
|
|
3. **Limited caching** - Cache read-only data (e.g., subscription plans)
|
|
4. **Background sync** - Sync when connection is restored
|
|
|
|
## Troubleshooting
|
|
|
|
### Banner not showing
|
|
- Check that `@react-native-community/netinfo` is installed
|
|
- Verify `useNetworkStatus()` hook is being called
|
|
- Test on device (not simulator for network issues)
|
|
|
|
### API calls not detecting offline
|
|
- Import from `offlineAwareApi` not `api`
|
|
- Check network permissions in app.json
|
|
|
|
### Retries not working
|
|
- Verify `retry: true` is passed in options
|
|
- Check that operation is async and returns ApiResponse
|
|
|
|
## Related Files
|
|
|
|
- `utils/networkStatus.ts` - Network detection utilities
|
|
- `services/offlineAwareApi.ts` - Offline-aware API wrapper
|
|
- `components/OfflineBanner.tsx` - Offline banner UI
|
|
- `hooks/useOfflineAwareData.ts` - Data fetching hooks
|
|
- `components/examples/OfflineAwareExample.tsx` - Usage examples
|
|
- `__tests__/offline/` - Tests
|