diff --git a/app.json b/app.json index 1fc1608..ff4a338 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "WellNuo", "slug": "WellNuo", - "version": "1.0.4", + "version": "1.0.5", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "wellnuo", @@ -28,6 +28,7 @@ }, "android": { "package": "com.wellnuo.app", + "softwareKeyboardLayoutMode": "resize", "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/images/android-icon-foreground.png", @@ -84,9 +85,9 @@ "extra": { "router": {}, "eas": { - "projectId": "a845255d-c966-4f12-aa60-c452c2d0c60d" + "projectId": "8618c68a-6942-47ec-94f5-787fdbe5c0b4" } }, - "owner": "serter20692" + "owner": "rzmrzli" } } diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index ebb4f0e..031e5b0 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -56,7 +56,7 @@ export default function TabLayout() { ), }} /> - {/* Voice tab hidden - using Debug for testing */} + {/* Voice tab - HIDDEN (calls go through Julia tab chat screen) */} - {/* Debug tab - hidden in production */} + {/* Debug tab - HIDDEN, no longer needed */} ([]); + const [beneficiariesLoaded, setBeneficiariesLoaded] = useState(false); // Load beneficiaries on mount useEffect(() => { const loadBeneficiaries = async () => { try { - const data = await api.getAllBeneficiaries(); - if (data) { - setBeneficiaries(data); + const response = await api.getAllBeneficiaries(); + if (response.ok && response.data) { + setBeneficiaries(response.data); + console.log('[VoiceCall] Beneficiaries loaded:', response.data.length); } } catch (error) { console.warn('[VoiceCall] Failed to load beneficiaries:', error); + } finally { + setBeneficiariesLoaded(true); } }; loadBeneficiaries(); @@ -57,26 +57,40 @@ export default function VoiceCallScreen() { // Build beneficiaryData for voice agent const beneficiaryData = useMemo((): BeneficiaryData | undefined => { - if (beneficiaries.length === 0) { + // Safety check - ensure beneficiaries is an array + if (!Array.isArray(beneficiaries) || beneficiaries.length === 0) { + console.log('[VoiceCall] No beneficiaries yet, skipping beneficiaryData'); return undefined; } - // Build beneficiary_names_dict from all beneficiaries - // Format: {"21": "papa", "69": "David"} - const beneficiaryNamesDict: Record = {}; - beneficiaries.forEach(b => { - beneficiaryNamesDict[b.id.toString()] = b.name; - }); + try { + // Build beneficiary_names_dict from all beneficiaries + // Format: {"21": "papa", "69": "David"} + const beneficiaryNamesDict: Record = {}; + beneficiaries.forEach(b => { + // Safety: check that b exists and has id and name + if (b && b.id != null && b.name) { + beneficiaryNamesDict[String(b.id)] = b.name; + } + }); - // Get deployment_id from current beneficiary or fallback to first one - const deploymentId = currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21'; + // Get deployment_id from current beneficiary or fallback to first one + const deploymentId = currentBeneficiary?.id != null + ? String(currentBeneficiary.id) + : beneficiaries[0]?.id != null + ? String(beneficiaries[0].id) + : '21'; - console.log('[VoiceCall] BeneficiaryData:', { deploymentId, beneficiaryNamesDict }); + console.log('[VoiceCall] BeneficiaryData:', { deploymentId, beneficiaryNamesDict }); - return { - deploymentId, - beneficiaryNamesDict, - }; + return { + deploymentId, + beneficiaryNamesDict, + }; + } catch (error) { + console.error('[VoiceCall] Error building beneficiaryData:', error); + return undefined; + } }, [beneficiaries, currentBeneficiary]); // LiveKit hook - ALL logic is here @@ -104,16 +118,51 @@ export default function VoiceCallScreen() { const rotateAnim = useRef(new Animated.Value(0)).current; const avatarScale = useRef(new Animated.Value(0.8)).current; - // Clear transcript and start call on mount + // Clear transcript on mount useEffect(() => { clearTranscript(); - connect(); - - return () => { - // Cleanup handled by the hook - }; }, []); + // Track if connect has been called to prevent duplicate calls + const connectCalledRef = useRef(false); + + // Start call ONLY after beneficiaries are loaded AND beneficiaryData is ready + // IMPORTANT: We must wait for beneficiaryData to be populated! + // Without deploymentId, Julia AI agent won't know which beneficiary to talk about. + useEffect(() => { + // Prevent duplicate connect calls + if (connectCalledRef.current) return; + + // Only connect when beneficiaryData has a valid deploymentId + if (beneficiariesLoaded && beneficiaryData?.deploymentId) { + console.log('[VoiceCall] Starting call with beneficiaryData:', JSON.stringify(beneficiaryData)); + connectCalledRef.current = true; + connect(); + } else if (beneficiariesLoaded) { + console.log('[VoiceCall] Waiting for beneficiaryData... Current state:', { + beneficiariesLoaded, + beneficiariesCount: beneficiaries.length, + beneficiaryData: beneficiaryData ? JSON.stringify(beneficiaryData) : 'undefined' + }); + } + }, [beneficiariesLoaded, beneficiaryData, beneficiaries.length, connect]); + + // Fallback: if beneficiaryData doesn't arrive in 5 seconds, connect anyway + // This handles edge cases where API fails or user has no beneficiaries + useEffect(() => { + if (connectCalledRef.current) return; + + const timeout = setTimeout(() => { + if (!connectCalledRef.current && beneficiariesLoaded) { + console.warn('[VoiceCall] Timeout: beneficiaryData not ready after 5s, connecting without it'); + connectCalledRef.current = true; + connect(); + } + }, 5000); + + return () => clearTimeout(timeout); + }, [beneficiariesLoaded, connect]); + // Navigate back on disconnect or error useEffect(() => { if (state === 'disconnected' || state === 'error') { @@ -189,13 +238,6 @@ export default function VoiceCallScreen() { router.back(); }; - // Toggle speaker/earpiece - const handleToggleSpeaker = async () => { - const newSpeakerState = !isSpeakerOn; - setIsSpeakerOn(newSpeakerState); - await setAudioOutput(newSpeakerState); - }; - // Format duration as MM:SS const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -313,7 +355,7 @@ export default function VoiceCallScreen() { - {/* Bottom controls - centered layout with 3 buttons */} + {/* Bottom controls - centered layout with 2 buttons */} {/* Mute button */} {isMuted ? 'Unmute' : 'Mute'} - {/* Speaker toggle button */} - - - {isSpeakerOn ? 'Speaker' : 'Earpiece'} - - {/* End call button */} @@ -475,7 +503,7 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingVertical: Spacing.xl, paddingHorizontal: Spacing.lg, - gap: 24, // Space between 3 buttons (Mute, Speaker, End Call) + gap: 40, // Space between 2 buttons (Mute, End Call) }, controlButton: { alignItems: 'center', diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx index 4990deb..81525da 100644 --- a/components/ui/Input.tsx +++ b/components/ui/Input.tsx @@ -5,6 +5,7 @@ import { Text, StyleSheet, TouchableOpacity, + Platform, type TextInputProps, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; @@ -121,6 +122,11 @@ const styles = StyleSheet.create({ paddingHorizontal: Spacing.md, fontSize: FontSizes.base, color: AppColors.textPrimary, + // Fix for Android password field text visibility + ...(Platform.OS === 'android' && { + fontFamily: 'Roboto', + includeFontPadding: false, + }), }, inputWithLeftIcon: { paddingLeft: 0,