499 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|