import React, { useState, useEffect } from "react"; import { View, InteractionManager, ActivityIndicator } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useFocusEffect } from "expo-router"; import { Button } from "~/components/ui/button"; import { Text } from "~/components/ui/text"; import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; import { router } from "expo-router"; import { ROUTES } from "~/lib/routes"; import { parseDisplayToCents, formatDisplayAmount, } from "~/lib/utils/monetaryUtils"; import { Big } from "big.js"; import { PinConfirmationModal } from "~/components/ui/pinConfirmationModal"; import { amountSchema, validate } from "~/lib/utils/validationSchemas"; import BackButton from "~/components/ui/backButton"; import { showAlert } from "~/lib/utils/alertUtils"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import { useTabStore } from "~/lib/stores"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; export default function AddCash() { const [amount, setAmount] = useState(""); const [showPinModal, setShowPinModal] = useState(false); const [isSecurityVerified, setIsSecurityVerified] = useState(false); const { setLastVisitedTab } = useTabStore(); const { t } = useTranslation(); 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 = React.useRef | null>( null ); 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); }; // Set the tab state when component mounts (defer non-critical work) useEffect(() => { const task = InteractionManager.runAfterInteractions(() => { if (__DEV__) { console.log("ADD CASH PAGE MOUNTED"); } setLastVisitedTab("/"); }); return () => task.cancel(); }, [setLastVisitedTab]); useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); // Reset state and show PIN modal when screen comes into focus // useFocusEffect only runs when screen is focused, so we can safely show modal here useFocusEffect( React.useCallback(() => { setIsSecurityVerified(false); setShowPinModal(true); setAmount(""); // Cleanup: hide modal when screen loses focus return () => { setShowPinModal(false); setIsSecurityVerified(false); }; }, []) ); // Handle number input and special actions const handleNumberPress = (input: string) => { if (input === "clear") { handleClear(); return; } if (input === "backspace") { handleBackspace(); return; } // Handle decimal point if (input === ".") { // Prevent multiple decimals if (amount.includes(".")) return; // If empty, start with "0." if (amount === "") { setAmount("0."); return; } // Add decimal point setAmount(amount + "."); return; } // Handle digit input (0-9) if (!/^[0-9]$/.test(input)) return; // Handle leading zeros if (amount === "0" && input !== ".") { setAmount(input); // Replace leading zero return; } const newAmount = amount + input; // Check decimal places limit (max 2 decimal places) if (newAmount.includes(".")) { const [whole, decimal] = newAmount.split("."); if (decimal && decimal.length > 2) return; } // Check maximum amount (max $999.99) try { const numValue = new Big(newAmount); if (numValue.gt(999.99)) return; } catch (error) { return; } // Check total length to prevent very long inputs if (newAmount.length > 6) return; // Max: 999.99 setAmount(newAmount); }; // Handle backspace const handleBackspace = () => { if (amount.length === 0) return; // Remove last character const newAmount = amount.slice(0, -1); setAmount(newAmount); }; // Clear all input const handleClear = () => { setAmount(""); }; // Validate if amount is valid for submission const isValidAmount = () => { if ( !amount || amount === "" || amount === "0" || amount === "0." || amount === "0.00" ) { return false; } const amountInCents = parseDisplayToCents(amount); return amountInCents >= 1000 && amountInCents <= 99999; // $10.00 to $999.99 }; // Handle PIN confirmation success const handlePinSuccess = () => { setShowPinModal(false); setIsSecurityVerified(true); }; // Handle add cash action (called after PIN is verified) const handleAddCash = () => { // Validate amount using valibot (minimum $10.00 = 1000 cents) const amountValidationResult = validate( amountSchema({ min: 1000, max: 99999, minDisplay: "$10.00" }), amount ); if (!amountValidationResult.success) { showToast( t("addcash.validationErrorTitle"), t("addcash.validationEnterAmount"), "error" ); return; } const amountInCents = parseDisplayToCents(amount); if (amountInCents < 1000) { // $10.00 minimum showToast( t("addcash.validationErrorTitle"), t("addcash.validationMinAmount"), "error" ); return; } if (amountInCents > 99999) { // $999.99 maximum showToast( t("addcash.validationErrorTitle"), t("addcash.validationMaxAmount"), "error" ); return; } console.log("Adding cash:", amountInCents, "cents"); // Navigate into the add-cash donation + checkout flow router.push({ pathname: ROUTES.DONATION, params: { amount: amountInCents.toString(), type: "add_cash", }, }); }; return ( {!isSecurityVerified ? ( {t("addcash.verifyingSecurity")} ) : ( <> {t("addcash.title")} {formatDisplayAmount(amount)} )} setShowPinModal(false)} onSuccess={handlePinSuccess} title={t("addcash.pinModalTitle")} /> ); }