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

432 lines
14 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
View,
ScrollView,
InteractionManager,
ActivityIndicator,
} 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, useLocalSearchParams } from "expo-router";
import { ROUTES } from "~/lib/routes";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { useUserWallet } from "~/lib/hooks/useUserWallet";
import {
validateBalanceWithFee,
getFeeInformation,
calculateTotalAmountForSending,
} from "~/lib/utils/feeUtils";
import {
parseDisplayToCents,
formatDisplayAmount,
} from "~/lib/utils/monetaryUtils";
import SendMoneyBar from "~/components/ui/sendMoneyBar";
import { Big } from "big.js";
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";
export default function SendOrRequestMoney() {
const { user } = useAuthWithProfile();
const { wallet } = useUserWallet(user);
const [amount, setAmount] = useState("");
const [showPinModal, setShowPinModal] = useState(false);
const [pendingAction, setPendingAction] = useState<"send" | "request" | null>(
null
);
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("SEND OR REQUEST MONEY PAGE MOUNTED");
}
});
return () => task.cancel();
}, []);
// Get contact params from navigation
const params = useLocalSearchParams<{
selectedContactId?: string;
selectedContactName?: string;
selectedContactPhone?: string;
}>();
// 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("");
setPendingAction(null);
// 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 (including processing fee)
const currentBalance = wallet?.balance || 0; // Balance in cents
const totalRequired = calculateTotalAmountForSending(amountInCents);
return currentBalance >= totalRequired;
};
// 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("sendorrequestmoney.validationMinAmount");
}
if (amountInCents > 99999) {
// $999.99
return t("sendorrequestmoney.validationMaxAmount");
}
// Check if amount exceeds wallet balance (including processing fee)
const currentBalance = wallet?.balance || 0; // Balance in cents
const balanceValidation = validateBalanceWithFee(
currentBalance,
amountInCents
);
if (!balanceValidation.hasSufficientBalance) {
const balanceInDollars = currentBalance / 100;
const requiredInDollars = balanceValidation.requiredBalance / 100;
return t("sendorrequestmoney.validationInsufficientBalance", {
required: requiredInDollars.toFixed(2),
fee: balanceValidation.feeInfo.formatted.fee,
available: balanceInDollars.toFixed(2),
});
}
return null;
};
// Handle PIN confirmation success
const handlePinSuccess = () => {
setShowPinModal(false);
setIsSecurityVerified(true);
};
// Handle send money action (called after PIN is verified)
const handleSendMoney = async () => {
const validationError = getValidationError();
if (validationError) {
showToast(
t("sendorrequestmoney.validationErrorTitle"),
validationError,
"error"
);
return;
}
const amountInCents = parseDisplayToCents(amount);
console.log("Sending money:", amountInCents, "cents");
router.push({
pathname: ROUTES.SELECT_RECIPIENT,
params: {
amount: amountInCents.toString(), // Pass cents as string
selectedContactId: params.selectedContactId,
selectedContactName: params.selectedContactName,
selectedContactPhone: params.selectedContactPhone,
},
});
};
const handleRequestMoney = async () => {
const validationError = getValidationError();
if (validationError) {
showToast(
t("sendorrequestmoney.validationErrorTitle"),
validationError,
"error"
);
return;
}
const amountInCents = parseDisplayToCents(amount);
console.log("Requesting money:", amountInCents, "cents");
router.push({
pathname: ROUTES.SELECT_DONOR,
params: {
amount: amountInCents.toString(), // Pass cents as string
selectedContactId: params.selectedContactId,
selectedContactName: params.selectedContactName,
selectedContactPhone: params.selectedContactPhone,
},
});
};
return (
<ScreenWrapper edges={[]}>
<View className="flex-1">
{!isSecurityVerified && !showPinModal ? (
<View className="flex-1 justify-center items-center bg-background">
<ActivityIndicator size="large" color="#1F7A47" />
<View className="h-4" />
<Text className="text-gray-600 font-dmsans-medium text-lg">
{t("sendorrequestmoney.verifyingSecurity")}
</Text>
</View>
) : (
<View className="flex-1">
<SendMoneyBar />
<View className="flex-1 px-5 pb-4 pt-10 justify-between">
<View>
<View className="items-center mb-2">
<View className="flex flex-row items-center space-y-1">
<Text className="text-lg font-dmsans-medium text-gray-600">
{t("sendorrequestmoney.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>
{/* Big amount */}
<View className="items-center mb-4">
<Text className="text-8xl py-2 font-dmsans-bold text-center text-black ">
{formatDisplayAmount(amount)}
</Text>
</View>
{/* Fee Information Display */}
<View className="w-full mb-2">
{amount && parseDisplayToCents(amount) > 0 && (
<View className="px-4">
{(() => {
const amountInCents = parseDisplayToCents(amount);
const feeInfo = getFeeInformation(amountInCents);
return (
<View className="p-3">
<Text className="text-sm font-dmsans-medium text-gray-600 text-center">
{t("sendorrequestmoney.processingFee", {
fee: feeInfo.formatted.fee,
percent: "1.25",
})}
</Text>
<Text className="text-sm font-dmsans-bold text-gray-800 text-center mt-1">
{t("sendorrequestmoney.totalLabel", {
total: feeInfo.formatted.total,
})}
</Text>
</View>
);
})()}
</View>
)}
</View>
</View>
<View className="pb-16">
<View className="">
<PhonePinKeypad
onKeyPress={handleNumberPress}
showDecimal={true}
/>
</View>
<View className="items-center flex flex-row justify-between w-full mt-4 gap-2">
<View className="w-1/2">
<Button
className="bg-primary rounded-3xl"
disabled={!isValidAmount()}
onPress={handleRequestMoney}
>
<Text className="font-dmsans-medium text-white">
{isValidAmount()
? t("sendorrequestmoney.requestButtonWithAmount", {
amount: formatDisplayAmount(amount),
})
: t("sendorrequestmoney.requestButton")}
</Text>
</Button>
</View>
<View className="w-1/2 ">
<Button
className="bg-primary rounded-3xl"
onPress={handleSendMoney}
disabled={!isValidAmount()}
>
<Text className="font-dmsans-medium text-white">
{isValidAmount()
? t("sendorrequestmoney.payButtonWithAmount", {
amount: formatDisplayAmount(amount),
})
: t("sendorrequestmoney.payButton")}
</Text>
</Button>
</View>
</View>
</View>
</View>
</View>
)}
</View>
{/* PIN Confirmation Modal */}
<PinConfirmationModal
visible={showPinModal}
onClose={() => {
setShowPinModal(false);
}}
onSuccess={handlePinSuccess}
title={t("sendorrequestmoney.pinModalTitle")}
/>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}