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, useFocusEffect } 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(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [canGoBack, setCanGoBack] = useState(false); const [authToken, setAuthToken] = useState(null); const [userName, setUserName] = useState(null); const [userId, setUserId] = useState(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(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]); // Hide menu when navigating away from page useFocusEffect( useCallback(() => { return () => { setIsMenuVisible(false); setIsEditModalVisible(false); }; }, []) ); 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 ( {beneficiaryName} Preparing dashboard... ); } // NO SUBSCRIPTION - Show payment screen with Stripe integration if (!hasActiveSubscription && beneficiary) { return ( {beneficiaryName} loadBeneficiary()} /> ); } if (error) { return ( {beneficiaryName} ); } return ( {/* Header */} {currentBeneficiary && ( currentBeneficiary.avatar ? ( ) : ( {currentBeneficiary.name.charAt(0).toUpperCase()} ) )} {beneficiaryName} {currentBeneficiary?.relationship && ( {currentBeneficiary.relationship} )} {/* WebView navigation only for real beneficiaries */} {!isLocal && canGoBack && ( )} {!isLocal && ( )} {/* Menu button */} setIsMenuVisible(!isMenuVisible)}> {/* Dropdown Menu */} {isMenuVisible && ( { setIsMenuVisible(false); handleEditPress(); }} > Edit { setIsMenuVisible(false); router.push(`/(tabs)/beneficiaries/${id}/share`); }} > Access { setIsMenuVisible(false); router.push(`/(tabs)/beneficiaries/${id}/equipment`); }} > Equipment )} {/* Backdrop to close menu */} {isMenuVisible && ( setIsMenuVisible(false)} /> )} {/* Dashboard Content - Mock for local, WebView for real */} {isLocal ? ( ) : ( 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={() => ( Loading dashboard... )} /> {isLoading && ( )} )} {/* Edit Modal */} setIsEditModalVisible(false)} > setIsEditModalVisible(false)} /> Edit Profile setIsEditModalVisible(false)} > {/* Avatar Picker */} {editForm.avatar ? ( ) : ( {editForm.name.charAt(0).toUpperCase() || '?'} )} {/* Name Field */} Name * setEditForm(prev => ({ ...prev, name: text }))} placeholder="Enter name" placeholderTextColor={AppColors.textMuted} /> {/* Address Field */} Address setEditForm(prev => ({ ...prev, address: text }))} placeholder="Enter address (optional)" placeholderTextColor={AppColors.textMuted} multiline numberOfLines={2} /> {/* Action Buttons */} setIsEditModalVisible(false)} > Cancel Save ); } 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, }, avatarSmallImage: { width: 36, height: 36, borderRadius: 18, 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, }, });