"use client"; import { CreditCard, Loader2, Wallet } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../../../shadcnui"; import { PaymentMethodInterface, StripeCustomerInterface, StripeCustomerService } from "../../stripe-customer"; import { PaymentMethodsContainer } from "../../stripe-customer/components"; import { StripeInvoiceInterface, StripeInvoiceService } from "../../stripe-invoice"; import { InvoicesContainer } from "../../stripe-invoice/components"; import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../../stripe-subscription"; import { SubscriptionsContainer } from "../../stripe-subscription/components"; import { SubscriptionWizard } from "../../stripe-subscription/components/wizards"; import { MeterInterface, MeterSummaryInterface, StripeUsageService } from "../../stripe-usage"; import { UsageContainer } from "../../stripe-usage/components"; import { BillingUsageSummaryCard, CustomerInfoCard, InvoicesSummaryCard, PaymentMethodSummaryCard, SubscriptionSummaryCard, } from "../cards"; import { BillingDetailModal } from "../modals/BillingDetailModal"; import { BillingAlertBanner } from "../widgets/BillingAlertBanner"; type ModalType = "subscriptions" | "payment-methods" | "invoices" | "usage" | null; type DataState = { customer: StripeCustomerInterface | null; subscriptions: StripeSubscriptionInterface[]; paymentMethods: PaymentMethodInterface[]; invoices: StripeInvoiceInterface[]; meters: MeterInterface[]; meterSummaries: Record; }; type LoadingState = { customer: boolean; subscriptions: boolean; paymentMethods: boolean; invoices: boolean; usage: boolean; }; type ErrorState = { customer: string | null; subscriptions: string | null; paymentMethods: string | null; invoices: string | null; usage: string | null; }; export function BillingDashboardContainer() { const [data, setData] = useState({ customer: null, subscriptions: [], paymentMethods: [], invoices: [], meters: [], meterSummaries: {}, }); const [loading, setLoading] = useState({ customer: true, subscriptions: true, paymentMethods: true, invoices: true, usage: true, }); const [errors, setErrors] = useState({ customer: null, subscriptions: null, paymentMethods: null, invoices: null, usage: null, }); const [activeModal, setActiveModal] = useState(null); const [noCustomerExists, setNoCustomerExists] = useState(false); const [creatingCustomer, setCreatingCustomer] = useState(false); const searchParams = useSearchParams(); // Wizard state - lifted from SubscriptionsContainer to avoid nested dialogs const [showWizard, setShowWizard] = useState(false); const [editingSubscription, setEditingSubscription] = useState(null); // Check if company has metered subscriptions const hasMeteredSubscriptions = useCallback((): boolean => { return data.subscriptions.some((sub) => sub.price?.recurring?.usageType === "metered"); }, [data.subscriptions]); // Check if user has active recurring subscription (for wizard filtering) const hasActiveRecurringSubscription = useMemo(() => { return data.subscriptions.some( (sub) => (sub.status === SubscriptionStatus.ACTIVE || sub.status === SubscriptionStatus.TRIALING) && sub.price?.priceType === "recurring", ); }, [data.subscriptions]); // Fetch all data - first check customer, then fetch rest if customer exists const fetchAllData = useCallback(async () => { setNoCustomerExists(false); // First, try to fetch customer let customer: StripeCustomerInterface | null = null; try { customer = await StripeCustomerService.getCustomer(); setData((prev) => ({ ...prev, customer })); setErrors((prev) => ({ ...prev, customer: null })); setNoCustomerExists(false); } catch (error: unknown) { console.error("[BillingDashboard] Failed to load customer:", error); // A 404 means the company has no billing customer yet - a setup prompt, not an error. // Match on the status, never on the message: `statusText` is empty over HTTP/2, so the // error text carries no reason phrase to match against. if ((error as { status?: number })?.status === 404) { setNoCustomerExists(true); } else { setErrors((prev) => ({ ...prev, customer: "Failed to load billing account" })); } } finally { setLoading((prev) => ({ ...prev, customer: false })); } // Without a customer there is nothing downstream to fetch. Release every section here, or the // cards that are only cleared inside their own fetchers stay skeletons forever. if (!customer) { setLoading({ customer: false, subscriptions: false, paymentMethods: false, invoices: false, usage: false, }); return; } // Fetch subscriptions const fetchSubscriptions = async () => { try { const subscriptions = await StripeSubscriptionService.listSubscriptions(); setData((prev) => ({ ...prev, subscriptions })); setErrors((prev) => ({ ...prev, subscriptions: null })); return subscriptions; } catch (error) { console.error("[BillingDashboard] Failed to load subscriptions:", error); setErrors((prev) => ({ ...prev, subscriptions: "Failed to load subscriptions" })); return []; } finally { setLoading((prev) => ({ ...prev, subscriptions: false })); } }; // Fetch payment methods const fetchPaymentMethods = async () => { try { const paymentMethods = await StripeCustomerService.listPaymentMethods(); setData((prev) => ({ ...prev, paymentMethods })); setErrors((prev) => ({ ...prev, paymentMethods: null })); } catch (error) { console.error("[BillingDashboard] Failed to load payment methods:", error); setErrors((prev) => ({ ...prev, paymentMethods: "Failed to load payment methods" })); } finally { setLoading((prev) => ({ ...prev, paymentMethods: false })); } }; // Fetch invoices const fetchInvoices = async () => { try { const invoices = await StripeInvoiceService.listInvoices(); setData((prev) => ({ ...prev, invoices })); setErrors((prev) => ({ ...prev, invoices: null })); } catch (error) { console.error("[BillingDashboard] Failed to load invoices:", error); setErrors((prev) => ({ ...prev, invoices: "Failed to load invoices" })); } finally { setLoading((prev) => ({ ...prev, invoices: false })); } }; // Execute all in parallel const [subscriptions] = await Promise.all([fetchSubscriptions(), fetchPaymentMethods(), fetchInvoices()]); // Check if there are metered subscriptions and fetch usage data const hasMetered = subscriptions.some( (sub: StripeSubscriptionInterface) => sub.price?.recurring?.usageType === "metered", ); if (hasMetered) { await fetchUsageData(); } else { setLoading((prev) => ({ ...prev, usage: false })); } }, []); // Create a new Stripe customer for the company const handleCreateCustomer = async () => { setCreatingCustomer(true); try { await StripeCustomerService.createCustomer(); setNoCustomerExists(false); // Refresh all data after customer creation await fetchAllData(); } catch (error) { console.error("[BillingDashboard] Failed to create customer:", error); setErrors((prev) => ({ ...prev, customer: "Failed to set up billing" })); } finally { setCreatingCustomer(false); } }; // Fetch usage data (called conditionally) const fetchUsageData = async () => { try { const meters = await StripeUsageService.listMeters(); setData((prev) => ({ ...prev, meters })); // Load summaries for each meter (current month) const summariesMap: Record = {}; const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); for (const meter of meters) { try { const meterSummaries = await StripeUsageService.getMeterSummaries({ meterId: meter.id, startTime: startOfMonth, endTime: endOfMonth, }); summariesMap[meter.id] = meterSummaries.length > 0 ? meterSummaries[0] : null; } catch (error) { console.error(`[BillingDashboard] Failed to load summaries for meter ${meter.id}:`, error); summariesMap[meter.id] = null; } } setData((prev) => ({ ...prev, meterSummaries: summariesMap })); setErrors((prev) => ({ ...prev, usage: null })); } catch (error) { console.error("[BillingDashboard] Failed to load usage data:", error); setErrors((prev) => ({ ...prev, usage: "Failed to load usage data" })); } finally { setLoading((prev) => ({ ...prev, usage: false })); } }; // Refresh data after modal actions const refreshData = useCallback(async () => { // Reset loading states for a refresh setLoading({ customer: true, subscriptions: true, paymentMethods: true, invoices: true, usage: true, }); await fetchAllData(); }, [fetchAllData]); // Handler to open wizard (can be called from SubscriptionsContainer) const handleOpenWizard = useCallback((subscription?: StripeSubscriptionInterface) => { setEditingSubscription(subscription || null); setShowWizard(true); }, []); // Handler to close wizard const handleWizardClose = useCallback(() => { setShowWizard(false); setEditingSubscription(null); refreshData(); }, [refreshData]); // Initial data load useEffect(() => { fetchAllData(); }, [fetchAllData]); // Handle URL action parameter for deep linking useEffect(() => { const action = searchParams.get("action"); if (action === "subscribe") { // Open wizard directly (no wrapper modal) setShowWizard(true); // Clear the URL param to prevent re-triggering on refresh window.history.replaceState({}, "", window.location.pathname); } }, [searchParams]); // Detect critical subscriptions for alert banners const criticalSubscriptions = data.subscriptions.filter( (sub) => sub.status === SubscriptionStatus.PAST_DUE || (sub.status === SubscriptionStatus.TRIALING && sub.trialEnd && new Date(sub.trialEnd).getTime() - new Date().getTime() <= 7 * 24 * 60 * 60 * 1000), ); // Handle modal close with refresh const handleModalClose = (open: boolean) => { if (!open) { setActiveModal(null); refreshData(); } }; // Get modal title based on type const getModalTitle = (type: ModalType): string => { switch (type) { case "subscriptions": return "Manage Subscriptions"; case "payment-methods": return "Payment Methods"; case "invoices": return "Invoice History"; case "usage": return "Usage Tracking"; default: return ""; } }; // Show loading state while checking for customer const isInitialLoading = loading.customer && !noCustomerExists && !data.customer; return (
{/* Header */}

Billing

{/* Initial Loading State */} {isInitialLoading && ( )} {/* No Customer State - Show Setup Prompt */} {noCustomerExists && !isInitialLoading && (
Set Up Billing Your company doesn't have a billing account yet. Set one up to manage subscriptions, payment methods, and view invoices.
{errors.customer && (

{errors.customer}

)}
)} {/* Main Dashboard Content - Only shown when customer exists */} {!noCustomerExists && !isInitialLoading && ( <> {/* Alert Banners */} {criticalSubscriptions.map((subscription) => ( setActiveModal("payment-methods")} onAddPayment={() => setActiveModal("payment-methods")} /> ))} {/* Summary Cards Grid */}
{ if (data.subscriptions.length === 0) { // No subscriptions - open wizard directly setShowWizard(true); } else { // Has subscriptions - open manage modal setActiveModal("subscriptions"); } }} /> setActiveModal("payment-methods")} /> setActiveModal("invoices")} /> {/* Usage Card - only shown when metered subscriptions exist */} {hasMeteredSubscriptions() && ( setActiveModal("usage")} /> )}
{/* Detail Modals */} {/* Subscription Wizard - rendered at dashboard level to avoid nested dialogs */} !open && handleWizardClose()} onSuccess={refreshData} hasActiveRecurringSubscription={hasActiveRecurringSubscription} subscription={editingSubscription ?? undefined} /> )}
); }