feat: Add Deployment ID setting in Profile

- Add deploymentId storage methods in api.ts (set/get/clear)
- Add Settings section in Profile with Deployment ID menu item
- Add modal dialog to edit deployment ID
- Update chat.tsx to use custom deployment ID from settings
- Priority: custom > currentBeneficiary > first beneficiary > fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2026-01-24 20:48:18 -08:00
parent 664759dee9
commit 9ae23cfef3
3 changed files with 184 additions and 6 deletions

View File

@ -433,6 +433,18 @@ export default function ChatScreen() {
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
const [loadingBeneficiaries, setLoadingBeneficiaries] = useState(false);
// Custom deployment ID from settings
const [customDeploymentId, setCustomDeploymentId] = useState<string | null>(null);
// Load custom deployment ID from settings
useEffect(() => {
const loadCustomDeploymentId = async () => {
const saved = await api.getDeploymentId();
setCustomDeploymentId(saved);
};
loadCustomDeploymentId();
}, []);
// Load beneficiaries
const loadBeneficiaries = useCallback(async () => {
setLoadingBeneficiaries(true);
@ -500,8 +512,9 @@ export default function ChatScreen() {
try {
// Build beneficiary data for the agent
// Priority: customDeploymentId from settings > currentBeneficiary > first beneficiary > fallback
const beneficiaryData: BeneficiaryData = {
deploymentId: currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21',
deploymentId: customDeploymentId || currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21',
beneficiaryNamesDict: {},
};
@ -539,7 +552,7 @@ export default function ChatScreen() {
} finally {
setIsConnectingVoice(false);
}
}, [isConnectingVoice, isCallActive, currentBeneficiary, beneficiaries, user, clearTranscript, startCall]);
}, [isConnectingVoice, isCallActive, currentBeneficiary, beneficiaries, user, clearTranscript, startCall, customDeploymentId]);
// End voice call
const endVoiceCall = useCallback(() => {
@ -647,8 +660,8 @@ export default function ChatScreen() {
beneficiaryNamesDict[b.id.toString()] = 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: custom from settings > current beneficiary > first beneficiary > fallback
const deploymentId = customDeploymentId || currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21';
// Call API with EXACT same params as voice agent
// SINGLE_DEPLOYMENT_MODE: sends only deployment_id (no beneficiary_names_dict)
@ -701,7 +714,7 @@ export default function ChatScreen() {
} finally {
setIsSending(false);
}
}, [input, isSending, getWellNuoToken]);
}, [input, isSending, getWellNuoToken, customDeploymentId, currentBeneficiary, beneficiaries]);
// Render message bubble
const renderMessage = ({ item }: { item: Message }) => {

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
@ -6,11 +6,14 @@ import {
ScrollView,
TouchableOpacity,
Alert,
TextInput,
Modal,
} from 'react-native';
import { router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '@/contexts/AuthContext';
import { api } from '@/services/api';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface MenuItemProps {
@ -50,6 +53,37 @@ function MenuItem({
export default function ProfileScreen() {
const { user, logout } = useAuth();
const [deploymentId, setDeploymentId] = useState<string>('');
const [showDeploymentModal, setShowDeploymentModal] = useState(false);
const [tempDeploymentId, setTempDeploymentId] = useState('');
// Load saved deployment ID
useEffect(() => {
const loadDeploymentId = async () => {
const saved = await api.getDeploymentId();
if (saved) {
setDeploymentId(saved);
}
};
loadDeploymentId();
}, []);
const openDeploymentModal = useCallback(() => {
setTempDeploymentId(deploymentId);
setShowDeploymentModal(true);
}, [deploymentId]);
const saveDeploymentId = useCallback(async () => {
const trimmed = tempDeploymentId.trim();
if (trimmed) {
await api.setDeploymentId(trimmed);
setDeploymentId(trimmed);
} else {
await api.clearDeploymentId();
setDeploymentId('');
}
setShowDeploymentModal(false);
}, [tempDeploymentId]);
const openTerms = () => {
router.push('/terms');
@ -98,6 +132,19 @@ export default function ProfileScreen() {
</View>
</View>
{/* Settings */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Settings</Text>
<View style={styles.menuCard}>
<MenuItem
icon="server-outline"
title="Deployment ID"
subtitle={deploymentId || 'Not set (auto)'}
onPress={openDeploymentModal}
/>
</View>
</View>
{/* Legal - Required for App Store */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Legal</Text>
@ -127,6 +174,46 @@ export default function ProfileScreen() {
{/* Version */}
<Text style={styles.version}>WellNuo v1.0.0</Text>
</ScrollView>
{/* Deployment ID Modal */}
<Modal
visible={showDeploymentModal}
transparent
animationType="fade"
onRequestClose={() => setShowDeploymentModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Deployment ID</Text>
<Text style={styles.modalDescription}>
Enter the deployment ID to connect to a specific device. Leave empty for automatic detection.
</Text>
<TextInput
style={styles.modalInput}
placeholder="e.g., 21"
placeholderTextColor={AppColors.textMuted}
value={tempDeploymentId}
onChangeText={setTempDeploymentId}
keyboardType="numeric"
autoFocus
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.modalButtonCancel}
onPress={() => setShowDeploymentModal(false)}
>
<Text style={styles.modalButtonCancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.modalButtonSave}
onPress={saveDeploymentId}
>
<Text style={styles.modalButtonSaveText}>Save</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
@ -252,4 +339,65 @@ const styles = StyleSheet.create({
color: AppColors.textMuted,
paddingVertical: Spacing.xl,
},
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: Spacing.lg,
},
modalContent: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.lg,
padding: Spacing.lg,
width: '100%',
maxWidth: 400,
},
modalTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
modalDescription: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginBottom: Spacing.md,
},
modalInput: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm + 4,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
marginBottom: Spacing.md,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: Spacing.sm,
},
modalButtonCancel: {
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
},
modalButtonCancelText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
modalButtonSave: {
backgroundColor: AppColors.primary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
borderRadius: BorderRadius.md,
},
modalButtonSaveText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
});

View File

@ -198,6 +198,23 @@ class ApiService {
}
}
// Deployment ID management
async setDeploymentId(deploymentId: string): Promise<void> {
await SecureStore.setItemAsync('deploymentId', deploymentId);
}
async getDeploymentId(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('deploymentId');
} catch {
return null;
}
}
async clearDeploymentId(): Promise<void> {
await SecureStore.deleteItemAsync('deploymentId');
}
// Beneficiaries (elderly people being monitored)
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
const token = await this.getToken();