diff --git a/app/(auth)/wifi-setup.tsx b/app/(auth)/wifi-setup.tsx
index af0db4a..68eb9d3 100644
--- a/app/(auth)/wifi-setup.tsx
+++ b/app/(auth)/wifi-setup.tsx
@@ -16,7 +16,6 @@ import {
ActivityIndicator,
Alert,
FlatList,
- Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
@@ -26,8 +25,12 @@ import {
espProvisioning,
type WellNuoDevice,
type WifiNetwork,
- type ESPDevice,
} from '@/services/espProvisioning';
+import {
+ validateWiFiCredentials,
+ getValidationErrorMessage,
+ sanitizeWiFiCredentials,
+} from '@/utils/wifiValidation';
type Step = 'scan' | 'connect' | 'wifi-select' | 'wifi-password' | 'provisioning' | 'complete';
@@ -139,12 +142,56 @@ export default function WifiSetupScreen() {
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 {
- await espProvisioning.provisionWifi(selectedWifi.ssid, wifiPassword);
+ // 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';
@@ -414,9 +461,13 @@ export default function WifiSetupScreen() {
{/* Connect button */}
{step === 'provisioning' ? (
<>
@@ -434,6 +485,13 @@ export default function WifiSetupScreen() {
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.
+
+ )}
);
@@ -468,7 +526,7 @@ export default function WifiSetupScreen() {
- You'll see updates in the dashboard
+ You'll see updates in the dashboard
diff --git a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
index 34d49e8..a65ca8f 100644
--- a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
+++ b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
@@ -35,6 +35,11 @@ import {
Spacing,
Shadows,
} from '@/constants/theme';
+import {
+ validateWiFiCredentials,
+ getValidationErrorMessage,
+ sanitizeWiFiCredentials,
+} from '@/utils/wifiValidation';
// Type for device passed via navigation params
interface DeviceParam {
@@ -450,20 +455,55 @@ export default function SetupWiFiScreen() {
Alert.alert('Error', 'Please select a WiFi network');
return;
}
- if (!password) {
- Alert.alert('Error', 'Please enter WiFi password');
+
+ // Validate credentials
+ const validation = validateWiFiCredentials(selectedNetwork.ssid, password);
+
+ if (!validation.valid) {
+ const errorMsg = getValidationErrorMessage(validation);
+ Alert.alert('Invalid WiFi Credentials', errorMsg);
return;
}
+ // Show warnings if present (but allow to continue)
+ if (validation.warnings.length > 0) {
+ const warningMsg = validation.warnings.join('\n');
+ Alert.alert(
+ 'WiFi Password Warning',
+ `${warningMsg}\n\nDo you want to continue?`,
+ [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Continue',
+ onPress: () => startBatchSetupProcess(),
+ },
+ ]
+ );
+ return;
+ }
+
+ // No warnings, proceed directly
+ await startBatchSetupProcess();
+ };
+
+ // Actual batch setup process
+ const startBatchSetupProcess = async () => {
+ if (!selectedNetwork) return;
+
+ // Sanitize credentials before saving and using
+ const sanitized = sanitizeWiFiCredentials({
+ ssid: selectedNetwork.ssid,
+ password,
+ });
+
// Save password for this network (by SSID) to SecureStore
try {
- await wifiPasswordStore.saveWiFiPassword(selectedNetwork.ssid, password);
+ await wifiPasswordStore.saveWiFiPassword(sanitized.ssid, sanitized.password);
// Update local state
- const updatedPasswords = { ...savedPasswords, [selectedNetwork.ssid]: password };
+ const updatedPasswords = { ...savedPasswords, [sanitized.ssid]: sanitized.password };
savedPasswordsRef.current = updatedPasswords;
setSavedPasswords(updatedPasswords);
-
} catch (error) {
// Continue with setup even if save fails
}
@@ -804,6 +844,13 @@ export default function SetupWiFiScreen() {
+ {/* Password requirements hint */}
+ {selectedNetwork && !selectedNetwork.ssid.includes('Open') && (
+
+ Password must be 8-63 characters for secured networks
+
+ )}
+
{/* Connect Button */}
{
+ describe('validateSSID', () => {
+ it('should accept valid SSIDs', () => {
+ const validSSIDs = [
+ 'MyHomeWiFi',
+ 'Guest Network',
+ 'Office-5G',
+ 'Café WiFi',
+ 'Network_2.4GHz',
+ 'a',
+ '1234567890123456789012345678901', // 31 chars
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', // 32 chars
+ ];
+
+ validSSIDs.forEach((ssid) => {
+ const result = validateSSID(ssid);
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+ });
+
+ it('should reject empty SSID', () => {
+ const result = validateSSID('');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Network name (SSID) cannot be empty');
+ });
+
+ it('should reject whitespace-only SSID', () => {
+ const result = validateSSID(' ');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Network name (SSID) cannot be empty');
+ });
+
+ it('should reject SSID longer than 32 bytes', () => {
+ const longSSID = 'A'.repeat(33);
+ const result = validateSSID(longSSID);
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Network name is too long (max 32 bytes)');
+ });
+
+ it('should reject SSID with control characters', () => {
+ const result = validateSSID('Network\x00Name');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Network name contains invalid control characters');
+ });
+
+ it('should warn about leading/trailing spaces', () => {
+ const result1 = validateSSID(' MyWiFi');
+ expect(result1.valid).toBe(true);
+ expect(result1.warnings).toContain(
+ 'Network name has leading or trailing spaces - this may cause connection issues'
+ );
+
+ const result2 = validateSSID('MyWiFi ');
+ expect(result2.valid).toBe(true);
+ expect(result2.warnings).toContain(
+ 'Network name has leading or trailing spaces - this may cause connection issues'
+ );
+ });
+
+ it('should warn about multiple consecutive spaces', () => {
+ const result = validateSSID('My WiFi');
+ expect(result.valid).toBe(true);
+ expect(result.warnings).toContain('Network name has multiple consecutive spaces');
+ });
+ });
+
+ describe('validatePassword', () => {
+ describe('WPA/WPA2/WPA3 networks', () => {
+ it('should accept valid WPA passwords', () => {
+ const validPasswords = [
+ 'MySecurePass123',
+ 'abcdefgh', // 8 chars minimum
+ 'A'.repeat(63), // 63 chars maximum
+ 'Pass with spaces!@#',
+ 'ComplexP@ssw0rd!',
+ ];
+
+ validPasswords.forEach((password) => {
+ const result = validatePassword(password, 'WPA2 PSK');
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+ });
+
+ it('should reject passwords shorter than 8 characters', () => {
+ const result = validatePassword('1234567', 'WPA2 PSK');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain(
+ 'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
+ );
+ });
+
+ it('should reject passwords longer than 63 characters', () => {
+ const longPassword = 'A'.repeat(64);
+ const result = validatePassword(longPassword, 'WPA2 PSK');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain(
+ 'Password cannot exceed 63 characters for WPA/WPA2/WPA3 networks'
+ );
+ });
+
+ it('should warn about weak passwords', () => {
+ const result = validatePassword('12345678', 'WPA2 PSK');
+ expect(result.valid).toBe(true);
+ expect(result.warnings).toContain('Password is weak - consider using at least 12 characters');
+ });
+
+ it('should warn about commonly used weak passwords', () => {
+ const weakPasswords = ['password', '12345678', 'qwerty123'];
+
+ weakPasswords.forEach((password) => {
+ const result = validatePassword(password, 'WPA2 PSK');
+ expect(result.warnings).toContain('This is a commonly used weak password');
+ });
+ });
+
+ it('should warn about numeric-only passwords', () => {
+ const result = validatePassword('12345678', 'WPA2 PSK');
+ expect(result.warnings).toContain(
+ 'Password contains only numbers - consider adding letters and symbols'
+ );
+ });
+
+ it('should warn about non-ASCII characters', () => {
+ const result = validatePassword('Password™123', 'WPA2 PSK');
+ expect(result.valid).toBe(true);
+ expect(result.warnings).toContain(
+ 'Password contains non-ASCII characters - this may cause issues on some devices'
+ );
+ });
+ });
+
+ describe('Open networks', () => {
+ it('should accept empty password for open networks', () => {
+ const result = validatePassword('', 'Open');
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+
+ it('should accept any password for open networks (will be ignored)', () => {
+ const result = validatePassword('any password', 'Open');
+ expect(result.valid).toBe(true);
+ });
+ });
+
+ describe('WEP networks', () => {
+ it('should accept valid 5-character WEP password', () => {
+ const result = validatePassword('abcde', 'WEP');
+ expect(result.valid).toBe(true);
+ expect(result.warnings).toContain(
+ 'WEP is deprecated and insecure - consider upgrading your router to WPA2/WPA3'
+ );
+ });
+
+ it('should accept valid 13-character WEP password', () => {
+ const result = validatePassword('abcdefghijklm', 'WEP');
+ expect(result.valid).toBe(true);
+ });
+
+ it('should accept valid 10-digit hex WEP password', () => {
+ const result = validatePassword('1234567890', 'WEP');
+ expect(result.valid).toBe(true);
+ });
+
+ it('should accept valid 26-digit hex WEP password', () => {
+ const result = validatePassword('12345678901234567890123456', 'WEP');
+ expect(result.valid).toBe(true);
+ });
+
+ it('should reject invalid WEP password length', () => {
+ const result = validatePassword('abc', 'WEP');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain(
+ 'WEP password must be 5 or 13 ASCII characters, or 10/26 hexadecimal digits'
+ );
+ });
+ });
+
+ describe('Secured networks without auth type', () => {
+ it('should require password', () => {
+ const result = validatePassword('');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Password is required for secured networks');
+ });
+
+ it('should apply WPA rules by default', () => {
+ const result = validatePassword('1234567'); // 7 chars
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain(
+ 'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
+ );
+ });
+ });
+
+ it('should reject passwords with control characters', () => {
+ const result = validatePassword('Pass\x00word123', 'WPA2 PSK');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Password contains invalid control characters');
+ });
+ });
+
+ describe('validateWiFiCredentials', () => {
+ it('should validate both SSID and password', () => {
+ const result = validateWiFiCredentials('MyWiFi', 'SecurePass123', 'WPA2 PSK');
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+
+ it('should combine errors from both validations', () => {
+ const result = validateWiFiCredentials('', '123', 'WPA2 PSK');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toContain('Network name (SSID) cannot be empty');
+ expect(result.errors).toContain(
+ 'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
+ );
+ });
+
+ it('should combine warnings from both validations', () => {
+ const result = validateWiFiCredentials(' MyWiFi ', 'password123', 'WPA2 PSK');
+ expect(result.valid).toBe(true);
+ expect(result.warnings.length).toBeGreaterThan(0);
+ });
+
+ it('should validate open network correctly', () => {
+ const result = validateWiFiCredentials('GuestNetwork', '', 'Open');
+ expect(result.valid).toBe(true);
+ });
+ });
+
+ describe('isStrongPassword', () => {
+ it('should accept strong passwords', () => {
+ const strongPasswords = [
+ 'MyP@ssw0rd123!',
+ 'Secure_Password_2024',
+ 'Compl3x!P@ssw0rd',
+ 'abCD1234!@#$',
+ ];
+
+ strongPasswords.forEach((password) => {
+ expect(isStrongPassword(password)).toBe(true);
+ });
+ });
+
+ it('should reject passwords shorter than 12 characters', () => {
+ expect(isStrongPassword('P@ssw0rd')).toBe(false);
+ });
+
+ it('should reject passwords with less than 3 character types', () => {
+ expect(isStrongPassword('onlylowercase')).toBe(false);
+ expect(isStrongPassword('12345678901234')).toBe(false);
+ expect(isStrongPassword('ONLYUPPERCASE')).toBe(false);
+ });
+
+ it('should accept passwords with 3 or 4 character types', () => {
+ // 3 types: lowercase, uppercase, numbers
+ expect(isStrongPassword('Password1234')).toBe(true);
+
+ // 4 types: lowercase, uppercase, numbers, special
+ expect(isStrongPassword('P@ssword1234')).toBe(true);
+ });
+ });
+
+ describe('sanitizeWiFiCredentials', () => {
+ it('should trim SSID whitespace', () => {
+ const result = sanitizeWiFiCredentials({
+ ssid: ' MyWiFi ',
+ password: 'password',
+ });
+ expect(result.ssid).toBe('MyWiFi');
+ });
+
+ it('should preserve password whitespace', () => {
+ const result = sanitizeWiFiCredentials({
+ ssid: 'MyWiFi',
+ password: ' password with spaces ',
+ });
+ expect(result.password).toBe(' password with spaces ');
+ });
+
+ it('should not modify valid credentials', () => {
+ const result = sanitizeWiFiCredentials({
+ ssid: 'MyWiFi',
+ password: 'SecurePass123',
+ });
+ expect(result.ssid).toBe('MyWiFi');
+ expect(result.password).toBe('SecurePass123');
+ });
+ });
+
+ describe('getValidationErrorMessage', () => {
+ it('should return empty string for valid credentials', () => {
+ const result = validateWiFiCredentials('MyWiFi', 'SecurePass123', 'WPA2 PSK');
+ expect(getValidationErrorMessage(result)).toBe('');
+ });
+
+ it('should return first error message', () => {
+ const result = validateWiFiCredentials('', '123', 'WPA2 PSK');
+ expect(getValidationErrorMessage(result)).toBe('Network name (SSID) cannot be empty');
+ });
+
+ it('should return default message if no specific error', () => {
+ const result = { valid: false, errors: [], warnings: [] };
+ expect(getValidationErrorMessage(result)).toBe('Invalid WiFi credentials');
+ });
+ });
+
+ describe('getValidationMessage', () => {
+ it('should return empty string for valid credentials with no warnings', () => {
+ const result = { valid: true, errors: [], warnings: [] };
+ expect(getValidationMessage(result)).toBe('');
+ });
+
+ it('should return errors only', () => {
+ const result = {
+ valid: false,
+ errors: ['Error 1', 'Error 2'],
+ warnings: [],
+ };
+ expect(getValidationMessage(result)).toBe('Error 1\nError 2');
+ });
+
+ it('should return warnings only', () => {
+ const result = {
+ valid: true,
+ errors: [],
+ warnings: ['Warning 1', 'Warning 2'],
+ };
+ expect(getValidationMessage(result)).toBe('Warning 1\nWarning 2');
+ });
+
+ it('should return both errors and warnings', () => {
+ const result = {
+ valid: false,
+ errors: ['Error 1'],
+ warnings: ['Warning 1'],
+ };
+ expect(getValidationMessage(result)).toBe('Error 1\nWarning 1');
+ });
+ });
+});
diff --git a/utils/wifiValidation.ts b/utils/wifiValidation.ts
new file mode 100644
index 0000000..f4a4827
--- /dev/null
+++ b/utils/wifiValidation.ts
@@ -0,0 +1,235 @@
+/**
+ * WiFi Credentials Validation Utility
+ *
+ * Provides comprehensive validation for WiFi SSID and password
+ * following IEEE 802.11 standards and common router implementations.
+ */
+
+export interface WiFiValidationResult {
+ valid: boolean;
+ errors: string[];
+ warnings: string[];
+}
+
+export interface WiFiCredentials {
+ ssid: string;
+ password: string;
+}
+
+/**
+ * Validate WiFi SSID
+ *
+ * Rules:
+ * - Length: 1-32 characters (IEEE 802.11 standard)
+ * - Can contain: letters, numbers, spaces, and special characters
+ * - Cannot be empty
+ * - Should not contain control characters
+ */
+export function validateSSID(ssid: string): WiFiValidationResult {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ // Check for empty SSID
+ if (!ssid || ssid.trim().length === 0) {
+ errors.push('Network name (SSID) cannot be empty');
+ return { valid: false, errors, warnings };
+ }
+
+ // Check length (IEEE 802.11 standard: max 32 bytes)
+ // Note: UTF-8 characters can be multiple bytes
+ const byteLength = new Blob([ssid]).size;
+ if (byteLength > 32) {
+ errors.push('Network name is too long (max 32 bytes)');
+ }
+
+ // Check for control characters (ASCII 0-31)
+ if (/[\x00-\x1F]/.test(ssid)) {
+ errors.push('Network name contains invalid control characters');
+ }
+
+ // Warnings for potentially problematic SSIDs
+ if (ssid.startsWith(' ') || ssid.endsWith(' ')) {
+ warnings.push('Network name has leading or trailing spaces - this may cause connection issues');
+ }
+
+ if (/\s{2,}/.test(ssid)) {
+ warnings.push('Network name has multiple consecutive spaces');
+ }
+
+ return {
+ valid: errors.length === 0,
+ errors,
+ warnings,
+ };
+}
+
+/**
+ * Validate WiFi Password
+ *
+ * Rules based on WPA/WPA2/WPA3 standards:
+ * - WPA/WPA2-PSK: 8-63 characters (ASCII)
+ * - Open networks: no password required
+ * - WEP: 5 or 13 characters (ASCII) or 10/26 hex digits (deprecated)
+ *
+ * Additional checks:
+ * - Should not be empty for secured networks
+ * - Should not contain control characters
+ * - Common weak passwords warning
+ */
+export function validatePassword(
+ password: string,
+ authType?: string
+): WiFiValidationResult {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ // Open networks don't need password
+ if (authType === 'Open') {
+ return { valid: true, errors, warnings };
+ }
+
+ // Check for empty password on secured networks
+ if (!password || password.length === 0) {
+ errors.push('Password is required for secured networks');
+ return { valid: false, errors, warnings };
+ }
+
+ // WPA/WPA2/WPA3 validation (most common)
+ if (!authType || authType.includes('WPA')) {
+ // Length check: 8-63 characters
+ if (password.length < 8) {
+ errors.push('Password must be at least 8 characters for WPA/WPA2/WPA3 networks');
+ }
+
+ if (password.length > 63) {
+ errors.push('Password cannot exceed 63 characters for WPA/WPA2/WPA3 networks');
+ }
+
+ // Check for ASCII only (non-ASCII can cause issues)
+
+ if (!/^[\x20-\x7E]*$/.test(password)) {
+ warnings.push('Password contains non-ASCII characters - this may cause issues on some devices');
+ }
+ }
+
+ // WEP validation (deprecated but still exists)
+ if (authType === 'WEP') {
+ const isValidWEP =
+ password.length === 5 || // 40-bit WEP
+ password.length === 13 || // 104-bit WEP
+ (password.length === 10 && /^[0-9A-Fa-f]+$/.test(password)) || // 40-bit hex
+ (password.length === 26 && /^[0-9A-Fa-f]+$/.test(password)); // 104-bit hex
+
+ if (!isValidWEP) {
+ errors.push('WEP password must be 5 or 13 ASCII characters, or 10/26 hexadecimal digits');
+ }
+
+ warnings.push('WEP is deprecated and insecure - consider upgrading your router to WPA2/WPA3');
+ }
+
+ // Check for control characters
+ if (/[\x00-\x1F]/.test(password)) {
+ errors.push('Password contains invalid control characters');
+ }
+
+ // Weak password warnings
+ if (password.length < 12 && !errors.length) {
+ warnings.push('Password is weak - consider using at least 12 characters');
+ }
+
+ // Common weak passwords
+ const weakPasswords = [
+ 'password', '12345678', 'qwerty123', 'abc123456',
+ 'password1', 'password123', '11111111', '00000000'
+ ];
+ if (weakPasswords.includes(password.toLowerCase())) {
+ warnings.push('This is a commonly used weak password');
+ }
+
+ // Check for only numbers
+ if (/^\d+$/.test(password) && password.length === 8) {
+ warnings.push('Password contains only numbers - consider adding letters and symbols');
+ }
+
+ return {
+ valid: errors.length === 0,
+ errors,
+ warnings,
+ };
+}
+
+/**
+ * Validate complete WiFi credentials (SSID + password)
+ */
+export function validateWiFiCredentials(
+ ssid: string,
+ password: string,
+ authType?: string
+): WiFiValidationResult {
+ const ssidResult = validateSSID(ssid);
+ const passwordResult = validatePassword(password, authType);
+
+ return {
+ valid: ssidResult.valid && passwordResult.valid,
+ errors: [...ssidResult.errors, ...passwordResult.errors],
+ warnings: [...ssidResult.warnings, ...passwordResult.warnings],
+ };
+}
+
+/**
+ * Check if password is strong enough for production use
+ * More strict than basic validation
+ */
+export function isStrongPassword(password: string): boolean {
+ if (password.length < 12) return false;
+
+ const hasUppercase = /[A-Z]/.test(password);
+ const hasLowercase = /[a-z]/.test(password);
+ const hasNumber = /\d/.test(password);
+ const hasSpecial = /[^A-Za-z0-9]/.test(password);
+
+ // Require at least 3 of 4 character types
+ const typesCount = [hasUppercase, hasLowercase, hasNumber, hasSpecial].filter(Boolean).length;
+ return typesCount >= 3;
+}
+
+/**
+ * Sanitize WiFi credentials before sending to device
+ * Trims whitespace and performs basic cleanup
+ */
+export function sanitizeWiFiCredentials(credentials: WiFiCredentials): WiFiCredentials {
+ return {
+ ssid: credentials.ssid.trim(),
+ password: credentials.password, // Don't trim password - spaces might be intentional
+ };
+}
+
+/**
+ * Get user-friendly error message for validation result
+ */
+export function getValidationErrorMessage(result: WiFiValidationResult): string {
+ if (result.valid) return '';
+
+ if (result.errors.length > 0) {
+ return result.errors[0]; // Return first error
+ }
+
+ return 'Invalid WiFi credentials';
+}
+
+/**
+ * Get combined error and warning message
+ */
+export function getValidationMessage(result: WiFiValidationResult): string {
+ const messages: string[] = [];
+
+ if (result.errors.length > 0) {
+ messages.push(...result.errors);
+ }
+
+ if (result.warnings.length > 0) {
+ messages.push(...result.warnings);
+ }
+
+ return messages.join('\n');
+}