1666 lines
49 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
RefreshControl,
Image,
Modal,
TextInput,
Alert,
KeyboardAvoidingView,
Platform,
Animated,
ActivityIndicator,
} from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import * as ImagePicker from 'expo-image-picker';
import { usePaymentSheet } from '@stripe/stripe-react-native';
import { api } from '@/services/api';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { useAuth } from '@/contexts/AuthContext';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { FullScreenError } from '@/components/ui/ErrorMessage';
import { Button } from '@/components/ui/Button';
import { SubscriptionPayment } from '@/components/SubscriptionPayment';
import { useToast } from '@/components/ui/Toast';
import MockDashboard from '@/components/MockDashboard';
import {
AppColors,
BorderRadius,
FontSizes,
Spacing,
FontWeights,
Shadows,
AvatarSizes,
} from '@/constants/theme';
import type { Beneficiary } from '@/types';
// Local beneficiaries have timestamp-based IDs (>1000000000)
const isLocalBeneficiary = (id: string | number): boolean => {
const numId = typeof id === 'string' ? parseInt(id, 10) : id;
return numId > 1000000000;
};
// Setup state types
type SetupState = 'loading' | 'awaiting_equipment' | 'no_devices' | 'no_subscription' | 'ready';
// Starter Kit info
const STARTER_KIT = {
name: 'WellNuo Starter Kit',
price: '$249',
features: [
'Motion sensor (PIR)',
'Door/window sensor',
'Temperature & humidity sensor',
'WellNuo Hub',
],
};
// No Devices Screen Component - Primary: Buy Kit, Secondary: I have sensors
function NoDevicesScreen({
beneficiary,
onActivate,
onGetSensors
}: {
beneficiary: Beneficiary;
onActivate: () => void;
onGetSensors: () => void;
}) {
return (
<View style={styles.setupContainer}>
<View style={styles.setupIconContainer}>
<Ionicons name="hardware-chip-outline" size={48} color={AppColors.primary} />
</View>
<Text style={styles.setupTitle}>Get Started with WellNuo</Text>
<Text style={styles.setupSubtitle}>
To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors.
</Text>
{/* Primary: Buy Kit Card */}
<View style={styles.buyKitCard}>
<Text style={styles.buyKitName}>{STARTER_KIT.name}</Text>
<Text style={styles.buyKitPrice}>{STARTER_KIT.price}</Text>
<View style={styles.buyKitFeatures}>
{STARTER_KIT.features.map((feature, index) => (
<View key={index} style={styles.buyKitFeatureRow}>
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
<Text style={styles.buyKitFeatureText}>{feature}</Text>
</View>
))}
</View>
<TouchableOpacity style={styles.buyKitButton} onPress={onGetSensors}>
<Ionicons name="cart" size={20} color={AppColors.white} />
<Text style={styles.buyKitButtonText}>Buy Now - {STARTER_KIT.price}</Text>
</TouchableOpacity>
</View>
{/* Secondary: I already have sensors */}
<TouchableOpacity style={styles.alreadyHaveLink} onPress={onActivate}>
<Text style={styles.alreadyHaveLinkText}>I already have sensors</Text>
<Ionicons name="arrow-forward" size={16} color={AppColors.textSecondary} />
</TouchableOpacity>
</View>
);
}
// Equipment status configuration
const equipmentStatusInfo = {
ordered: {
icon: 'cube-outline' as const,
title: 'Kit Ordered',
subtitle: 'Your WellNuo kit is being prepared for shipping',
color: AppColors.info,
bgColor: AppColors.infoLight,
steps: [
{ label: 'Order placed', done: true },
{ label: 'Preparing', done: true },
{ label: 'Shipped', done: false },
{ label: 'Delivered', done: false },
],
},
shipped: {
icon: 'car-outline' as const,
title: 'In Transit',
subtitle: 'Your WellNuo kit is on its way',
color: AppColors.warning,
bgColor: AppColors.warningLight,
steps: [
{ label: 'Order placed', done: true },
{ label: 'Preparing', done: true },
{ label: 'Shipped', done: true },
{ label: 'Delivered', done: false },
],
},
delivered: {
icon: 'checkmark-circle-outline' as const,
title: 'Delivered',
subtitle: 'Your kit has arrived! Time to set it up.',
color: AppColors.success,
bgColor: AppColors.successLight,
steps: [
{ label: 'Order placed', done: true },
{ label: 'Preparing', done: true },
{ label: 'Shipped', done: true },
{ label: 'Delivered', done: true },
],
},
};
// Awaiting Equipment Screen Component
function AwaitingEquipmentScreen({
beneficiary,
onActivate,
onMarkReceived,
}: {
beneficiary: Beneficiary;
onActivate: () => void;
onMarkReceived: () => void;
}) {
const status = beneficiary.equipmentStatus as 'ordered' | 'shipped' | 'delivered';
const info = equipmentStatusInfo[status] || equipmentStatusInfo.ordered;
const isDelivered = status === 'delivered';
return (
<View style={styles.setupContainer}>
<View style={[styles.setupIconContainer, { backgroundColor: info.bgColor }]}>
<Ionicons name={info.icon} size={48} color={info.color} />
</View>
<Text style={styles.setupTitle}>{info.title}</Text>
<Text style={styles.setupSubtitle}>{info.subtitle}</Text>
{/* Progress steps */}
<View style={styles.progressContainer}>
{info.steps.map((step, index) => (
<View key={index} style={styles.progressStep}>
<View style={[
styles.progressDot,
step.done && styles.progressDotDone,
!step.done && styles.progressDotPending,
]}>
{step.done && (
<Ionicons name="checkmark" size={12} color={AppColors.white} />
)}
</View>
<Text style={[
styles.progressLabel,
step.done && styles.progressLabelDone,
]}>
{step.label}
</Text>
{index < info.steps.length - 1 && (
<View style={[
styles.progressLine,
step.done && styles.progressLineDone,
]} />
)}
</View>
))}
</View>
{/* Tracking number if available */}
{beneficiary.trackingNumber && (
<View style={styles.trackingCard}>
<Ionicons name="locate-outline" size={20} color={AppColors.textSecondary} />
<View style={styles.trackingInfo}>
<Text style={styles.trackingLabel}>Tracking Number</Text>
<Text style={styles.trackingNumber}>{beneficiary.trackingNumber}</Text>
</View>
</View>
)}
{/* Actions */}
<View style={styles.awaitingActions}>
{isDelivered ? (
<Button
title="Activate Sensors"
onPress={onActivate}
fullWidth
size="lg"
/>
) : (
<>
<TouchableOpacity style={styles.receivedButton} onPress={onMarkReceived}>
<Ionicons name="checkmark-circle-outline" size={20} color={AppColors.primary} />
<Text style={styles.receivedButtonText}>I received my kit</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
);
}
export default function BeneficiaryDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary, removeLocalBeneficiary } = useBeneficiary();
const toast = useToast();
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
// Check if this is a local beneficiary
const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]);
// Determine setup state
// Flow: No devices → Connect Sensors → Subscription → Dashboard
const setupState = useMemo((): SetupState => {
if (isLoading) return 'loading';
if (!beneficiary) return 'loading';
// Check if awaiting equipment (ordered, shipped, or delivered but not activated)
const equipmentStatus = beneficiary.equipmentStatus;
if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) {
return 'awaiting_equipment';
}
// Check if has devices - required first step
const hasDevices = beneficiary.hasDevices ||
(beneficiary.devices && beneficiary.devices.length > 0) ||
beneficiary.device_id;
if (!hasDevices) return 'no_devices';
// Check subscription - required after devices connected
const subscription = beneficiary.subscription;
if (!subscription || subscription.status === 'none' || subscription.status === 'expired') {
return 'no_subscription';
}
return 'ready';
}, [beneficiary, isLoading]);
// Dropdown menu state
const [isMenuVisible, setIsMenuVisible] = useState(false);
// Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
address: '',
avatar: '' as string | undefined,
});
// Modal animation
const fadeAnim = React.useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: isEditModalVisible ? 1 : 0,
duration: 250,
useNativeDriver: true,
}).start();
}, [isEditModalVisible]);
const loadBeneficiary = useCallback(async (showLoading = true) => {
if (!id) return;
if (showLoading) setIsLoading(true);
setError(null);
try {
// For local beneficiaries, get from context
if (isLocal) {
const localBeneficiary = localBeneficiaries.find(
(b) => b.id === parseInt(id, 10)
);
if (localBeneficiary) {
setBeneficiary(localBeneficiary);
} else {
setError('Beneficiary not found');
}
} else {
// For real beneficiaries, fetch from API
const response = await api.getBeneficiary(parseInt(id, 10));
if (response.ok && response.data) {
setBeneficiary(response.data);
} else {
setError(response.error?.message || 'Failed to load beneficiary');
}
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [id, isLocal, localBeneficiaries]);
useEffect(() => {
loadBeneficiary();
}, [loadBeneficiary]);
// Sync beneficiary data when localBeneficiaries changes (especially after avatar update)
useEffect(() => {
if (isLocal && id && beneficiary) {
const updated = localBeneficiaries.find(b => b.id === parseInt(id, 10));
if (updated && updated.avatar !== beneficiary.avatar) {
setBeneficiary(updated);
}
}
}, [localBeneficiaries, id, isLocal]);
const handleRefresh = useCallback(() => {
setIsRefreshing(true);
loadBeneficiary(false);
}, [loadBeneficiary]);
const handleActivateSensors = () => {
router.push({
pathname: '/(auth)/activate',
params: { beneficiaryId: id!, lovedOneName: beneficiary?.name },
});
};
const handleGetSensors = () => {
// Navigate to purchase screen with beneficiary info
router.push({
pathname: '/(auth)/purchase',
params: { beneficiaryId: id!, lovedOneName: beneficiary?.name },
});
};
const handleMarkReceived = async () => {
if (!beneficiary || !id) return;
try {
// Update local beneficiary status to delivered
if (isLocal) {
await updateLocalBeneficiary(parseInt(id, 10), {
equipmentStatus: 'delivered',
});
// Reload to refresh UI
loadBeneficiary(false);
}
// For API beneficiaries, would call backend here
} catch (err) {
toast.error('Error', 'Failed to update status');
}
};
const handleActivateFromStatus = () => {
router.push({
pathname: '/(auth)/activate',
params: { lovedOneName: beneficiary?.name, beneficiaryId: id },
});
};
const handleEditPress = () => {
if (beneficiary) {
setEditForm({
name: beneficiary.name || '',
address: beneficiary.address || '',
avatar: beneficiary.avatar,
});
setIsEditModalVisible(true);
}
};
const handlePickAvatar = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
toast.error('Permission needed', 'Please allow access to your photo library.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});
if (!result.canceled && result.assets[0]) {
setEditForm(prev => ({ ...prev, avatar: result.assets[0].uri }));
}
};
const handleSaveEdit = async () => {
if (!editForm.name.trim()) {
toast.error('Error', 'Name is required');
return;
}
if (isLocal && id) {
const updated = await updateLocalBeneficiary(parseInt(id, 10), {
name: editForm.name.trim(),
address: editForm.address.trim() || undefined,
avatar: editForm.avatar,
});
if (updated) {
setBeneficiary(updated);
setIsEditModalVisible(false);
toast.success('Saved', 'Profile updated successfully');
} else {
toast.error('Error', 'Failed to save changes.');
}
} else {
toast.info('Demo Mode', 'Saving beneficiary data requires backend API.');
setIsEditModalVisible(false);
}
};
const showComingSoon = (featureName: string) => {
toast.info('Coming Soon', `${featureName} is currently in development.`);
};
const handleDeleteBeneficiary = () => {
if (!isLocal || !id) return;
Alert.alert(
'Remove Beneficiary',
`Are you sure you want to remove ${beneficiary?.name}? This action cannot be undone.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Remove',
style: 'destructive',
onPress: async () => {
try {
await removeLocalBeneficiary(parseInt(id, 10));
router.replace('/(tabs)');
} catch (err) {
toast.error('Error', 'Failed to remove beneficiary');
}
},
},
]
);
};
if (isLoading) {
return <LoadingSpinner fullScreen message="Loading..." />;
}
if (error || !beneficiary) {
return (
<FullScreenError
message={error || 'Beneficiary not found'}
onRetry={() => loadBeneficiary()}
/>
);
}
const statusColor = beneficiary.status === 'online' ? AppColors.online : AppColors.offline;
// Render based on setup state
const renderContent = () => {
switch (setupState) {
case 'awaiting_equipment':
return (
<AwaitingEquipmentScreen
beneficiary={beneficiary}
onActivate={handleActivateFromStatus}
onMarkReceived={handleMarkReceived}
/>
);
case 'no_devices':
return (
<NoDevicesScreen
beneficiary={beneficiary}
onActivate={handleActivateSensors}
onGetSensors={handleGetSensors}
/>
);
case 'no_subscription':
return (
<SubscriptionPayment
beneficiary={beneficiary}
onSuccess={() => loadBeneficiary(false)}
/>
);
case 'ready':
default:
return (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={AppColors.primary}
/>
}
>
{/* Profile Card */}
<View style={styles.profileCard}>
<View style={styles.avatarWrapper}>
{beneficiary.avatar ? (
<Image source={{ uri: beneficiary.avatar }} style={styles.avatarImage} />
) : (
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{beneficiary.name.charAt(0).toUpperCase()}
</Text>
</View>
)}
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
</View>
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
{beneficiary.address && (
<View style={styles.locationRow}>
<Ionicons name="location" size={14} color={AppColors.textMuted} />
<Text style={styles.locationText}>{beneficiary.address}</Text>
</View>
)}
<View style={styles.statusBadge}>
<View style={[styles.statusIndicator, { backgroundColor: statusColor }]} />
<Text style={styles.statusText}>
{beneficiary.status === 'online' ? 'Online now' : 'Offline'}
</Text>
</View>
{beneficiary.last_activity && (
<Text style={styles.lastActivity}>
Last activity: {beneficiary.last_activity}
</Text>
)}
</View>
{/* Quick Actions */}
<Text style={styles.sectionTitle}>Quick Actions</Text>
<View style={styles.actionsRow}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => {
setCurrentBeneficiary(beneficiary);
router.push('/(tabs)/chat');
}}
>
<View style={[styles.actionButtonIcon, { backgroundColor: AppColors.accentLight }]}>
<Ionicons name="chatbubbles" size={22} color={AppColors.accent} />
</View>
<Text style={styles.actionButtonText}>Chat with Julia</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => router.push(`/(tabs)/beneficiaries/${id}/equipment`)}
>
<View style={[styles.actionButtonIcon, { backgroundColor: AppColors.primaryLighter }]}>
<Ionicons name="hardware-chip" size={22} color={AppColors.primary} />
</View>
<Text style={styles.actionButtonText}>Equipment</Text>
</TouchableOpacity>
</View>
{/* Subscription Section */}
<Text style={styles.sectionTitle}>Subscription</Text>
<View style={styles.subscriptionCard}>
<View style={styles.subscriptionHeader}>
<View style={styles.subscriptionBadgeContainer}>
<View style={styles.subscriptionIconBg}>
<Ionicons name="diamond" size={18} color={AppColors.accent} />
</View>
<View>
<Text style={styles.subscriptionPlan}>WellNuo Pro</Text>
<Text style={styles.subscriptionStatus}>Active</Text>
</View>
</View>
<View style={styles.priceBadge}>
<Text style={styles.priceText}>$49</Text>
<Text style={styles.priceUnit}>/mo</Text>
</View>
</View>
<View style={styles.subscriptionDivider} />
<View style={styles.subscriptionFeatures}>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
<Text style={styles.featureText}>24/7 AI monitoring</Text>
</View>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
<Text style={styles.featureText}>Unlimited chat with Julia</Text>
</View>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={18} color={AppColors.success} />
<Text style={styles.featureText}>Activity reports</Text>
</View>
</View>
<TouchableOpacity
style={styles.manageButton}
onPress={() => showComingSoon('Subscription Management')}
>
<Text style={styles.manageButtonText}>Manage Subscription</Text>
<Ionicons name="chevron-forward" size={18} color={AppColors.primary} />
</TouchableOpacity>
</View>
{/* Settings */}
<Text style={styles.sectionTitle}>Settings</Text>
<View style={styles.settingsCard}>
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Notification Settings')}>
<View style={styles.settingsRowLeft}>
<View style={[styles.settingsIcon, { backgroundColor: AppColors.primarySubtle }]}>
<Ionicons name="notifications-outline" size={20} color={AppColors.primary} />
</View>
<Text style={styles.settingsRowText}>Notifications</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
</TouchableOpacity>
<View style={styles.settingsDivider} />
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Alert Rules')}>
<View style={styles.settingsRowLeft}>
<View style={[styles.settingsIcon, { backgroundColor: AppColors.warningLight }]}>
<Ionicons name="alert-circle-outline" size={20} color={AppColors.warning} />
</View>
<Text style={styles.settingsRowText}>Alert Rules</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
</TouchableOpacity>
<View style={styles.settingsDivider} />
<TouchableOpacity style={styles.settingsRow} onPress={() => showComingSoon('Connected Sensors')}>
<View style={styles.settingsRowLeft}>
<View style={[styles.settingsIcon, { backgroundColor: AppColors.infoLight }]}>
<Ionicons name="hardware-chip-outline" size={20} color={AppColors.info} />
</View>
<Text style={styles.settingsRowText}>Connected Sensors</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
{/* Danger Zone - only for local beneficiaries */}
{isLocal && (
<>
<Text style={[styles.sectionTitle, styles.dangerTitle]}>Danger Zone</Text>
<TouchableOpacity style={styles.dangerButton} onPress={handleDeleteBeneficiary}>
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
<Text style={styles.dangerButtonText}>Remove Beneficiary</Text>
</TouchableOpacity>
</>
)}
{/* Activity Dashboard */}
<MockDashboard beneficiaryName={beneficiary.name} />
</ScrollView>
);
}
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.headerButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
<View>
<TouchableOpacity style={styles.headerButton} onPress={() => setIsMenuVisible(!isMenuVisible)}>
<Ionicons name="ellipsis-vertical" size={22} color={AppColors.textPrimary} />
</TouchableOpacity>
{/* Dropdown Menu */}
{isMenuVisible && (
<View style={styles.dropdownMenu}>
<TouchableOpacity
style={styles.dropdownItem}
onPress={() => {
setIsMenuVisible(false);
handleEditPress();
}}
>
<Ionicons name="create-outline" size={20} color={AppColors.textPrimary} />
<Text style={styles.dropdownItemText}>Edit</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.dropdownItem}
onPress={() => {
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/share`);
}}
>
<Ionicons name="share-outline" size={20} color={AppColors.textPrimary} />
<Text style={styles.dropdownItemText}>Share</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.dropdownItem}
onPress={() => {
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/subscription`);
}}
>
<Ionicons name="diamond-outline" size={20} color={AppColors.textPrimary} />
<Text style={styles.dropdownItemText}>Subscription</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.dropdownItem}
onPress={() => {
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/equipment`);
}}
>
<Ionicons name="hardware-chip-outline" size={20} color={AppColors.textPrimary} />
<Text style={styles.dropdownItemText}>Equipment</Text>
</TouchableOpacity>
{isLocal && (
<TouchableOpacity
style={[styles.dropdownItem, styles.dropdownItemDanger]}
onPress={() => {
setIsMenuVisible(false);
handleDeleteBeneficiary();
}}
>
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
<Text style={[styles.dropdownItemText, styles.dropdownItemTextDanger]}>Remove</Text>
</TouchableOpacity>
)}
</View>
)}
</View>
</View>
{/* Backdrop to close menu */}
{isMenuVisible && (
<TouchableOpacity
style={styles.menuBackdrop}
activeOpacity={1}
onPress={() => setIsMenuVisible(false)}
/>
)}
{renderContent()}
{/* Edit Modal */}
<Modal
visible={isEditModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setIsEditModalVisible(false)}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<Animated.View style={[styles.modalBackdrop, { opacity: fadeAnim }]}>
<TouchableOpacity
style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={() => setIsEditModalVisible(false)}
/>
</Animated.View>
<View style={styles.modalContent}>
{/* Modal Header */}
<View style={styles.modalHeader}>
<View style={styles.modalHeaderContent}>
<View style={styles.modalIconContainer}>
<Ionicons name="person" size={22} color={AppColors.primary} />
</View>
<Text style={styles.modalTitle}>Edit Profile</Text>
</View>
<TouchableOpacity style={styles.modalCloseButton} onPress={() => setIsEditModalVisible(false)}>
<Ionicons name="close" size={22} color={AppColors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.modalForm} showsVerticalScrollIndicator={false}>
{/* Avatar Section */}
<View style={styles.editAvatarSection}>
<TouchableOpacity style={styles.editAvatarContainer} onPress={handlePickAvatar}>
{editForm.avatar ? (
<Image source={{ uri: editForm.avatar }} style={styles.editAvatarImage} />
) : (
<Text style={styles.editAvatarText}>
{editForm.name ? editForm.name.charAt(0).toUpperCase() : '+'}
</Text>
)}
<View style={styles.editAvatarBadge}>
<Ionicons name="camera" size={14} color={AppColors.white} />
</View>
</TouchableOpacity>
<Text style={styles.editAvatarHint}>Tap to change photo</Text>
</View>
{/* Form Fields */}
<View style={styles.formSection}>
<Text style={styles.inputLabel}>Name</Text>
<View style={styles.inputContainer}>
<Ionicons name="person-outline" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.textInput}
value={editForm.name}
onChangeText={(text) => setEditForm({ ...editForm, name: text })}
placeholder="Enter name"
placeholderTextColor={AppColors.textMuted}
/>
</View>
</View>
<View style={styles.formSection}>
<Text style={styles.inputLabel}>Address</Text>
<View style={styles.inputContainer}>
<Ionicons name="location-outline" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.textInput}
value={editForm.address}
onChangeText={(text) => setEditForm({ ...editForm, address: text })}
placeholder="Enter address"
placeholderTextColor={AppColors.textMuted}
/>
</View>
</View>
{/* Info Box */}
<View style={styles.infoBox}>
<Ionicons name="information-circle" size={20} color={AppColors.info} />
<Text style={styles.infoBoxText}>
Wellness data is collected automatically from connected sensors.
</Text>
</View>
</ScrollView>
{/* Modal Footer */}
<View style={styles.modalFooter}>
<TouchableOpacity style={styles.cancelButton} onPress={() => setIsEditModalVisible(false)}>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.saveButton} onPress={handleSaveEdit}>
<Ionicons name="checkmark" size={20} color={AppColors.white} />
<Text style={styles.saveButtonText}>Save</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
// Header
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
zIndex: 1001,
},
headerButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
// Setup Screens
setupContainer: {
flex: 1,
padding: Spacing.xl,
alignItems: 'center',
justifyContent: 'center',
},
setupIconContainer: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
setupTitle: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
textAlign: 'center',
marginBottom: Spacing.sm,
},
setupSubtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 24,
marginBottom: Spacing.xl,
paddingHorizontal: Spacing.md,
},
setupOptions: {
width: '100%',
gap: Spacing.md,
},
setupOptionCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
...Shadows.sm,
},
setupOptionIcon: {
width: 56,
height: 56,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
setupOptionTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
setupOptionText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 20,
},
setupOptionArrow: {
position: 'absolute',
top: Spacing.lg,
right: Spacing.lg,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
},
// Buy Kit Card Styles
buyKitCard: {
width: '100%',
backgroundColor: AppColors.white,
borderRadius: BorderRadius.xl,
padding: Spacing.xl,
borderWidth: 2,
borderColor: AppColors.primary,
alignItems: 'center',
...Shadows.md,
},
buyKitName: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
buyKitPrice: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
marginBottom: Spacing.lg,
},
buyKitFeatures: {
width: '100%',
marginBottom: Spacing.lg,
gap: Spacing.sm,
},
buyKitFeatureRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
},
buyKitFeatureText: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
},
buyKitButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
backgroundColor: AppColors.primary,
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.xl,
borderRadius: BorderRadius.lg,
width: '100%',
},
buyKitButtonText: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
alreadyHaveLink: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
marginTop: Spacing.xl,
paddingVertical: Spacing.md,
},
alreadyHaveLinkText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
// Equipment Progress Styles
progressContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'flex-start',
marginBottom: Spacing.xl,
width: '100%',
paddingHorizontal: Spacing.md,
},
progressStep: {
alignItems: 'center',
flex: 1,
position: 'relative',
},
progressDot: {
width: 24,
height: 24,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.xs,
},
progressDotDone: {
backgroundColor: AppColors.success,
},
progressDotPending: {
backgroundColor: AppColors.border,
},
progressLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
},
progressLabelDone: {
color: AppColors.textPrimary,
fontWeight: FontWeights.medium,
},
progressLine: {
position: 'absolute',
top: 12,
left: '50%',
right: '-50%',
height: 2,
backgroundColor: AppColors.border,
zIndex: -1,
},
progressLineDone: {
backgroundColor: AppColors.success,
},
trackingCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginBottom: Spacing.lg,
width: '100%',
gap: Spacing.md,
...Shadows.sm,
},
trackingInfo: {
flex: 1,
},
trackingLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
trackingNumber: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
awaitingActions: {
width: '100%',
},
receivedButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
borderWidth: 1,
borderColor: AppColors.primary,
gap: Spacing.sm,
},
receivedButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.primary,
},
// Subscription Price Card
subscriptionPriceCard: {
width: '100%',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.xl,
...Shadows.sm,
},
subscriptionPriceHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Spacing.md,
},
subscriptionPriceLabel: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
subscriptionPriceBadge: {
flexDirection: 'row',
alignItems: 'baseline',
},
subscriptionPriceAmount: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
subscriptionPriceUnit: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginLeft: 2,
},
subscriptionPriceFeatures: {
gap: Spacing.sm,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
},
featureRowText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
// Profile Card
profileCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.xl,
alignItems: 'center',
marginBottom: Spacing.lg,
...Shadows.sm,
},
avatarWrapper: {
position: 'relative',
marginBottom: Spacing.md,
},
avatar: {
width: AvatarSizes.xl,
height: AvatarSizes.xl,
borderRadius: AvatarSizes.xl / 2,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
avatarImage: {
width: AvatarSizes.xl,
height: AvatarSizes.xl,
borderRadius: AvatarSizes.xl / 2,
},
avatarText: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.white,
},
statusDot: {
position: 'absolute',
bottom: 4,
right: 4,
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 3,
borderColor: AppColors.surface,
},
beneficiaryName: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
locationRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
marginBottom: Spacing.sm,
},
locationText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surfaceSecondary,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
statusIndicator: {
width: 8,
height: 8,
borderRadius: 4,
},
statusText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
},
lastActivity: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: Spacing.sm,
},
// Section
sectionTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: Spacing.md,
},
// Quick Actions Row
actionsRow: {
flexDirection: 'row',
gap: Spacing.md,
marginBottom: Spacing.lg,
},
actionButton: {
flex: 1,
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
alignItems: 'center',
...Shadows.xs,
},
actionButtonIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.sm,
},
actionButtonText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textPrimary,
},
// Subscription Card
subscriptionCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.lg,
...Shadows.sm,
},
subscriptionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
subscriptionBadgeContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
subscriptionIconBg: {
width: 44,
height: 44,
borderRadius: BorderRadius.lg,
backgroundColor: AppColors.accentLight,
justifyContent: 'center',
alignItems: 'center',
},
subscriptionPlan: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
subscriptionStatus: {
fontSize: FontSizes.sm,
color: AppColors.success,
fontWeight: FontWeights.medium,
},
priceBadge: {
flexDirection: 'row',
alignItems: 'baseline',
},
priceText: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
priceUnit: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginLeft: 2,
},
subscriptionDivider: {
height: 1,
backgroundColor: AppColors.borderLight,
marginVertical: Spacing.md,
},
subscriptionFeatures: {
gap: Spacing.sm,
marginBottom: Spacing.md,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
},
featureText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
manageButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: Spacing.md,
backgroundColor: AppColors.primarySubtle,
borderRadius: BorderRadius.lg,
gap: Spacing.xs,
},
manageButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.primary,
},
// Settings Card
settingsCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
marginBottom: Spacing.lg,
...Shadows.xs,
},
settingsRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: Spacing.md,
},
settingsRowLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
settingsIcon: {
width: 36,
height: 36,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
settingsRowText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.textPrimary,
},
settingsDivider: {
height: 1,
backgroundColor: AppColors.borderLight,
marginLeft: 60,
},
// Danger Zone
dangerTitle: {
color: AppColors.error,
},
dangerButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.errorLight,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
gap: Spacing.sm,
marginBottom: Spacing.lg,
},
dangerButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.error,
},
// Modal
modalOverlay: {
flex: 1,
justifyContent: 'flex-end',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: AppColors.overlay,
},
modalContent: {
backgroundColor: AppColors.background,
borderTopLeftRadius: BorderRadius['2xl'],
borderTopRightRadius: BorderRadius['2xl'],
maxHeight: '85%',
...Shadows.xl,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.lg,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
modalHeaderContent: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
modalIconContainer: {
width: 40,
height: 40,
borderRadius: BorderRadius.lg,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
},
modalTitle: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
modalCloseButton: {
width: 36,
height: 36,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
},
modalForm: {
padding: Spacing.lg,
},
// Edit Avatar
editAvatarSection: {
alignItems: 'center',
marginBottom: Spacing.lg,
},
editAvatarContainer: {
width: AvatarSizes.lg,
height: AvatarSizes.lg,
borderRadius: AvatarSizes.lg / 2,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
editAvatarImage: {
width: AvatarSizes.lg,
height: AvatarSizes.lg,
borderRadius: AvatarSizes.lg / 2,
},
editAvatarText: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.white,
},
editAvatarBadge: {
position: 'absolute',
bottom: 0,
right: 0,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 3,
borderColor: AppColors.background,
},
editAvatarHint: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginTop: Spacing.sm,
},
// Form
formSection: {
marginBottom: Spacing.md,
},
inputLabel: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
marginBottom: Spacing.xs,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surfaceSecondary,
borderRadius: BorderRadius.lg,
paddingHorizontal: Spacing.md,
borderWidth: 1.5,
borderColor: AppColors.borderLight,
gap: Spacing.sm,
},
textInput: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
paddingVertical: Spacing.md,
},
infoBox: {
flexDirection: 'row',
backgroundColor: AppColors.infoLight,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
gap: Spacing.sm,
marginTop: Spacing.md,
},
infoBoxText: {
flex: 1,
fontSize: FontSizes.sm,
color: AppColors.info,
lineHeight: 20,
},
modalFooter: {
flexDirection: 'row',
padding: Spacing.lg,
gap: Spacing.md,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
cancelButton: {
flex: 1,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
backgroundColor: AppColors.surfaceSecondary,
alignItems: 'center',
justifyContent: 'center',
},
cancelButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
},
saveButton: {
flex: 2,
flexDirection: 'row',
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
backgroundColor: AppColors.primary,
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
...Shadows.primary,
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// Dropdown Menu
dropdownMenu: {
position: 'absolute',
top: 44,
right: 0,
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
minWidth: 160,
...Shadows.lg,
zIndex: 1000,
},
dropdownItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
gap: Spacing.md,
},
dropdownItemText: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
dropdownItemDanger: {
borderTopWidth: 1,
borderTopColor: AppColors.borderLight,
},
dropdownItemTextDanger: {
color: AppColors.error,
},
menuBackdrop: {
...StyleSheet.absoluteFillObject,
zIndex: 999,
},
});