1666 lines
49 KiB
TypeScript
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,
|
|
},
|
|
});
|