import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert, ActivityIndicator, ScrollView, KeyboardAvoidingView, Platform, RefreshControl, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { useAuth } from '@/contexts/AuthContext'; import { useToast } from '@/components/ui/Toast'; import { api } from '@/services/api'; import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, } from '@/constants/theme'; import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; type Role = 'caretaker' | 'guardian'; // 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; }; interface Invitation { id: string; email: string; role: string; label?: string; status: string; created_at: string; } export default function ShareAccessScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { currentBeneficiary, localBeneficiaries, updateLocalBeneficiary } = useBeneficiary(); const { user } = useAuth(); const toast = useToast(); const [email, setEmail] = useState(''); const [role, setRole] = useState('caretaker'); const [isLoading, setIsLoading] = useState(false); const [invitations, setInvitations] = useState([]); const [isLoadingInvitations, setIsLoadingInvitations] = useState(true); const [refreshing, setRefreshing] = useState(false); const beneficiaryName = currentBeneficiary?.name || 'this person'; const currentUserEmail = user?.email?.toLowerCase(); // Check if this is a local beneficiary const isLocal = useMemo(() => id ? isLocalBeneficiary(id) : false, [id]); // Get local beneficiary data const localBeneficiary = useMemo(() => { if (!isLocal || !id) return null; return localBeneficiaries.find(b => b.id === parseInt(id, 10)); }, [isLocal, id, localBeneficiaries]); // Load invitations on mount useEffect(() => { if (id) { loadInvitations(); } }, [id, isLocal, localBeneficiary]); const loadInvitations = async () => { if (!id) return; // For local beneficiaries, use stored invitations if (isLocal && localBeneficiary) { setInvitations(localBeneficiary.invitations || []); setIsLoadingInvitations(false); setRefreshing(false); return; } // For API beneficiaries try { const response = await api.getInvitations(id); if (response.ok && response.data) { setInvitations(response.data.invitations || []); } } catch (error) { // Failed to load invitations } finally { setIsLoadingInvitations(false); setRefreshing(false); } }; const onRefresh = useCallback(() => { setRefreshing(true); loadInvitations(); }, [id]); const validateEmail = (email: string): boolean => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; const handleSendInvite = async () => { const trimmedEmail = email.trim().toLowerCase(); if (!trimmedEmail) { Alert.alert('Error', 'Please enter an email address.'); return; } if (!validateEmail(trimmedEmail)) { Alert.alert('Error', 'Please enter a valid email address.'); return; } // Check if inviting self if (currentUserEmail && trimmedEmail === currentUserEmail) { Alert.alert('Error', 'You cannot invite yourself.'); return; } // Check if already invited const alreadyInvited = invitations.some(inv => inv.email.toLowerCase() === trimmedEmail); if (alreadyInvited) { Alert.alert('Error', 'This person has already been invited.'); return; } setIsLoading(true); // For local beneficiaries, store invitation locally if (isLocal && id) { try { const newInvitation: Invitation = { id: `local_${Date.now()}`, email: trimmedEmail, role: role, status: 'pending', created_at: new Date().toISOString(), }; const currentInvitations = localBeneficiary?.invitations || []; await updateLocalBeneficiary(parseInt(id, 10), { invitations: [...currentInvitations, newInvitation], }); setEmail(''); setRole('caretaker'); setInvitations([...currentInvitations, newInvitation]); toast.success('Invitation Sent', `Invitation sent to ${trimmedEmail}`); } catch (error) { toast.error('Error', 'Failed to send invitation'); } finally { setIsLoading(false); } return; } // For API beneficiaries try { const response = await api.sendInvitation({ beneficiaryId: id!, email: trimmedEmail, role: role, }); if (response.ok) { setEmail(''); setRole('caretaker'); loadInvitations(); toast.success('Success', `Invitation sent to ${trimmedEmail}`); } else { Alert.alert('Error', response.error?.message || 'Failed to send invitation'); } } catch (error) { Alert.alert('Error', 'Failed to send invitation. Please try again.'); } finally { setIsLoading(false); } }; const handleRemoveInvitation = (invitation: Invitation) => { Alert.alert( 'Remove Access', `Remove ${invitation.email} from accessing ${beneficiaryName}?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Remove', style: 'destructive', onPress: async () => { // For local beneficiaries if (isLocal && id) { try { const currentInvitations = localBeneficiary?.invitations || []; const updatedInvitations = currentInvitations.filter(inv => inv.id !== invitation.id); await updateLocalBeneficiary(parseInt(id, 10), { invitations: updatedInvitations, }); setInvitations(updatedInvitations); toast.success('Removed', 'Access removed successfully'); } catch (error) { toast.error('Error', 'Failed to remove access'); } return; } // For API beneficiaries try { const response = await api.deleteInvitation(invitation.id); if (response.ok) { loadInvitations(); toast.success('Removed', 'Access removed successfully'); } else { Alert.alert('Error', 'Failed to remove access'); } } catch (error) { Alert.alert('Error', 'Failed to remove access'); } }, }, ] ); }; const getRoleLabel = (role: string): string => { if (role === 'custodian') return 'Custodian'; if (role === 'guardian') return 'Guardian'; return 'Caretaker'; }; const handleChangeRole = (invitation: Invitation) => { const newRole = invitation.role === 'caretaker' ? 'guardian' : 'caretaker'; const newRoleLabel = newRole === 'guardian' ? 'Guardian' : 'Caretaker'; Alert.alert( 'Change Role', `Change ${invitation.email}'s role to ${newRoleLabel}?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Change', onPress: async () => { // For local beneficiaries if (isLocal && id) { try { const currentInvitations = localBeneficiary?.invitations || []; const updatedInvitations = currentInvitations.map(inv => inv.id === invitation.id ? { ...inv, role: newRole } : inv ); await updateLocalBeneficiary(parseInt(id, 10), { invitations: updatedInvitations, }); setInvitations(updatedInvitations); toast.success('Updated', `Role changed to ${newRoleLabel}`); } catch (error) { toast.error('Error', 'Failed to change role'); } return; } // For API beneficiaries try { const response = await api.updateInvitation(invitation.id, newRole as 'caretaker' | 'guardian'); if (response.ok) { loadInvitations(); toast.success('Updated', `Role changed to ${newRoleLabel}`); } else { Alert.alert('Error', response.error?.message || 'Failed to change role'); } } catch (error) { Alert.alert('Error', 'Failed to change role'); } }, }, ] ); }; const getStatusColor = (status: string): string => { switch (status) { case 'accepted': return AppColors.success; case 'pending': return AppColors.warning; case 'rejected': return AppColors.error; default: return AppColors.textMuted; } }; return ( {/* Header */} router.back()}> Access } > {/* Invite Section */} Invite Someone {/* Role Toggle - above email */} setRole('caretaker')} > Caretaker setRole('guardian')} > Guardian {role === 'caretaker' ? 'Can view activity and chat with Julia' : 'Full access: edit info, manage subscription'} {isLoading ? ( ) : ( )} {/* People with Access */} People with Access {isLoadingInvitations ? ( ) : invitations.length === 0 ? ( No one else has access yet ) : ( {invitations.map((invitation) => ( {invitation.email.charAt(0).toUpperCase()} {invitation.email} handleChangeRole(invitation)} > {getRoleLabel(invitation.role)} {invitation.status} handleRemoveInvitation(invitation)} > ))} )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: AppColors.background, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, backButton: { padding: Spacing.xs, }, headerTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, content: { flex: 1, }, scrollContent: { padding: Spacing.lg, paddingBottom: Spacing.xxl, }, section: { marginBottom: Spacing.xl, }, sectionTitle: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textSecondary, marginBottom: Spacing.md, textTransform: 'uppercase', letterSpacing: 0.5, }, inputRow: { flexDirection: 'row', gap: Spacing.sm, }, input: { flex: 1, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, paddingHorizontal: Spacing.md, paddingVertical: Spacing.md, fontSize: FontSizes.base, color: AppColors.textPrimary, borderWidth: 1, borderColor: AppColors.border, }, sendButton: { width: 48, height: 48, borderRadius: BorderRadius.lg, backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', }, sendButtonDisabled: { opacity: 0.7, }, roleToggle: { flexDirection: 'row', backgroundColor: AppColors.surfaceSecondary, borderRadius: BorderRadius.lg, padding: 4, marginTop: Spacing.md, }, roleButton: { flex: 1, paddingVertical: Spacing.sm, alignItems: 'center', borderRadius: BorderRadius.md, }, roleButtonActive: { backgroundColor: AppColors.surface, }, roleButtonText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textSecondary, }, roleButtonTextActive: { color: AppColors.primary, fontWeight: FontWeights.semibold, }, roleHint: { fontSize: FontSizes.xs, color: AppColors.textMuted, marginTop: Spacing.sm, textAlign: 'center', }, loadingContainer: { padding: Spacing.xl, alignItems: 'center', }, emptyState: { alignItems: 'center', padding: Spacing.xl, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, }, emptyText: { fontSize: FontSizes.sm, color: AppColors.textMuted, marginTop: Spacing.sm, }, invitationsList: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, overflow: 'hidden', }, invitationItem: { flexDirection: 'row', alignItems: 'center', padding: Spacing.md, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, invitationInfo: { flex: 1, flexDirection: 'row', alignItems: 'center', }, invitationAvatar: { width: 36, height: 36, borderRadius: 18, backgroundColor: AppColors.primaryLight, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.md, }, avatarText: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.white, }, invitationDetails: { flex: 1, }, invitationEmail: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textPrimary, }, invitationMeta: { flexDirection: 'row', alignItems: 'center', marginTop: 2, }, roleChip: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.primaryLighter, paddingHorizontal: Spacing.sm, paddingVertical: 2, borderRadius: BorderRadius.full, gap: 2, }, invitationRole: { fontSize: FontSizes.xs, color: AppColors.primary, fontWeight: FontWeights.medium, }, statusDot: { width: 6, height: 6, borderRadius: 3, marginHorizontal: Spacing.xs, }, invitationStatus: { fontSize: FontSizes.xs, textTransform: 'capitalize', }, removeButton: { padding: Spacing.xs, }, });