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 (
Get Started with WellNuo
To start monitoring {beneficiary.name}'s wellness, you need WellNuo sensors.
{/* Primary: Buy Kit Card */}
{STARTER_KIT.name}
{STARTER_KIT.price}
{STARTER_KIT.features.map((feature, index) => (
{feature}
))}
{isProcessing ? (
) : (
<>
Buy Now - {STARTER_KIT.price}
>
)}
{/* Security Badge */}
Secure payment by Stripe
{/* Secondary: I already have sensors */}
I already have sensors
);
}
// 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 (
{info.title}
{info.subtitle}
{/* Progress steps */}
{info.steps.map((step, index) => (
{step.done && (
)}
{step.label}
{index < info.steps.length - 1 && (
)}
))}
{/* Tracking number if available */}
{beneficiary.trackingNumber && (
Tracking Number
{beneficiary.trackingNumber}
)}
{/* Actions */}
{isDelivered ? (
) : (
<>
I received my kit
>
)}
);
}
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(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState(null);
// Developer toggle for WebView
const [showWebView, setShowWebView] = useState(false);
const [authToken, setAuthToken] = useState(null);
const [userName, setUserName] = useState(null);
const [userId, setUserId] = useState(null);
const webViewRef = useRef(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 ;
}
if (error || !beneficiary) {
return (
loadBeneficiary()}
/>
);
}
const statusColor = beneficiary.status === 'online' ? AppColors.online : AppColors.offline;
// Render based on setup state
const renderContent = () => {
switch (setupState) {
case 'awaiting_equipment':
return (
);
case 'no_devices':
return (
);
case 'no_subscription':
return (
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 (
(
Loading dashboard...
)}
/>
{/* Developer Toggle - always visible to switch back */}
Developer Mode
Show WebView dashboard
);
}
// Native mode
return (
}
>
{/* Developer Toggle for WebView */}
Developer Mode
Show WebView dashboard
{/* Activity Dashboard */}
);
}
};
return (
{/* Header */}
router.back()}>
{/* Avatar + Name */}
{beneficiary.avatar ? (
) : (
{beneficiary.name.charAt(0).toUpperCase()}
)}
{beneficiary.name}
setIsMenuVisible(!isMenuVisible)}>
{/* Dropdown Menu */}
{isMenuVisible && (
{
setIsMenuVisible(false);
handleEditPress();
}}
>
Edit
{
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/share`);
}}
>
Share
{
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/subscription`);
}}
>
Subscription
{
setIsMenuVisible(false);
router.push(`/(tabs)/beneficiaries/${id}/equipment`);
}}
>
Equipment
{isLocal && (
{
setIsMenuVisible(false);
handleDeleteBeneficiary();
}}
>
Remove
)}
)}
{/* Backdrop to close menu */}
{isMenuVisible && (
setIsMenuVisible(false)}
/>
)}
{renderContent()}
{/* Edit Modal */}
setIsEditModalVisible(false)}
>
setIsEditModalVisible(false)}
/>
{/* Modal Header */}
Edit Profile
setIsEditModalVisible(false)}>
{/* Avatar Section */}
{editForm.avatar ? (
) : (
{editForm.name ? editForm.name.charAt(0).toUpperCase() : '+'}
)}
Tap to change photo
{/* Form Fields */}
Name
setEditForm({ ...editForm, name: text })}
placeholder="Enter name"
placeholderTextColor={AppColors.textMuted}
/>
Address
setEditForm({ ...editForm, address: text })}
placeholder="Enter address"
placeholderTextColor={AppColors.textMuted}
/>
{/* Info Box */}
Wellness data is collected automatically from connected sensors.
{/* Modal Footer */}
setIsEditModalVisible(false)}>
Cancel
Save
);
}
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,
},
});