- Implement Next.js middleware for route protection - Create Zustand auth store for web (similar to mobile) - Add comprehensive tests for middleware and auth store - Protect authenticated routes (/dashboard, /profile) - Redirect unauthenticated users to /login - Redirect authenticated users from auth routes to /dashboard - Handle session expiration with 401 callback - Set access token cookie for middleware - All tests passing (105 tests total)
795 lines
24 KiB
TypeScript
795 lines
24 KiB
TypeScript
/**
|
|
* 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<Step>('scan');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Devices
|
|
const [devices, setDevices] = useState<WellNuoDevice[]>([]);
|
|
const [selectedDevice, setSelectedDevice] = useState<WellNuoDevice | null>(null);
|
|
|
|
// WiFi
|
|
const [wifiNetworks, setWifiNetworks] = useState<WifiNetwork[]>([]);
|
|
const [selectedWifi, setSelectedWifi] = useState<WifiNetwork | null>(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 (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.content}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.title}>WiFi Setup</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
{/* Icon */}
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name="bluetooth" size={64} color={AppColors.primary} />
|
|
</View>
|
|
|
|
{/* Instructions */}
|
|
<Text style={styles.instructions}>
|
|
{step === 'connect'
|
|
? `Connecting to ${selectedDevice?.name}...`
|
|
: 'Searching for WellNuo sensors nearby...'}
|
|
</Text>
|
|
|
|
{/* Loading or device list */}
|
|
{isLoading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>
|
|
{step === 'connect' ? 'Connecting...' : 'Scanning for devices...'}
|
|
</Text>
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.errorContainer}>
|
|
<Ionicons name="alert-circle" size={48} color={AppColors.error} />
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={handleScanDevices}>
|
|
<Text style={styles.retryButtonText}>Retry Scan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={devices}
|
|
keyExtractor={(item) => item.name}
|
|
style={styles.deviceList}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
style={styles.deviceItem}
|
|
onPress={() => handleSelectDevice(item)}
|
|
>
|
|
<View style={styles.deviceIcon}>
|
|
<Ionicons name="hardware-chip" size={24} color={AppColors.primary} />
|
|
</View>
|
|
<View style={styles.deviceInfo}>
|
|
<Text style={styles.deviceName}>{item.name}</Text>
|
|
{item.wellId && (
|
|
<Text style={styles.deviceMeta}>Sensor ID: {item.wellId}</Text>
|
|
)}
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
|
</TouchableOpacity>
|
|
)}
|
|
ListEmptyComponent={
|
|
<Text style={styles.emptyText}>No devices found</Text>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{/* Rescan button */}
|
|
{!isLoading && devices.length > 0 && (
|
|
<TouchableOpacity style={styles.secondaryButton} onPress={handleScanDevices}>
|
|
<Ionicons name="refresh" size={20} color={AppColors.primary} />
|
|
<Text style={styles.secondaryButtonText}>Scan Again</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// Step 3: Select WiFi network
|
|
if (step === 'wifi-select') {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.content}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity
|
|
style={styles.backButton}
|
|
onPress={async () => {
|
|
await espProvisioning.disconnect();
|
|
setStep('scan');
|
|
}}
|
|
>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.title}>Select WiFi</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
{/* Connected device info */}
|
|
<View style={styles.connectedDevice}>
|
|
<Ionicons name="checkmark-circle" size={20} color={AppColors.success} />
|
|
<Text style={styles.connectedText}>Connected to {selectedDevice?.name}</Text>
|
|
</View>
|
|
|
|
{/* Instructions */}
|
|
<Text style={styles.instructions}>
|
|
Select the WiFi network for your sensor
|
|
</Text>
|
|
|
|
{/* Loading or WiFi list */}
|
|
{isLoading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Scanning WiFi networks...</Text>
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.errorContainer}>
|
|
<Ionicons name="alert-circle" size={48} color={AppColors.error} />
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={handleScanWifi}>
|
|
<Text style={styles.retryButtonText}>Retry Scan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={wifiNetworks}
|
|
keyExtractor={(item, index) => `${item.ssid}-${index}`}
|
|
style={styles.deviceList}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
style={styles.deviceItem}
|
|
onPress={() => handleSelectWifi(item)}
|
|
>
|
|
<WiFiSignalIndicator rssi={item.rssi} size="small" />
|
|
<View style={styles.deviceInfo}>
|
|
<Text style={styles.deviceName}>{item.ssid}</Text>
|
|
<Text style={[styles.deviceMeta, { color: getSignalStrengthColor(item.rssi) }]}>
|
|
{getSignalStrengthLabel(item.rssi)} • {item.auth}
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
|
</TouchableOpacity>
|
|
)}
|
|
ListEmptyComponent={
|
|
<Text style={styles.emptyText}>No WiFi networks found</Text>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{/* Rescan button */}
|
|
{!isLoading && (
|
|
<TouchableOpacity style={styles.secondaryButton} onPress={handleScanWifi}>
|
|
<Ionicons name="refresh" size={20} color={AppColors.primary} />
|
|
<Text style={styles.secondaryButtonText}>Scan Again</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// Step 4: Enter WiFi password
|
|
if (step === 'wifi-password' || step === 'provisioning') {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity
|
|
style={styles.backButton}
|
|
onPress={() => setStep('wifi-select')}
|
|
disabled={step === 'provisioning'}
|
|
>
|
|
<Ionicons
|
|
name="arrow-back"
|
|
size={24}
|
|
color={step === 'provisioning' ? AppColors.textMuted : AppColors.textPrimary}
|
|
/>
|
|
</TouchableOpacity>
|
|
<Text style={styles.title}>Enter Password</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
{/* WiFi icon */}
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name="wifi" size={64} color={AppColors.primary} />
|
|
</View>
|
|
|
|
{/* Selected network */}
|
|
<View style={styles.selectedNetwork}>
|
|
<Text style={styles.selectedNetworkLabel}>Network:</Text>
|
|
<Text style={styles.selectedNetworkName}>{selectedWifi?.ssid}</Text>
|
|
</View>
|
|
|
|
{/* Password input */}
|
|
<View style={styles.inputContainer}>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={wifiPassword}
|
|
onChangeText={setWifiPassword}
|
|
placeholder="Enter WiFi password"
|
|
placeholderTextColor={AppColors.textMuted}
|
|
secureTextEntry={!showPassword}
|
|
autoCorrect={false}
|
|
editable={step !== 'provisioning'}
|
|
/>
|
|
<TouchableOpacity
|
|
style={styles.showPasswordButton}
|
|
onPress={() => setShowPassword(!showPassword)}
|
|
>
|
|
<Ionicons
|
|
name={showPassword ? 'eye-off' : 'eye'}
|
|
size={24}
|
|
color={AppColors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<View style={styles.inlineError}>
|
|
<Ionicons name="alert-circle" size={16} color={AppColors.error} />
|
|
<Text style={styles.inlineErrorText}>{error}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Connect button */}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.primaryButton,
|
|
(step === 'provisioning' || (selectedWifi?.auth !== 'Open' && !wifiPassword)) &&
|
|
styles.buttonDisabled,
|
|
]}
|
|
onPress={handleProvision}
|
|
disabled={step === 'provisioning' || (selectedWifi?.auth !== 'Open' && !wifiPassword)}
|
|
>
|
|
{step === 'provisioning' ? (
|
|
<>
|
|
<ActivityIndicator color={AppColors.white} style={{ marginRight: 8 }} />
|
|
<Text style={styles.primaryButtonText}>Configuring...</Text>
|
|
</>
|
|
) : (
|
|
<Text style={styles.primaryButtonText}>Connect to WiFi</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{/* Note for open networks */}
|
|
{selectedWifi?.auth === 'Open' && (
|
|
<Text style={styles.noteText}>
|
|
This is an open network. You can leave the password empty.
|
|
</Text>
|
|
)}
|
|
|
|
{/* Password requirements */}
|
|
{selectedWifi?.auth?.includes('WPA') && (
|
|
<Text style={styles.noteText}>
|
|
Password must be 8-63 characters for WPA/WPA2/WPA3 networks.
|
|
</Text>
|
|
)}
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// Step 5: Complete
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.content}>
|
|
{/* Success */}
|
|
<View style={styles.successContainer}>
|
|
<View style={styles.successIcon}>
|
|
<Ionicons name="checkmark-circle" size={80} color={AppColors.success} />
|
|
</View>
|
|
|
|
<Text style={styles.successTitle}>WiFi Configured!</Text>
|
|
<Text style={styles.successMessage}>
|
|
Your sensor <Text style={styles.highlight}>{selectedDevice?.name}</Text> is now
|
|
connected to <Text style={styles.highlight}>{selectedWifi?.ssid}</Text>
|
|
</Text>
|
|
|
|
{/* Next steps */}
|
|
<View style={styles.nextSteps}>
|
|
<Text style={styles.nextStepsTitle}>What happens next:</Text>
|
|
<View style={styles.stepItem}>
|
|
<Ionicons name="checkmark" size={20} color={AppColors.success} />
|
|
<Text style={styles.stepText}>Sensor will connect to your WiFi</Text>
|
|
</View>
|
|
<View style={styles.stepItem}>
|
|
<Ionicons name="checkmark" size={20} color={AppColors.success} />
|
|
<Text style={styles.stepText}>Data will start syncing to WellNuo</Text>
|
|
</View>
|
|
<View style={styles.stepItem}>
|
|
<Ionicons name="checkmark" size={20} color={AppColors.success} />
|
|
<Text style={styles.stepText}>You'll see updates in the dashboard</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Complete button */}
|
|
<TouchableOpacity style={styles.primaryButton} onPress={handleComplete}>
|
|
<Text style={styles.primaryButtonText}>Continue</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|