Sergei 8af7a11cd9 Fix WiFi credentials cache implementation in SecureStore
- Fix saveWiFiPassword to use encrypted passwords map instead of decrypted
- Fix getWiFiPassword to decrypt from encrypted storage
- Fix test expectations for migration and encryption functions
- Remove unused error variables to fix linting warnings
- All 27 tests now passing with proper encryption/decryption flow

The WiFi credentials cache feature was already implemented but had bugs
where encrypted and decrypted password maps were being mixed. This commit
ensures proper encryption is maintained throughout the storage lifecycle.
2026-01-31 15:55:24 -08:00

1014 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, mac } = 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 && mac) {
const attachResponse = await api.attachDeviceToBeneficiary(
id!,
wellId,
mac
);
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,
},
});