wellnua-lite/app/(tabs)/profile.tsx
Sergei 85896f442f Show beneficiary name instead of deployment ID in chat
- Add deploymentName state to chat screen
- Load and display beneficiary name in initial welcome message
- Save deployment name to SecureStore when validating in profile
- End call and clear chat when deployment changes
- Fix text input not clearing after sending message

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-25 10:30:01 -08:00

471 lines
13 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
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 {
icon: keyof typeof Ionicons.glyphMap;
iconColor?: string;
iconBgColor?: string;
title: string;
subtitle?: string;
onPress?: () => void;
showChevron?: boolean;
}
function MenuItem({
icon,
iconColor = AppColors.primary,
iconBgColor = '#DBEAFE',
title,
subtitle,
onPress,
showChevron = true,
}: MenuItemProps) {
return (
<TouchableOpacity style={styles.menuItem} onPress={onPress}>
<View style={[styles.menuIconContainer, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<View style={styles.menuTextContainer}>
<Text style={styles.menuTitle}>{title}</Text>
{subtitle && <Text style={styles.menuSubtitle}>{subtitle}</Text>}
</View>
{showChevron && (
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
)}
</TouchableOpacity>
);
}
export default function ProfileScreen() {
const { user, logout } = useAuth();
const [deploymentId, setDeploymentId] = useState<string>('');
const [deploymentName, setDeploymentName] = useState<string>('');
const [showDeploymentModal, setShowDeploymentModal] = useState(false);
const [tempDeploymentId, setTempDeploymentId] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
// Load saved deployment ID or auto-populate from first available
useEffect(() => {
const loadDeploymentId = async () => {
const saved = await api.getDeploymentId();
if (saved) {
// Use saved deployment ID
setDeploymentId(saved);
// Validate to get the deployment name
const result = await api.validateDeploymentId(saved);
if (result.ok && result.data?.valid && result.data.name) {
setDeploymentName(result.data.name);
}
} else {
// No saved ID - auto-populate from first available deployment
const firstResult = await api.getFirstDeploymentId();
if (firstResult.ok && firstResult.data) {
setDeploymentId(firstResult.data.deploymentId);
setDeploymentName(firstResult.data.name);
// Also save it so it persists
await api.setDeploymentId(firstResult.data.deploymentId);
}
}
};
loadDeploymentId();
}, []);
const openDeploymentModal = useCallback(() => {
setTempDeploymentId(deploymentId);
setValidationError(null);
setShowDeploymentModal(true);
}, [deploymentId]);
const saveDeploymentId = useCallback(async () => {
const trimmed = tempDeploymentId.trim();
setValidationError(null);
if (trimmed) {
setIsValidating(true);
try {
const result = await api.validateDeploymentId(trimmed);
if (result.ok && result.data?.valid) {
await api.setDeploymentId(trimmed);
if (result.data.name) {
await api.setDeploymentName(result.data.name);
}
setDeploymentId(trimmed);
setDeploymentName(result.data.name || '');
setShowDeploymentModal(false);
} else if (result.ok && !result.data?.valid) {
setValidationError('Invalid Deployment ID. Please check and try again.');
} else {
setValidationError(result.error?.message || 'Failed to validate Deployment ID');
}
} catch {
setValidationError('Network error. Please try again.');
} finally {
setIsValidating(false);
}
} else {
await api.clearDeploymentId();
setDeploymentId('');
setDeploymentName('');
setShowDeploymentModal(false);
}
}, [tempDeploymentId]);
const openTerms = () => {
router.push('/terms');
};
const openPrivacy = () => {
router.push('/privacy');
};
const handleLogout = () => {
Alert.alert(
'Logout',
'Are you sure you want to logout?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Logout',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/(auth)/login');
},
},
],
{ cancelable: true }
);
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Profile</Text>
</View>
{/* User Info */}
<View style={styles.userCard}>
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>
{user?.user_name?.charAt(0).toUpperCase() || 'U'}
</Text>
</View>
<View style={styles.userInfo}>
<Text style={styles.userName}>{user?.user_name || 'User'}</Text>
</View>
</View>
{/* Settings */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Settings</Text>
<View style={styles.menuCard}>
<MenuItem
icon="server-outline"
title="Deployment"
subtitle={deploymentId ? (deploymentName || `ID: ${deploymentId}`) : 'Auto'}
onPress={openDeploymentModal}
/>
</View>
</View>
{/* Legal - Required for App Store */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Legal</Text>
<View style={styles.menuCard}>
<MenuItem
icon="document-text-outline"
title="Terms of Service"
onPress={openTerms}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="shield-outline"
title="Privacy Policy"
onPress={openPrivacy}
/>
</View>
</View>
{/* Logout Button */}
<View style={styles.section}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Ionicons name="log-out-outline" size={20} color={AppColors.error} />
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
{/* 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, validationError && styles.modalInputError]}
placeholder="e.g., 21"
placeholderTextColor={AppColors.textMuted}
value={tempDeploymentId}
onChangeText={(text) => {
setTempDeploymentId(text);
setValidationError(null);
}}
keyboardType="numeric"
autoFocus
editable={!isValidating}
/>
{validationError && (
<Text style={styles.errorText}>{validationError}</Text>
)}
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.modalButtonCancel}
onPress={() => setShowDeploymentModal(false)}
disabled={isValidating}
>
<Text style={[styles.modalButtonCancelText, isValidating && styles.disabledText]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalButtonSave, isValidating && styles.modalButtonDisabled]}
onPress={saveDeploymentId}
disabled={isValidating}
>
<Text style={styles.modalButtonSaveText}>
{isValidating ? 'Validating...' : 'Save'}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
header: {
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
headerTitle: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
},
userCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
padding: Spacing.lg,
marginBottom: Spacing.md,
},
avatarContainer: {
width: 64,
height: 64,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
avatarText: {
fontSize: FontSizes['2xl'],
fontWeight: '600',
color: AppColors.white,
},
userInfo: {
flex: 1,
marginLeft: Spacing.md,
},
userName: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
editButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.surface,
justifyContent: 'center',
alignItems: 'center',
},
section: {
marginBottom: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
menuCard: {
backgroundColor: AppColors.background,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
menuIconContainer: {
width: 36,
height: 36,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
menuTextContainer: {
flex: 1,
marginLeft: Spacing.md,
},
menuTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
menuSubtitle: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
menuDivider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 36 + Spacing.md,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.background,
paddingVertical: Spacing.md,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
},
logoutText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.error,
marginLeft: Spacing.sm,
},
version: {
textAlign: 'center',
fontSize: FontSizes.xs,
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,
},
modalInputError: {
borderColor: AppColors.error,
marginBottom: Spacing.xs,
},
errorText: {
color: AppColors.error,
fontSize: FontSizes.sm,
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,
},
modalButtonDisabled: {
backgroundColor: AppColors.textMuted,
},
disabledText: {
opacity: 0.5,
},
});