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

522 lines
16 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import {
Text,
View,
ScrollView,
TextInput,
ActivityIndicator,
} from "react-native";
import { Button } from "~/components/ui/button";
import { useLocalSearchParams, router } from "expo-router";
import BackButton from "~/components/ui/backButton";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { TransactionService } from "~/lib/services/transactionService";
import {
calculateTotalAmountForSending,
calculateProcessingFee,
} from "~/lib/utils/feeUtils";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import { ROUTES } from "~/lib/routes";
import { applyReferral } from "~/lib/services/referralService";
import { awardPoints } from "~/lib/services/pointsService";
export default function TransConfirm() {
const { t } = useTranslation();
const params = useLocalSearchParams<{
transactionId?: string;
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;
fromSelectRecipientFlow?: string;
}>();
// Parse amount from cents to dollars
const amountInCents = parseInt(params.amount || "0");
const amountInDollars = amountInCents / 100;
// Donation amount (entered on donation screen)
const hasDonation =
params.donationSkipped === "false" && !!params.donationAmount;
const donationAmountNumber = params.donationAmount
? Number(params.donationAmount)
: NaN;
const donationAmountDisplay = isNaN(donationAmountNumber)
? null
: donationAmountNumber.toFixed(2);
const donationAmountDollars =
!isNaN(donationAmountNumber) && hasDonation ? donationAmountNumber : 0;
// Calculate processing fee (1.25% of the amount)
const processingFeeInCents = Math.round(amountInCents * 0.0125);
const processingFeeInDollars = processingFeeInCents / 100;
// Subtotal = main amount + donation
const subtotalInDollars = amountInDollars + donationAmountDollars;
// Total = subtotal + processing fee
const totalInDollars = subtotalInDollars + processingFeeInDollars;
const donationTypeLabel =
params.donationType === "monthly" ? "Monthly" : "One-Time";
const transactionDate = new Date();
const formattedDate = transactionDate.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
const { user, wallet, refreshWallet } = useAuthWithProfile();
const [isSending, setIsSending] = useState(false);
const [referralCode, setReferralCode] = useState("");
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 = 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(() => {
return () => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
const handleConfirm = async () => {
const isAddCashFlow = params.type === "add_cash";
if (isAddCashFlow) {
// If a referral code is provided in the add-cash flow, apply it here
if (referralCode.trim() && user?.uid) {
try {
setIsSending(true);
const referralResult = await applyReferral({
referralCode: referralCode.trim(),
uid: user.uid,
referralReason: "transaction",
contextId: params.transactionId || "add_cash",
});
console.log(
"[TransConfirm] Add-cash referral apply result",
referralResult
);
if (!referralResult.success) {
showToast(
t("transconfirm.toastErrorTitle"),
referralResult.error || "Failed to apply referral code",
"error"
);
}
} catch (referralError) {
console.error(
"[TransConfirm] Error while applying add-cash referral code",
referralError
);
showToast(
t("transconfirm.toastErrorTitle"),
"Failed to apply referral code",
"error"
);
} finally {
setIsSending(false);
}
}
router.push({
pathname: ROUTES.CHECKOUT,
params: {
...params,
},
});
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;
}
// Check if user has sufficient balance (including processing fee)
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;
}
const isFromSelectRecipientFlow = params.fromSelectRecipientFlow === "true";
if (isFromSelectRecipientFlow) {
router.push({
pathname: "checkout",
params: {
...params,
},
});
return;
}
setIsSending(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) {
try {
await awardPoints("send_money");
} catch (error) {
console.warn(
"[TransConfirm] Failed to award send money points",
error
);
}
await refreshWallet();
if (referralCode.trim() && result.transactionId) {
try {
const referralResult = await applyReferral({
referralCode: referralCode.trim(),
uid: user.uid,
referralReason: "transaction",
contextId: result.transactionId,
});
console.log("[TransConfirm] Referral apply result", referralResult);
if (!referralResult.success) {
console.warn(
"[TransConfirm] Failed to apply referral:",
referralResult.error
);
showToast(
t("transconfirm.toastErrorTitle"),
referralResult.error || "Failed to apply referral code",
"error"
);
}
} catch (referralError) {
console.error(
"[TransConfirm] Error while applying referral code",
referralError
);
showToast(
t("transconfirm.toastErrorTitle"),
"Failed to apply referral code",
"error"
);
}
}
// Navigate to success screen with message
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 {
setIsSending(false);
}
};
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 w-full">
<ScrollView
className="w-full flex-1"
contentContainerStyle={{ flexGrow: 1, paddingBottom: 24 }}
>
<View className="flex-1 w-full">
<View className="pt-6">
<BackButton />
</View>
{/* Centered content: title + card */}
<View className="flex-1 justify-center">
<View
className="flex flex-col pb-5 items-center"
style={{ gap: 4 }}
>
<Text className="text-xl font-dmsans-bold text-black">
{t("transconfirm.title")}
</Text>
</View>
<View className="bg-secondary-foreground mx-6 rounded py-10 mt-2">
<View className="flex items-center py-5" style={{ gap: 8 }}>
<Text className="text-2xl font-dmsans-bold text-primary">
${amountInDollars.toFixed(2)}
</Text>
<Text
className="text-sm text-gray-500 font-dmsans"
style={{ textAlign: "center" }}
>
{params.type === "add_cash"
? "You are planning to add money to your wallet."
: t("transconfirm.planningDescription", {
recipientName: params.recipientName,
})}
</Text>
</View>
<View
className="flex px-2 py-5 border-t border-b border-gray-300 m-3"
style={{ gap: 8 }}
>
<Text className="text-sm font-dmsans-bold mt-5">
{t("transconfirm.sectionTitle")}
</Text>
{params.note && (
<View className="flex flex-row justify-between w-full">
<Text className="font-dmsans">
{t("transconfirm.noteLabel")}
</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
{params.note}
</Text>
</View>
)}
{hasDonation && donationAmountDisplay && (
<>
<View className="flex flex-row justify-between w-full">
<Text className="font-dmsans">Donation</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
${donationAmountDisplay}
</Text>
</View>
<View className="flex flex-row justify-between w-full">
<Text className="font-dmsans">Donation Type</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
{donationTypeLabel}
</Text>
</View>
</>
)}
<View className="flex flex-row justify-between w-full mt-3">
<Text className="font-dmsans">Date</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
{formattedDate}
</Text>
</View>
<View className="flex flex-row justify-between w-full">
<Text className="font-dmsans">
{t("transconfirm.processingFeeLabel")}
</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
${processingFeeInDollars.toFixed(2)}
</Text>
</View>
<View className="flex flex-row justify-between w-full">
<Text className="font-dmsans">
{t("transconfirm.subtotalLabel")}
</Text>
<Text
className="font-dmsans"
style={{ textAlign: "right" }}
>
${subtotalInDollars.toFixed(2)}
</Text>
</View>
<View className="flex flex-row justify-between w-full border-t border-gray-300 pt-5">
<Text className="font-dmsans-bold text-base">
{t("transconfirm.totalLabel")}
</Text>
<Text
className="font-dmsans text-base"
style={{ textAlign: "right" }}
>
${totalInDollars.toFixed(2)}
</Text>
</View>
</View>
</View>
<View className="px-6 mt-6">
<Text className="text-base font-dmsans text-black mb-2">
{t("transconfirm.referralCodeLabel", "Referral Code")}
</Text>
<TextInput
value={referralCode}
onChangeText={setReferralCode}
placeholder="678"
placeholderTextColor="#9CA3AF"
className="w-full rounded-2xl px-4 py-3 bg-[#FFF5E9] border border-[#D9E2D5] font-dmsans text-base text-black"
/>
</View>
</View>
</View>
</ScrollView>
<View className="w-full px-5 pb-8 pt-4" style={{ gap: 12 }}>
<Button
className="bg-primary rounded-full"
onPress={handleConfirm}
disabled={isSending}
>
{isSending ? (
<View
className="flex-row items-center justify-center"
style={{ gap: 8 }}
>
<ActivityIndicator size="small" color="#FFFFFF" />
<Text className="font-dmsans text-white">
{t("transconfirm.buttonProcessing")}
</Text>
</View>
) : (
<Text className="font-dmsans text-white">
{t("transconfirm.buttonConfirm")}
</Text>
)}
</Button>
</View>
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}