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,