1881 lines
53 KiB
TypeScript
1881 lines
53 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
RefreshControl,
|
|
Image,
|
|
Modal,
|
|
TextInput,
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Animated,
|
|
ActivityIndicator,
|
|
Switch,
|
|
} from 'react-native';
|
|
import { WebView } from 'react-native-webview';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
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';
|
|
|
|
// Stripe API
|
|
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
|
|
|
// WebView Dashboard URL - uses test NDK account for demo data
|
|
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
|
const TEST_NDK_DEPLOYMENT_ID = '1'; // anandk test deployment with real sensor data
|
|
|
|
// Starter Kit info
|
|
const STARTER_KIT = {
|
|
name: 'WellNuo Starter Kit',
|
|
price: '$249',
|
|
priceValue: 249,
|
|
features: [
|
|
'Motion sensor (PIR)',
|
|
'Door/window sensor',
|
|
'Temperature & humidity sensor',
|
|
'WellNuo Hub',
|
|
'Mobile app access',
|
|
'1 year subscription included',
|
|
],
|
|
};
|
|
|
|
// No Devices Screen Component - Primary: Buy Kit with Stripe, Secondary: I have sensors
|
|
function NoDevicesScreen({
|
|
beneficiary,
|
|
beneficiaryId,
|
|
onActivate,
|
|
onPurchaseSuccess,
|
|
userEmail,
|
|
userId,
|
|
}: {
|
|
beneficiary: Beneficiary;
|
|
beneficiaryId: string;
|
|
onActivate: () => void;
|
|
onPurchaseSuccess: () => void;
|
|
userEmail?: string;
|
|
userId?: string;
|
|
}) {
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
|
|
const toast = useToast();
|
|
|
|
const handlePurchase = async () => {
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
// 1. Create Payment Sheet on server
|
|
const response = await fetch(`${STRIPE_API_URL}/create-payment-sheet`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: userEmail || 'guest@wellnuo.com',
|
|
amount: STARTER_KIT.priceValue * 100, // Convert to cents ($249.00)
|
|
metadata: {
|
|
userId: userId || 'guest',
|
|
beneficiaryName: beneficiary.name,
|
|
beneficiaryId: beneficiaryId,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.paymentIntent) {
|
|
throw new Error(data.error || 'Failed to create payment sheet');
|
|
}
|
|
|
|
// 2. Initialize the Payment Sheet
|
|
const { error: initError } = await initPaymentSheet({
|
|
merchantDisplayName: 'WellNuo',
|
|
paymentIntentClientSecret: data.paymentIntent,
|
|
customerId: data.customer,
|
|
customerEphemeralKeySecret: data.ephemeralKey,
|
|
defaultBillingDetails: {
|
|
email: userEmail || '',
|
|
},
|
|
returnURL: 'wellnuo://stripe-redirect',
|
|
applePay: {
|
|
merchantCountryCode: 'US',
|
|
},
|
|
googlePay: {
|
|
merchantCountryCode: 'US',
|
|
testEnv: true,
|
|
},
|
|
});
|
|
|
|
if (initError) {
|
|
throw new Error(initError.message);
|
|
}
|
|
|
|
// 3. Present the Payment Sheet
|
|
const { error: presentError } = await presentPaymentSheet();
|
|
|
|
if (presentError) {
|
|
if (presentError.code === 'Canceled') {
|
|
// User cancelled - do nothing
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
throw new Error(presentError.message);
|
|
}
|
|
|
|
// 4. Payment successful!
|
|
toast.success('Order Placed!', 'Your WellNuo Starter Kit is on its way.');
|
|
onPurchaseSuccess();
|
|
} catch (error) {
|
|
console.error('Payment error:', error);
|
|
toast.error(
|
|
'Payment Failed',
|
|
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
|
|
);
|
|
}
|
|
|
|
setIsProcessing(false);
|
|
};
|
|
|
|
return (
|
|
<ScrollView contentContainerStyle={styles.setupScrollContent}>
|
|
<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, isProcessing && styles.buyKitButtonDisabled]}
|
|
onPress={handlePurchase}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="card" size={20} color={AppColors.white} />
|
|
<Text style={styles.buyKitButtonText}>Buy Now - {STARTER_KIT.price}</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{/* Security Badge */}
|
|
<View style={styles.securityBadge}>
|
|
<Ionicons name="shield-checkmark" size={16} color={AppColors.success} />
|
|
<Text style={styles.securityText}>Secure payment by Stripe</Text>
|
|
</View>
|
|
</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>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
|
|
// 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 { user } = useAuth();
|
|
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);
|
|
|
|
// Developer toggle for WebView
|
|
const [showWebView, setShowWebView] = useState(false);
|
|
const [authToken, setAuthToken] = useState<string | null>(null);
|
|
const [userName, setUserName] = useState<string | null>(null);
|
|
const [userId, setUserId] = useState<string | null>(null);
|
|
const webViewRef = useRef<WebView>(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 has devices - used in multiple places
|
|
const hasDevices = beneficiary.hasDevices ||
|
|
(beneficiary.devices && beneficiary.devices.length > 0) ||
|
|
beneficiary.device_id;
|
|
|
|
// Check equipment status
|
|
const equipmentStatus = beneficiary.equipmentStatus;
|
|
|
|
// If equipment is ordered/shipped/delivered but NOT yet activated (no devices)
|
|
// show awaiting equipment screen
|
|
if (equipmentStatus && ['ordered', 'shipped', 'delivered'].includes(equipmentStatus)) {
|
|
// But if user already has devices (activated), skip to next step
|
|
if (!hasDevices) {
|
|
return 'awaiting_equipment';
|
|
}
|
|
// Has devices = already activated, continue to subscription check
|
|
}
|
|
|
|
// No devices and no equipment ordered = show purchase screen
|
|
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]);
|
|
|
|
// Load credentials for WebView
|
|
useEffect(() => {
|
|
const loadCredentials = async () => {
|
|
try {
|
|
const token = await SecureStore.getItemAsync('accessToken');
|
|
const user = await SecureStore.getItemAsync('userName');
|
|
const uid = await SecureStore.getItemAsync('userId');
|
|
setAuthToken(token);
|
|
setUserName(user);
|
|
setUserId(uid);
|
|
} catch (err) {
|
|
console.error('Failed to load credentials:', err);
|
|
}
|
|
};
|
|
loadCredentials();
|
|
}, []);
|
|
|
|
// 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 handlePurchaseSuccess = async () => {
|
|
// Update beneficiary with ordered status and subscription (kit includes 1 year)
|
|
if (id && isLocal) {
|
|
// Calculate subscription end date (1 year from now)
|
|
const subscriptionEnd = new Date();
|
|
subscriptionEnd.setFullYear(subscriptionEnd.getFullYear() + 1);
|
|
|
|
await updateLocalBeneficiary(parseInt(id, 10), {
|
|
equipmentStatus: 'ordered',
|
|
// Kit includes 1 year subscription
|
|
subscription: {
|
|
status: 'pending', // Will activate when sensors are connected
|
|
plan: 'yearly',
|
|
startDate: new Date().toISOString(),
|
|
endDate: subscriptionEnd.toISOString(),
|
|
},
|
|
});
|
|
}
|
|
// Reload beneficiary to show new state
|
|
loadBeneficiary(false);
|
|
};
|
|
|
|
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.`);
|
|
};
|
|
|
|
// JavaScript to inject token into localStorage for WebView
|
|
const injectedJavaScript = authToken
|
|
? `
|
|
(function() {
|
|
try {
|
|
var authData = {
|
|
username: '${userName || ''}',
|
|
token: '${authToken}',
|
|
user_id: ${userId || 'null'}
|
|
};
|
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
|
console.log('Auth data injected:', authData.username);
|
|
} catch(e) {
|
|
console.error('Failed to inject token:', e);
|
|
}
|
|
})();
|
|
true;
|
|
`
|
|
: '';
|
|
|
|
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}
|
|
beneficiaryId={id!}
|
|
onActivate={handleActivateSensors}
|
|
onPurchaseSuccess={handlePurchaseSuccess}
|
|
userEmail={user?.email}
|
|
userId={user?.user_id}
|
|
/>
|
|
);
|
|
|
|
case 'no_subscription':
|
|
return (
|
|
<SubscriptionPayment
|
|
beneficiary={beneficiary}
|
|
onSuccess={() => loadBeneficiary(false)}
|
|
/>
|
|
);
|
|
|
|
case 'ready':
|
|
default:
|
|
// WebView mode - uses test NDK deployment for demo data
|
|
if (showWebView) {
|
|
const webViewUrl = `${DASHBOARD_URL}?deployment_id=${TEST_NDK_DEPLOYMENT_ID}`;
|
|
return (
|
|
<View style={styles.webViewContainer}>
|
|
<WebView
|
|
ref={webViewRef}
|
|
source={{ uri: webViewUrl }}
|
|
style={styles.webView}
|
|
javaScriptEnabled={true}
|
|
domStorageEnabled={true}
|
|
startInLoadingState={true}
|
|
allowsBackForwardNavigationGestures={true}
|
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
injectedJavaScript={injectedJavaScript}
|
|
renderLoading={() => (
|
|
<View style={styles.webViewLoading}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.webViewLoadingText}>Loading dashboard...</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
{/* Developer Toggle - always visible to switch back */}
|
|
<View style={styles.webViewToggleOverlay}>
|
|
<View style={styles.devToggleCard}>
|
|
<View style={styles.devToggleLeft}>
|
|
<Ionicons name="code-slash" size={20} color={AppColors.warning} />
|
|
<View>
|
|
<Text style={styles.devToggleLabel}>Developer Mode</Text>
|
|
<Text style={styles.devToggleHint}>Show WebView dashboard</Text>
|
|
</View>
|
|
</View>
|
|
<Switch
|
|
value={showWebView}
|
|
onValueChange={setShowWebView}
|
|
trackColor={{ false: AppColors.border, true: AppColors.primaryLight }}
|
|
thumbColor={showWebView ? AppColors.primary : AppColors.textMuted}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Native mode
|
|
return (
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={AppColors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Developer Toggle for WebView */}
|
|
<View style={styles.devToggleCard}>
|
|
<View style={styles.devToggleLeft}>
|
|
<Ionicons name="code-slash" size={20} color={AppColors.warning} />
|
|
<View>
|
|
<Text style={styles.devToggleLabel}>Developer Mode</Text>
|
|
<Text style={styles.devToggleHint}>Show WebView dashboard</Text>
|
|
</View>
|
|
</View>
|
|
<Switch
|
|
value={showWebView}
|
|
onValueChange={setShowWebView}
|
|
trackColor={{ false: AppColors.border, true: AppColors.primaryLight }}
|
|
thumbColor={showWebView ? AppColors.primary : AppColors.textMuted}
|
|
/>
|
|
</View>
|
|
|
|
{/* 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>
|
|
|
|
{/* Avatar + Name */}
|
|
<View style={styles.headerCenter}>
|
|
<View style={styles.headerAvatarWrapper}>
|
|
{beneficiary.avatar ? (
|
|
<Image source={{ uri: beneficiary.avatar }} style={styles.headerAvatarImage} />
|
|
) : (
|
|
<View style={styles.headerAvatar}>
|
|
<Text style={styles.headerAvatarText}>
|
|
{beneficiary.name.charAt(0).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={[styles.headerStatusDot, { backgroundColor: statusColor }]} />
|
|
</View>
|
|
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
|
</View>
|
|
|
|
<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,
|
|
},
|
|
headerCenter: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginHorizontal: Spacing.md,
|
|
gap: Spacing.sm,
|
|
},
|
|
headerAvatarWrapper: {
|
|
position: 'relative',
|
|
},
|
|
headerAvatar: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
backgroundColor: AppColors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
headerAvatarImage: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
},
|
|
headerAvatarText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.white,
|
|
},
|
|
headerStatusDot: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
right: 0,
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: 6,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.background,
|
|
},
|
|
// Developer Toggle
|
|
devToggleCard: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
backgroundColor: AppColors.warningLight,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
marginBottom: Spacing.lg,
|
|
borderWidth: 1,
|
|
borderColor: AppColors.warning,
|
|
},
|
|
devToggleLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
devToggleLabel: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
devToggleHint: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
// WebView
|
|
webViewContainer: {
|
|
flex: 1,
|
|
},
|
|
webViewToggleOverlay: {
|
|
position: 'absolute',
|
|
bottom: 100, // Above tab bar
|
|
left: Spacing.md,
|
|
right: Spacing.md,
|
|
},
|
|
webView: {
|
|
flex: 1,
|
|
},
|
|
webViewLoading: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
webViewLoadingText: {
|
|
marginTop: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
// Setup Screens
|
|
setupScrollContent: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
setupContainer: {
|
|
padding: Spacing.xl,
|
|
alignItems: '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,
|
|
},
|
|
buyKitButtonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
securityBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: Spacing.xs,
|
|
marginTop: Spacing.md,
|
|
},
|
|
securityText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.success,
|
|
},
|
|
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,
|
|
},
|
|
});
|