228 lines
9.0 KiB
TypeScript
228 lines
9.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { createBoricaPayment } from '../../services/api/paymentService';
|
|
import { X, Loader2, AlertCircle, CheckCircle, CreditCard } from 'lucide-react';
|
|
import { toast } from 'react-toastify';
|
|
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
|
|
|
interface BoricaPaymentModalProps {
|
|
isOpen: boolean;
|
|
bookingId: number;
|
|
amount: number;
|
|
currency?: string;
|
|
onSuccess: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
|
|
isOpen,
|
|
bookingId,
|
|
amount,
|
|
currency: propCurrency,
|
|
onSuccess,
|
|
onClose,
|
|
}) => {
|
|
const { currency: contextCurrency } = useFormatCurrency();
|
|
const currency = propCurrency || contextCurrency || 'BGN';
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [paymentRequest, setPaymentRequest] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const initializeBorica = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const currentUrl = window.location.origin;
|
|
const returnUrl = `${currentUrl}/payment/borica/return?bookingId=${bookingId}`;
|
|
|
|
const response = await createBoricaPayment(
|
|
bookingId,
|
|
amount,
|
|
currency,
|
|
returnUrl
|
|
);
|
|
|
|
if (response.success && response.data) {
|
|
const { payment_request } = response.data;
|
|
|
|
if (!payment_request) {
|
|
throw new Error('Payment request not received from server');
|
|
}
|
|
|
|
setPaymentRequest(payment_request);
|
|
} else {
|
|
throw new Error(response.message || 'Failed to initialize Borica payment');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error initializing Borica:', err);
|
|
const errorMessage = err.response?.data?.message || err.message || 'Failed to initialize Borica payment';
|
|
setError(errorMessage);
|
|
toast.error(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
initializeBorica();
|
|
}, [isOpen, bookingId, amount, currency]);
|
|
|
|
const handleSubmitPayment = () => {
|
|
if (!paymentRequest) return;
|
|
|
|
// Create a form and submit it to Borica gateway
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = paymentRequest.gateway_url;
|
|
form.style.display = 'none';
|
|
|
|
// Add all form fields
|
|
const fields = {
|
|
TERMINAL: paymentRequest.terminal_id,
|
|
MERCHANT: paymentRequest.merchant_id,
|
|
ORDER: paymentRequest.order_id,
|
|
AMOUNT: paymentRequest.amount,
|
|
CURRENCY: paymentRequest.currency,
|
|
DESC: paymentRequest.description,
|
|
TIMESTAMP: paymentRequest.timestamp,
|
|
P_SIGN: paymentRequest.signature,
|
|
TRTYPE: paymentRequest.trtype,
|
|
BACKREF: paymentRequest.return_url,
|
|
};
|
|
|
|
Object.entries(fields).forEach(([key, value]) => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = key;
|
|
input.value = value as string;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-2 sm:p-4">
|
|
<div
|
|
className="fixed inset-0 bg-black/90 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
<div className="relative w-full max-w-md max-h-[95vh] bg-gradient-to-br from-[#0a0a0a] via-[#1a1a1a] to-[#0a0a0a] rounded-xl sm:rounded-2xl border border-[#d4af37]/30 shadow-2xl shadow-[#d4af37]/20 overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-[#d4af37]/20 bg-gradient-to-r from-[#1a1a1a] to-[#0a0a0a] flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<h2 className="text-base sm:text-lg md:text-xl font-serif font-bold text-white tracking-tight truncate flex-1">
|
|
Borica Payment
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1.5 sm:p-2 hover:bg-[#d4af37]/10 rounded-lg transition-colors text-gray-400 hover:text-white flex-shrink-0"
|
|
>
|
|
<X className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto p-3 sm:p-4 md:p-6">
|
|
{loading ? (
|
|
<div className="flex flex-col items-center justify-center py-8 sm:py-12">
|
|
<Loader2 className="w-10 h-10 sm:w-12 sm:h-12 animate-spin text-[#d4af37] mb-3 sm:mb-4" />
|
|
<p className="text-xs sm:text-sm text-gray-400">Initializing Borica payment...</p>
|
|
</div>
|
|
) : error ? (
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 sm:p-4">
|
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
<AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-red-400 mt-0.5 flex-shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="text-xs sm:text-sm font-semibold text-red-300 mb-1">
|
|
Payment Initialization Failed
|
|
</h3>
|
|
<p className="text-xs text-red-200/80 leading-relaxed">
|
|
{error || 'Unable to initialize Borica payment. Please try again.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : paymentRequest ? (
|
|
<div className="space-y-4 sm:space-y-6">
|
|
<div className="text-center">
|
|
<div className="mb-3 sm:mb-4 flex justify-center">
|
|
<CreditCard className="w-12 h-12 sm:w-16 sm:h-16 text-[#d4af37]" />
|
|
</div>
|
|
<h3 className="text-base sm:text-lg font-serif font-semibold text-white mb-1.5 sm:mb-2">
|
|
Complete Payment with Borica
|
|
</h3>
|
|
<p className="text-xs sm:text-sm text-gray-400 mb-3 sm:mb-4 leading-relaxed px-2">
|
|
You will be redirected to Borica payment gateway to securely complete your payment of{' '}
|
|
<span className="font-semibold text-[#d4af37]">
|
|
{new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency,
|
|
}).format(amount)}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-[#1a1a1a]/50 border border-[#d4af37]/20 rounded-lg p-3 sm:p-4 space-y-2 sm:space-y-3">
|
|
<div className="flex justify-between items-center text-xs sm:text-sm">
|
|
<span className="text-gray-400">Order ID:</span>
|
|
<span className="text-white font-mono">{paymentRequest.order_id}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs sm:text-sm">
|
|
<span className="text-gray-400">Amount:</span>
|
|
<span className="text-[#d4af37] font-semibold">
|
|
{new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency,
|
|
}).format(amount)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-3 sm:p-4">
|
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
<AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-blue-400 mt-0.5 flex-shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs text-blue-200/80 leading-relaxed">
|
|
Your payment will be processed securely through Borica payment gateway.
|
|
You will be redirected to complete the transaction.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{!loading && !error && paymentRequest && (
|
|
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-t border-[#d4af37]/20 bg-gradient-to-r from-[#1a1a1a] to-[#0a0a0a] flex-shrink-0 flex gap-2 sm:gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="flex-1 px-3 sm:px-4 py-2 sm:py-2.5 text-xs sm:text-sm font-medium text-gray-300 bg-[#1a1a1a] border border-gray-700 rounded-lg hover:bg-[#2a2a2a] hover:border-gray-600 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSubmitPayment}
|
|
className="flex-1 px-3 sm:px-4 py-2 sm:py-2.5 text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#b8941f] rounded-lg hover:from-[#e5c048] hover:to-[#c9a428] transition-all shadow-lg shadow-[#d4af37]/20 flex items-center justify-center gap-2"
|
|
>
|
|
<CreditCard className="w-4 h-4" />
|
|
Proceed to Payment
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BoricaPaymentModal;
|
|
|