/** * Plans & Billing Page - Consolidated * Tabs: Current Plan, Credits Overview, Billing History */ import { useState, useEffect, useRef } from 'react'; import { CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle, Loader2, AlertCircle, CheckCircle, Download } from 'lucide-react'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import Button from '../../components/ui/button/Button'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { PricingTable, PricingPlan } from '../../components/ui/pricing-table'; import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel'; import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; import { getCreditBalance, getCreditPackages, getInvoices, getAvailablePaymentMethods, purchaseCreditPackage, downloadInvoicePDF, getPayments, submitManualPayment, createPaymentMethod, deletePaymentMethod, setDefaultPaymentMethod, type CreditBalance, type CreditPackage, type Invoice, type PaymentMethod, type Payment, getPlans, getSubscriptions, createSubscription, cancelSubscription, type Plan, type Subscription, } from '../../services/billing.api'; import { useAuthStore } from '../../store/authStore'; type TabType = 'plan' | 'credits' | 'purchase' | 'invoices'; export default function PlansAndBillingPage() { const [activeTab, setActiveTab] = useState('plan'); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [planLoadingId, setPlanLoadingId] = useState(null); const [purchaseLoadingId, setPurchaseLoadingId] = useState(null); // Data states const [creditBalance, setCreditBalance] = useState(null); const [packages, setPackages] = useState([]); const [invoices, setInvoices] = useState([]); const [payments, setPayments] = useState([]); const [paymentMethods, setPaymentMethods] = useState([]); const [plans, setPlans] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(undefined); const [manualPayment, setManualPayment] = useState({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '', }); const [newPaymentMethod, setNewPaymentMethod] = useState({ type: 'bank_transfer', display_name: '', instructions: '', }); const { user } = useAuthStore.getState(); const hasLoaded = useRef(false); const isAwsAdmin = user?.account?.slug === 'aws-admin'; const handleBillingError = (err: any, fallback: string) => { const message = err?.message || fallback; setError(message); toast?.error?.(message); }; const toast = useToast(); useEffect(() => { if (hasLoaded.current) return; hasLoaded.current = true; loadData(); }, []); const loadData = async (allowRetry = true) => { try { setLoading(true); // Fetch in controlled sequence to avoid burst 429s on auth/system scopes const balanceData = await getCreditBalance(); // Small gap between auth endpoints to satisfy tight throttles const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); const packagesPromise = getCreditPackages(); const invoicesPromise = getInvoices({}); const paymentsPromise = getPayments({}); const methodsPromise = getAvailablePaymentMethods(); const plansData = await getPlans(); await wait(400); // Subscriptions: retry once on 429 after short backoff; do not hard-fail page let subsData: { results: Subscription[] } = { results: [] }; try { subsData = await getSubscriptions(); } catch (subErr: any) { if (subErr?.status === 429 && allowRetry) { await wait(2500); try { subsData = await getSubscriptions(); } catch { subsData = { results: [] }; } } else { subsData = { results: [] }; } } const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([ packagesPromise, invoicesPromise, paymentsPromise, methodsPromise, ]); setCreditBalance(balanceData); setPackages(packagesData.results || []); setInvoices(invoicesData.results || []); setPayments(paymentsData.results || []); // Prefer manual payment method id 14 as default (tenant-facing) const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false); setPaymentMethods(methods); if (methods.length > 0) { // Preferred ordering: bank_transfer (default), then manual const bank = methods.find((m) => m.type === 'bank_transfer'); const manual = methods.find((m) => m.type === 'manual'); const selected = bank || manual || methods.find((m) => m.is_default) || methods[0]; setSelectedPaymentMethod((prev) => prev || selected.type || selected.id); } // Surface all active plans (avoid hiding plans and showing empty state) const activePlans = (plansData.results || []).filter((p) => p.is_active !== false); // Exclude Enterprise plan for non aws-admin accounts const filteredPlans = activePlans.filter((p) => { const name = (p.name || '').toLowerCase(); const slug = (p.slug || '').toLowerCase(); const isEnterprise = name.includes('enterprise') || slug === 'enterprise'; return isAwsAdmin ? true : !isEnterprise; }); // Ensure the user's assigned plan is included even if subscriptions list is empty const accountPlan = user?.account?.plan; const isAccountEnterprise = (() => { if (!accountPlan) return false; const name = (accountPlan.name || '').toLowerCase(); const slug = (accountPlan.slug || '').toLowerCase(); return name.includes('enterprise') || slug === 'enterprise'; })(); const shouldIncludeAccountPlan = accountPlan && (!isAccountEnterprise || isAwsAdmin); if (shouldIncludeAccountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) { filteredPlans.push(accountPlan as any); } setPlans(filteredPlans); const subs = subsData.results || []; if (subs.length === 0 && shouldIncludeAccountPlan && accountPlan) { subs.push({ id: accountPlan.id || 0, plan: accountPlan, status: 'active', } as any); } setSubscriptions(subs); } catch (err: any) { // Handle throttling gracefully: don't block the page on subscriptions throttle if (err?.status === 429 && allowRetry) { setError('Request was throttled. Retrying...'); setTimeout(() => loadData(false), 2500); } else if (err?.status === 429) { setError(''); // suppress lingering banner } else { setError(err.message || 'Failed to load billing data'); console.error('Billing load error:', err); } } finally { setLoading(false); } }; const handleSelectPlan = async (planId: number) => { try { if (!selectedPaymentMethod && paymentMethods.length > 0) { setError('Select a payment method to continue'); return; } setPlanLoadingId(planId); await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod }); toast?.success?.('Subscription updated'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to update subscription'); } finally { setPlanLoadingId(null); } }; const handleCancelSubscription = async () => { if (!currentSubscription?.id) { setError('No active subscription to cancel'); return; } try { setPlanLoadingId(currentSubscription.id); await cancelSubscription(currentSubscription.id); toast?.success?.('Subscription cancellation requested'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to cancel subscription'); } finally { setPlanLoadingId(null); } }; const handlePurchase = async (packageId: number) => { try { if (!selectedPaymentMethod && paymentMethods.length > 0) { setError('Select a payment method to continue'); return; } setPurchaseLoadingId(packageId); await purchaseCreditPackage({ package_id: packageId, payment_method: (selectedPaymentMethod as any) || 'stripe', }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to purchase credits'); } finally { setPurchaseLoadingId(null); setLoading(false); } }; const handleDownloadInvoice = async (invoiceId: number) => { try { const blob = await downloadInvoicePDF(invoiceId); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `invoice-${invoiceId}.pdf`; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (err: any) { handleBillingError(err, 'Failed to download invoice'); } }; const handleSubmitManualPayment = async () => { try { const payload = { invoice_id: manualPayment.invoice_id ? Number(manualPayment.invoice_id) : undefined, amount: manualPayment.amount, payment_method: manualPayment.payment_method || (selectedPaymentMethod as any) || 'manual', reference: manualPayment.reference, notes: manualPayment.notes, }; await submitManualPayment(payload as any); toast?.success?.('Manual payment submitted'); setManualPayment({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '' }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to submit payment'); } }; const handleAddPaymentMethod = async () => { if (!newPaymentMethod.display_name.trim()) { setError('Payment method name is required'); return; } try { await createPaymentMethod(newPaymentMethod as any); toast?.success?.('Payment method added'); setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to add payment method'); } }; const handleRemovePaymentMethod = async (id: string) => { try { await deletePaymentMethod(id); toast?.success?.('Payment method removed'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to remove payment method'); } }; const handleSetDefaultPaymentMethod = async (id: string) => { try { await setDefaultPaymentMethod(id); toast?.success?.('Default payment method updated'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to set default'); } }; if (loading) { return (
); } const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0]; const currentPlanId = typeof currentSubscription?.plan === 'object' ? currentSubscription.plan.id : currentSubscription?.plan; // Fallback to account plan if subscription is missing const accountPlanId = user?.account?.plan?.id; const effectivePlanId = currentPlanId || accountPlanId; const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan; const hasActivePlan = Boolean(effectivePlanId); const hasPaymentMethods = paymentMethods.length > 0; const subscriptionStatus = currentSubscription?.status || (hasActivePlan ? 'active' : 'none'); const hasPendingManualPayment = payments.some((p) => p.status === 'pending_approval'); const tabs = [ { id: 'plan' as TabType, label: 'Current Plan', icon: }, { id: 'credits' as TabType, label: 'Credits Overview', icon: }, { id: 'purchase' as TabType, label: 'Purchase Credits', icon: }, { id: 'invoices' as TabType, label: 'Billing History', icon: }, ]; return (

