canina/frontend/application/components/TopUpModal.tsx
parsaaghayi dc88677169
Some checks failed
Deploy Canina / deploy (push) Waiting to run
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 11s
fix(security): resolve all critical, high, and medium security vulnerabilities
2026-09-23 22:05:12 +03:30

199 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X, CreditCard, TrendingUp, ShieldCheck } from "lucide-react";
import { toPersian, cn } from "../lib/utils";
import { toast } from "sonner";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import api from "../lib/services/api";
interface TopUpModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: (amount: number) => void;
initialAmount?: number;
}
const PRESET_AMOUNTS = [
{ label: "۵۰۰,۰۰۰ تومان", value: 500000 },
{ label: "۱,۰۰۰,۰۰۰ تومان", value: 1000000 },
{ label: "۲,۰۰۰,۰۰۰ تومان", value: 2000000 },
];
export default function TopUpModal({ isOpen, onClose, initialAmount }: TopUpModalProps) {
const [amount, setAmount] = useState<string>(() => (initialAmount ? String(initialAmount) : ""));
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
if (initialAmount && initialAmount > 0) {
setAmount(String(initialAmount));
}
}, [initialAmount, isOpen]);
const isCardToCardEnabled = useSettingsStore((state) =>
state.getBoolean("payment_card_to_card_enabled", false),
);
const handlePresetSelect = (val: number) => {
setSelectedPreset(val);
setAmount(val.toString());
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value.replace(/,/g, "");
if (/^\d*$/.test(val)) {
setAmount(val);
setSelectedPreset(null);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!useUserStore.getState().isLoggedIn) {
toast.error("جهت افزایش موجودی کیف پول لطفاً ابتدا وارد حساب کاربری خود شوید.");
return;
}
const finalAmount = parseInt(amount);
if (isNaN(finalAmount) || finalAmount < 10000) {
toast.error("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است");
return;
}
setLoading(true);
try {
const res = await api.post("/payment/zibal/wallet/initiate", {
amount: finalAmount,
});
if (res.data?.paymentUrl) {
toast.success("در حال انتقال امن به درگاه پرداخت زیبال...");
window.location.href = res.data.paymentUrl;
} else {
toast.error("خطا در ایجاد تراکنش درگاه پرداخت زیبال");
setLoading(false);
}
} catch (err: unknown) {
const gErr = err as { response?: { data?: { message?: string } }; message?: string };
const errMsg = gErr.response?.data?.message || gErr.message || "خطا در برقراری ارتباط با درگاه پرداخت";
toast.error(errMsg);
setLoading(false);
}
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="bg-white w-full max-w-md rounded-[3rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right"
dir="rtl"
>
<div className="bg-medical-gray-50 px-8 py-6 border-b border-medical-gray-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white shadow-md shadow-canina-blue/20">
<TrendingUp className="w-5 h-5" />
</div>
<h3 className="text-xl font-black text-medical-gray-900 italic">شارژ آنلاین کیف پول</h3>
</div>
<button
onClick={onClose}
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
<div className="space-y-3">
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یکی از مبالغ پیشنهادی را انتخاب کنید</label>
<div className="flex flex-wrap gap-2">
{PRESET_AMOUNTS.map((preset) => (
<button
key={preset.value}
type="button"
onClick={() => handlePresetSelect(preset.value)}
className={cn(
"px-5 py-3 rounded-2xl text-sm font-black transition-all border cursor-pointer",
selectedPreset === preset.value
? "bg-canina-blue text-white border-canina-blue shadow-lg shadow-canina-blue/20"
: "bg-white text-medical-gray-700 border-medical-gray-100 hover:border-canina-blue"
)}
>
{preset.label}
</button>
))}
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یا مبلغ دلخواه خود را وارد کنید (تومان)</label>
<div className="relative">
<input
autoFocus
type="text"
value={amount ? parseInt(amount).toLocaleString() : ""}
onChange={handleInputChange}
className="w-full bg-medical-gray-50 border border-medical-gray-100 rounded-3xl py-5 px-8 outline-none font-black text-2xl text-center text-medical-gray-900 transition-all focus:ring-4 focus:ring-canina-blue/10 focus:border-canina-blue italic"
placeholder="۰"
/>
{amount && (
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xs font-bold text-medical-gray-400">تومان</div>
)}
</div>
</div>
{isCardToCardEnabled ? (
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-4 space-y-2 text-right">
<div className="flex items-center gap-2 text-canina-blue font-bold text-xs">
<CreditCard className="w-4 h-4" />
<span>اطلاعات کارت جهت واریز کارتبهکارت:</span>
</div>
<div className="text-[11px] text-gray-700 space-y-1 font-mono dir-ltr bg-white p-2.5 rounded-xl border border-blue-100">
<div>کارت: <strong>۶۲۱۹-۸۶۱۹-۷۴۰۱-۲۰۷۱</strong> (بانک سامان)</div>
<div>شبا: <strong>IR-65 0560 6118 2800 5725 1015 01</strong></div>
<div className="text-gray-500 font-sans text-[10px] pt-1 border-t">به نام: پارسادرمانی کنینا (نوواگارد) - پارسا آقایی</div>
</div>
<p className="text-[10px] font-bold text-blue-800 leading-relaxed pt-1">
پس از واریز، فیش توسط تیم پشتیبانی بررسی و تایید خواهد شد.
</p>
</div>
) : (
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-3.5 flex items-center gap-3 text-emerald-800 text-xs font-bold">
<ShieldCheck className="w-5 h-5 text-emerald-600 shrink-0" />
<span>پرداخت امن از طریق درگاه مستقیم بانکی زیبال و اتصال به کلیه کارتهای عضو شتاب</span>
</div>
)}
<button
type="submit"
disabled={loading || !amount}
className="w-full py-5 bg-canina-blue text-white rounded-[2rem] font-black text-base flex items-center justify-center gap-3 hover:bg-indigo-700 transition-all shadow-xl shadow-canina-blue/20 disabled:opacity-50 disabled:cursor-not-allowed group cursor-pointer"
>
{loading ? (
<div className="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin" />
) : (
<>
<CreditCard className="w-6 h-6 group-hover:scale-110 transition-transform" />
اتصال به درگاه و شارژ آنلاین
</>
)}
</button>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
);
}