842 lines
26 KiB
TypeScript
842 lines
26 KiB
TypeScript
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Modal, TextInput, Image, ScrollView, KeyboardAvoidingView, Platform, Alert, Animated } from 'react-native';
|
|
import { WebView } from 'react-native-webview';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useLocalSearchParams, router } from 'expo-router';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
import * as ImagePicker from 'expo-image-picker';
|
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
import { api } from '@/services/api';
|
|
import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, AvatarSizes } from '@/constants/theme';
|
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
|
import { useToast } from '@/components/ui/Toast';
|
|
import MockDashboard from '@/components/MockDashboard';
|
|
import { SubscriptionPayment } from '@/components/SubscriptionPayment';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
// Dashboard URL with beneficiary ID (deployment_id)
|
|
const getDashboardUrl = (deploymentId: string) =>
|
|
`https://react.eluxnetworks.net/dashboard/${deploymentId}`;
|
|
|
|
// Local beneficiaries have timestamp-based IDs (>1000000000)
|
|
// Real deployments have small IDs (21, 38, 29, etc.)
|
|
const isLocalBeneficiary = (id: string | number): boolean => {
|
|
const numId = typeof id === 'string' ? parseInt(id, 10) : id;
|
|
return numId > 1000000000;
|
|
};
|
|
|
|
export default function BeneficiaryDashboardScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { currentBeneficiary, setCurrentBeneficiary, localBeneficiaries, updateLocalBeneficiary } = useBeneficiary();
|
|
const toast = useToast();
|
|
const webViewRef = useRef<WebView>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [canGoBack, setCanGoBack] = useState(false);
|
|
const [authToken, setAuthToken] = useState<string | null>(null);
|
|
const [userName, setUserName] = useState<string | null>(null);
|
|
const [userId, setUserId] = useState<string | null>(null);
|
|
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
|
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
|
|
|
// Edit modal state
|
|
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
|
const [editForm, setEditForm] = useState({
|
|
name: '',
|
|
address: '',
|
|
avatar: '' as string | undefined,
|
|
});
|
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
// Beneficiary data for subscription check
|
|
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
|
const [isBeneficiaryLoading, setIsBeneficiaryLoading] = useState(true);
|
|
|
|
// Check if this is a local (mock) beneficiary
|
|
const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]);
|
|
|
|
// Check subscription status
|
|
const hasActiveSubscription = useMemo(() => {
|
|
if (!beneficiary) return false;
|
|
const subscription = beneficiary.subscription;
|
|
return subscription && subscription.status === 'active';
|
|
}, [beneficiary]);
|
|
|
|
// Load beneficiary data to check subscription
|
|
const loadBeneficiary = useCallback(async () => {
|
|
if (!id) return;
|
|
|
|
setIsBeneficiaryLoading(true);
|
|
try {
|
|
if (isLocal) {
|
|
const localBeneficiary = localBeneficiaries.find(
|
|
(b) => b.id === parseInt(id, 10)
|
|
);
|
|
if (localBeneficiary) {
|
|
setBeneficiary(localBeneficiary);
|
|
}
|
|
} else {
|
|
const response = await api.getBeneficiary(parseInt(id, 10));
|
|
if (response.ok && response.data) {
|
|
setBeneficiary(response.data);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load beneficiary:', err);
|
|
} finally {
|
|
setIsBeneficiaryLoading(false);
|
|
}
|
|
}, [id, isLocal, localBeneficiaries]);
|
|
|
|
useEffect(() => {
|
|
loadBeneficiary();
|
|
}, [loadBeneficiary]);
|
|
|
|
// Edit modal animation
|
|
useEffect(() => {
|
|
Animated.timing(fadeAnim, {
|
|
toValue: isEditModalVisible ? 1 : 0,
|
|
duration: 250,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}, [isEditModalVisible]);
|
|
|
|
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') {
|
|
Alert.alert('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);
|
|
setCurrentBeneficiary(updated);
|
|
setIsEditModalVisible(false);
|
|
toast.success('Profile Updated', 'Changes saved successfully');
|
|
} else {
|
|
toast.error('Error', 'Failed to save changes.');
|
|
}
|
|
} else {
|
|
// For API beneficiaries - would call backend here
|
|
toast.info('Coming Soon', 'Editing requires backend API.');
|
|
setIsEditModalVisible(false);
|
|
}
|
|
};
|
|
|
|
// Build dashboard URL with beneficiary ID
|
|
const dashboardUrl = id ? getDashboardUrl(id) : 'https://react.eluxnetworks.net/dashboard';
|
|
|
|
const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
|
|
|
|
// Load token, username, and userId from SecureStore
|
|
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);
|
|
console.log('Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
|
} catch (err) {
|
|
console.error('Failed to load credentials:', err);
|
|
} finally {
|
|
setIsTokenLoaded(true);
|
|
}
|
|
};
|
|
loadCredentials();
|
|
}, []);
|
|
|
|
// JavaScript to inject token into localStorage before page loads
|
|
// Web app uses auth2 key with JSON object: {username, token, user_id}
|
|
const injectedJavaScript = authToken
|
|
? `
|
|
(function() {
|
|
try {
|
|
// Web app expects auth2 as JSON object with these exact fields
|
|
var authData = {
|
|
username: '${userName || ''}',
|
|
token: '${authToken}',
|
|
user_id: ${userId || 'null'}
|
|
};
|
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
|
console.log('Auth data injected:', authData.username, 'user_id:', authData.user_id);
|
|
} catch(e) {
|
|
console.error('Failed to inject token:', e);
|
|
}
|
|
})();
|
|
true;
|
|
`
|
|
: '';
|
|
|
|
const handleRefresh = () => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
webViewRef.current?.reload();
|
|
};
|
|
|
|
const handleWebViewBack = () => {
|
|
if (canGoBack) {
|
|
webViewRef.current?.goBack();
|
|
}
|
|
};
|
|
|
|
const handleNavigationStateChange = (navState: any) => {
|
|
setCanGoBack(navState.canGoBack);
|
|
};
|
|
|
|
const handleError = () => {
|
|
setError('Failed to load dashboard. Please check your internet connection.');
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const handleGoBack = () => {
|
|
router.back();
|
|
};
|
|
|
|
// Wait for beneficiary data and token to load
|
|
if (isBeneficiaryLoading || (!isTokenLoaded && !isLocal)) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Preparing dashboard...</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// NO SUBSCRIPTION - Show payment screen with Stripe integration
|
|
if (!hasActiveSubscription && beneficiary) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<SubscriptionPayment
|
|
beneficiary={beneficiary}
|
|
onSuccess={() => loadBeneficiary()}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
<FullScreenError message={error} onRetry={handleRefresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.headerCenter}>
|
|
{currentBeneficiary && (
|
|
<View style={styles.avatarSmall}>
|
|
<Text style={styles.avatarText}>
|
|
{currentBeneficiary.name.charAt(0).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View>
|
|
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
|
{currentBeneficiary?.relationship && (
|
|
<Text style={styles.headerSubtitle}>{currentBeneficiary.relationship}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.headerActions}>
|
|
{/* WebView navigation only for real beneficiaries */}
|
|
{!isLocal && canGoBack && (
|
|
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
|
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
)}
|
|
{!isLocal && (
|
|
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
|
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Menu button */}
|
|
<TouchableOpacity style={styles.menuButton} 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="people-outline" size={20} color={AppColors.textPrimary} />
|
|
<Text style={styles.dropdownItemText}>Access</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>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Backdrop to close menu */}
|
|
{isMenuVisible && (
|
|
<TouchableOpacity
|
|
style={styles.menuBackdrop}
|
|
activeOpacity={1}
|
|
onPress={() => setIsMenuVisible(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Dashboard Content - Mock for local, WebView for real */}
|
|
{isLocal ? (
|
|
<MockDashboard beneficiaryName={beneficiaryName} />
|
|
) : (
|
|
<View style={styles.webViewContainer}>
|
|
<WebView
|
|
ref={webViewRef}
|
|
source={{ uri: dashboardUrl }}
|
|
style={styles.webView}
|
|
onLoadStart={() => setIsLoading(true)}
|
|
onLoadEnd={() => setIsLoading(false)}
|
|
onError={handleError}
|
|
onHttpError={handleError}
|
|
onNavigationStateChange={handleNavigationStateChange}
|
|
javaScriptEnabled={true}
|
|
domStorageEnabled={true}
|
|
startInLoadingState={true}
|
|
scalesPageToFit={true}
|
|
allowsBackForwardNavigationGestures={true}
|
|
// Inject token into localStorage BEFORE content loads
|
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
// Also inject after load in case page reads localStorage late
|
|
injectedJavaScript={injectedJavaScript}
|
|
renderLoading={() => (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
|
|
{isLoading && (
|
|
<View style={styles.loadingOverlay}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{/* Edit Modal */}
|
|
<Modal
|
|
visible={isEditModalVisible}
|
|
transparent
|
|
animationType="none"
|
|
onRequestClose={() => setIsEditModalVisible(false)}
|
|
>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.modalOverlay}
|
|
>
|
|
<Animated.View style={[styles.modalOverlay, { opacity: fadeAnim }]}>
|
|
<TouchableOpacity
|
|
style={styles.modalBackdrop}
|
|
activeOpacity={1}
|
|
onPress={() => setIsEditModalVisible(false)}
|
|
/>
|
|
<View style={styles.modalContent}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>Edit Profile</Text>
|
|
<TouchableOpacity
|
|
style={styles.modalCloseButton}
|
|
onPress={() => setIsEditModalVisible(false)}
|
|
>
|
|
<Ionicons name="close" size={24} color={AppColors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
{/* Avatar Picker */}
|
|
<TouchableOpacity style={styles.avatarPicker} onPress={handlePickAvatar}>
|
|
{editForm.avatar ? (
|
|
<Image source={{ uri: editForm.avatar }} style={styles.avatarPickerImage} />
|
|
) : (
|
|
<View style={styles.avatarPickerPlaceholder}>
|
|
<Text style={styles.avatarPickerLetter}>
|
|
{editForm.name.charAt(0).toUpperCase() || '?'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.avatarPickerBadge}>
|
|
<Ionicons name="camera" size={16} color={AppColors.white} />
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
{/* Name Field */}
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.inputLabel}>Name *</Text>
|
|
<TextInput
|
|
style={styles.textInput}
|
|
value={editForm.name}
|
|
onChangeText={(text) => setEditForm(prev => ({ ...prev, name: text }))}
|
|
placeholder="Enter name"
|
|
placeholderTextColor={AppColors.textMuted}
|
|
/>
|
|
</View>
|
|
|
|
{/* Address Field */}
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.inputLabel}>Address</Text>
|
|
<TextInput
|
|
style={[styles.textInput, styles.textInputMultiline]}
|
|
value={editForm.address}
|
|
onChangeText={(text) => setEditForm(prev => ({ ...prev, address: text }))}
|
|
placeholder="Enter address (optional)"
|
|
placeholderTextColor={AppColors.textMuted}
|
|
multiline
|
|
numberOfLines={2}
|
|
/>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Action Buttons */}
|
|
<View style={styles.modalActions}>
|
|
<TouchableOpacity
|
|
style={styles.cancelButton}
|
|
onPress={() => setIsEditModalVisible(false)}
|
|
>
|
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.saveButton}
|
|
onPress={handleSaveEdit}
|
|
>
|
|
<Text style={styles.saveButtonText}>Save</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
</KeyboardAvoidingView>
|
|
</Modal>
|
|
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
backgroundColor: AppColors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
zIndex: 1001,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerCenter: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginLeft: Spacing.sm,
|
|
},
|
|
avatarSmall: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: BorderRadius.full,
|
|
backgroundColor: AppColors.primaryLight,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: Spacing.sm,
|
|
},
|
|
avatarText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: '600',
|
|
color: AppColors.white,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: '700',
|
|
color: AppColors.textPrimary,
|
|
},
|
|
headerSubtitle: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
headerActions: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
position: 'relative',
|
|
},
|
|
actionButton: {
|
|
padding: Spacing.xs,
|
|
marginLeft: Spacing.xs,
|
|
},
|
|
menuButton: {
|
|
padding: Spacing.xs,
|
|
marginLeft: Spacing.sm,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
// Dropdown Menu
|
|
dropdownMenu: {
|
|
position: 'absolute',
|
|
top: 40,
|
|
right: 0,
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
minWidth: 160,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 12,
|
|
elevation: 8,
|
|
zIndex: 1000,
|
|
},
|
|
dropdownItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: Spacing.md,
|
|
paddingHorizontal: Spacing.lg,
|
|
gap: Spacing.md,
|
|
},
|
|
dropdownItemText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
menuBackdrop: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 999,
|
|
},
|
|
webViewContainer: {
|
|
flex: 1,
|
|
},
|
|
webView: {
|
|
flex: 1,
|
|
},
|
|
loadingContainer: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
loadingOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
},
|
|
loadingText: {
|
|
marginTop: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
// No Subscription Styles
|
|
noSubscriptionContainer: {
|
|
flex: 1,
|
|
padding: Spacing.xl,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
noSubIconContainer: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
backgroundColor: AppColors.accentLight,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
noSubTitle: {
|
|
fontSize: FontSizes['2xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
noSubSubtitle: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 24,
|
|
marginBottom: Spacing.xl,
|
|
paddingHorizontal: Spacing.md,
|
|
},
|
|
noSubPriceCard: {
|
|
width: '100%',
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.xl,
|
|
...Shadows.sm,
|
|
},
|
|
noSubPriceHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
noSubPlanName: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
noSubPlanDesc: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
marginTop: 2,
|
|
},
|
|
noSubPriceBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'baseline',
|
|
},
|
|
noSubPriceAmount: {
|
|
fontSize: FontSizes['2xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.primary,
|
|
},
|
|
noSubPriceUnit: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
marginLeft: 2,
|
|
},
|
|
noSubFeatures: {
|
|
gap: Spacing.sm,
|
|
},
|
|
noSubFeatureRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
},
|
|
noSubFeatureText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
// Edit Modal Styles
|
|
modalOverlay: {
|
|
flex: 1,
|
|
justifyContent: 'flex-end',
|
|
},
|
|
modalBackdrop: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
},
|
|
modalContent: {
|
|
backgroundColor: AppColors.surface,
|
|
borderTopLeftRadius: BorderRadius['2xl'],
|
|
borderTopRightRadius: BorderRadius['2xl'],
|
|
padding: Spacing.lg,
|
|
maxHeight: '80%',
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
modalTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
modalCloseButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
avatarPicker: {
|
|
alignSelf: 'center',
|
|
marginBottom: Spacing.xl,
|
|
position: 'relative',
|
|
},
|
|
avatarPickerImage: {
|
|
width: AvatarSizes.xl,
|
|
height: AvatarSizes.xl,
|
|
borderRadius: AvatarSizes.xl / 2,
|
|
},
|
|
avatarPickerPlaceholder: {
|
|
width: AvatarSizes.xl,
|
|
height: AvatarSizes.xl,
|
|
borderRadius: AvatarSizes.xl / 2,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
avatarPickerLetter: {
|
|
fontSize: FontSizes['3xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.primary,
|
|
},
|
|
avatarPickerBadge: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
right: 0,
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: AppColors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderWidth: 3,
|
|
borderColor: AppColors.surface,
|
|
},
|
|
inputGroup: {
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
inputLabel: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.textSecondary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
textInput: {
|
|
backgroundColor: AppColors.background,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textPrimary,
|
|
borderWidth: 1,
|
|
borderColor: AppColors.border,
|
|
},
|
|
textInputMultiline: {
|
|
minHeight: 80,
|
|
textAlignVertical: 'top',
|
|
},
|
|
modalActions: {
|
|
flexDirection: 'row',
|
|
gap: Spacing.md,
|
|
marginTop: Spacing.lg,
|
|
},
|
|
cancelButton: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
alignItems: 'center',
|
|
},
|
|
cancelButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
saveButton: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
alignItems: 'center',
|
|
},
|
|
saveButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
});
|