Plans & Billing

Manage your subscription, credits, and billing information

{/* Activation / pending payment notice */} {!hasActivePlan && (
No active plan. Choose a plan below to activate your account.
)} {hasPendingManualPayment && (
We received your manual payment. It’s pending admin approval; activation will complete once approved.
)} {error && (

{error}

)} {/* Tabs */}
{/* Tab Content */}
{/* Current Plan Tab */} {activeTab === 'plan' && (
{/* 2/3 Current Plan + 1/3 Plan Features Layout */}
{/* Current Plan Card - 2/3 width */}

Your Current Plan

{!hasActivePlan && (
No active plan found. Please choose a plan to activate your account.
)}
{currentPlan?.name || 'No Plan Selected'}
{currentPlan?.description || 'Select a plan to unlock full access.'}
{hasActivePlan ? subscriptionStatus : 'plan required'}
Monthly Credits
{creditBalance?.plan_credits_per_month?.toLocaleString?.() || 0}
Current Balance
{creditBalance?.credits?.toLocaleString?.() || 0}
Period Ends
{currentSubscription?.current_period_end ? new Date(currentSubscription.current_period_end).toLocaleDateString() : '—'}
{hasActivePlan && ( )}
{/* Plan Features Card - 1/3 width with 2-column layout */}

Plan Features

{(currentPlan?.features && currentPlan.features.length > 0 ? currentPlan.features : ['ai_writer', 'image_gen', 'auto_publish', 'custom_prompts', 'email_support', 'api_access']) .map((feature: string) => (
{feature}
))}
{/* Upgrade/Downgrade Section with Pricing Table */}
{ const discount = plan.annual_discount_percent || 15; return { id: plan.id, name: plan.name, monthlyPrice: plan.price || 0, price: plan.price || 0, annualDiscountPercent: discount, period: `/${plan.interval || 'month'}`, description: plan.description || 'Standard plan', features: plan.features && plan.features.length > 0 ? plan.features : ['Monthly credits included', 'Module access', 'Email support'], buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Select Plan', highlighted: plan.is_featured || false, disabled: plan.id === currentPlanId || planLoadingId === plan.id, // Plan limits max_sites: plan.max_sites, max_users: plan.max_users, max_keywords: plan.max_keywords, max_clusters: plan.max_clusters, max_content_ideas: plan.max_content_ideas, max_content_words: plan.max_content_words, max_images_basic: plan.max_images_basic, max_images_premium: plan.max_images_premium, included_credits: plan.included_credits, }; })} showToggle={true} onPlanSelect={(plan) => plan.id && handleSelectPlan(plan.id)} />

Plan Change Policy

  • • Upgrades take effect immediately and you'll be charged a prorated amount
  • • Downgrades take effect at the end of your current billing period
  • • Unused credits from your current plan will carry over
  • • You can cancel your subscription at any time
)} {/* Credits Overview Tab */} {activeTab === 'credits' && (
Current Balance
{creditBalance?.credits.toLocaleString() || 0}
credits available
Used This Month
{creditBalance?.credits_used_this_month.toLocaleString() || 0}
credits consumed
Monthly Included
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
from your plan

Credit Usage Summary

Remaining Credits {creditBalance?.credits_remaining.toLocaleString() || 0}
{/* Credit Cost Breakdown */}

Credit Cost Analytics

Cost breakdown by operation type

{/* Credit Costs Reference */}
)} {/* Purchase Credits Tab */} {activeTab === 'purchase' && (

Purchase Additional Credits

Top up your credit balance with our packages

{packages.map((pkg) => (

{pkg.name}

{pkg.credits.toLocaleString()} credits
${pkg.price}
{pkg.description && (

{pkg.description}

)}
))} {packages.length === 0 && (
No credit packages available at this time
)}
{/* Payment Methods Info */} {!hasPaymentMethods && paymentMethods.length === 0 && (

Payment Method Required

Please contact support to set up a payment method before purchasing credits.

)}
)} {/* Billing History Tab */} {activeTab === 'invoices' && (
{/* Invoices Section */}

Invoices

{invoices.length === 0 ? ( ) : ( invoices.map((invoice) => ( )) )}
Invoice Date Amount Status Actions
No invoices yet
{invoice.invoice_number} {new Date(invoice.created_at).toLocaleDateString()} ${invoice.total_amount} {invoice.status}
{/* Payments Section */}

Payments

Recent payments and manual submissions

{payments.length === 0 ? ( ) : ( payments.map((payment) => ( )) )}
Invoice Amount Method Status Date
No payments yet
{payment.invoice_number || payment.invoice_id || '-'} ${payment.amount} {payment.payment_method} {payment.status} {new Date(payment.created_at).toLocaleDateString()}
{/* Payment Methods Section */}

Payment Methods

Manage your payment methods

{paymentMethods.map((method) => (
{method.display_name}
{method.type}
{method.instructions && (
{method.instructions}
)}
{method.is_enabled && ( Active )} {method.is_default ? ( Default ) : ( )}
))} {paymentMethods.length === 0 && (
No payment methods configured
)}
)}
); }