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

314 lines
8.6 KiB
TypeScript

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<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);
};
// 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 (
<ScreenWrapper edges={[]}>
{!isSecurityVerified ? (
<View className="flex-1 justify-center items-center bg-white">
<ActivityIndicator size="large" color="hsl(147,55%,28%)" />
<View className="h-4" />
<Text className="text-gray-600 font-dmsans text-lg">
{t("addcash.verifyingSecurity")}
</Text>
</View>
) : (
<>
<BackButton />
<View className="flex h-full">
<View className="flex-1 justify-start items-center px-5 h-1/6 ">
<View className="h-12" />
<Text className="text-3xl font-dmsans text-primary">
{t("addcash.title")}
</Text>
</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={handleAddCash}
disabled={!isValidAmount()}
>
<Text className="font-dmsans text-white">
{isValidAmount()
? t("addcash.addButtonWithAmount", {
amount: formatDisplayAmount(amount),
})
: t("addcash.addButton")}
</Text>
</Button>
</View>
<View className="h-12" />
</View>
</View>
</>
)}
<PinConfirmationModal
visible={showPinModal}
onClose={() => setShowPinModal(false)}
onSuccess={handlePinSuccess}
title={t("addcash.pinModalTitle")}
/>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}