Fix deployment error handling, build info display, Android UI improvements
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
7149d25ba4
commit
994e2faadb
@ -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() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.version}>WellNuo v1.0.0</Text>
|
||||
<Text style={styles.version}>WellNuo v1.0.0{'\n'}{BUILD_DISPLAY}</Text>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
>
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
@ -259,7 +262,7 @@ export default function VerifyOTPScreen() {
|
||||
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={styles.iconCircle}>
|
||||
<Ionicons name="mail-open" size={48} color={AppColors.primary} />
|
||||
<Ionicons name="mail-open" size={Platform.OS === 'android' ? 36 : 48} color={AppColors.primary} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Record<string, string>>({});
|
||||
|
||||
// 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() {
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||
<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}>
|
||||
@ -724,6 +744,7 @@ export default function SetupWiFiScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
5
constants/build-info.ts
Normal file
5
constants/build-info.ts
Normal file
@ -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';
|
||||
@ -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"
|
||||
|
||||
41
scripts/generate-build-info.js
Normal file
41
scripts/generate-build-info.js
Normal file
@ -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}`);
|
||||
@ -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();
|
||||
|
||||
@ -291,11 +291,17 @@ export class RealBLEManager implements IBLEManager {
|
||||
// Wrap callback in try-catch to prevent crashes
|
||||
try {
|
||||
if (error) {
|
||||
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: (error as any)?.errorCode || 'null',
|
||||
errorCode: errCode || 'null',
|
||||
reason: (error as any)?.reason || 'null',
|
||||
});
|
||||
}
|
||||
safeReject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user