/** * WiFi Setup Screen * * Allows user to configure WiFi on WellNuo ESP32 sensors via Bluetooth. * Uses @orbital-systems/react-native-esp-idf-provisioning for BLE communication. */ import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, ActivityIndicator, Alert, FlatList, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights } from '@/constants/theme'; import { espProvisioning, type WellNuoDevice, type WifiNetwork, } from '@/services/espProvisioning'; import { validateWiFiCredentials, getValidationErrorMessage, sanitizeWiFiCredentials, } from '@/utils/wifiValidation'; import { WiFiSignalIndicator, getSignalStrengthLabel, getSignalStrengthColor, } from '@/components/WiFiSignalIndicator'; type Step = 'scan' | 'connect' | 'wifi-select' | 'wifi-password' | 'provisioning' | 'complete'; export default function WifiSetupScreen() { const params = useLocalSearchParams<{ lovedOneName?: string; beneficiaryId?: string }>(); const lovedOneName = params.lovedOneName || ''; // State const [step, setStep] = useState('scan'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Devices const [devices, setDevices] = useState([]); const [selectedDevice, setSelectedDevice] = useState(null); // WiFi const [wifiNetworks, setWifiNetworks] = useState([]); const [selectedWifi, setSelectedWifi] = useState(null); const [wifiPassword, setWifiPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); // Cleanup on unmount useEffect(() => { return () => { espProvisioning.disconnect(); }; }, []); // Step 1: Scan for BLE devices const handleScanDevices = useCallback(async () => { setIsLoading(true); setError(null); setDevices([]); try { const foundDevices = await espProvisioning.scanForDevices(10000); if (foundDevices.length === 0) { setError('No WellNuo sensors found. Make sure your sensor is powered on and in setup mode.'); } else { setDevices(foundDevices); } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; setError(`Failed to scan: ${errorMessage}`); } finally { setIsLoading(false); } }, []); // Auto-scan on mount useEffect(() => { handleScanDevices(); }, [handleScanDevices]); // Step 2: Connect to selected device const handleSelectDevice = async (device: WellNuoDevice) => { setSelectedDevice(device); setStep('connect'); setIsLoading(true); setError(null); try { await espProvisioning.connect(device.device); setStep('wifi-select'); // Auto-scan WiFi networks after connecting handleScanWifi(); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; setError(`Failed to connect: ${errorMessage}`); setStep('scan'); } finally { setIsLoading(false); } }; // Step 3: Scan for WiFi networks const handleScanWifi = async () => { setIsLoading(true); setError(null); setWifiNetworks([]); try { const networks = await espProvisioning.scanWifiNetworks(); // Sort by signal strength const sorted = networks.sort((a, b) => b.rssi - a.rssi); setWifiNetworks(sorted); if (sorted.length === 0) { setError('No WiFi networks found. Make sure you are in range of your WiFi network.'); } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; setError(`Failed to scan WiFi: ${errorMessage}`); } finally { setIsLoading(false); } }; // Step 4: Select WiFi network const handleSelectWifi = (network: WifiNetwork) => { setSelectedWifi(network); setWifiPassword(''); setStep('wifi-password'); }; // Step 5: Provision with password const handleProvision = async () => { if (!selectedWifi) return; // Validate credentials before provisioning const validation = validateWiFiCredentials( selectedWifi.ssid, wifiPassword, selectedWifi.auth ); if (!validation.valid) { const errorMsg = getValidationErrorMessage(validation); setError(errorMsg); return; } // Show warning if present (but allow to continue) if (validation.warnings.length > 0) { const warningMsg = validation.warnings.join('\n'); Alert.alert( 'Warning', `${warningMsg}\n\nDo you want to continue?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Continue', onPress: () => provisionDevice(), }, ] ); return; } // No warnings, proceed directly await provisionDevice(); }; // Actual provisioning logic const provisionDevice = async () => { if (!selectedWifi) return; setStep('provisioning'); setIsLoading(true); setError(null); try { // Sanitize credentials before sending const sanitized = sanitizeWiFiCredentials({ ssid: selectedWifi.ssid, password: wifiPassword, }); await espProvisioning.provisionWifi(sanitized.ssid, sanitized.password); setStep('complete'); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; setError(`Failed to configure WiFi: ${errorMessage}`); setStep('wifi-password'); } finally { setIsLoading(false); } }; // Complete and navigate const handleComplete = async () => { await espProvisioning.disconnect(); // Navigate to activate screen or back if (params.beneficiaryId) { router.replace({ pathname: '/(auth)/activate', params: { beneficiaryId: params.beneficiaryId, lovedOneName }, }); } else { router.back(); } }; // Signal strength rendering removed - using WiFiSignalIndicator component instead // Step 1: Scan for devices if (step === 'scan' || step === 'connect') { return ( {/* Header */} router.back()}> WiFi Setup {/* Icon */} {/* Instructions */} {step === 'connect' ? `Connecting to ${selectedDevice?.name}...` : 'Searching for WellNuo sensors nearby...'} {/* Loading or device list */} {isLoading ? ( {step === 'connect' ? 'Connecting...' : 'Scanning for devices...'} ) : error ? ( {error} Retry Scan ) : ( item.name} style={styles.deviceList} renderItem={({ item }) => ( handleSelectDevice(item)} > {item.name} {item.wellId && ( Sensor ID: {item.wellId} )} )} ListEmptyComponent={ No devices found } /> )} {/* Rescan button */} {!isLoading && devices.length > 0 && ( Scan Again )} ); } // Step 3: Select WiFi network if (step === 'wifi-select') { return ( {/* Header */} { await espProvisioning.disconnect(); setStep('scan'); }} > Select WiFi {/* Connected device info */} Connected to {selectedDevice?.name} {/* Instructions */} Select the WiFi network for your sensor {/* Loading or WiFi list */} {isLoading ? ( Scanning WiFi networks... ) : error ? ( {error} Retry Scan ) : ( `${item.ssid}-${index}`} style={styles.deviceList} renderItem={({ item }) => ( handleSelectWifi(item)} > {item.ssid} {getSignalStrengthLabel(item.rssi)} • {item.auth} )} ListEmptyComponent={ No WiFi networks found } /> )} {/* Rescan button */} {!isLoading && ( Scan Again )} ); } // Step 4: Enter WiFi password if (step === 'wifi-password' || step === 'provisioning') { return ( {/* Header */} setStep('wifi-select')} disabled={step === 'provisioning'} > Enter Password {/* WiFi icon */} {/* Selected network */} Network: {selectedWifi?.ssid} {/* Password input */} setShowPassword(!showPassword)} > {/* Error */} {error && ( {error} )} {/* Connect button */} {step === 'provisioning' ? ( <> Configuring... ) : ( Connect to WiFi )} {/* Note for open networks */} {selectedWifi?.auth === 'Open' && ( This is an open network. You can leave the password empty. )} {/* Password requirements */} {selectedWifi?.auth?.includes('WPA') && ( Password must be 8-63 characters for WPA/WPA2/WPA3 networks. )} ); } // Step 5: Complete return ( {/* Success */} WiFi Configured! Your sensor {selectedDevice?.name} is now connected to {selectedWifi?.ssid} {/* Next steps */} What happens next: Sensor will connect to your WiFi Data will start syncing to WellNuo You'll see updates in the dashboard {/* Complete button */} Continue ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: AppColors.background, }, content: { flex: 1, padding: Spacing.lg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.xl, }, backButton: { padding: Spacing.sm, marginLeft: -Spacing.sm, }, title: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, }, placeholder: { width: 40, }, iconContainer: { alignItems: 'center', marginBottom: Spacing.lg, }, instructions: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', marginBottom: Spacing.lg, }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, loadingText: { marginTop: Spacing.md, fontSize: FontSizes.base, color: AppColors.textSecondary, }, errorContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: Spacing.lg, }, errorText: { marginTop: Spacing.md, fontSize: FontSizes.base, color: AppColors.error, textAlign: 'center', }, retryButton: { marginTop: Spacing.lg, paddingHorizontal: Spacing.xl, paddingVertical: Spacing.md, backgroundColor: AppColors.primary, borderRadius: BorderRadius.lg, }, retryButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, deviceList: { flex: 1, marginBottom: Spacing.md, }, deviceItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, marginBottom: Spacing.sm, gap: Spacing.md, }, deviceIcon: { width: 40, height: 40, borderRadius: 20, backgroundColor: `${AppColors.primary}20`, alignItems: 'center', justifyContent: 'center', }, deviceInfo: { flex: 1, }, deviceName: { fontSize: FontSizes.base, fontWeight: FontWeights.medium, color: AppColors.textPrimary, }, deviceMeta: { fontSize: FontSizes.sm, color: AppColors.textMuted, marginTop: 2, }, emptyText: { fontSize: FontSizes.base, color: AppColors.textMuted, textAlign: 'center', marginTop: Spacing.xl, }, secondaryButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.sm, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, borderWidth: 1, borderColor: AppColors.primary, }, secondaryButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.medium, color: AppColors.primary, }, connectedDevice: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.sm, backgroundColor: `${AppColors.success}15`, paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, borderRadius: BorderRadius.md, marginBottom: Spacing.md, }, connectedText: { fontSize: FontSizes.sm, color: AppColors.success, fontWeight: FontWeights.medium, }, selectedNetwork: { alignItems: 'center', marginBottom: Spacing.lg, }, selectedNetworkLabel: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, selectedNetworkName: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginTop: 4, }, inputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, borderWidth: 1, borderColor: AppColors.border, marginBottom: Spacing.md, }, input: { flex: 1, paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, fontSize: FontSizes.base, color: AppColors.textPrimary, }, showPasswordButton: { padding: Spacing.md, }, inlineError: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.md, }, inlineErrorText: { fontSize: FontSizes.sm, color: AppColors.error, flex: 1, }, primaryButton: { flexDirection: 'row', backgroundColor: AppColors.primary, paddingVertical: Spacing.lg, borderRadius: BorderRadius.lg, alignItems: 'center', justifyContent: 'center', marginTop: Spacing.md, }, buttonDisabled: { opacity: 0.7, }, primaryButtonText: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.white, }, noteText: { fontSize: FontSizes.sm, color: AppColors.textMuted, textAlign: 'center', marginTop: Spacing.md, }, successContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, successIcon: { marginBottom: Spacing.xl, }, successTitle: { fontSize: FontSizes['2xl'], fontWeight: FontWeights.bold, color: AppColors.textPrimary, marginBottom: Spacing.md, }, successMessage: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', marginBottom: Spacing.xl, paddingHorizontal: Spacing.lg, }, highlight: { fontWeight: FontWeights.bold, color: AppColors.primary, }, nextSteps: { width: '100%', backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.lg, }, nextStepsTitle: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginBottom: Spacing.md, }, stepItem: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm, }, stepText: { fontSize: FontSizes.sm, color: AppColors.textSecondary, flex: 1, }, });