WellNuo/docs/OFFLINE_MODE.md
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

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