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

499 lines
17 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { View, Text, ScrollView, Pressable } from "react-native";
import Svg, { G, Path } from "react-native-svg";
import { Button } from "~/components/ui/button";
import { LucideChevronRightCircle } from "lucide-react-native";
import { useLocalSearchParams, router } from "expo-router";
import { TransactionService } from "~/lib/services/transactionService";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { useUserWallet } from "~/lib/hooks/useUserWallet";
import BackButton from "~/components/ui/backButton";
import { AwashIcon, TeleBirrIcon } from "~/components/ui/icons";
import {
calculateTotalAmountForSending,
calculateProcessingFee,
} from "~/lib/utils/feeUtils";
import { showAlert } from "~/lib/utils/alertUtils";
import { ROUTES } from "~/lib/routes";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import BottomSheet from "~/components/ui/bottomSheet";
import { useTranslation } from "react-i18next";
export default function SendBank() {
const { t } = useTranslation();
const params = useLocalSearchParams<{
amount: string;
recipientName: string;
recipientPhoneNumber: string;
recipientType: "saved" | "contact";
recipientId: string;
note: string;
transactionType?: "send" | "cashout";
}>();
const { user } = useAuthWithProfile();
const { wallet, refreshWallet } = useUserWallet(user);
const [selectedBank, setSelectedBank] = useState<"awash" | "telebirr" | null>(
null
);
const [isAccountSheetVisible, setIsAccountSheetVisible] = useState(false);
const [selectedAccountId, setSelectedAccountId] = useState<string | null>(
null
);
const [isProcessing, setIsProcessing] = useState(false);
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 handleBankSelection = (bankProvider: "awash" | "telebirr") => {
setSelectedBank(bankProvider);
if (bankProvider !== "awash") {
setSelectedAccountId(null);
setIsAccountSheetVisible(false);
}
};
const handleSendOrCashOutTransaction = async () => {
if (!selectedBank) {
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastNoMethod"),
"error"
);
return;
}
if (!params.amount || !user?.uid) {
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastMissingInfo"),
"error"
);
return;
}
if (selectedBank === "awash" && wallet?.cards && wallet.cards.length > 0) {
if (!selectedAccountId) {
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastNoAccount"),
"error"
);
return;
}
}
const amountInCents = parseInt(params.amount); // Amount is already in cents
if (isNaN(amountInCents) || amountInCents <= 0) {
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastInvalidAmount"),
"error"
);
return;
}
// Check if user has sufficient balance
if (params.transactionType === "cashout") {
// For cashout, no processing fee
if (!wallet || wallet.balance < amountInCents) {
const required = (amountInCents / 100).toFixed(2);
const available = ((wallet?.balance ?? 0) / 100).toFixed(2);
showToast(
t("sendbank.toastInsufficientBalanceTitle"),
t("sendbank.toastInsufficientBalanceCashoutDescription", {
required,
available,
}),
"error"
);
return;
}
} else {
// For send money, include processing fee
const totalRequired = calculateTotalAmountForSending(amountInCents);
if (!wallet || wallet.balance < totalRequired) {
const processingFee = calculateProcessingFee(amountInCents);
const required = (totalRequired / 100).toFixed(2);
const fee = (processingFee / 100).toFixed(2);
const available = ((wallet?.balance ?? 0) / 100).toFixed(2);
showToast(
t("sendbank.toastInsufficientBalanceTitle"),
t("sendbank.toastInsufficientBalanceSendDescription", {
required,
fee,
available,
}),
"error"
);
return;
}
}
setIsProcessing(true);
try {
let result;
let successMessage;
console.log("PARAMS TRANSACTION TYPE", params.transactionType);
if (params.transactionType === "cashout") {
// Handle cash out transaction
result = await TransactionService.cashOut(user.uid, {
amount: amountInCents, // Amount is already in cents
bankProvider: selectedBank,
note: params.note || "Cash out to bank account",
});
successMessage = `Successfully cashed out $${(
amountInCents / 100
).toFixed(2)} to your ${selectedBank} account`;
} else {
// Handle send money transaction
if (!params.recipientName) {
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastMissingRecipient"),
"error"
);
setIsProcessing(false);
return;
}
result = await TransactionService.sendMoney(user.uid, {
amount: amountInCents, // Amount is already in cents
recipientName: params.recipientName,
recipientPhoneNumber: params.recipientPhoneNumber,
recipientType: params.recipientType,
recipientId: params.recipientId,
note: params.note || "",
});
successMessage = `Successfully transferred $${(
amountInCents / 100
).toFixed(2)} to ${params.recipientName}`;
}
if (result.success) {
await refreshWallet();
if (params.transactionType === "cashout") {
router.replace({
pathname: ROUTES.CASHOUT_COMPLETION,
params: {
note: successMessage,
amount: (amountInCents / 100).toFixed(2),
},
});
} else {
router.replace({
pathname: ROUTES.TASK_COMPLETION,
params: {
message: successMessage,
amount: (amountInCents / 100).toFixed(2),
recipientName: params.recipientName,
bankProvider: selectedBank,
},
});
}
} else {
showToast(
t("sendbank.toastErrorTitle"),
result.error || t("sendbank.toastProcessFailed"),
"error"
);
}
} catch (error) {
console.error("Error processing transaction:", error);
showToast(
t("sendbank.toastErrorTitle"),
t("sendbank.toastProcessFailedWithRetry"),
"error"
);
} finally {
setIsProcessing(false);
}
};
const handleClose = () => {
router.back();
};
return (
<ScreenWrapper edges={[]}>
<BackButton />
<View>
{params.amount && (
<View className="px-5 pt-10 ">
<View className="">
<Text className="text-lg font-dmsans-bold text-primary text-center">
${(parseInt(params.amount) / 100).toFixed(2)}{" "}
{params.transactionType === "cashout"
? t("sendbank.amountTitleCashOut")
: t("sendbank.amountTitleToRecipient", {
recipientName: params.recipientName,
})}
</Text>
</View>
<View>
{params.note && (
<Text className="text-sm font-dmsans-medium text-gray-500 text-center">
{t("sendbank.noteWithText", { note: params.note })}
</Text>
)}
<BottomSheet
visible={isAccountSheetVisible}
onClose={() => setIsAccountSheetVisible(false)}
>
<View className="mb-4">
<Text className="text-xl font-dmsans-bold text-primary text-center">
{t("sendbank.chooseAccountTitle")}
</Text>
</View>
{wallet?.cards && wallet.cards.length > 0 ? (
<>
{wallet.cards.map((card) => (
<Pressable
key={card.id}
className={`flex flex-row items-center justify-between mb-3 rounded-2xl border px-4 py-3 ${
selectedAccountId === card.id
? "border-secondary bg-orange-50"
: "border-gray-200 bg-white"
}`}
onPress={() => setSelectedAccountId(card.id)}
>
<View className="flex-row items-center">
<View className="w-10 h-10 mr-3 rounded-full bg-primary/10 items-center justify-center">
<AwashIcon width={28} height={16} />
</View>
<View>
<Text className="text-xs text-gray-500 font-dmsans-medium">
{t("sendbank.accountLabel")}
</Text>
<Text className="text-base text-gray-900 font-dmsans-bold">
{card.cardNumber}
</Text>
</View>
</View>
<View
className={`w-5 h-5 rounded-full border-2 ${
selectedAccountId === card.id
? "border-secondary bg-secondary"
: "border-gray-300"
}`}
/>
</Pressable>
))}
<View className="mt-2">
<Button
className="bg-primary rounded-3xl w-full"
disabled={!selectedAccountId}
onPress={() => setIsAccountSheetVisible(false)}
>
<Text className="text-white font-dmsans-medium text-base">
{t("sendbank.continueButton")}
</Text>
</Button>
</View>
</>
) : (
<Text className="text-center text-gray-500 font-dmsans">
{t("sendbank.noAccounts")}
</Text>
)}
</BottomSheet>
</View>
</View>
)}
</View>
<View className="h-20" />
<ScrollView className="flex px-5 space-y-3 w-full ">
<View className="flex flex-col space-y-1 py-5 items-left">
<Text className="text-xl font-dmsans text-primary">
{t("sendbank.paymentOptionsTitle")}
</Text>
<Text className="text-base font-dmsans text-gray-400">
{selectedBank
? t("sendbank.paymentOptionsSelected", {
providerName:
selectedBank === "awash"
? t("sendbank.awashName")
: t("sendbank.telebirrName"),
})
: t("sendbank.paymentOptionsUnselected")}
</Text>
</View>
<View className="flex flex-col items-left ">
<View className="flex flex-col space-y-3 mt-3">
{/* Telebirr Section */}
<Text className="text-base font-dmsans-medium text-gray-600 mb-1">
{t("sendbank.telebirrName")}
</Text>
<Pressable
onPress={() => handleBankSelection("telebirr")}
disabled={isProcessing}
>
<View
className={`flex flex-row w-full justify-between items-center py-4 rounded-md px-3 ${
selectedBank === "telebirr"
? "bg-orange-100 border-2 border-orange-300"
: "bg-green-50"
}`}
>
<View className="flex flex-row space-x-3 items-center flex-1">
<View className="p-2 rounded">
<TeleBirrIcon />
</View>
<View className="w-2" />
<View className="flex flex-col">
<Text className="font-dmsans text-primary">
{t("sendbank.telebirrName")}
</Text>
<Text className="font-dmsans-medium text-secondary text-sm">
{t("sendbank.telebirrSubtitle")}
</Text>
</View>
</View>
<View className="flex space-y-1">
<LucideChevronRightCircle
color={selectedBank === "telebirr" ? "#EA580C" : "#FFB84D"}
size={24}
/>
</View>
</View>
</Pressable>
<View className="h-5" />
{/* Bank Section with Awash */}
<Text className="text-base font-dmsans-medium text-gray-600 mb-1">
Bank
</Text>
<Pressable
onPress={() => {
handleBankSelection("awash");
setIsAccountSheetVisible(true);
}}
disabled={isProcessing}
>
<View
className={`flex flex-row w-full justify-between items-center py-4 rounded-md px-3 ${
selectedBank === "awash"
? "bg-blue-100 border-2 border-blue-300"
: "bg-green-50"
}`}
>
<View className="flex flex-row space-x-3 items-center flex-1">
<View className="p-2 rounded">
<AwashIcon />
</View>
<View className="flex flex-col">
<Text className="font-dmsans text-primary">
{t("sendbank.awashName")}
</Text>
<Text className="font-dmsans-medium text-secondary text-sm">
{t("sendbank.awashSubtitle")}
</Text>
</View>
</View>
<View className="flex space-y-1">
<LucideChevronRightCircle
color={selectedBank === "awash" ? "#2563EB" : "#FFB84D"}
size={24}
/>
</View>
</View>
</Pressable>
</View>
</View>
</ScrollView>
{/* Send Button */}
{selectedBank && !isProcessing && (
<View className="flex flex-col items-center justify-center py-6 px-5 w-full">
<Button
className="bg-primary rounded-md w-full py-4"
onPress={handleSendOrCashOutTransaction}
>
<Text className="text-white font-dmsans-medium text-lg">
{(() => {
const amountText = params.amount
? (parseInt(params.amount) / 100).toFixed(2)
: "0.00";
const providerName =
selectedBank === "awash"
? t("sendbank.awashName")
: t("sendbank.telebirrName");
return params.transactionType === "cashout"
? t("sendbank.sendButtonCashOut", {
amount: amountText,
providerName,
})
: t("sendbank.sendButtonSend", {
amount: amountText,
providerName,
});
})()}
</Text>
</Button>
</View>
)}
<View className="flex flex-col space-y-3 py-5 items-center">
<Text className="text-base left-2 font-dmsans-medium text-black pb-4">
{t("sendbank.poweredBy")}
</Text>
<AwashIcon width={200} height={25} />
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}