303 lines
9.0 KiB
TypeScript
303 lines
9.0 KiB
TypeScript
import React from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
} from "react-native";
|
|
import { Button } from "~/components/ui/button";
|
|
import { LucideCreditCard, LucidePlus, LucideTrash } from "lucide-react-native";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import { router } from "expo-router";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import { useUserWallet } from "~/lib/hooks/useUserWallet";
|
|
import { CreditCard } from "~/lib/services/walletService";
|
|
import TopBar from "~/components/ui/topBar";
|
|
import {
|
|
ApplePayIcon,
|
|
CreditDebitCardIcon,
|
|
GooglePayIcon,
|
|
} from "~/components/ui/icons";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { Input } from "~/components/ui/input";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import { useTranslation } from "react-i18next";
|
|
import PermissionAlertModal from "~/components/ui/permissionAlertModal";
|
|
|
|
// Individual Card Component
|
|
const CardItem = ({
|
|
card,
|
|
onRemove,
|
|
}: {
|
|
card: CreditCard;
|
|
onRemove: (card: CreditCard) => void;
|
|
}) => {
|
|
const getCardColor = (cardType: string) => {
|
|
switch (cardType?.toLowerCase()) {
|
|
case "visa":
|
|
return "bg-blue-50";
|
|
case "mastercard":
|
|
return "bg-green-50";
|
|
case "american express":
|
|
return "bg-green-50";
|
|
case "discover":
|
|
return "bg-orange-50";
|
|
default:
|
|
return "bg-gray-50";
|
|
}
|
|
};
|
|
|
|
const handleRemovePress = (card: CreditCard) => {
|
|
setSelectedCard(card);
|
|
setRemoveModalVisible(true);
|
|
};
|
|
|
|
const handleRemove = () => {
|
|
onRemove(card);
|
|
};
|
|
|
|
return (
|
|
<View
|
|
className={`flex flex-row justify-between w-full items-center py-4 ${getCardColor(
|
|
card.cardType || ""
|
|
)} rounded-md px-3`}
|
|
>
|
|
<View className="flex flex-row space-x-3 items-center flex-1">
|
|
<View className="bg-[#FFB668]/15 p-5 h-15 items-center justify-center flex rounded">
|
|
<CreditDebitCardIcon width={30} height={30} />
|
|
</View>
|
|
<View className="w-4" />
|
|
<View className="flex space-y-1 flex-1">
|
|
<Text className="font-dmsans text-primary">
|
|
{card.cardType || "Card"}
|
|
</Text>
|
|
<Text className="font-dmsans-medium text-secondary text-sm">
|
|
{card.cardNumber}
|
|
</Text>
|
|
<Text className="font-dmsans-medium text-gray-500 text-xs">
|
|
Expires {card.expiryDate}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className="flex flex-row space-x-2 items-center">
|
|
<TouchableOpacity onPress={handleRemove} className="p-2 rounded-full">
|
|
<LucideTrash color="#FFB84D" className="text-red-600" size={20} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default function ListCard() {
|
|
const { user } = useAuthWithProfile();
|
|
const { wallet, loading, error, removeCreditCard } = useUserWallet(user);
|
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
const { t } = useTranslation();
|
|
|
|
const [toastVisible, setToastVisible] = React.useState(false);
|
|
const [toastTitle, setToastTitle] = React.useState("");
|
|
const [toastDescription, setToastDescription] = React.useState<
|
|
string | undefined
|
|
>(undefined);
|
|
const [toastVariant, setToastVariant] = React.useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(
|
|
null
|
|
);
|
|
|
|
const [removeModalVisible, setRemoveModalVisible] = React.useState(false);
|
|
const [selectedCard, setSelectedCard] = React.useState<CreditCard | 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);
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
return () => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleRemoveCard = async (cardId: string) => {
|
|
try {
|
|
await removeCreditCard(cardId);
|
|
showToast(
|
|
t("cards.toastRemoveSuccessTitle"),
|
|
t("cards.toastRemoveSuccess"),
|
|
"success"
|
|
);
|
|
} catch (error) {
|
|
showToast(
|
|
t("cards.toastRemoveErrorTitle"),
|
|
t("cards.toastRemoveError"),
|
|
"error"
|
|
);
|
|
}
|
|
};
|
|
|
|
const renderCards = () => {
|
|
if (loading) {
|
|
return (
|
|
<View className="flex items-center justify-center">
|
|
<Text className="text-gray-500 font-dmsans">
|
|
{t("cardmang.loading")}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View className="flex items-center justify-center py-10">
|
|
<Text className="text-red-500 font-dmsans">
|
|
{t("cardmang.errorTitle")}
|
|
</Text>
|
|
<Text className="text-gray-400 font-dmsans-medium text-sm mt-1">
|
|
{error}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!wallet?.cards || wallet.cards.length === 0) {
|
|
return (
|
|
<View className="flex items-center justify-center py-10">
|
|
<LucideCreditCard color="#D1D5DB" size={48} />
|
|
<Text className="text-gray-500 font-dmsans mt-4">
|
|
{t("cardmang.emptyTitle")}
|
|
</Text>
|
|
<Text className="text-gray-400 font-dmsans-medium text-sm mt-1 text-center px-4">
|
|
{t("cardmang.emptySubtitle")}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<FlatList
|
|
data={wallet.cards}
|
|
keyExtractor={(item, index) => item.id + String(index)}
|
|
scrollEnabled={false}
|
|
ItemSeparatorComponent={() => <View className="h-2" />}
|
|
renderItem={({ item }) => (
|
|
<CardItem card={item} onRemove={handleRemovePress} />
|
|
)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={["bottom"]}>
|
|
<View className="flex items-center h-full w-full">
|
|
<ScrollView
|
|
contentContainerStyle={{ paddingBottom: 96 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<TopBar />
|
|
<View className="flex flex-col px-5 space-y-1 py-5 items-left">
|
|
<Text className="text-xl font-dmsans text-primary">
|
|
{t("cardmang.title")}
|
|
</Text>
|
|
<View className="h-2" />
|
|
<Text className="text-base font-dmsans text-gray-400">
|
|
{t("cardmang.subtitle")}
|
|
</Text>
|
|
</View>
|
|
<View className="px-5">
|
|
<Input
|
|
value={searchQuery}
|
|
onChangeText={setSearchQuery}
|
|
placeholderText={t("cardmang.searchPlaceholder")}
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#D9DBE9] bg-white"
|
|
placeholderColor="#7E7E7E"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
</View>
|
|
<View className="flex flex-col items-left px-5 py-5 w-full">
|
|
{/* Add Card Button */}
|
|
<View className="flex flex-row items-center w-full">
|
|
<Button
|
|
className="flex flex-row items-center space-x-2 bg-primary rounded-md p-3 w-full"
|
|
onPress={() => router.push(ROUTES.ADD_CARD)}
|
|
>
|
|
<LucidePlus color="#FFB84D" className="w-[14px] h-[14px]" />
|
|
<View className="w-2" />
|
|
<Text className="text-white text-sm font-dmsans-regular">
|
|
{t("cardmang.addCardButton")}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
|
|
<View className="h-2" />
|
|
|
|
{/* Cards List */}
|
|
<View className="flex flex-col w-full mt-5">
|
|
<Text className="text-lg font-dmsans-medium text-primary mb-4">
|
|
{t("cardmang.paymentOptionsTitle")}
|
|
</Text>
|
|
{renderCards()}
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
|
|
<PermissionAlertModal
|
|
visible={removeModalVisible}
|
|
title={t("cardmang.removeTitle") || "Remove Card"}
|
|
message={
|
|
selectedCard
|
|
? t(
|
|
"cardmang.removeMessage",
|
|
"Are you sure you want to remove this card?"
|
|
)
|
|
: "Are you sure you want to remove this card?"
|
|
}
|
|
primaryText={t("cardmang.removeConfirm", "Remove")}
|
|
secondaryText={t("cardmang.removeCancel", "Cancel")}
|
|
primaryVariant="danger"
|
|
onPrimary={async () => {
|
|
if (selectedCard) {
|
|
await handleRemoveCard(selectedCard.id);
|
|
}
|
|
setRemoveModalVisible(false);
|
|
setSelectedCard(null);
|
|
}}
|
|
onSecondary={() => {
|
|
setRemoveModalVisible(false);
|
|
setSelectedCard(null);
|
|
}}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|