/** * Pay Invoice Modal * Allows users to pay pending invoices using Stripe, PayPal, or submit bank transfer confirmation * * Payment Method Logic (Following SignUpFormUnified pattern): * - Pakistan (PK): Bank Transfer + Credit Card (Stripe) - NO PayPal * - Other countries: Credit Card (Stripe) + PayPal only * - Default selection: User's configured default payment method from AccountPaymentMethod */ import { useState, useEffect } from 'react'; import { Modal } from '../ui/modal'; import Button from '../ui/button/Button'; import Label from '../form/Label'; import Input from '../form/input/InputField'; import TextArea from '../form/input/TextArea'; import { Loader2Icon, UploadIcon, XIcon, CheckCircleIcon, CreditCardIcon, Building2Icon, WalletIcon } from '../../icons'; import { API_BASE_URL } from '../../services/api'; import { useAuthStore } from '../../store/authStore'; import { subscribeToPlan, getAvailablePaymentMethods, purchaseCredits } from '../../services/billing.api'; interface BankDetails { bank_name: string; account_title: string; account_number: string; iban?: string; swift_code?: string; } interface Invoice { id: number; invoice_number: string; invoice_type?: 'subscription' | 'credit_package' | 'addon' | 'custom'; credit_package_id?: string | number | null; total?: string; total_amount?: string; currency?: string; status?: string; payment_method?: string; subscription?: { id?: number; plan?: { id: number; name: string; slug?: string; }; } | null; } interface PayInvoiceModalProps { isOpen: boolean; onClose: () => void; onSuccess?: () => void; invoice: Invoice; /** User's billing country code (e.g., 'US', 'PK') */ userCountry?: string; /** User's default payment method type */ defaultPaymentMethod?: string; } type PaymentOption = 'stripe' | 'paypal' | 'bank_transfer'; export default function PayInvoiceModal({ isOpen, onClose, onSuccess, invoice, userCountry = 'US', defaultPaymentMethod, }: PayInvoiceModalProps) { const isPakistan = userCountry?.toUpperCase() === 'PK'; // Determine available payment options based on country // PK users: Stripe (Card) + Bank Transfer - NO PayPal // Other users: Stripe (Card) + PayPal only const availableOptions: PaymentOption[] = isPakistan ? ['stripe', 'bank_transfer'] : ['stripe', 'paypal']; // Determine initial selection based on: // 1. User's default payment method (if available for this country) // 2. Invoice's stored payment_method // 3. First available option (stripe) const getInitialOption = (): PaymentOption => { // Check user's default payment method first if (defaultPaymentMethod) { if (defaultPaymentMethod === 'stripe' || defaultPaymentMethod === 'card') return 'stripe'; if (defaultPaymentMethod === 'bank_transfer' && isPakistan) return 'bank_transfer'; if (defaultPaymentMethod === 'paypal' && !isPakistan) return 'paypal'; } // Then check invoice's stored payment method if (invoice.payment_method) { if (invoice.payment_method === 'stripe' || invoice.payment_method === 'card') return 'stripe'; if (invoice.payment_method === 'bank_transfer' && isPakistan) return 'bank_transfer'; if (invoice.payment_method === 'paypal' && !isPakistan) return 'paypal'; } // Fall back to first available (always stripe) return 'stripe'; }; const [selectedOption, setSelectedOption] = useState(getInitialOption()); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(false); // Bank transfer form state const [bankFormData, setBankFormData] = useState({ manual_reference: '', manual_notes: '', proof_url: '', }); const [uploadedFileName, setUploadedFileName] = useState(''); const [uploading, setUploading] = useState(false); // Bank details loaded from backend const [bankDetails, setBankDetails] = useState(null); const [bankDetailsLoading, setBankDetailsLoading] = useState(false); const amount = parseFloat(invoice.total_amount || invoice.total || '0'); const currency = invoice.currency?.toUpperCase() || 'USD'; const planId = invoice.subscription?.plan?.id; const planSlug = invoice.subscription?.plan?.slug; const isCreditInvoice = invoice.invoice_type === 'credit_package'; const creditPackageId = invoice.credit_package_id ? String(invoice.credit_package_id) : null; // Check if user's default method is selected (for showing badge) const isDefaultMethod = (option: PaymentOption): boolean => { if (!defaultPaymentMethod) return false; if (option === 'stripe' && (defaultPaymentMethod === 'stripe' || defaultPaymentMethod === 'card')) return true; if (option === 'bank_transfer' && defaultPaymentMethod === 'bank_transfer') return true; if (option === 'paypal' && defaultPaymentMethod === 'paypal') return true; return false; }; // Reset state when modal opens useEffect(() => { if (isOpen) { setSelectedOption(getInitialOption()); setError(''); setSuccess(false); setBankFormData({ manual_reference: '', manual_notes: '', proof_url: '' }); setUploadedFileName(''); setBankDetails(null); } }, [isOpen]); // Load bank details from backend when bank_transfer is selected for PK users useEffect(() => { if (!isOpen || !isPakistan || selectedOption !== 'bank_transfer') return; if (bankDetails) return; // Already loaded const loadBankDetails = async () => { setBankDetailsLoading(true); try { const { results } = await getAvailablePaymentMethods(userCountry); const bankMethod = results.find( (m) => m.type === 'bank_transfer' && m.is_enabled ) as any; if (bankMethod?.bank_name && (bankMethod?.account_title || bankMethod?.account_number)) { setBankDetails({ bank_name: bankMethod.bank_name, account_title: bankMethod.account_title || '', account_number: bankMethod.account_number || '', iban: bankMethod.iban, swift_code: bankMethod.swift_code, }); } } catch (err) { console.error('Failed to load bank details:', err); } finally { setBankDetailsLoading(false); } }; loadBankDetails(); }, [isOpen, isPakistan, selectedOption, userCountry, bankDetails]); const handleStripePayment = async () => { if (isCreditInvoice) { if (!creditPackageId) { setError('Unable to process card payment. Credit package not found on invoice. Please contact support.'); return; } try { setLoading(true); setError(''); const result = await purchaseCredits(creditPackageId, 'stripe', { return_url: `${window.location.origin}/account/usage?purchase=success`, cancel_url: `${window.location.origin}/account/usage?purchase=canceled`, }); window.location.href = result.redirect_url; } catch (err: any) { setError(err.message || 'Failed to initiate card payment'); setLoading(false); } return; } // Use plan slug if available, otherwise fall back to id const planIdentifier = planSlug || (planId ? String(planId) : null); if (!planIdentifier) { setError('Unable to process card payment. Invoice has no associated plan. Please contact support.'); return; } try { setLoading(true); setError(''); // Use the subscribeToPlan function which properly handles Stripe checkout const result = await subscribeToPlan(planIdentifier, 'stripe', { return_url: `${window.location.origin}/account/plans?success=true`, cancel_url: `${window.location.origin}/account/plans?canceled=true`, }); // Redirect to Stripe Checkout window.location.href = result.redirect_url; } catch (err: any) { setError(err.message || 'Failed to initiate card payment'); setLoading(false); } }; const handlePayPalPayment = async () => { if (isCreditInvoice) { if (!creditPackageId) { setError('Unable to process PayPal payment. Credit package not found on invoice. Please contact support.'); return; } try { setLoading(true); setError(''); const result = await purchaseCredits(creditPackageId, 'paypal', { return_url: `${window.location.origin}/account/usage?purchase=success`, cancel_url: `${window.location.origin}/account/usage?purchase=canceled`, }); window.location.href = result.redirect_url; } catch (err: any) { setError(err.message || 'Failed to initiate PayPal payment'); setLoading(false); } return; } // Use plan slug if available, otherwise fall back to id const planIdentifier = planSlug || (planId ? String(planId) : null); if (!planIdentifier) { setError('Unable to process PayPal payment. Invoice has no associated plan. Please contact support.'); return; } try { setLoading(true); setError(''); // Use the subscribeToPlan function which properly handles PayPal const result = await subscribeToPlan(planIdentifier, 'paypal', { return_url: `${window.location.origin}/account/plans?paypal=success&plan_id=${planIdentifier}`, cancel_url: `${window.location.origin}/account/plans?paypal=cancel`, }); // Redirect to PayPal window.location.href = result.redirect_url; } catch (err: any) { setError(err.message || 'Failed to initiate PayPal payment'); setLoading(false); } }; const handleFileSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { setError('File size must be less than 5MB'); return; } const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf']; if (!allowedTypes.includes(file.type)) { setError('Only JPEG, PNG, and PDF files are allowed'); return; } setUploadedFileName(file.name); setError(''); setUploading(true); try { // Placeholder URL - actual S3 upload would go here const placeholderUrl = `https://s3.amazonaws.com/igny8-payments/${Date.now()}-${file.name}`; setBankFormData({ ...bankFormData, proof_url: placeholderUrl }); } catch (err) { setError('Failed to upload file'); } finally { setUploading(false); } }; const handleBankSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); if (!bankFormData.manual_reference.trim()) { setError('Transaction reference is required'); return; } try { setLoading(true); const token = useAuthStore.getState().token; const response = await fetch(`${API_BASE_URL}/v1/billing/admin/payments/confirm/`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), }, credentials: 'include', body: JSON.stringify({ invoice_id: invoice.id, payment_method: 'bank_transfer', amount: invoice.total_amount || invoice.total || '0', manual_reference: bankFormData.manual_reference.trim(), manual_notes: bankFormData.manual_notes.trim() || undefined, proof_url: bankFormData.proof_url || undefined, }), }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || data.message || 'Failed to submit payment confirmation'); } setSuccess(true); setTimeout(() => { onClose(); onSuccess?.(); }, 2500); } catch (err: any) { setError(err.message || 'Failed to submit payment'); } finally { setLoading(false); } }; const handlePayNow = () => { if (selectedOption === 'stripe') { handleStripePayment(); } else if (selectedOption === 'paypal') { handlePayPalPayment(); } // Bank transfer uses form submit }; return (
{success ? (

{selectedOption === 'bank_transfer' ? 'Payment Submitted!' : 'Redirecting...'}

{selectedOption === 'bank_transfer' ? 'Your payment confirmation has been submitted for review.' : 'You will be redirected to complete your payment.'}

) : ( <> {/* Header */}

Pay Invoice

#{invoice.invoice_number}

{currency} {amount.toFixed(2)}
{isPakistan && selectedOption === 'bank_transfer' && (
≈ PKR {Math.round(amount * 278).toLocaleString()}
)}
{error && (
{error}
)} {/* Payment Method Selection */} {availableOptions.length > 1 && (
{/* Stripe (Card) - Always available */} {/* PayPal - Only for non-Pakistan */} {!isPakistan && availableOptions.includes('paypal') && ( )} {/* Bank Transfer - Only for Pakistan */} {isPakistan && availableOptions.includes('bank_transfer') && ( )}
)} {/* Stripe Payment */} {selectedOption === 'stripe' && (

Pay securely with your credit or debit card via Stripe.

)} {/* PayPal Payment - Only for non-Pakistan */} {selectedOption === 'paypal' && !isPakistan && (

Pay securely using your PayPal account.

)} {/* Bank Transfer - Only for Pakistan */} {selectedOption === 'bank_transfer' && isPakistan && (
{/* Bank Details */}

Bank Transfer Details

{bankDetailsLoading ? (
Loading bank details...
) : bankDetails ? (

Bank: {bankDetails.bank_name}

Account Title: {bankDetails.account_title}

Account #: {bankDetails.account_number}

{bankDetails.iban &&

IBAN: {bankDetails.iban}

}

Reference: {invoice.invoice_number}

) : (

Bank details not available. Please contact support.

)}
{/* Transaction Reference */}
setBankFormData({ ...bankFormData, manual_reference: e.target.value })} placeholder="Enter your bank transaction reference" disabled={loading} />
{/* Notes */}