podzahr/components/PaymentModal.tsx
nfel 895afc44af
main: add many things to app :)
Signed-off-by: nfel <nfilsaraee@gmail.com>
2025-12-30 01:30:31 +03:30

357 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FaTimes, FaCreditCard, FaLock, FaUniversity } from 'react-icons/fa';
import { Album } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
type PaymentMethod = 'ipg' | 'card-to-card';
interface PaymentModalProps {
album: Album | null;
onClose: () => void;
onSuccess: (albumId: string, transactionId: string) => void;
}
export default function PaymentModal({ album, onClose, onSuccess }: PaymentModalProps) {
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>('ipg');
const [cardNumber, setCardNumber] = useState('');
const [cardName, setCardName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [email, setEmail] = useState('');
const [txReceipt, setTxReceipt] = useState('');
const [processing, setProcessing] = useState(false);
const [error, setError] = useState('');
const formatCardNumber = (value: string) => {
const numbers = value.replace(/\s/g, '');
const groups = numbers.match(/.{1,4}/g);
return groups ? groups.join(' ') : numbers;
};
const formatPhoneNumber = (value: string) => {
// Remove all non-digits
let numbers = value.replace(/\D/g, '');
// Remove leading 98 if user types it (we'll add +98 prefix)
if (numbers.startsWith('98')) {
numbers = numbers.slice(2);
}
// Limit to 10 digits (Iranian mobile number length)
numbers = numbers.slice(0, 10);
// Format as +98 XXXX XXX XXXX
if (numbers.length === 0) return '';
if (numbers.length <= 4) return `+98 ${numbers}`;
if (numbers.length <= 7) return `+98 ${numbers.slice(0, 4)} ${numbers.slice(4)}`;
return `+98 ${numbers.slice(0, 4)} ${numbers.slice(4, 7)} ${numbers.slice(7)}`;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
// Basic validation
if (!cardName.trim()) {
setError('Name is required');
return;
}
// Validate Iranian phone number (should have 10 digits after +98)
const phoneDigits = phoneNumber.replace(/\D/g, '');
// Remove the country code (98) if present to get local number
const localNumber = phoneDigits.startsWith('98') ? phoneDigits.slice(2) : phoneDigits;
if (localNumber.length !== 10 || !localNumber.startsWith('9')) {
setError('Invalid phone number. Must start with 9 and have 10 digits');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('Invalid email address');
return;
}
// Validate based on payment method
if (paymentMethod === 'card-to-card') {
if (cardNumber.replace(/\s/g, '').length !== 16) {
setError('Invalid card number');
return;
}
if (!txReceipt.trim()) {
setError('Transaction receipt is required');
return;
}
}
if (!album) return;
setProcessing(true);
try {
// Handle IPG payment
if (paymentMethod === 'ipg') {
const response = await fetch('/api/payment/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
albumId: album.id,
amount: album.price,
customerName: cardName.trim(),
email: email.trim(),
phoneNumber: phoneNumber,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to initiate payment');
}
// Redirect to ZarinPal payment gateway
window.location.href = data.paymentUrl;
return;
}
// Handle card-to-card payment
const transactionId = txReceipt.trim() || 'TXN-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9).toUpperCase();
// Create purchase via API (pending approval for card-to-card)
const response = await fetch('/api/purchases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
albumId: album.id,
transactionId,
customerName: cardName.trim(),
email: email.trim(),
phoneNumber: phoneNumber,
txReceipt: txReceipt.trim(),
purchaseDate: new Date().getTime(),
approvalStatus: 'pending',
paymentMethod: 'card-to-card',
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to process purchase');
}
setProcessing(false);
onSuccess(album.id, transactionId);
} catch (err: any) {
setProcessing(false);
setError(err.message || 'Failed to process payment');
}
};
if (!album) return null;
return (
<AnimatePresence>
{album && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-paper-dark/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="paper-card max-w-md w-full p-6 border-4 border-paper-dark shadow-paper-lg"
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-paper-dark border-b-4 border-paper-dark inline-block pb-1">
Purchase Album
</h2>
<button
onClick={onClose}
className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-paper-brown transition-colors"
aria-label="Close"
>
<FaTimes className="text-lg text-paper-dark" />
</button>
</div>
{/* Album Info */}
<div className="mb-4 p-3 bg-paper-light border-2 border-paper-brown">
<h3 className="font-semibold text-paper-dark">{album.title}</h3>
<p className="text-sm text-paper-gray">{album.songs.length} tracks {album.genre}</p>
<p className="text-xl font-bold text-paper-brown mt-1">{formatPrice(album.price)}</p>
</div>
{/* Payment Method Selector */}
<div className="mb-4">
<label className="block text-sm font-medium text-paper-dark mb-2">
Payment Method / روش پرداخت
</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPaymentMethod('ipg')}
className={`p-3 border-2 transition-all ${
paymentMethod === 'ipg'
? 'bg-paper-brown text-paper-light border-paper-dark shadow-paper'
: 'bg-paper-light text-paper-dark border-paper-brown hover:bg-paper-sand'
}`}
>
<FaUniversity className="text-lg mx-auto mb-1" />
<div className="text-xs font-medium">IPG</div>
<div className="text-xs">درگاه پرداخت</div>
</button>
<button
type="button"
onClick={() => setPaymentMethod('card-to-card')}
className={`p-3 border-2 transition-all ${
paymentMethod === 'card-to-card'
? 'bg-paper-brown text-paper-light border-paper-dark shadow-paper'
: 'bg-paper-light text-paper-dark border-paper-brown hover:bg-paper-sand'
}`}
>
<FaCreditCard className="text-lg mx-auto mb-1" />
<div className="text-xs font-medium">Card to Card</div>
<div className="text-xs">کارت به کارت</div>
</button>
</div>
</div>
{/* Payment Form */}
<form onSubmit={handleSubmit} className="space-y-3">
{/* Show Card Number only for card-to-card */}
{paymentMethod === 'card-to-card' && (
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
<FaCreditCard className="inline mr-2" />
Card Number
</label>
<input
type="text"
value={cardNumber}
onChange={(e) => {
const formatted = formatCardNumber(e.target.value.replace(/\D/g, '').slice(0, 16));
setCardNumber(formatted);
}}
placeholder="1234 5678 9012 3456"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
)}
{/* Full Name */}
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
Full Name
</label>
<input
type="text"
value={cardName}
onChange={(e) => setCardName(e.target.value)}
placeholder="John Doe"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Phone Number */}
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
Phone Number / شماره تماس
</label>
<input
type="tel"
value={phoneNumber}
onChange={(e) => {
const formatted = formatPhoneNumber(e.target.value);
setPhoneNumber(formatted);
}}
placeholder="+98 9390 084 053"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
Email Address
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Transaction Receipt - only for card-to-card */}
{paymentMethod === 'card-to-card' && (
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
Transaction Receipt / رسید پرداخت
</label>
<input
type="text"
value={txReceipt}
onChange={(e) => setTxReceipt(e.target.value)}
placeholder="TXN-123456789"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
)}
{/* Error Message */}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-2 bg-red-100 border-2 border-red-400 text-red-700 text-sm"
>
{error}
</motion.div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={processing}
className="w-full py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm"
>
{processing ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-paper-light"></div>
Processing...
</>
) : (
<>
<FaLock />
{paymentMethod === 'ipg' ? 'Proceed to Payment Gateway' : `Complete Purchase - ${formatPrice(album.price)}`}
</>
)}
</button>
{/* Security Notice */}
<p className="text-xs text-paper-gray text-center mt-2">
<FaLock className="inline mr-1" />
This is a demo payment form. No real charges will be made.
</p>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}