Removed all console.log, console.error, console.warn, console.info, and console.debug statements from the main source code to clean up production output. Changes: - Removed 400+ console statements from TypeScript/TSX files - Cleaned BLE services (BLEManager.ts, MockBLEManager.ts) - Cleaned API services, contexts, hooks, and components - Cleaned WiFi setup and sensor management screens - Preserved console statements in test files (*.test.ts, __tests__/) - TypeScript compilation verified successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1015 lines
29 KiB
TypeScript
1015 lines
29 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
TextInput,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import * as Device from 'expo-device';
|
|
import { useBLE } from '@/contexts/BLEContext';
|
|
import { api } from '@/services/api';
|
|
import * as wifiPasswordStore from '@/services/wifiPasswordStore';
|
|
import type { WiFiNetwork } from '@/services/ble';
|
|
import type {
|
|
SensorSetupState,
|
|
SensorSetupStep,
|
|
SensorSetupStatus,
|
|
} from '@/types';
|
|
import BatchSetupProgress from '@/components/BatchSetupProgress';
|
|
import SetupResultsScreen from '@/components/SetupResultsScreen';
|
|
import {
|
|
AppColors,
|
|
BorderRadius,
|
|
FontSizes,
|
|
FontWeights,
|
|
Spacing,
|
|
Shadows,
|
|
} from '@/constants/theme';
|
|
|
|
// Type for device passed via navigation params
|
|
interface DeviceParam {
|
|
id: string;
|
|
name: string;
|
|
mac: string;
|
|
wellId?: number;
|
|
}
|
|
|
|
type SetupPhase = 'wifi_selection' | 'batch_setup' | 'results';
|
|
|
|
// Initialize steps for a sensor
|
|
function createInitialSteps(): SensorSetupStep[] {
|
|
return [
|
|
{ name: 'connect', status: 'pending' },
|
|
{ name: 'unlock', status: 'pending' },
|
|
{ name: 'wifi', status: 'pending' },
|
|
{ name: 'attach', status: 'pending' },
|
|
{ name: 'reboot', status: 'pending' },
|
|
];
|
|
}
|
|
|
|
// Initialize sensor state
|
|
function createSensorState(device: DeviceParam): SensorSetupState {
|
|
return {
|
|
deviceId: device.id,
|
|
deviceName: device.name,
|
|
wellId: device.wellId,
|
|
mac: device.mac,
|
|
status: 'pending',
|
|
steps: createInitialSteps(),
|
|
};
|
|
}
|
|
|
|
export default function SetupWiFiScreen() {
|
|
const { id, devices: devicesParam } = useLocalSearchParams<{
|
|
id: string;
|
|
devices: string; // JSON string of DeviceParam[]
|
|
}>();
|
|
|
|
const {
|
|
getWiFiList,
|
|
setWiFi,
|
|
connectDevice,
|
|
disconnectDevice,
|
|
rebootDevice,
|
|
} = useBLE();
|
|
|
|
// Parse devices from navigation params
|
|
const selectedDevices: DeviceParam[] = React.useMemo(() => {
|
|
if (!devicesParam) return [];
|
|
try {
|
|
return JSON.parse(devicesParam);
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}, [devicesParam]);
|
|
|
|
// Use first device for WiFi scanning
|
|
const firstDevice = selectedDevices[0];
|
|
const deviceId = firstDevice?.id;
|
|
|
|
// UI Phase
|
|
const [phase, setPhase] = useState<SetupPhase>('wifi_selection');
|
|
|
|
// WiFi selection state
|
|
const [networks, setNetworks] = useState<WiFiNetwork[]>([]);
|
|
const [isLoadingNetworks, setIsLoadingNetworks] = useState(false);
|
|
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork | null>(null);
|
|
const [password, setPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
// Batch setup state
|
|
const [sensors, setSensors] = useState<SensorSetupState[]>([]);
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const setupInProgressRef = useRef(false);
|
|
const shouldCancelRef = useRef(false);
|
|
|
|
// Saved WiFi passwords map (SSID -> password)
|
|
// Using useState to trigger re-renders when passwords are loaded
|
|
const [savedPasswords, setSavedPasswords] = useState<Record<string, string>>({});
|
|
const [passwordsLoaded, setPasswordsLoaded] = useState(false);
|
|
// Also keep ref for saving (to avoid stale closures)
|
|
const savedPasswordsRef = useRef<Record<string, string>>({});
|
|
|
|
// Load saved WiFi passwords on mount
|
|
useEffect(() => {
|
|
const loadSavedPasswords = async () => {
|
|
try {
|
|
// Migrate from AsyncStorage to SecureStore if needed
|
|
await wifiPasswordStore.migrateFromAsyncStorage();
|
|
|
|
// Load all saved passwords from SecureStore
|
|
const passwords = await wifiPasswordStore.getAllWiFiPasswords();
|
|
savedPasswordsRef.current = passwords;
|
|
setSavedPasswords(passwords);
|
|
} catch (error) {
|
|
} finally {
|
|
setPasswordsLoaded(true);
|
|
}
|
|
};
|
|
loadSavedPasswords();
|
|
loadWiFiNetworks();
|
|
}, []);
|
|
|
|
const loadWiFiNetworks = async () => {
|
|
if (!deviceId) {
|
|
return;
|
|
}
|
|
setIsLoadingNetworks(true);
|
|
|
|
try {
|
|
// First connect to the device before requesting WiFi list
|
|
const connected = await connectDevice(deviceId);
|
|
|
|
if (!connected) {
|
|
throw new Error('Could not connect to sensor. Please move closer and try again.');
|
|
}
|
|
|
|
const wifiList = await getWiFiList(deviceId);
|
|
setNetworks(wifiList);
|
|
} catch (error: any) {
|
|
Alert.alert('Error', error.message || 'Failed to get WiFi networks. Please try again.');
|
|
} finally {
|
|
setIsLoadingNetworks(false);
|
|
}
|
|
};
|
|
|
|
const handleSelectNetwork = (network: WiFiNetwork) => {
|
|
setSelectedNetwork(network);
|
|
// Auto-fill saved password for this network (use state, not ref)
|
|
const savedPwd = savedPasswords[network.ssid];
|
|
if (savedPwd) {
|
|
setPassword(savedPwd);
|
|
} else {
|
|
setPassword('');
|
|
}
|
|
};
|
|
|
|
// Auto-fill password when passwords finish loading (if network already selected)
|
|
useEffect(() => {
|
|
if (passwordsLoaded && selectedNetwork && !password) {
|
|
const savedPwd = savedPasswords[selectedNetwork.ssid];
|
|
if (savedPwd) {
|
|
setPassword(savedPwd);
|
|
}
|
|
}
|
|
}, [passwordsLoaded, savedPasswords, selectedNetwork]);
|
|
|
|
// Update a specific step for a sensor
|
|
const updateSensorStep = useCallback((
|
|
deviceId: string,
|
|
stepName: SensorSetupStep['name'],
|
|
stepStatus: SensorSetupStep['status'],
|
|
error?: string
|
|
) => {
|
|
setSensors(prev => prev.map(sensor => {
|
|
if (sensor.deviceId !== deviceId) return sensor;
|
|
return {
|
|
...sensor,
|
|
steps: sensor.steps.map(step =>
|
|
step.name === stepName
|
|
? { ...step, status: stepStatus, error }
|
|
: step
|
|
),
|
|
};
|
|
}));
|
|
}, []);
|
|
|
|
// Update sensor status
|
|
const updateSensorStatus = useCallback((
|
|
deviceId: string,
|
|
status: SensorSetupStatus,
|
|
error?: string
|
|
) => {
|
|
setSensors(prev => prev.map(sensor =>
|
|
sensor.deviceId === deviceId
|
|
? { ...sensor, status, error, endTime: Date.now() }
|
|
: sensor
|
|
));
|
|
}, []);
|
|
|
|
// Process a single sensor
|
|
const processSensor = useCallback(async (
|
|
sensor: SensorSetupState,
|
|
ssid: string,
|
|
pwd: string
|
|
): Promise<boolean> => {
|
|
const { deviceId, wellId, deviceName } = sensor;
|
|
const isSimulator = !Device.isDevice;
|
|
|
|
// Set start time
|
|
setSensors(prev => prev.map(s =>
|
|
s.deviceId === deviceId
|
|
? { ...s, startTime: Date.now() }
|
|
: s
|
|
));
|
|
|
|
try {
|
|
// Step 1: Connect
|
|
updateSensorStep(deviceId, 'connect', 'in_progress');
|
|
updateSensorStatus(deviceId, 'connecting');
|
|
const connected = await connectDevice(deviceId);
|
|
if (!connected) throw new Error('Could not connect to sensor');
|
|
updateSensorStep(deviceId, 'connect', 'completed');
|
|
|
|
if (shouldCancelRef.current) return false;
|
|
|
|
// Step 2: Unlock (PIN is handled by connectDevice in BLE manager)
|
|
updateSensorStep(deviceId, 'unlock', 'in_progress');
|
|
updateSensorStatus(deviceId, 'unlocking');
|
|
// PIN unlock is automatic in connectDevice, mark as completed
|
|
updateSensorStep(deviceId, 'unlock', 'completed');
|
|
|
|
if (shouldCancelRef.current) return false;
|
|
|
|
// Step 3: Set WiFi
|
|
updateSensorStep(deviceId, 'wifi', 'in_progress');
|
|
updateSensorStatus(deviceId, 'setting_wifi');
|
|
// setWiFi now throws with detailed error message if it fails
|
|
await setWiFi(deviceId, ssid, pwd);
|
|
updateSensorStep(deviceId, 'wifi', 'completed');
|
|
|
|
if (shouldCancelRef.current) return false;
|
|
|
|
// Step 4: Attach to deployment via API
|
|
updateSensorStep(deviceId, 'attach', 'in_progress');
|
|
updateSensorStatus(deviceId, 'attaching');
|
|
|
|
if (!isSimulator && wellId) {
|
|
const attachResponse = await api.attachDeviceToBeneficiary(
|
|
id!,
|
|
wellId,
|
|
ssid,
|
|
pwd
|
|
);
|
|
if (!attachResponse.ok) {
|
|
const errorDetail = attachResponse.error || 'Unknown API error';
|
|
throw new Error(`Failed to register sensor: ${errorDetail}`);
|
|
}
|
|
}
|
|
updateSensorStep(deviceId, 'attach', 'completed');
|
|
|
|
if (shouldCancelRef.current) return false;
|
|
|
|
// Step 5: Reboot
|
|
// The reboot command will cause the sensor to disconnect (this is expected!)
|
|
// We send the command and immediately mark it as success - no need to wait for response
|
|
updateSensorStep(deviceId, 'reboot', 'in_progress');
|
|
updateSensorStatus(deviceId, 'rebooting');
|
|
|
|
try {
|
|
await rebootDevice(deviceId);
|
|
} catch (rebootError: any) {
|
|
// Ignore BLE errors during reboot - the device disconnects on purpose
|
|
}
|
|
|
|
updateSensorStep(deviceId, 'reboot', 'completed');
|
|
|
|
// Success! The sensor is now rebooting and will connect to WiFi
|
|
updateSensorStatus(deviceId, 'success');
|
|
return true;
|
|
|
|
} catch (error: any) {
|
|
const errorMsg = error.message || 'Unknown error';
|
|
|
|
// Find current step and mark as failed
|
|
setSensors(prev => prev.map(s => {
|
|
if (s.deviceId !== deviceId) return s;
|
|
const currentStep = s.steps.find(step => step.status === 'in_progress');
|
|
return {
|
|
...s,
|
|
status: 'error' as SensorSetupStatus,
|
|
error: errorMsg,
|
|
steps: s.steps.map(step =>
|
|
step.status === 'in_progress'
|
|
? { ...step, status: 'failed' as const, error: errorMsg }
|
|
: step
|
|
),
|
|
};
|
|
}));
|
|
|
|
// Disconnect on error
|
|
try {
|
|
await disconnectDevice(deviceId);
|
|
} catch (e) {
|
|
// Ignore disconnect errors
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}, [
|
|
id, connectDevice, disconnectDevice, setWiFi, rebootDevice,
|
|
updateSensorStep, updateSensorStatus
|
|
]);
|
|
|
|
// Run batch setup sequentially
|
|
const runBatchSetup = useCallback(async () => {
|
|
if (setupInProgressRef.current) return;
|
|
setupInProgressRef.current = true;
|
|
shouldCancelRef.current = false;
|
|
|
|
const ssid = selectedNetwork!.ssid;
|
|
const pwd = password;
|
|
|
|
for (let i = currentIndex; i < sensors.length; i++) {
|
|
if (shouldCancelRef.current) {
|
|
break;
|
|
}
|
|
|
|
setCurrentIndex(i);
|
|
const sensor = sensors[i];
|
|
|
|
// Skip already processed sensors
|
|
if (sensor.status === 'success' || sensor.status === 'skipped') {
|
|
continue;
|
|
}
|
|
|
|
// If sensor has error and we're not retrying, pause
|
|
if (sensor.status === 'error' && isPaused) {
|
|
break;
|
|
}
|
|
|
|
// Reset sensor state if retrying
|
|
if (sensor.status === 'error') {
|
|
setSensors(prev => prev.map(s =>
|
|
s.deviceId === sensor.deviceId
|
|
? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() }
|
|
: s
|
|
));
|
|
}
|
|
|
|
const success = await processSensor(
|
|
sensors[i],
|
|
ssid,
|
|
pwd
|
|
);
|
|
|
|
// Check for cancellation after each sensor
|
|
if (shouldCancelRef.current) break;
|
|
|
|
// If failed, pause for user input
|
|
if (!success) {
|
|
setIsPaused(true);
|
|
setupInProgressRef.current = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// All done
|
|
setupInProgressRef.current = false;
|
|
|
|
// Check if we should show results
|
|
const finalSensors = sensors;
|
|
const allProcessed = finalSensors.every(
|
|
s => s.status === 'success' || s.status === 'error' || s.status === 'skipped'
|
|
);
|
|
|
|
if (allProcessed || shouldCancelRef.current) {
|
|
setPhase('results');
|
|
}
|
|
}, [sensors, currentIndex, selectedNetwork, password, isPaused, processSensor]);
|
|
|
|
// Start batch setup
|
|
const handleStartBatchSetup = async () => {
|
|
if (!selectedNetwork) {
|
|
Alert.alert('Error', 'Please select a WiFi network');
|
|
return;
|
|
}
|
|
if (!password) {
|
|
Alert.alert('Error', 'Please enter WiFi password');
|
|
return;
|
|
}
|
|
|
|
// Save password for this network (by SSID) to SecureStore
|
|
try {
|
|
await wifiPasswordStore.saveWiFiPassword(selectedNetwork.ssid, password);
|
|
|
|
// Update local state
|
|
const updatedPasswords = { ...savedPasswords, [selectedNetwork.ssid]: password };
|
|
savedPasswordsRef.current = updatedPasswords;
|
|
setSavedPasswords(updatedPasswords);
|
|
|
|
} catch (error) {
|
|
// Continue with setup even if save fails
|
|
}
|
|
|
|
// Initialize sensor states
|
|
const initialStates = selectedDevices.map(createSensorState);
|
|
setSensors(initialStates);
|
|
setCurrentIndex(0);
|
|
setIsPaused(false);
|
|
setPhase('batch_setup');
|
|
};
|
|
|
|
// Start processing after phase change
|
|
useEffect(() => {
|
|
if (phase === 'batch_setup' && sensors.length > 0 && !setupInProgressRef.current) {
|
|
runBatchSetup();
|
|
}
|
|
}, [phase, sensors.length, runBatchSetup]);
|
|
|
|
// Retry failed sensor
|
|
const handleRetry = (deviceId: string) => {
|
|
const index = sensors.findIndex(s => s.deviceId === deviceId);
|
|
if (index >= 0) {
|
|
setSensors(prev => prev.map(s =>
|
|
s.deviceId === deviceId
|
|
? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() }
|
|
: s
|
|
));
|
|
setCurrentIndex(index);
|
|
setIsPaused(false);
|
|
runBatchSetup();
|
|
}
|
|
};
|
|
|
|
// Skip failed sensor
|
|
const handleSkip = (deviceId: string) => {
|
|
setSensors(prev => prev.map(s =>
|
|
s.deviceId === deviceId
|
|
? { ...s, status: 'skipped' as SensorSetupStatus }
|
|
: s
|
|
));
|
|
setIsPaused(false);
|
|
|
|
// Move to next sensor
|
|
const nextIndex = currentIndex + 1;
|
|
if (nextIndex < sensors.length) {
|
|
setCurrentIndex(nextIndex);
|
|
runBatchSetup();
|
|
} else {
|
|
setPhase('results');
|
|
}
|
|
};
|
|
|
|
// Cancel all
|
|
const handleCancelAll = () => {
|
|
Alert.alert(
|
|
'Cancel Setup',
|
|
'Are you sure you want to cancel? Progress will be lost.',
|
|
[
|
|
{ text: 'Continue Setup', style: 'cancel' },
|
|
{
|
|
text: 'Cancel',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
shouldCancelRef.current = true;
|
|
setupInProgressRef.current = false;
|
|
// Disconnect all devices
|
|
selectedDevices.forEach(d => disconnectDevice(d.id));
|
|
router.back();
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
// Done - navigate back
|
|
const handleDone = () => {
|
|
router.replace(`/(tabs)/beneficiaries/${id}/equipment` as any);
|
|
};
|
|
|
|
// Retry a single sensor from results screen
|
|
const handleRetryFromResults = (deviceId: string) => {
|
|
const index = sensors.findIndex(s => s.deviceId === deviceId);
|
|
if (index >= 0) {
|
|
// Reset the sensor state
|
|
setSensors(prev => prev.map(s =>
|
|
s.deviceId === deviceId
|
|
? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() }
|
|
: s
|
|
));
|
|
setCurrentIndex(index);
|
|
setIsPaused(false);
|
|
// Go back to batch setup phase
|
|
setPhase('batch_setup');
|
|
}
|
|
};
|
|
|
|
const getSignalStrength = (rssi: number): string => {
|
|
if (rssi >= -50) return 'Excellent';
|
|
if (rssi >= -60) return 'Good';
|
|
if (rssi >= -70) return 'Fair';
|
|
return 'Weak';
|
|
};
|
|
|
|
const getSignalColor = (rssi: number) => {
|
|
if (rssi >= -50) return AppColors.success;
|
|
if (rssi >= -60) return AppColors.info;
|
|
if (rssi >= -70) return AppColors.warning;
|
|
return AppColors.error;
|
|
};
|
|
|
|
const getSignalIcon = (rssi: number) => {
|
|
if (rssi >= -50) return 'wifi';
|
|
if (rssi >= -60) return 'wifi';
|
|
if (rssi >= -70) return 'wifi-outline';
|
|
return 'wifi-outline';
|
|
};
|
|
|
|
// Results screen
|
|
if (phase === 'results') {
|
|
return (
|
|
<SetupResultsScreen
|
|
sensors={sensors}
|
|
onRetry={handleRetryFromResults}
|
|
onDone={handleDone}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Batch setup progress screen
|
|
if (phase === 'batch_setup') {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.header}>
|
|
<View style={styles.placeholder} />
|
|
<Text style={styles.headerTitle}>Setting Up Sensors</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<View style={styles.batchContent}>
|
|
<BatchSetupProgress
|
|
sensors={sensors}
|
|
currentIndex={currentIndex}
|
|
ssid={selectedNetwork?.ssid || ''}
|
|
isPaused={isPaused}
|
|
onRetry={handleRetry}
|
|
onSkip={handleSkip}
|
|
onCancelAll={handleCancelAll}
|
|
/>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// WiFi selection screen (default)
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity
|
|
style={styles.backButton}
|
|
onPress={() => {
|
|
selectedDevices.forEach(d => disconnectDevice(d.id));
|
|
router.back();
|
|
}}
|
|
>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Setup WiFi</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<KeyboardAvoidingView
|
|
style={styles.content}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 20}
|
|
>
|
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
|
{/* Device Info Card */}
|
|
<View style={styles.deviceCard}>
|
|
<View style={styles.deviceIcon}>
|
|
<Ionicons name="water" size={32} color={AppColors.primary} />
|
|
</View>
|
|
<View style={styles.deviceInfo}>
|
|
{selectedDevices.length === 1 ? (
|
|
<>
|
|
<Text style={styles.deviceName}>{firstDevice?.name}</Text>
|
|
<Text style={styles.deviceMeta}>Well ID: {firstDevice?.wellId}</Text>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Text style={styles.deviceName}>{selectedDevices.length} Sensors Selected</Text>
|
|
<Text style={styles.deviceMeta}>
|
|
{selectedDevices.map(d => d.name).join(', ')}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Instructions */}
|
|
<View style={styles.instructionsCard}>
|
|
<Text style={styles.instructionsText}>
|
|
{selectedDevices.length === 1
|
|
? 'Select the WiFi network your sensor should connect to. Make sure the network has internet access.'
|
|
: `Select the WiFi network for all ${selectedDevices.length} sensors. They will all be configured with the same WiFi credentials.`}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* WiFi Networks List */}
|
|
{isLoadingNetworks ? (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Scanning for WiFi networks...</Text>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>Available Networks ({networks.length})</Text>
|
|
<TouchableOpacity style={styles.refreshButton} onPress={loadWiFiNetworks}>
|
|
<Ionicons name="refresh" size={18} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{networks.length === 0 ? (
|
|
<View style={styles.emptyState}>
|
|
<Ionicons name="wifi-outline" size={48} color={AppColors.textMuted} />
|
|
<Text style={styles.emptyText}>No WiFi networks found</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={loadWiFiNetworks}>
|
|
<Text style={styles.retryText}>Try Again</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<View style={styles.networksList}>
|
|
{networks.map((network, index) => {
|
|
const isSelected = selectedNetwork?.ssid === network.ssid;
|
|
const hasSavedPassword = passwordsLoaded && savedPasswords[network.ssid];
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={`${network.ssid}-${index}`}
|
|
style={[
|
|
styles.networkCard,
|
|
isSelected && styles.networkCardSelected,
|
|
]}
|
|
onPress={() => handleSelectNetwork(network)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.networkInfo}>
|
|
<Ionicons
|
|
name={getSignalIcon(network.rssi)}
|
|
size={24}
|
|
color={getSignalColor(network.rssi)}
|
|
/>
|
|
<View style={styles.networkDetails}>
|
|
<View style={styles.networkNameRow}>
|
|
<Text style={styles.networkName}>{network.ssid}</Text>
|
|
{hasSavedPassword && (
|
|
<Ionicons name="key" size={14} color={AppColors.success} style={styles.savedPasswordIcon} />
|
|
)}
|
|
</View>
|
|
<Text style={[styles.signalText, { color: getSignalColor(network.rssi) }]}>
|
|
{getSignalStrength(network.rssi)} ({network.rssi} dBm)
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
{isSelected && (
|
|
<Ionicons name="checkmark-circle" size={24} color={AppColors.primary} />
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Password Input (shown when network selected) */}
|
|
{selectedNetwork && (
|
|
<View style={styles.passwordCard}>
|
|
<Text style={styles.passwordLabel}>WiFi Password</Text>
|
|
<View style={styles.passwordInputContainer}>
|
|
<TextInput
|
|
style={styles.passwordInput}
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="Enter password"
|
|
secureTextEntry={!showPassword}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
/>
|
|
<TouchableOpacity
|
|
style={styles.togglePasswordButton}
|
|
onPress={() => setShowPassword(!showPassword)}
|
|
>
|
|
<Ionicons
|
|
name={showPassword ? 'eye-off' : 'eye'}
|
|
size={20}
|
|
color={AppColors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Connect Button */}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.connectButton,
|
|
!password && styles.connectButtonDisabled,
|
|
]}
|
|
onPress={handleStartBatchSetup}
|
|
disabled={!password}
|
|
>
|
|
<Ionicons name="checkmark" size={20} color={AppColors.white} />
|
|
<Text style={styles.connectButtonText}>
|
|
{selectedDevices.length === 1
|
|
? 'Connect & Complete Setup'
|
|
: `Connect All ${selectedDevices.length} Sensors`}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Help Card */}
|
|
<View style={styles.helpCard}>
|
|
<View style={styles.helpHeader}>
|
|
<Ionicons name="information-circle" size={20} color={AppColors.info} />
|
|
<Text style={styles.helpTitle}>Important</Text>
|
|
</View>
|
|
<Text style={styles.helpText}>
|
|
• The sensor will reboot after WiFi is configured{'\n'}
|
|
• It may take up to 1 minute for the sensor to connect{'\n'}
|
|
• Make sure the WiFi password is correct{'\n'}
|
|
• The network must have internet access
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
batchContent: {
|
|
flex: 1,
|
|
padding: Spacing.lg,
|
|
},
|
|
// Device Card
|
|
deviceCard: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.sm,
|
|
},
|
|
deviceIcon: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: BorderRadius.lg,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: Spacing.md,
|
|
},
|
|
deviceInfo: {
|
|
flex: 1,
|
|
},
|
|
deviceName: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: 2,
|
|
},
|
|
deviceMeta: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
},
|
|
// Instructions
|
|
instructionsCard: {
|
|
backgroundColor: AppColors.infoLight,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
instructionsText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.info,
|
|
lineHeight: 20,
|
|
},
|
|
// Loading
|
|
loadingContainer: {
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
...Shadows.sm,
|
|
},
|
|
loadingText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
marginTop: Spacing.md,
|
|
},
|
|
// Section Header
|
|
sectionHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
refreshButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
// Empty State
|
|
emptyState: {
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
...Shadows.sm,
|
|
},
|
|
emptyText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textMuted,
|
|
marginTop: Spacing.md,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
retryButton: {
|
|
paddingVertical: Spacing.sm,
|
|
paddingHorizontal: Spacing.lg,
|
|
},
|
|
retryText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.primary,
|
|
},
|
|
// Networks List
|
|
networksList: {
|
|
gap: Spacing.sm,
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
networkCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
...Shadows.xs,
|
|
},
|
|
networkCardSelected: {
|
|
borderWidth: 2,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
networkInfo: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
networkDetails: {
|
|
flex: 1,
|
|
},
|
|
networkNameRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
networkName: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: 2,
|
|
},
|
|
savedPasswordIcon: {
|
|
marginLeft: Spacing.xs,
|
|
marginBottom: 2,
|
|
},
|
|
signalText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.medium,
|
|
},
|
|
// Password Input
|
|
passwordCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.sm,
|
|
},
|
|
passwordLabel: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textSecondary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
passwordInputContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.background,
|
|
borderRadius: BorderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: AppColors.border,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
passwordInput: {
|
|
flex: 1,
|
|
paddingVertical: Spacing.sm,
|
|
paddingHorizontal: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
togglePasswordButton: {
|
|
padding: Spacing.md,
|
|
},
|
|
// Connect Button
|
|
connectButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
gap: Spacing.sm,
|
|
...Shadows.md,
|
|
},
|
|
connectButtonDisabled: {
|
|
opacity: 0.5,
|
|
},
|
|
connectButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
// Help Card
|
|
helpCard: {
|
|
backgroundColor: AppColors.infoLight,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
},
|
|
helpHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
helpTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.info,
|
|
},
|
|
helpText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.info,
|
|
lineHeight: 20,
|
|
},
|
|
});
|