import React, { useEffect, useRef, useState } from "react"; import { View, ScrollView, Image, TouchableOpacity } from "react-native"; import { useLocalSearchParams, router } from "expo-router"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import BackButton from "~/components/ui/backButton"; import { Text } from "~/components/ui/text"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; import { TransactionService } from "~/lib/services/transactionService"; import { GoogleIcon } from "~/components/ui/icons"; import { Icons } from "~/assets/icons"; import { WalletService } from "~/lib/services/walletService"; import { ROUTES } from "~/lib/routes"; import { calculateTotalAmountForSending, calculateProcessingFee, } from "~/lib/utils/feeUtils"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; type PaymentOptionCardProps = { label: string; icon: React.ReactNode; selected?: boolean; onPress?: () => void; }; const PaymentOptionCard = ({ label, icon, selected, onPress, }: PaymentOptionCardProps) => { return ( {icon} {selected && ( )} {label} ); }; export default function CheckoutScreen() { const { t } = useTranslation(); const params = useLocalSearchParams<{ amount?: string; type?: string; recipientName?: string; recipientPhoneNumber?: string; recipientType?: string; recipientId?: string; note?: string; donationSkipped?: string; donationType?: string; donationAmount?: string; donateAnonymously?: string; donationCampaignId?: string; donationCampaignTitle?: string; ticketTierName?: string; ticketTierPrice?: string; eventName?: string; eventId?: string; ticketTierId?: string; ticketCount?: string; }>(); const { user, profile, wallet, refreshWallet } = useAuthWithProfile(); const [selectedPayment, setSelectedPayment] = useState< "card" | "apple" | "google" >("card"); const [email, setEmail] = useState(""); const [nameOnCard, setNameOnCard] = useState(""); const [cardNumber, setCardNumber] = useState(""); const [expiry, setExpiry] = useState(""); const [cvv, setCvv] = useState(""); const [showCvv, setShowCvv] = useState(false); const [address, setAddress] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [toastVisible, setToastVisible] = useState(false); const [toastTitle, setToastTitle] = useState(""); const [toastDescription, setToastDescription] = useState( undefined ); const [toastVariant, setToastVariant] = useState< "success" | "error" | "warning" | "info" >("info"); const toastTimeoutRef = useRef | null>(null); const isAddCashFlow = params.type === "add_cash"; const isEventTicketFlow = params.type === "event_ticket"; const headerTitle = isEventTicketFlow ? params.ticketTierName || "-" : isAddCashFlow ? profile?.fullName || user?.email || "-" : params.recipientName || "-"; const headerSubtitle = isEventTicketFlow ? params.eventName || "" : isAddCashFlow ? profile?.phoneNumber || profile?.email || "" : params.recipientPhoneNumber || ""; const amountInCents = parseInt(params.amount || "0"); const amountInDollars = amountInCents / 100; const hasDonation = params.donationSkipped === "false" && !!params.donationAmount; const donationAmountNumber = params.donationAmount ? Number(params.donationAmount) : NaN; const donationAmountDollars = !isNaN(donationAmountNumber) && hasDonation ? donationAmountNumber : 0; const processingFeeInCents = Math.round(amountInCents * 0.0125); const processingFeeInDollars = processingFeeInCents / 100; const subtotalInDollars = amountInDollars + donationAmountDollars; const totalInDollars = subtotalInDollars + processingFeeInDollars; // Card input helpers (mirrored from AddCard screen) const formatCardNumber = (value: string) => { const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); const matches = v.match(/\d{4,16}/g); const match = (matches && matches[0]) || ""; const parts: string[] = []; for (let i = 0, len = match.length; i < len; i += 4) { parts.push(match.substring(i, i + 4)); } if (parts.length) { return parts.join(" "); } else { return v; } }; const formatExpiry = (value: string) => { const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); if (v.length >= 2) { return `${v.substring(0, 2)}/${v.substring(2, 4)}`; } return v; }; const handleCardNumberChange = (value: string) => { const formatted = formatCardNumber(value); if (formatted.length <= 19) { // 16 digits + 3 spaces setCardNumber(formatted); } }; const handleExpiryChange = (value: string) => { const formatted = formatExpiry(value); if (formatted.length <= 5) { // MM/YY setExpiry(formatted); } }; const handleCvvChange = (value: string) => { const v = value.replace(/[^0-9]/gi, ""); if (v.length <= 4) { setCvv(v); } }; const showToast = ( title: string, description?: string, variant: "success" | "error" | "warning" | "info" = "info" ) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVariant(variant); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); useEffect(() => { if (!profile) return; if (!email && profile.email) { setEmail(profile.email); } if (!nameOnCard && profile.fullName) { setNameOnCard(profile.fullName); } if (!address && profile.address) { setAddress(profile.address); } }, [profile, email, nameOnCard, address]); const handlePay = async () => { if (isAddCashFlow) { if (!params.amount || !user?.uid || !wallet) { showToast( t("selectacc.toastErrorTitle"), t("selectacc.toastMissingInfo"), "error" ); return; } const amountInCents = parseInt(params.amount); if (isNaN(amountInCents) || amountInCents <= 0) { showToast( t("addcash.validationErrorTitle"), t("addcash.validationEnterAmount"), "error" ); return; } setIsProcessing(true); try { const currentBalance = wallet.balance || 0; const newBalance = currentBalance + amountInCents; const result = await WalletService.updateWalletBalance( user.uid, newBalance ); if (result.success) { await refreshWallet(); router.replace({ pathname: ROUTES.ADDCASH_COMPLETION, params: { amount: (amountInCents / 100).toFixed(2), }, }); } else { showToast( t("selectacc.toastErrorTitle"), result.error || t("selectacc.toastAddCashFailed"), "error" ); } } catch (error) { console.error("Error adding cash from checkout:", error); showToast( t("selectacc.toastErrorTitle"), t("selectacc.toastAddCashFailedWithRetry"), "error" ); } finally { setIsProcessing(false); } return; } if (params.type === "event_ticket") { if (!params.amount || !user?.uid) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastMissingDetails"), "error" ); return; } const amountInCents = parseInt(params.amount); if (isNaN(amountInCents) || amountInCents <= 0) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastInvalidAmount"), "error" ); return; } setIsProcessing(true); try { router.push({ pathname: ROUTES.TRANSACTION_DETAIL, params: { amount: params.amount, type: "event_ticket", recipientName: params.eventName || "", date: new Date().toISOString(), status: "Completed", note: params.ticketTierName || "", flowType: "event_ticket", ticketTierId: params.ticketTierId || "", ticketCount: params.ticketCount || "1", eventId: params.eventId || "", }, }); } finally { setIsProcessing(false); } return; } if (!params.amount || !user?.uid) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastMissingDetails"), "error" ); return; } const amountInCents = parseInt(params.amount); if (isNaN(amountInCents) || amountInCents <= 0) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastInvalidAmount"), "error" ); return; } const totalRequired = calculateTotalAmountForSending(amountInCents); if (!wallet) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastWalletNotFound"), "error" ); 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("transconfirm.toastInsufficientBalanceTitle"), t("transconfirm.toastInsufficientBalanceDescription", { required, fee, available, }), "error" ); return; } if ( !params.recipientName || !params.recipientPhoneNumber || !params.recipientType || !params.recipientId ) { showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastRecipientMissing"), "error" ); return; } setIsProcessing(true); try { const result = await TransactionService.sendMoney(user.uid, { amount: amountInCents, recipientName: params.recipientName, recipientPhoneNumber: params.recipientPhoneNumber, recipientType: params.recipientType as "saved" | "contact", recipientId: params.recipientId, note: params.note?.trim() || "", }); if (result.success) { await refreshWallet(); router.replace({ pathname: "/(screens)/taskcomp", params: { message: `Transaction completed on your end. $${( amountInCents / 100 ).toFixed(2)} will be claimed by ${ params.recipientName } within 7 days, or the money will revert to your wallet.`, amount: (amountInCents / 100).toFixed(2), recipientName: params.recipientName, recipientPhoneNumber: params.recipientPhoneNumber, }, }); } else { showToast( t("transconfirm.toastErrorTitle"), result.error || t("transconfirm.toastSendFailed"), "error" ); } } catch (error) { console.error("Error sending money:", error); showToast( t("transconfirm.toastErrorTitle"), t("transconfirm.toastSendFailedWithRetry"), "error" ); } finally { setIsProcessing(false); } }; return ( {t("checkout.title")} {isEventTicketFlow ? "Ticket" : t("checkout.recipientLabel")} {headerTitle} {headerSubtitle} {t("checkout.totalLabel")} ${totalInDollars.toFixed(2)} {t("checkout.subtitle")} {/* Payment options */} {t("checkout.paymentOptionsTitle")} } selected={selectedPayment === "card"} onPress={() => setSelectedPayment("card")} /> } selected={selectedPayment === "apple"} onPress={() => setSelectedPayment("apple")} /> } selected={selectedPayment === "google"} onPress={() => setSelectedPayment("google")} /> {selectedPayment === "card" ? ( {t("checkout.cardInfoTitle")} ) : ( {selectedPayment === "apple" ? t("checkout.appleIdTitle") : t("checkout.paymentEmailTitle")} )} {/* Contact information */} {t("checkout.contactInfoTitle")} {/* Billing address */} {t("checkout.billingAddressTitle")} ); }