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

425 lines
12 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { ScrollView, View, TouchableOpacity, Keyboard } from "react-native";
import { LucideEye, EyeOff, Trash2, CreditCard } from "lucide-react-native";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Text } from "~/components/ui/text";
import { router, useLocalSearchParams } from "expo-router";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { useUserWallet } from "~/lib/hooks/useUserWallet";
import { CreditCard as CreditCardType } from "~/lib/services/walletService";
import { useTabStore } from "~/lib/stores";
import { addCardSchema, validate } from "~/lib/utils/validationSchemas";
import BackButton from "~/components/ui/backButton";
import { ROUTES } from "~/lib/routes";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
// Credit Card Component
const CreditCardComponent = ({
card,
onRemove,
}: {
card: CreditCardType;
onRemove: () => void;
}) => {
const getCardColor = (cardType: string) => {
switch (cardType?.toLowerCase()) {
case "visa":
return "bg-blue-700";
case "mastercard":
return "bg-red-600";
case "american express":
return "bg-green-700";
case "discover":
return "bg-orange-600";
default:
return "bg-gray-700";
}
};
const getCardIcon = (cardType: string) => {
switch (cardType?.toLowerCase()) {
case "visa":
return "VISA";
case "mastercard":
return "MC";
case "american express":
return "AMEX";
case "discover":
return "DISC";
default:
return "CARD";
}
};
return (
<View
className={`w-80 h-48 rounded-xl ${getCardColor(
card.cardType || ""
)} p-6 relative shadow-lg`}
style={{ overflow: "visible" }}
>
{/* Card Brand and Remove Button */}
<View className="flex-row justify-between items-start mb-4">
<View className="bg-white/20 px-3 py-1 rounded-md">
<Text className="text-white font-dmsans-bold text-sm">
{getCardIcon(card.cardType || "")}
</Text>
</View>
<TouchableOpacity
onPress={() => {
console.log("Remove button pressed for card:", card.id);
onRemove();
}}
className="bg-gray-500/80 p-3 rounded-full min-w-10 min-h-10 flex items-center justify-center"
activeOpacity={0.7}
style={{ zIndex: 10 }}
>
<Trash2 color="#FFFFFF" size={18} />
</TouchableOpacity>
</View>
{/* Card Number */}
<View className="mb-6">
<Text className="text-white font-dmsans-mono text-lg tracking-wider">
{card.cardNumber}
</Text>
</View>
{/* Card Details */}
<View className="flex-row justify-between items-end">
<View>
<Text className="text-white/70 font-dmsans text-xs uppercase tracking-wide mb-1">
Valid Thru
</Text>
<Text className="text-white font-dmsans-medium text-sm">
{card.expiryDate}
</Text>
</View>
<View>
<Text className="text-white/70 font-dmsans text-xs uppercase tracking-wide mb-1">
Card Type
</Text>
<Text className="text-white font-dmsans-medium text-sm capitalize">
{card.cardType || "Unknown"}
</Text>
</View>
</View>
{/* Decorative Elements */}
<View className="absolute top-4 right-4 opacity-20">
<CreditCard color="#FFFFFF" size={32} />
</View>
<View className="absolute bottom-4 right-4 opacity-10">
<View className="w-12 h-8 bg-white/30 rounded-sm" />
</View>
</View>
);
};
export default function AddCard() {
const { user } = useAuthWithProfile();
const { addCreditCard, loading, error } = useUserWallet(user);
const { setLastVisitedTab } = useTabStore();
const { from } = useLocalSearchParams<{ from?: string }>();
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);
};
// Set the tab state when component mounts based on where user came from
useEffect(() => {
if (from === "addcash") {
setLastVisitedTab("/(tabs)/cardmang");
} else {
setLastVisitedTab("/"); // Default to home if no specific from parameter
}
}, [setLastVisitedTab, from]);
useEffect(() => {
return () => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
// Form state
const [cardNumber, setCardNumber] = useState("");
const [expiryDate, setExpiryDate] = useState("");
const [cvv, setCvv] = useState("");
const [showCvv, setShowCvv] = useState(false);
// Format card number as user types (add spaces every 4 digits)
const formatCardNumber = (value: string) => {
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
const matches = v.match(/\d{4,16}/g);
const match = (matches && matches[0]) || "";
const parts = [];
for (let i = 0, len = match.length; i < len; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
return parts.join(" ");
} else {
return v;
}
};
// Format expiry date as MM/YY
const formatExpiryDate = (value: string) => {
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
if (v.length >= 2) {
return `${v.substring(0, 2)}/${v.substring(2, 4)}`;
}
return v;
};
const handleCardNumberChange = (value: string) => {
const formatted = formatCardNumber(value);
if (formatted.length <= 19) {
// 16 digits + 3 spaces
setCardNumber(formatted);
}
};
const handleExpiryDateChange = (value: string) => {
const formatted = formatExpiryDate(value);
if (formatted.length <= 5) {
// MM/YY
setExpiryDate(formatted);
}
};
const handleCvvChange = (value: string) => {
const v = value.replace(/[^0-9]/gi, "");
if (v.length <= 4) {
setCvv(v);
}
};
const handleAddCard = async () => {
Keyboard.dismiss();
// Basic field validation
if (!cardNumber.trim()) {
showToast(
t("addcard.validationErrorTitle"),
t("addcard.validationCardNumberRequired"),
"error"
);
return;
}
if (!expiryDate.trim()) {
showToast(
t("addcard.validationErrorTitle"),
t("addcard.validationExpiryRequired"),
"error"
);
return;
}
if (!cvv.trim()) {
showToast(
t("addcard.validationErrorTitle"),
t("addcard.validationCvvRequired"),
"error"
);
return;
}
// Validate form using valibot
const validationResult = validate(addCardSchema, {
cardNumber,
expiryDate,
cvv,
});
if (!validationResult.success) {
showToast(
t("addcard.validationErrorTitle"),
validationResult.error || t("addcard.validationInvalidCard"),
"error"
);
return;
}
try {
await addCreditCard({
cardNumber: validationResult.data.cardNumber.replace(/\s/g, ""), // Remove spaces
expiryDate: validationResult.data.expiryDate,
cvv: validationResult.data.cvv,
});
router.replace(ROUTES.CARD_ADDED);
} catch (err) {
showToast(
t("addcard.toastErrorTitle"),
t("addcard.toastAddFailed"),
"error"
);
}
};
// Show errors
useEffect(() => {
if (error) {
showToast(t("addcard.toastErrorTitle"), error, "error");
}
}, [error]);
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 w-full justify-between">
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 24 }}
>
<BackButton />
<View className="flex flex-col space-y-1 py-5 pt-8 items-center">
<Text className="text-2xl font-semibold font-dmsans text-black">
{t("addcard.title")}
</Text>
</View>
<View className="h-12" />
<View className="px-5">
<View>
<Text className="text-primary font-dmsans-medium">
{t("addcard.sectionCardTitle")}
</Text>
</View>
<View className="h-4" />
<View>
<Text className="font-dmsans-medium">
{t("addcard.sectionCardSubtitle")}
</Text>
</View>
<View className="h-12" />
<Label className="text-base font-dmsans-medium">
{t("addcard.cardNumberLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("addcard.cardNumberPlaceholder")}
value={cardNumber}
onChangeText={handleCardNumberChange}
keyboardType="numeric"
maxLength={19}
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
/>
<View className="h-4" />
<Label className="text-base font-dmsans-medium">
{t("addcard.expiryDateLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("addcard.expiryDatePlaceholder")}
value={expiryDate}
onChangeText={handleExpiryDateChange}
keyboardType="numeric"
maxLength={5}
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
/>
<View className="h-4" />
<Label className="text-base font-dmsans-medium">
{t("addcard.cvvLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("addcard.cvvPlaceholder")}
value={cvv}
onChangeText={handleCvvChange}
keyboardType="numeric"
secureTextEntry={!showCvv}
maxLength={4}
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
rightIcon={
<TouchableOpacity onPress={() => setShowCvv(!showCvv)}>
{showCvv ? (
<EyeOff color="#4B5563" size={20} />
) : (
<LucideEye color="#4B5563" size={20} />
)}
</TouchableOpacity>
}
/>
<View className="h-4" />
</View>
</ScrollView>
<View className="w-full px-5 pb-8">
<Button
className="bg-primary rounded-3xl"
onPress={handleAddCard}
disabled={
loading || !cardNumber.trim() || !expiryDate.trim() || !cvv.trim()
}
>
<Text className="font-dmsans text-white">
{loading ? t("addcard.addButtonLoading") : t("addcard.addButton")}
</Text>
</Button>
</View>
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}