// BLE Context - Global state for Bluetooth management import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable, checkBLEReadiness, BulkOperationResult, BulkWiFiResult, ReconnectConfig, ReconnectState, BLEConnectionState, } from '@/services/ble'; import { setOnLogoutBLECleanupCallback } from '@/services/api'; import { BleManager } from 'react-native-ble-plx'; interface BLEContextType { // State foundDevices: WPDevice[]; isScanning: boolean; connectedDevices: Set; isBLEAvailable: boolean; error: string | null; permissionError: boolean; // true if error is related to permissions reconnectingDevices: Set; // Devices currently attempting to reconnect // Actions scanDevices: () => Promise; stopScan: () => void; connectDevice: (deviceId: string) => Promise; disconnectDevice: (deviceId: string) => Promise; getWiFiList: (deviceId: string) => Promise; setWiFi: (deviceId: string, ssid: string, password: string) => Promise; getCurrentWiFi: (deviceId: string) => Promise; rebootDevice: (deviceId: string) => Promise; cleanupBLE: () => Promise; clearError: () => void; checkPermissions: () => Promise; // Manual permission check with UI prompts // Bulk operations bulkDisconnect: (deviceIds: string[]) => Promise; bulkReboot: (deviceIds: string[]) => Promise; bulkSetWiFi: ( devices: { id: string; name: string }[], ssid: string, password: string, onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void ) => Promise; // Reconnect functionality enableAutoReconnect: (deviceId: string, deviceName?: string) => void; disableAutoReconnect: (deviceId: string) => void; manualReconnect: (deviceId: string) => Promise; cancelReconnect: (deviceId: string) => void; getReconnectState: (deviceId: string) => ReconnectState | undefined; setReconnectConfig: (config: Partial) => void; getConnectionState: (deviceId: string) => BLEConnectionState; } const BLEContext = createContext(undefined); export function BLEProvider({ children }: { children: ReactNode }) { const [foundDevices, setFoundDevices] = useState([]); const [isScanning, setIsScanning] = useState(false); const [connectedDevices, setConnectedDevices] = useState>(new Set()); const [reconnectingDevices, setReconnectingDevices] = useState>(new Set()); const [error, setError] = useState(null); const [permissionError, setPermissionError] = useState(false); // Lazy BleManager instance for permission checks const [bleManagerInstance] = useState(() => new BleManager()); const isPermissionError = (errorMessage: string): boolean => { const permissionKeywords = [ 'permission', 'unauthorized', 'not granted', 'denied', 'bluetooth is disabled', 'enable it in settings', ]; const lowerMessage = errorMessage.toLowerCase(); return permissionKeywords.some(keyword => lowerMessage.includes(keyword)); }; const checkPermissions = useCallback(async (): Promise => { try { setError(null); setPermissionError(false); const ready = await checkBLEReadiness(bleManagerInstance); return ready; } catch { const errorMsg = 'Failed to check Bluetooth permissions'; setError(errorMsg); setPermissionError(true); return false; } }, [bleManagerInstance]); const scanDevices = useCallback(async () => { try { setError(null); setPermissionError(false); setIsScanning(true); const devices = await bleManager.scanDevices(); // Sort by RSSI (strongest first) const sorted = devices.sort((a, b) => b.rssi - a.rssi); setFoundDevices(sorted); } catch (err: any) { const errorMsg = err.message || 'Failed to scan for devices'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); throw err; } finally { setIsScanning(false); } }, []); const stopScan = useCallback(() => { bleManager.stopScan(); setIsScanning(false); }, []); const connectDevice = useCallback(async (deviceId: string): Promise => { try { setError(null); setPermissionError(false); const success = await bleManager.connectDevice(deviceId); if (success) { setConnectedDevices(prev => new Set(prev).add(deviceId)); } else { // Connection failed but no exception - set user-friendly error const errorMsg = 'Could not connect to sensor. Please move closer and try again.'; setError(errorMsg); setPermissionError(false); } return success; } catch (err: any) { const errorMsg = err.message || 'Failed to connect to device'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); return false; } }, []); const disconnectDevice = useCallback(async (deviceId: string): Promise => { try { await bleManager.disconnectDevice(deviceId); setConnectedDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); } catch (err: any) { setError(err.message || 'Failed to disconnect device'); } }, []); const getWiFiList = useCallback(async (deviceId: string): Promise => { try { setError(null); const networks = await bleManager.getWiFiList(deviceId); return networks; } catch (err: any) { setError(err.message || 'Failed to get WiFi networks'); throw err; } }, []); const setWiFi = useCallback( async (deviceId: string, ssid: string, password: string): Promise => { try { setError(null); return await bleManager.setWiFi(deviceId, ssid, password); } catch (err: any) { setError(err.message || 'Failed to configure WiFi'); throw err; } }, [] ); const getCurrentWiFi = useCallback( async (deviceId: string): Promise => { try { setError(null); return await bleManager.getCurrentWiFi(deviceId); } catch (err: any) { setError(err.message || 'Failed to get current WiFi'); throw err; } }, [] ); const rebootDevice = useCallback(async (deviceId: string): Promise => { try { setError(null); await bleManager.rebootDevice(deviceId); // Remove from connected devices setConnectedDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); } catch (err: any) { setError(err.message || 'Failed to reboot device'); throw err; } }, []); const clearError = useCallback(() => { setError(null); setPermissionError(false); }, []); const cleanupBLE = useCallback(async () => { try { // Stop any ongoing scan if (isScanning) { stopScan(); } // Cleanup via bleManager (disconnects all devices) await bleManager.cleanup(); // Clear context state setFoundDevices([]); setConnectedDevices(new Set()); setError(null); } catch { // Don't throw - we want to allow logout to proceed even if BLE cleanup fails } }, [isScanning, stopScan]); // Bulk operations const bulkDisconnect = useCallback(async (deviceIds: string[]): Promise => { try { setError(null); setPermissionError(false); const results = await bleManager.bulkDisconnect(deviceIds); // Update connected devices set const successfulDisconnects = results.filter(r => r.success).map(r => r.deviceId); if (successfulDisconnects.length > 0) { setConnectedDevices(prev => { const next = new Set(prev); successfulDisconnects.forEach(id => next.delete(id)); return next; }); } return results; } catch (err: any) { const errorMsg = err.message || 'Bulk disconnect failed'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); throw err; } }, []); const bulkReboot = useCallback(async (deviceIds: string[]): Promise => { try { setError(null); setPermissionError(false); const results = await bleManager.bulkReboot(deviceIds); // Devices that were rebooted are no longer connected const successfulReboots = results.filter(r => r.success).map(r => r.deviceId); if (successfulReboots.length > 0) { setConnectedDevices(prev => { const next = new Set(prev); successfulReboots.forEach(id => next.delete(id)); return next; }); } return results; } catch (err: any) { const errorMsg = err.message || 'Bulk reboot failed'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); throw err; } }, []); const bulkSetWiFi = useCallback(async ( devices: { id: string; name: string }[], ssid: string, password: string, onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void ): Promise => { try { setError(null); setPermissionError(false); const results = await bleManager.bulkSetWiFi(devices, ssid, password, onProgress); // Update connected devices - successful setups result in reboots (disconnected) const successfulSetups = results.filter(r => r.success).map(r => r.deviceId); if (successfulSetups.length > 0) { setConnectedDevices(prev => { const next = new Set(prev); successfulSetups.forEach(id => next.delete(id)); return next; }); } return results; } catch (err: any) { const errorMsg = err.message || 'Bulk WiFi configuration failed'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); throw err; } }, []); // Reconnect functionality const enableAutoReconnect = useCallback((deviceId: string, deviceName?: string) => { bleManager.enableAutoReconnect(deviceId, deviceName); }, []); const disableAutoReconnect = useCallback((deviceId: string) => { bleManager.disableAutoReconnect(deviceId); setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); }, []); const manualReconnect = useCallback(async (deviceId: string): Promise => { try { setError(null); setPermissionError(false); setReconnectingDevices(prev => new Set(prev).add(deviceId)); const success = await bleManager.manualReconnect(deviceId); if (success) { setConnectedDevices(prev => new Set(prev).add(deviceId)); } setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); return success; } catch (err: any) { const errorMsg = err.message || 'Reconnection failed'; setError(errorMsg); setPermissionError(isPermissionError(errorMsg)); setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); return false; } }, []); const cancelReconnect = useCallback((deviceId: string) => { bleManager.cancelReconnect(deviceId); setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); }, []); const getReconnectState = useCallback((deviceId: string): ReconnectState | undefined => { return bleManager.getReconnectState(deviceId); }, []); const setReconnectConfig = useCallback((config: Partial) => { bleManager.setReconnectConfig(config); }, []); const getConnectionState = useCallback((deviceId: string): BLEConnectionState => { return bleManager.getConnectionState(deviceId); }, []); // Register BLE cleanup callback for logout useEffect(() => { setOnLogoutBLECleanupCallback(cleanupBLE); return () => { setOnLogoutBLECleanupCallback(null); }; }, [cleanupBLE]); // Set up BLE event listeners for connection state changes useEffect(() => { const handleBLEEvent = (deviceId: string, event: string, data?: any) => { switch (event) { case 'disconnected': // Device unexpectedly disconnected setConnectedDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); // Check if auto-reconnect is in progress if (data?.unexpected) { const reconnectState = bleManager.getReconnectState(deviceId); if (reconnectState?.isReconnecting) { setReconnectingDevices(prev => new Set(prev).add(deviceId)); } } break; case 'ready': // Device connected or reconnected setConnectedDevices(prev => new Set(prev).add(deviceId)); setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); break; case 'connection_failed': // Check if max reconnect attempts reached if (data?.reconnectFailed) { setReconnectingDevices(prev => { const next = new Set(prev); next.delete(deviceId); return next; }); setError(`Failed to reconnect to device after multiple attempts`); } break; case 'state_changed': if (data?.reconnecting) { setReconnectingDevices(prev => new Set(prev).add(deviceId)); } break; } }; bleManager.addEventListener(handleBLEEvent); return () => { bleManager.removeEventListener(handleBLEEvent); }; }, []); const value: BLEContextType = { foundDevices, isScanning, connectedDevices, reconnectingDevices, isBLEAvailable, error, permissionError, scanDevices, stopScan, connectDevice, disconnectDevice, getWiFiList, setWiFi, getCurrentWiFi, rebootDevice, cleanupBLE, clearError, checkPermissions, bulkDisconnect, bulkReboot, bulkSetWiFi, // Reconnect functionality enableAutoReconnect, disableAutoReconnect, manualReconnect, cancelReconnect, getReconnectState, setReconnectConfig, getConnectionState, }; return {children}; } export function useBLE() { const context = useContext(BLEContext); if (context === undefined) { throw new Error('useBLE must be used within a BLEProvider'); } return context; }