Amba-Agent-App/app/(root)/(screens)/cashout.tsx
2026-01-16 00:22:35 +03:00

341 lines
9.9 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { View, ScrollView, InteractionManager } from "react-native";
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 { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { useUserWallet } from "~/lib/hooks/useUserWallet";
import {
parseDisplayToCents,
formatDisplayAmount,
} from "~/lib/utils/monetaryUtils";
import { Big } from "big.js";
import BackButton from "~/components/ui/backButton";
import { PinConfirmationModal } from "~/components/ui/pinConfirmationModal";
import { amountSchema, validate } from "~/lib/utils/validationSchemas";
import { showAlert } from "~/lib/utils/alertUtils";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import FourDotLoader from "~/components/ui/FourDotLoader";
export default function CashOut() {
const { user } = useAuthWithProfile();
const { wallet } = useUserWallet(user);
const [amount, setAmount] = useState("");
const [showPinModal, setShowPinModal] = useState(false);
const [isSecurityVerified, setIsSecurityVerified] = useState(false);
const { t } = useTranslation();
const [toastVisible, setToastVisible] = useState(false);
const [toastTitle, setToastTitle] = useState("");
const [toastDescription, setToastDescription] = useState<string | undefined>(
undefined
);
const [toastVariant, setToastVariant] = useState<
"success" | "error" | "warning" | "info"
>("info");
const toastTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | 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);
};
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
if (__DEV__) {
console.log("CASHOUT PAGE MOUNTED");
}
});
return () => task.cancel();
}, []);
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 (copied from addcash.tsx)
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);
if (amountInCents < 1 || amountInCents > 99999) {
// 1 cent to $999.99
return false;
}
// Check if amount is within wallet balance (no processing fee for cashout)
const currentBalance = wallet?.balance || 0; // Balance in cents
return currentBalance >= amountInCents;
};
// Get validation error message
const getValidationError = () => {
// Validate basic amount using valibot
const amountValidationResult = validate(
amountSchema({ min: 1, max: 99999 }),
amount
);
if (!amountValidationResult.success) {
return amountValidationResult.error;
}
const amountInCents = parseDisplayToCents(amount);
if (amountInCents < 1) {
return t("cashout.validationMinAmount");
}
if (amountInCents > 99999) {
// $999.99
return t("cashout.validationMaxAmount");
}
// Check if amount exceeds wallet balance (no processing fee for cashout)
const currentBalance = wallet?.balance || 0; // Balance in cents
if (currentBalance < amountInCents) {
const balanceInDollars = currentBalance / 100;
const requiredInDollars = amountInCents / 100;
return t("cashout.validationInsufficientBalance", {
required: requiredInDollars.toFixed(2),
available: balanceInDollars.toFixed(2),
});
}
return null;
};
// Handle PIN confirmation success
const handlePinSuccess = () => {
setShowPinModal(false);
setIsSecurityVerified(true);
};
// Handle cash out action (called after PIN is verified)
const handleCashOut = async () => {
const validationError = getValidationError();
if (validationError) {
showToast(t("cashout.validationErrorTitle"), validationError, "error");
return;
}
const amountInCents = parseDisplayToCents(amount);
console.log("Cashing out:", amountInCents, "cents");
router.push({
pathname: ROUTES.SEND_BANK,
params: {
amount: amountInCents.toString(), // Pass cents as string
recipientName: user?.displayName || "Self",
recipientPhoneNumber: user?.phoneNumber || "",
recipientType: "saved",
recipientId: user?.uid || "",
note: "Cash out to bank account",
transactionType: "cashout",
},
});
};
return (
<ScreenWrapper edges={[]}>
{!isSecurityVerified && !showPinModal ? (
<View className="flex-1 justify-center items-center bg-white">
<Text className="text-gray-600 font-dmsans text-lg mb-4">
{t("cashout.verifyingSecurity")}
</Text>
<FourDotLoader />
</View>
) : (
<View className="h-full">
<BackButton />
<View className="flex h-full w-full">
<View className="flex-1 justify-start items-center px-5 h-1/6 ">
<View className="h-12" />
{/* Wallet Balance Display */}
<View className="flex flex-row items-center space-y-1">
<View className="h-12" />
<Text className="text-lg font-dmsans-medium text-gray-600">
{t("cashout.availableBalanceLabel")}
</Text>
<View className="w-2" />
<Text className="text-lg font-dmsans-medium text-gray-800">
${wallet ? (wallet.balance / 100).toFixed(2) : "0.00"}
</Text>
</View>
</View>
<View className="h-2/6">
<Text className="text-8xl font-dmsans-bold text-center text-black pt-2">
${formatDisplayAmount(amount)}
</Text>
</View>
<View className="h-3/6 flex justify-around">
<View className="px-8">
<PhonePinKeypad
onKeyPress={handleNumberPress}
showDecimal={true}
/>
</View>
<View className="px-5">
<Button
className="bg-primary rounded-3xl mb-5"
onPress={handleCashOut}
disabled={!isValidAmount()}
>
<Text className="font-dmsans text-white">
{isValidAmount()
? t("cashout.buttonWithAmount", {
amount: formatDisplayAmount(amount),
})
: t("cashout.button")}
</Text>
</Button>
</View>
<View className="h-12" />
</View>
</View>
</View>
)}
{/* PIN Confirmation Modal */}
<PinConfirmationModal
visible={showPinModal}
onClose={() => setShowPinModal(false)}
onSuccess={handlePinSuccess}
title={t("cashout.pinModalTitle")}
/>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}