/** * Plans & Billing Page - Subscription & Payment Management * Tabs: Current Plan, Upgrade Plan, Billing History * Tab selection driven by URL path for sidebar navigation * * Note: Usage tracking is consolidated in UsageAnalyticsPage (/account/usage) */ import { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle, Loader2, AlertCircle, CheckCircle, Download, Zap, Globe, Users, X } 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 { PricingPlan } from '../../components/ui/pricing-table'; import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1'; import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel'; // import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; // Hidden from regular users // import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; // Moved to UsageAnalyticsPage import { convertToPricingPlan } from '../../utils/pricingHelpers'; 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' | 'upgrade' | 'invoices'; // Map URL paths to tab types function getTabFromPath(pathname: string): TabType { if (pathname.includes('/upgrade')) return 'upgrade'; if (pathname.includes('/history')) return 'invoices'; return 'plan'; } export default function PlansAndBillingPage() { const location = useLocation(); // Derive active tab from URL path const activeTab = getTabFromPath(location.pathname); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [planLoadingId, setPlanLoadingId] = useState(null); const [purchaseLoadingId, setPurchaseLoadingId] = useState(null); const [showCancelConfirm, setShowCancelConfirm] = useState(false); // 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'); // Page titles based on active tab const pageTitles = { plan: { title: 'Current Plan', description: 'View your subscription details and features' }, upgrade: { title: 'Upgrade Plan', description: 'Compare plans and upgrade your subscription' }, invoices: { title: 'Billing History', description: 'View invoices and manage payment methods' }, }; return (
{/* Page Header with Breadcrumb */}
Plans & Billing {pageTitles[activeTab].title}

{pageTitles[activeTab].title}

{pageTitles[activeTab].description}

{/* 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}

)} {/* Tab Content */}
{/* Current Plan Tab */} {activeTab === 'plan' && (
{/* Current Plan Overview */}
{/* Main Plan Card */}

Your Current Plan

{!hasActivePlan && (

No Active Plan

Choose a plan below to activate your account and unlock all features.

)}
{currentPlan?.name || 'No Plan Selected'}
{currentPlan?.description || 'Select a plan to unlock full access.'}
{hasActivePlan ? subscriptionStatus : 'Inactive'}
{/* Quick Stats Grid */}
Monthly Credits
{creditBalance?.plan_credits_per_month?.toLocaleString?.() || 0}
Current Balance
{creditBalance?.credits?.toLocaleString?.() || 0}
Renewal Date
{currentSubscription?.current_period_end ? new Date(currentSubscription.current_period_end).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'}
{/* Action Buttons */}
{hasActivePlan && ( )}
{/* Plan Features Card */}

Included Features

{(currentPlan?.features && currentPlan.features.length > 0 ? currentPlan.features : ['AI Content Writer', 'Image Generation', 'Auto Publishing', 'Custom Prompts', 'Email Support', 'API Access']) .map((feature: string, index: number) => (
{feature}
))}
{/* Plan Limits Overview */} {hasActivePlan && (

Quick Limits Overview

Key plan limits at a glance

Sites
{currentPlan?.max_sites === 9999 ? '∞' : currentPlan?.max_sites || 0}
Team Members
{currentPlan?.max_users === 9999 ? '∞' : currentPlan?.max_users || 0}
Content Words/mo
{currentPlan?.max_content_words === 9999999 ? '∞' : currentPlan?.max_content_words ? `${(currentPlan.max_content_words / 1000).toFixed(0)}K` : 0}
Monthly Credits
{currentPlan?.included_credits?.toLocaleString?.() || 0}
)}
)} {/* Purchase/Upgrade Tab */} {activeTab === 'upgrade' && (
{/* Upgrade Plans Section */}

{hasActivePlan ? 'Upgrade or Change Your Plan' : 'Choose Your Plan'}

Select the plan that best fits your needs

{ // Only show paid plans (exclude Free Plan) const planName = (plan.name || '').toLowerCase(); const planPrice = plan.price || 0; return planPrice > 0 && !planName.includes('free'); }) .map(plan => ({ ...convertToPricingPlan(plan), buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Choose Plan', disabled: plan.id === currentPlanId || planLoadingId === plan.id, }))} showToggle={true} onPlanSelect={(plan) => plan.id && handleSelectPlan(plan.id)} />
{/* Plan Change Policy */}

Plan Change Policy

  • Upgrades take effect immediately with prorated billing
  • Downgrades take effect at the end of your current billing period
  • Unused credits carry over when changing plans
  • Cancel anytime - no long-term commitments
{/* Purchase Additional Credits Section - Hidden from regular users - removed for simplification */}
)} {/* 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
)}
)}
{/* Cancellation Confirmation Modal */} {showCancelConfirm && (

Cancel Subscription

Are you sure you want to cancel?

Your subscription will remain active until the end of your current billing period. After that:

  • You'll lose access to premium features
  • Remaining credits will be preserved for 30 days
  • You can resubscribe anytime to restore access
)}
); }