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;
}