import React, { useState, useEffect, useRef } from "react"; import { View, Text, ScrollView, TouchableOpacity, FlatList, ActivityIndicator, } from "react-native"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; import { LucideUser } from "lucide-react-native"; import BackButton from "~/components/ui/backButton"; import { useContactsStore, useRecipientsStore } from "~/lib/stores"; import { useLocalSearchParams, router } from "expo-router"; import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; import { calculateTotalAmountForSending, calculateProcessingFee, } from "~/lib/utils/feeUtils"; import { ROUTES } from "~/lib/routes"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; import { UserSearchService } from "~/lib/services/userSearchService"; const RecipientCard = ({ name, phoneNumber, selected, onPress, }: { name: string; phoneNumber: string; selected: boolean; onPress: () => void; }) => { return ( {name} {phoneNumber} ); }; // Selected Recipient Pill Component const SelectedRecipientPill = ({ name, phoneNumber, onRemove, }: { name: string; phoneNumber: string; onRemove: () => void; }) => { return ( {name} × ); }; export default function SelectRecip() { const { t } = useTranslation(); const params = useLocalSearchParams<{ amount: string; selectedContactId?: string; selectedContactName?: string; selectedContactPhone?: string; }>(); const { user, wallet, refreshWallet } = useAuthWithProfile(); const { contacts, loading, error, hasPermission, requestPermission } = useContactsStore(); const { recipients, loading: recipientsLoading, error: recipientsError, } = useRecipientsStore(); const [selectedRecipient, setSelectedRecipient] = useState( null ); const [note, setNote] = useState(""); const [search, setSearch] = useState(""); const [isSending, setIsSending] = useState(false); const [hasInitialized, setHasInitialized] = useState(false); const [remoteRecipient, setRemoteRecipient] = useState<{ id: string; name: string; phoneNumber: string; type: "saved" | "contact"; } | null>(null); const remoteSearchTimeoutRef = useRef | null>( null ); const [toastVisible, setToastVisible] = useState(false); const [toastTitle, setToastTitle] = useState(""); const [toastDescription, setToastDescription] = useState< string | undefined >(); const toastTimeoutRef = useRef | null>(null); const showToast = (title: string, description?: string) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); // Remote user search by email/username (users collection) useEffect(() => { if (remoteSearchTimeoutRef.current) { clearTimeout(remoteSearchTimeoutRef.current); remoteSearchTimeoutRef.current = null; } const term = search.trim(); if (!term || !term.includes("@")) { setRemoteRecipient(null); return; } remoteSearchTimeoutRef.current = setTimeout(async () => { const profile = await UserSearchService.findUserByEmail( term.toLowerCase() ); if (!profile) { setRemoteRecipient(null); return; } setRemoteRecipient({ id: profile.uid, name: profile.fullName || profile.email || "User", phoneNumber: profile.phoneNumber || "No phone number", // Treat remote user as a saved recipient-type for transaction metadata type: "saved", }); }, 500); return () => { if (remoteSearchTimeoutRef.current) { clearTimeout(remoteSearchTimeoutRef.current); remoteSearchTimeoutRef.current = null; } }; }, [search]); // Combine contacts and recipients into a single list const getAllRecipients = () => { const allRecipients: Array<{ id: string; name: string; phoneNumber: string; type: "saved" | "contact"; }> = []; // Add saved recipients recipients.forEach((recipient) => { allRecipients.push({ id: recipient.id, name: recipient.fullName, phoneNumber: recipient.phoneNumber, type: "saved", }); }); // Add contacts (only if permission granted) if (hasPermission && contacts) { contacts.forEach((contact) => { allRecipients.push({ id: contact.id, name: contact.name, phoneNumber: contact.phoneNumbers?.[0]?.number || "No phone number", type: "contact", }); }); } // Include remote user search result (from users collection) if present if (remoteRecipient) { const alreadyExists = allRecipients.some( (recipient) => recipient.id === remoteRecipient.id ); if (!alreadyExists) { allRecipients.unshift(remoteRecipient); } } // If a contact was selected from the modal or QR scan, prioritize it if (params.selectedContactId) { const existingIndex = allRecipients.findIndex( (recipient) => recipient.id === params.selectedContactId ); if (existingIndex > -1) { const selectedContact = allRecipients.splice(existingIndex, 1)[0]; allRecipients.unshift(selectedContact); } else if (params.selectedContactName && params.selectedContactPhone) { // Synthetic recipient from QR scan or external source allRecipients.unshift({ id: params.selectedContactId, name: params.selectedContactName, phoneNumber: params.selectedContactPhone, type: "contact", }); } } return allRecipients; }; // Handle initial selection from contact modal useEffect(() => { if (params.selectedContactId && !hasInitialized && contacts && recipients) { // Find the selected contact in the combined list const allRecipients = getAllRecipients(); const selectedContact = allRecipients.find( (recipient) => recipient.id === params.selectedContactId ); if (selectedContact) { setSelectedRecipient(`${selectedContact.type}-${selectedContact.id}`); setHasInitialized(true); } } }, [ params.selectedContactId, hasInitialized, contacts, recipients, remoteRecipient, ]); const handleRecipientSelect = (id: string) => { setSelectedRecipient(id); console.log("Selected recipient:", id); }; const handleRemoveSelected = () => { setSelectedRecipient(null); setSearch(""); }; // Get selected recipient data const getSelectedRecipientData = () => { if (!selectedRecipient) return null; const allRecipients = getAllRecipients(); return allRecipients.find( (recipient) => `${recipient.type}-${recipient.id}` === selectedRecipient ); }; const selectedRecipientData = getSelectedRecipientData(); const handleSendMoney = async () => { if (!selectedRecipient || !params.amount || !user?.uid) { showToast( t("selectrecip.toastErrorTitle"), t("selectrecip.toastMissingInfo") ); return; } setIsSending(true); // Let the UI update (show spinner) before running validation and navigation await new Promise((resolve) => setTimeout(resolve, 0)); let didNavigate = false; try { const amountInCents = parseInt(params.amount); // Amount is already in cents if (isNaN(amountInCents) || amountInCents <= 0) { showToast( t("selectrecip.toastErrorTitle"), t("selectrecip.toastInvalidAmount") ); return; } // Check if user has sufficient balance (including processing fee) const totalRequired = calculateTotalAmountForSending(amountInCents); if (!wallet) { showToast( t("selectrecip.toastErrorTitle"), t("selectrecip.toastWalletNotFound") ); return; } if (wallet.balance < totalRequired) { const processingFee = calculateProcessingFee(amountInCents); const required = (totalRequired / 100).toFixed(2); const fee = (processingFee / 100).toFixed(2); const available = (wallet.balance / 100).toFixed(2); showToast( t("selectrecip.toastInsufficientBalanceTitle"), t("selectrecip.toastInsufficientBalanceDescription", { required, fee, available, }) ); return; } // Find the selected recipient details const allRecipients = getAllRecipients(); const selectedRecipientData = allRecipients.find( (recipient) => `${recipient.type}-${recipient.id}` === selectedRecipient ); if (!selectedRecipientData) { showToast( t("selectrecip.toastErrorTitle"), t("selectrecip.toastRecipientNotFound") ); return; } // Navigate to donation screen first, then continue to confirmation from there router.push({ pathname: ROUTES.DONATION, params: { amount: params.amount, recipientName: selectedRecipientData.name, recipientPhoneNumber: selectedRecipientData.phoneNumber, recipientType: selectedRecipientData.type, recipientId: selectedRecipientData.id, note: note.trim() || "", type: "send", }, }); didNavigate = true; // Clear the note and selected recipient after navigation setNote(""); setSelectedRecipient(null); } finally { // If we didn't navigate (validation error), re-enable the button if (!didNavigate) { setIsSending(false); } // If we did navigate, keep isSending=true; this screen will unmount } }; const allRecipients = getAllRecipients(); // Filter recipients based on search input const filteredRecipients = allRecipients.filter((recipient) => { if (!search.trim()) return true; const searchTerm = search.toLowerCase().trim(); const nameMatch = recipient.name.toLowerCase().includes(searchTerm); const phoneMatch = recipient.phoneNumber.toLowerCase().includes(searchTerm); return nameMatch || phoneMatch; }); return ( {/* Horizontal Divider */} {t("selectrecip.toLabel")} {selectedRecipientData ? ( ) : ( )} {t("selectrecip.forLabel")} {/* Combined Recipients List */} {t("selectrecip.recipientsTitle", { count: filteredRecipients.length, })} {/* Loading State */} {loading || recipientsLoading ? ( {t("selectrecip.loadingRecipients")} ) : error || recipientsError ? ( /* Error State */ {t("selectrecip.errorTitle")} {t("selectrecip.errorWithMessage", { error: error || recipientsError, })} ) : !hasPermission ? ( /* Permission Required */ {t("selectrecip.contactsPermissionTitle")} {t("selectrecip.contactsPermissionSubtitle")} ) : filteredRecipients.length === 0 ? ( /* Empty State */ {search.trim() ? t("selectrecip.emptyTitleSearch") : t("selectrecip.emptyTitleDefault")} {search.trim() ? t("selectrecip.emptySubtitleSearch") : t("selectrecip.emptySubtitleDefault")} ) : ( /* Recipients List */ `${item.type}-${item.id}`} scrollEnabled={false} ItemSeparatorComponent={() => } renderItem={({ item }) => ( handleRecipientSelect(`${item.type}-${item.id}`) } /> )} /> )} ); }