/** * Offline-Aware API Wrapper * * Wraps API calls with offline detection and graceful error handling. * Provides consistent error messages and retry logic for network failures. */ import { api } from './api'; import { isOnline, retryWithBackoff, DEFAULT_RETRY_CONFIG, RetryConfig } from '@/utils/networkStatus'; import type { ApiResponse, ApiError } from '@/types'; /** * Network-related error codes */ export const NETWORK_ERROR_CODES = { OFFLINE: 'NETWORK_OFFLINE', TIMEOUT: 'NETWORK_TIMEOUT', UNREACHABLE: 'NETWORK_UNREACHABLE', } as const; /** * User-friendly error messages for network issues */ export const NETWORK_ERROR_MESSAGES = { 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 error occurred. Please check your connection.', } as const; /** * Check if error is network-related */ export function isNetworkError(error: ApiError): boolean { if (!error) return false; const code = error.code?.toUpperCase() || ''; const message = error.message?.toLowerCase() || ''; return ( code === 'NETWORK_ERROR' || code === 'NETWORK_OFFLINE' || code === 'NETWORK_TIMEOUT' || code === 'NETWORK_UNREACHABLE' || message.includes('network') || message.includes('offline') || message.includes('connection') || message.includes('timeout') || message.includes('fetch') ); } /** * Get user-friendly error message for network errors */ export function getNetworkErrorMessage(error: ApiError): string { if (!isNetworkError(error)) { return error.message || 'An error occurred'; } const code = error.code?.toUpperCase(); if (code === 'NETWORK_OFFLINE') return NETWORK_ERROR_MESSAGES.OFFLINE; if (code === 'NETWORK_TIMEOUT') return NETWORK_ERROR_MESSAGES.TIMEOUT; if (code === 'NETWORK_UNREACHABLE') return NETWORK_ERROR_MESSAGES.UNREACHABLE; return NETWORK_ERROR_MESSAGES.GENERIC; } /** * Wrap an API call with offline detection * * @param apiCall - The API function to call * @param options - Configuration options * @returns Promise with API response * * @example * const response = await withOfflineCheck(() => api.getAllBeneficiaries()); * if (!response.ok) { * Alert.alert('Error', getNetworkErrorMessage(response.error)); * } */ export async function withOfflineCheck( apiCall: () => Promise>, options: { retry?: boolean; retryConfig?: RetryConfig; offlineMessage?: string; } = {} ): Promise> { // Check if online before attempting request const online = await isOnline(); if (!online) { return { ok: false, error: { message: options.offlineMessage || NETWORK_ERROR_MESSAGES.OFFLINE, code: NETWORK_ERROR_CODES.OFFLINE, }, }; } try { // Execute API call with optional retry if (options.retry) { return await retryWithBackoff(apiCall, options.retryConfig || DEFAULT_RETRY_CONFIG); } return await apiCall(); } catch (error) { // Convert exception to ApiResponse format const apiError: ApiError = { message: error instanceof Error ? error.message : 'Unknown error', code: 'NETWORK_ERROR', }; return { ok: false, error: apiError, }; } } /** * Offline-aware API service * Wraps the main API service with network detection * * Use this instead of direct `api` imports for better offline handling */ export const offlineAwareApi = { // ==================== Authentication ==================== async checkEmail(email: string) { return withOfflineCheck(() => api.checkEmail(email), { retry: true, offlineMessage: 'Cannot verify email while offline', }); }, async requestOTP(email: string) { return withOfflineCheck(() => api.requestOTP(email), { retry: true, offlineMessage: 'Cannot send verification code while offline', }); }, async verifyOTP(email: string, code: string) { return withOfflineCheck(() => api.verifyOTP(email, code), { retry: true, offlineMessage: 'Cannot verify code while offline', }); }, async getProfile() { return withOfflineCheck(() => api.getProfile(), { retry: true, }); }, async updateProfile(updates: Parameters[0]) { return withOfflineCheck(() => api.updateProfile(updates), { offlineMessage: 'Cannot update profile while offline', }); }, async updateProfileAvatar(imageUri: string | null) { return withOfflineCheck(() => api.updateProfileAvatar(imageUri), { offlineMessage: 'Cannot upload avatar while offline', }); }, // ==================== Beneficiaries ==================== async getAllBeneficiaries() { return withOfflineCheck(() => api.getAllBeneficiaries(), { retry: true, }); }, async getWellNuoBeneficiary(id: number) { return withOfflineCheck(() => api.getWellNuoBeneficiary(id), { retry: true, }); }, async createBeneficiary(data: Parameters[0]) { return withOfflineCheck(() => api.createBeneficiary(data), { offlineMessage: 'Cannot add beneficiary while offline', }); }, async updateWellNuoBeneficiary(id: number, updates: Parameters[1]) { return withOfflineCheck(() => api.updateWellNuoBeneficiary(id, updates), { offlineMessage: 'Cannot update beneficiary while offline', }); }, async updateBeneficiaryAvatar(id: number, imageUri: string | null) { return withOfflineCheck(() => api.updateBeneficiaryAvatar(id, imageUri), { offlineMessage: 'Cannot upload avatar while offline', }); }, async updateBeneficiaryCustomName(id: number, customName: string | null) { return withOfflineCheck(() => api.updateBeneficiaryCustomName(id, customName), { offlineMessage: 'Cannot update name while offline', }); }, async deleteBeneficiary(id: number) { return withOfflineCheck(() => api.deleteBeneficiary(id), { offlineMessage: 'Cannot remove beneficiary while offline', }); }, // ==================== Devices / Sensors ==================== async getDevicesForBeneficiary(beneficiaryId: string) { return withOfflineCheck(() => api.getDevicesForBeneficiary(beneficiaryId), { retry: true, }); }, async attachDeviceToBeneficiary(beneficiaryId: string, wellId: number, deviceMac: string) { return withOfflineCheck(() => api.attachDeviceToBeneficiary(beneficiaryId, wellId, deviceMac), { offlineMessage: 'Cannot attach sensor while offline', }); }, async updateDeviceMetadata(deviceId: string, updates: Parameters[1]) { return withOfflineCheck(() => api.updateDeviceMetadata(deviceId, updates), { offlineMessage: 'Cannot update sensor settings while offline', }); }, async detachDeviceFromBeneficiary(beneficiaryId: string, deviceId: string) { return withOfflineCheck(() => api.detachDeviceFromBeneficiary(beneficiaryId, deviceId), { offlineMessage: 'Cannot remove sensor while offline', }); }, async getSensorHealthHistory(deviceId: string, timeRange: '24h' | '7d' | '30d' = '24h') { return withOfflineCheck(() => api.getSensorHealthHistory(deviceId, timeRange), { retry: true, }); }, // ==================== Subscriptions ==================== async cancelSubscription(beneficiaryId: number) { return withOfflineCheck(() => api.cancelSubscription(beneficiaryId), { offlineMessage: 'Cannot cancel subscription while offline', }); }, async reactivateSubscription(beneficiaryId: number) { return withOfflineCheck(() => api.reactivateSubscription(beneficiaryId), { offlineMessage: 'Cannot reactivate subscription while offline', }); }, async getTransactionHistory(beneficiaryId: number, limit = 10) { return withOfflineCheck(() => api.getTransactionHistory(beneficiaryId, limit), { retry: true, }); }, // ==================== Invitations ==================== async sendInvitation(params: Parameters[0]) { return withOfflineCheck(() => api.sendInvitation(params), { offlineMessage: 'Cannot send invitation while offline', }); }, async getInvitations(beneficiaryId: string) { return withOfflineCheck(() => api.getInvitations(beneficiaryId), { retry: true, }); }, async deleteInvitation(invitationId: string) { return withOfflineCheck(() => api.deleteInvitation(invitationId), { offlineMessage: 'Cannot delete invitation while offline', }); }, async updateInvitation(invitationId: string, role: 'caretaker' | 'guardian') { return withOfflineCheck(() => api.updateInvitation(invitationId, role), { offlineMessage: 'Cannot update invitation while offline', }); }, async acceptInvitation(code: string) { return withOfflineCheck(() => api.acceptInvitation(code), { offlineMessage: 'Cannot accept invitation while offline', }); }, // ==================== Notifications ==================== async getNotificationSettings() { return withOfflineCheck(() => api.getNotificationSettings(), { retry: true, }); }, async updateNotificationSettings(settings: Parameters[0]) { return withOfflineCheck(() => api.updateNotificationSettings(settings), { offlineMessage: 'Cannot update notification settings while offline', }); }, async getNotificationHistory(options?: Parameters[0]) { return withOfflineCheck(() => api.getNotificationHistory(options), { retry: true, }); }, // ==================== AI Chat (Legacy API) ==================== async sendMessage(question: string, deploymentId: string) { return withOfflineCheck(() => api.sendMessage(question, deploymentId), { offlineMessage: 'Cannot send message while offline', }); }, // ==================== Equipment Activation ==================== async activateBeneficiary(beneficiaryId: number, serialNumber: string) { return withOfflineCheck(() => api.activateBeneficiary(beneficiaryId, serialNumber), { offlineMessage: 'Cannot activate equipment while offline', }); }, async updateBeneficiaryEquipmentStatus( id: number, status: 'none' | 'ordered' | 'shipped' | 'delivered' ) { return withOfflineCheck(() => api.updateBeneficiaryEquipmentStatus(id, status), { offlineMessage: 'Cannot update equipment status while offline', }); }, }; /** * Export utility functions */ export { isNetworkError as isOfflineError, getNetworkErrorMessage as getOfflineErrorMessage };