From 994e2faadb58d17acfaa8bfdd6545cf47eadf3c8 Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 27 Jan 2026 21:49:02 -0800 Subject: [PATCH] Fix deployment error handling, build info display, Android UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add build number/timestamp display on login screen - Improve error message when beneficiary has no deployment (user-friendly text instead of crash) - Fix verify-otp screen layout for Android (smaller spacing, icon sizes) - Add KeyboardAvoidingView to setup-wifi screen - Save WiFi passwords per SSID (auto-fill on reconnect) - Suppress BLE "operation cancelled" noise in logs - Add build-info generation script (npm run build-info) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(auth)/login.tsx | 3 +- app/(auth)/verify-otp.tsx | 25 ++++++----- app/(tabs)/beneficiaries/[id]/setup-wifi.tsx | 45 ++++++++++++++------ constants/build-info.ts | 5 +++ package.json | 5 ++- scripts/generate-build-info.js | 41 ++++++++++++++++++ services/api.ts | 2 +- services/ble/BLEManager.ts | 16 ++++--- 8 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 constants/build-info.ts create mode 100644 scripts/generate-build-info.js diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index 3e787d9..9896cb7 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -3,6 +3,7 @@ import { ErrorMessage } from '@/components/ui/ErrorMessage'; import { Input } from '@/components/ui/Input'; import { AppColors, FontSizes, Spacing } from '@/constants/theme'; import { useAuth } from '@/contexts/AuthContext'; +import { BUILD_DISPLAY } from '@/constants/build-info'; import { router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { @@ -172,7 +173,7 @@ export default function LoginScreen() { - WellNuo v1.0.0 + WellNuo v1.0.0{'\n'}{BUILD_DISPLAY} ); diff --git a/app/(auth)/verify-otp.tsx b/app/(auth)/verify-otp.tsx index d5ce7d8..366bf28 100644 --- a/app/(auth)/verify-otp.tsx +++ b/app/(auth)/verify-otp.tsx @@ -9,6 +9,7 @@ import { TouchableOpacity, TextInput, ActivityIndicator, + Keyboard, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; @@ -246,12 +247,14 @@ export default function VerifyOTPScreen() { return ( @@ -259,7 +262,7 @@ export default function VerifyOTPScreen() { - + @@ -342,7 +345,7 @@ const styles = StyleSheet.create({ scrollContent: { flexGrow: 1, paddingHorizontal: Spacing.lg, - paddingTop: Spacing.xl, + paddingTop: Platform.OS === 'android' ? Spacing.md : Spacing.xl, paddingBottom: Spacing.xl, }, autoLoginContainer: { @@ -361,29 +364,29 @@ const styles = StyleSheet.create({ height: 44, justifyContent: 'center', alignItems: 'flex-start', - marginBottom: Spacing.xl, + marginBottom: Platform.OS === 'android' ? Spacing.sm : Spacing.xl, }, iconContainer: { alignItems: 'center', - marginBottom: Spacing.xl, + marginBottom: Platform.OS === 'android' ? Spacing.md : Spacing.xl, }, iconCircle: { - width: 100, - height: 100, - borderRadius: 50, + width: Platform.OS === 'android' ? 72 : 100, + height: Platform.OS === 'android' ? 72 : 100, + borderRadius: Platform.OS === 'android' ? 36 : 50, backgroundColor: `${AppColors.primary}15`, justifyContent: 'center', alignItems: 'center', }, header: { alignItems: 'center', - marginBottom: Spacing.lg, + marginBottom: Platform.OS === 'android' ? Spacing.sm : Spacing.lg, }, title: { fontSize: FontSizes['2xl'], fontWeight: '700', color: AppColors.textPrimary, - marginBottom: Spacing.md, + marginBottom: Platform.OS === 'android' ? Spacing.xs : Spacing.md, textAlign: 'center', }, subtitle: { @@ -401,7 +404,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'center', gap: Spacing.sm, - marginVertical: Spacing.xl, + marginVertical: Platform.OS === 'android' ? Spacing.md : Spacing.xl, }, codeBox: { width: 48, diff --git a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx index aad28d7..fa3b3b7 100644 --- a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx +++ b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx @@ -8,6 +8,8 @@ import { Alert, ActivityIndicator, TextInput, + KeyboardAvoidingView, + Platform, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -111,19 +113,28 @@ export default function SetupWiFiScreen() { const setupInProgressRef = useRef(false); const shouldCancelRef = useRef(false); - // Load saved WiFi password on mount + // Saved WiFi passwords map (SSID -> password) + const savedPasswordsRef = useRef>({}); + + // Load saved WiFi passwords on mount useEffect(() => { - const loadSavedPassword = async () => { + const loadSavedPasswords = async () => { try { - const savedPassword = await AsyncStorage.getItem('LAST_WIFI_PASSWORD'); - if (savedPassword) { - setPassword(savedPassword); + const saved = await AsyncStorage.getItem('WIFI_PASSWORDS'); + if (saved) { + savedPasswordsRef.current = JSON.parse(saved); + console.log('[SetupWiFi] Loaded saved passwords for', Object.keys(savedPasswordsRef.current).length, 'networks'); + } + // Also load legacy single password + const legacyPassword = await AsyncStorage.getItem('LAST_WIFI_PASSWORD'); + if (legacyPassword && !saved) { + setPassword(legacyPassword); } } catch (error) { - console.log('[SetupWiFi] Failed to load saved password:', error); + console.log('[SetupWiFi] Failed to load saved passwords:', error); } }; - loadSavedPassword(); + loadSavedPasswords(); loadWiFiNetworks(); }, []); @@ -162,7 +173,9 @@ export default function SetupWiFiScreen() { const handleSelectNetwork = (network: WiFiNetwork) => { setSelectedNetwork(network); - setPassword(''); + // Auto-fill saved password for this network + const savedPwd = savedPasswordsRef.current[network.ssid]; + setPassword(savedPwd || ''); }; // Update a specific step for a sensor @@ -395,10 +408,12 @@ export default function SetupWiFiScreen() { return; } - // Save password for next time + // Save password for this network (by SSID) try { - await AsyncStorage.setItem('LAST_WIFI_PASSWORD', password); - console.log('[SetupWiFi] Password saved for future use'); + savedPasswordsRef.current[selectedNetwork.ssid] = password; + await AsyncStorage.setItem('WIFI_PASSWORDS', JSON.stringify(savedPasswordsRef.current)); + await AsyncStorage.setItem('LAST_WIFI_PASSWORD', password); // legacy compat + console.log('[SetupWiFi] Password saved for network:', selectedNetwork.ssid); } catch (error) { console.log('[SetupWiFi] Failed to save password:', error); } @@ -571,7 +586,12 @@ export default function SetupWiFiScreen() { - + + {/* Device Info Card */} @@ -724,6 +744,7 @@ export default function SetupWiFiScreen() { + ); } diff --git a/constants/build-info.ts b/constants/build-info.ts new file mode 100644 index 0000000..83364b1 --- /dev/null +++ b/constants/build-info.ts @@ -0,0 +1,5 @@ +// Auto-generated by scripts/generate-build-info.js +// DO NOT EDIT MANUALLY +export const BUILD_NUMBER = 1; +export const BUILD_TIMESTAMP = '2026-01-28T05:16:19.402Z'; +export const BUILD_DISPLAY = 'build 1 · Jan 27, 21:16'; diff --git a/package.json b/package.json index 5808950..2bd018d 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", - "android": "expo run:android", - "ios": "expo run:ios", + "build-info": "node scripts/generate-build-info.js", + "android": "npm run build-info && expo run:android", + "ios": "npm run build-info && expo run:ios", "web": "expo start --web", "lint": "expo lint", "postinstall": "patch-package" diff --git a/scripts/generate-build-info.js b/scripts/generate-build-info.js new file mode 100644 index 0000000..aca386f --- /dev/null +++ b/scripts/generate-build-info.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +/** + * Generates build-info.ts with auto-incrementing build number and timestamp. + * Run before each build: node scripts/generate-build-info.js + */ + +const fs = require('fs'); +const path = require('path'); + +const BUILD_INFO_PATH = path.join(__dirname, '..', 'constants', 'build-info.ts'); + +// Read current build number +let buildNumber = 0; +try { + const existing = fs.readFileSync(BUILD_INFO_PATH, 'utf8'); + const match = existing.match(/BUILD_NUMBER\s*=\s*(\d+)/); + if (match) { + buildNumber = parseInt(match[1], 10); + } +} catch { + // First build +} + +buildNumber++; + +const now = new Date(); +const timestamp = now.toISOString(); +// Short format for UI: "Jan 28, 08:15" +const shortDate = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +const shortTime = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); + +const content = `// Auto-generated by scripts/generate-build-info.js +// DO NOT EDIT MANUALLY +export const BUILD_NUMBER = ${buildNumber}; +export const BUILD_TIMESTAMP = '${timestamp}'; +export const BUILD_DISPLAY = 'build ${buildNumber} · ${shortDate}, ${shortTime}'; +`; + +fs.writeFileSync(BUILD_INFO_PATH, content, 'utf8'); +console.log(`Build info generated: #${buildNumber} at ${timestamp}`); diff --git a/services/api.ts b/services/api.ts index a48e8a0..ad5237c 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1842,7 +1842,7 @@ class ApiService { if (!deploymentId) { console.error('[API] Beneficiary has no deploymentId'); - throw new Error('Beneficiary has no deployment'); + throw new Error('No deployment configured for this beneficiary. Please remove and re-add the beneficiary to fix this.'); } const creds = await this.getLegacyCredentials(); diff --git a/services/ble/BLEManager.ts b/services/ble/BLEManager.ts index 4dd79dd..a58af45 100644 --- a/services/ble/BLEManager.ts +++ b/services/ble/BLEManager.ts @@ -291,11 +291,17 @@ export class RealBLEManager implements IBLEManager { // Wrap callback in try-catch to prevent crashes try { if (error) { - console.error('[BLE] Notification error:', { - message: error?.message || 'null', - errorCode: (error as any)?.errorCode || 'null', - reason: (error as any)?.reason || 'null', - }); + const errCode = (error as any)?.errorCode; + // errorCode 2 = "Operation was cancelled" — normal BLE cleanup, not a real error + if (errCode === 2) { + console.log('[BLE] Notification cancelled (normal cleanup)'); + } else { + console.error('[BLE] Notification error:', { + message: error?.message || 'null', + errorCode: errCode || 'null', + reason: (error as any)?.reason || 'null', + }); + } safeReject(error); return; }