Files
igny8/frontend/src/components/billing/PendingPaymentBanner.tsx
2026-01-07 10:19:34 +00:00

338 lines
13 KiB
TypeScript

/**
* Pending Payment Banner
* Shows alert banner when account status is 'pending_payment'
* Displays invoice details and provides link to payment confirmation
*/
import { useState, useEffect } from 'react';
import { AlertCircleIcon, CreditCardIcon, XIcon } from '../../icons';
import { Link } from 'react-router-dom';
import Button from '../ui/button/Button';
import { useAuthStore } from '../../store/authStore';
import { API_BASE_URL } from '../../services/api';
import PayInvoiceModal from './PayInvoiceModal';
interface Invoice {
id: number;
invoice_number: string;
total?: string; // For backward compatibility
total_amount?: string; // Backend returns 'total_amount'
currency: string;
status: string;
due_date?: string;
created_at: string;
payment_method?: string; // For checking bank_transfer
subscription?: {
id?: number;
plan?: {
id: number;
name: string;
slug?: string;
};
};
}
interface PendingPaymentBannerProps {
className?: string;
}
export default function PendingPaymentBanner({ className = '' }: PendingPaymentBannerProps) {
const [invoice, setInvoice] = useState<Invoice | null>(null);
const [paymentMethod, setPaymentMethod] = useState<any>(null);
const [availableGateways, setAvailableGateways] = useState<{ stripe: boolean; paypal: boolean; manual: boolean }>({
stripe: false,
paypal: false,
manual: true,
});
const [loading, setLoading] = useState(true);
const [dismissed, setDismissed] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const { user, refreshUser } = useAuthStore();
const accountStatus = user?.account?.status;
const isPendingPayment = accountStatus === 'pending_payment';
// Clear dismissed state when account is no longer pending payment
// This ensures the banner shows again if account reverts to pending
useEffect(() => {
if (!isPendingPayment) {
sessionStorage.removeItem('payment-banner-dismissed');
}
}, [isPendingPayment]);
useEffect(() => {
if (isPendingPayment && !dismissed) {
loadPendingInvoice();
} else {
setLoading(false);
}
}, [isPendingPayment, dismissed]);
const loadPendingInvoice = async () => {
try {
setLoading(true);
const token = useAuthStore.getState().token;
// Fetch pending invoices for this account
const response = await fetch(`${API_BASE_URL}/v1/billing/invoices/?status=pending&limit=1`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
credentials: 'include',
});
const data = await response.json();
if (response.ok && data.success && data.results?.length > 0) {
setInvoice(data.results[0]);
// Load user's account payment methods
try {
const apmResponse = await fetch(`${API_BASE_URL}/v1/billing/payment-methods/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
credentials: 'include',
});
const apmData = await apmResponse.json();
if (apmResponse.ok && apmData.success && apmData.results?.length > 0) {
const defaultMethod = apmData.results.find((m: any) => m.is_default && m.is_verified) ||
apmData.results.find((m: any) => m.is_verified) ||
apmData.results[0];
if (defaultMethod) {
setPaymentMethod(defaultMethod);
}
}
} catch (err) {
console.error('Failed to load account payment methods:', err);
}
// Load available payment gateways by checking their config endpoints
try {
const [stripeRes, paypalRes] = await Promise.all([
fetch(`${API_BASE_URL}/v1/billing/stripe/config/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
credentials: 'include',
}),
fetch(`${API_BASE_URL}/v1/billing/paypal/config/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
credentials: 'include',
}),
]);
const stripeData = await stripeRes.json().catch(() => ({}));
const paypalData = await paypalRes.json().catch(() => ({}));
setAvailableGateways({
stripe: stripeRes.ok && stripeData.success && !!stripeData.publishable_key,
paypal: paypalRes.ok && paypalData.success && !!paypalData.client_id,
manual: true,
});
} catch (err) {
console.error('Failed to load payment gateways:', err);
}
}
} catch (err) {
console.error('Failed to load pending invoice:', err);
} finally {
setLoading(false);
}
};
const handleDismiss = () => {
setDismissed(true);
// Store dismissal in sessionStorage to persist during session
sessionStorage.setItem('payment-banner-dismissed', 'true');
};
const handlePaymentSuccess = async () => {
setShowPaymentModal(false);
// Refresh user data to update account status
await refreshUser();
};
// Don't show if not pending payment, loading, or dismissed
if (!isPendingPayment || loading || dismissed) {
return null;
}
// Check if already dismissed in this session
if (sessionStorage.getItem('payment-banner-dismissed') === 'true') {
return null;
}
// If no invoice found, show simplified banner
if (!invoice) {
return (
<div className={`relative border-l-4 border-warning-500 bg-warning-50 dark:bg-warning-900/20 ${className}`}>
<div className="p-4">
<div className="flex items-start gap-4">
<AlertCircleIcon className="w-6 h-6 text-warning-600 dark:text-warning-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-warning-900 dark:text-warning-100">
Payment Required
</h3>
<p className="mt-1 text-sm text-warning-800 dark:text-warning-200">
Your account is pending payment. Please complete your payment to activate your subscription.
</p>
<div className="mt-3 flex gap-2">
<Link to="/account/plans">
<Button variant="primary" size="sm">
Complete Payment
</Button>
</Link>
<Link to="/dashboard">
<Button variant="outline" size="sm">
Go to Dashboard
</Button>
</Link>
</div>
</div>
<button
onClick={handleDismiss}
className="p-1 hover:bg-warning-100 dark:hover:bg-warning-800/40 rounded transition-colors"
>
<XIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
</button>
</div>
</div>
</div>
);
}
// Format due date
const formatDate = (dateString?: string) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const isDueSoon = invoice.due_date && new Date(invoice.due_date) <= new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
const isOverdue = invoice.due_date && new Date(invoice.due_date) < new Date();
return (
<>
<div className={`relative border-l-4 ${isOverdue ? 'border-error-500 bg-error-50 dark:bg-error-900/20' : 'border-warning-500 bg-warning-50 dark:bg-warning-900/20'} ${className}`}>
<div className="p-4">
<div className="flex items-start gap-4">
<AlertCircleIcon
className={`w-6 h-6 flex-shrink-0 mt-0.5 ${isOverdue ? 'text-error-600 dark:text-error-400' : 'text-warning-600 dark:text-warning-400'}`}
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className={`font-semibold ${isOverdue ? 'text-error-900 dark:text-error-100' : 'text-warning-900 dark:text-warning-100'}`}>
{isOverdue ? 'Payment Overdue' : 'Payment Required'}
</h3>
{isDueSoon && !isOverdue && (
<span className="px-2 py-0.5 text-xs font-medium rounded bg-warning-200 text-warning-900 dark:bg-warning-700 dark:text-warning-100">
Due Soon
</span>
)}
</div>
<p className={`mt-1 text-sm ${isOverdue ? 'text-error-800 dark:text-error-200' : 'text-warning-800 dark:text-warning-200'}`}>
Your subscription is pending payment confirmation. Complete your payment to activate your account and unlock all features.
</p>
{/* Invoice Details */}
<div className="mt-3 grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div>
<span className={`block font-medium ${isOverdue ? 'text-error-700 dark:text-error-300' : 'text-warning-700 dark:text-warning-300'}`}>
Invoice
</span>
<span className={`${isOverdue ? 'text-error-900 dark:text-error-100' : 'text-warning-900 dark:text-warning-100'}`}>
#{invoice.invoice_number}
</span>
</div>
<div>
<span className={`block font-medium ${isOverdue ? 'text-error-700 dark:text-error-300' : 'text-warning-700 dark:text-warning-300'}`}>
Amount
</span>
<span className={`${isOverdue ? 'text-error-900 dark:text-error-100' : 'text-warning-900 dark:text-warning-100'}`}>
{invoice.currency} {invoice.total_amount}
</span>
</div>
<div>
<span className={`block font-medium ${isOverdue ? 'text-error-700 dark:text-error-300' : 'text-warning-700 dark:text-warning-300'}`}>
Status
</span>
<span className={`${isOverdue ? 'text-error-900 dark:text-error-100' : 'text-warning-900 dark:text-warning-100'} capitalize`}>
{invoice.status}
</span>
</div>
<div>
<span className={`block font-medium ${isOverdue ? 'text-error-700 dark:text-error-300' : 'text-warning-700 dark:text-warning-300'}`}>
{isOverdue ? 'Was Due' : 'Due Date'}
</span>
<span className={`${isOverdue ? 'text-error-900 dark:text-error-100' : 'text-warning-900 dark:text-warning-100'}`}>
{formatDate(invoice.due_date)}
</span>
</div>
</div>
{/* Action Buttons */}
<div className="mt-4 flex flex-wrap gap-3">
<Button
variant="primary"
size="sm"
startIcon={<CreditCardIcon className="w-4 h-4" />}
onClick={() => setShowPaymentModal(true)}
>
Pay Now
</Button>
{/* Only show Bank Transfer Details for PK users with bank_transfer payment method */}
{(user?.account as any)?.billing_country?.toUpperCase() === 'PK' &&
(paymentMethod?.type === 'bank_transfer' || invoice.payment_method === 'bank_transfer') && (
<Link to="/account/plans">
<Button variant="outline" size="sm">
View Bank Transfer Details
</Button>
</Link>
)}
</div>
</div>
{/* Dismiss Button */}
<button
onClick={handleDismiss}
className={`p-1 rounded transition-colors ${
isOverdue
? 'hover:bg-error-100 dark:hover:bg-error-800/40 text-error-600 dark:text-error-400'
: 'hover:bg-warning-100 dark:hover:bg-warning-800/40 text-warning-600 dark:text-warning-400'
}`}
>
<XIcon className="w-5 h-5" />
</button>
</div>
</div>
</div>
{/* Payment Modal with All Options */}
{showPaymentModal && invoice && (
<PayInvoiceModal
isOpen={showPaymentModal}
onClose={() => setShowPaymentModal(false)}
onSuccess={handlePaymentSuccess}
invoice={invoice}
userCountry={(user?.account as any)?.billing_country || 'US'}
defaultPaymentMethod={paymentMethod?.type}
/>
)}
</>
);
}