"use client"; import { ArrowUpRightIcon, CheckIcon, DownloadIcon, PlusIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ReactNode, useState } from "react"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; export interface BillingPlan { name: string; price: number; interval: "month" | "year"; billed: string; renewsOn: string; seatsUsed: number; seats: number; includes?: string[]; } export interface BillingLine { label: string; amount: number; } export interface BillingInvoicePreview { dueOn: string; lines: BillingLine[]; cycleDay: number; cycleLength: number; } export interface BillingCard { holder: string; last4: string; expires: string; kind?: string; } export interface BillingUsage { label: string; used: number; limit: number; unit?: string; format?: (n: number) => string; } export type InvoiceStatus = "paid" | "failed" | "refunded" | "open"; export interface BillingInvoice { id: string; date: string; amount: number; status: InvoiceStatus; } export interface BillingOverviewProps { title?: string; subtitle?: string; plan: BillingPlan; next: BillingInvoicePreview; card: BillingCard; billingEmail?: string; usage: BillingUsage[]; invoices: BillingInvoice[]; currency?: string; onChangePlan?: () => void; onUpdateCard?: () => void; onAddSeats?: () => void; onExport?: () => void; onDownload?: (invoice: BillingInvoice) => void; onRetry?: (invoice: BillingInvoice) => Promise; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const press = "transition-[box-shadow,scale,background-color] duration-150 active:scale-[0.97]"; const STATUS: Record = { paid: { label: "Paid", className: "bg-success/10 text-success-foreground" }, failed: { label: "Failed", className: "bg-destructive/10 text-destructive-foreground" }, refunded: { label: "Refunded", className: "bg-muted text-muted-foreground" }, open: { label: "Open", className: "bg-info/10 text-info-foreground" }, }; function money(n: number, currency: string) { return `${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function CardLabel({ children, action }: { children: string; action?: ReactNode }) { return (

{children}

{action}
); } function PaymentCard({ card }: { card: BillingCard }) { return (
{card.kind ?? "Business debit"}

•••• •••• •••• {card.last4}

{card.holder} {card.expires}
); } function UsageRow({ item, index }: { item: BillingUsage; index: number }) { const reduce = useReducedMotion(); const pct = Math.min(1, item.used / item.limit); const tone = pct >= 0.9 ? "warning" : "default"; const fmt = item.format ?? ((n: number) => n.toLocaleString("en-US")); return (
  • {item.label} {tone === "warning" && ( Near limit )} {fmt(item.used)} / {fmt(item.limit)} {item.unit ? ` ${item.unit}` : ""}
  • ); } function InvoiceRow({ invoice, currency, onDownload, onRetry, onPaid, }: { invoice: BillingInvoice; currency: string; onDownload?: (invoice: BillingInvoice) => void; onRetry?: (invoice: BillingInvoice) => Promise; onPaid?: (invoice: BillingInvoice) => void; }) { const [status, setStatus] = useState(invoice.status); const [retrying, setRetrying] = useState(false); const s = STATUS[status]; const retry = async () => { if (!onRetry) return; setRetrying(true); const ok = await onRetry(invoice); setRetrying(false); if (ok) { setStatus("paid"); onPaid?.(invoice); } }; return (
  • {invoice.date}

    {invoice.id}

    {invoice.id}

    {money(invoice.amount, currency)}

    {status === "failed" && onRetry ? ( ) : ( {status === "paid" && invoice.status === "failed" && } {s.label} )}
  • ); } export function BillingOverview({ title = "Billing", subtitle = "Your plan, what you’ve used this cycle, and every invoice.", plan, next, card, billingEmail, usage, invoices, currency = "$", onChangePlan, onUpdateCard, onAddSeats, onExport, onDownload, onRetry, className, }: BillingOverviewProps) { const reduce = useReducedMotion(); const total = next.lines.reduce((sum, l) => sum + l.amount, 0); const cycle = next.cycleDay / next.cycleLength; const [recovered, setRecovered] = useState([]); const failed = invoices.find((i) => i.status === "failed" && !recovered.includes(i.id)); return (

    {title}

    {subtitle}

    {failed && (

    Your {failed.date} payment didn’t go through.{" "} We’ll try again in 3 days, or retry it from the list below.

    )}
    Current plan

    {plan.name}

    {plan.billed}

    {money(plan.price, currency)} per {plan.interval} · renews{" "} {plan.renewsOn}

    Seats {plan.seatsUsed} of {plan.seats} used
    {Array.from({ length: plan.seats }, (_, i) => ( ))}
    {plan.includes && plan.includes.length > 0 && (
      {plan.includes.map((f) => (
    • {f}
    • ))}
    )}
    Next invoice

    {money(total, currency)}

    Estimated · charged {next.dueOn}

    {next.lines.map((l) => (
    {l.label}
    {money(l.amount, currency)}
    ))}
    Billing cycle Day {next.cycleDay} of {next.cycleLength}
    Update } > Payment method

    Default •••• {card.last4}

    {billingEmail && (

    Billing email {billingEmail}

    )}
    Usage this cycle
      {usage.map((u, i) => ( ))}

    Overage is billed at the end of the cycle. Usage resets {plan.renewsOn.split(",")[0]}.

    Invoices

    Date Invoice Amount Status
      {invoices.map((inv) => ( setRecovered((ids) => [...ids, i.id])} onRetry={onRetry} /> ))}
    ); }