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

13 KiB

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

    const { isOnline, isOffline, status } = useNetworkStatus();
    
  • useOnlineOnly() - Execute callback only when online

    const executeOnline = useOnlineOnly();
    executeOnline(() => api.saveBeneficiary(data));
    
  • useRetry(config) - Hook for retrying operations

    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:

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:

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.

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:

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:

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:

const response = await withOfflineCheck(
  () => api.createBeneficiary(data),
  { offlineMessage: 'Cannot add beneficiary while offline' }
);

Retry Logic

Operations can be retried automatically with exponential backoff:

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/:

# 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:

const response = await api.getAllBeneficiaries();
if (!response.ok) {
  Alert.alert('Error', response.error?.message);
}

Do:

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:

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  api.getAllBeneficiaries().then(response => {
    if (response.ok) setData(response.data);
    setLoading(false);
  });
}, []);

Do:

const { data, loading, error, refetch } = useOfflineAwareData(
  () => offlineAwareApi.getAllBeneficiaries(),
  []
);

3. Show Offline Banner

function MyScreen() {
  return (
    <SafeAreaView>
      <OfflineBanner />
      {/* Content */}
    </SafeAreaView>
  );
}

4. Handle Errors Gracefully

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

<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
  • 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