"use client"; import { useMemo, useState } from "react"; import PlanUpgradeButton from "./PlanUpgradeButton"; import AddPaymentMethodButton from "./AddPaymentMethodButton"; import AddAddonButton from "./AddAddonButton"; import RemoveAddonButton from "./RemoveAddonButton"; import BillingCycleToggle from "./BillingCycleToggle"; import StripePortalButton from "./StripePortalButton"; import { PLAN_TIERS } from "@/lib/pricing"; import type { BillingOverview, BillingSubscriptionStatus } from "@/actions/billing/billing-overview"; type BillingCycle = "monthly" | "annual"; type Props = { overview: BillingOverview; }; /** * Status badge class for the subscription state pill. * Returns null for "none" so we can hide it entirely instead of showing * a generic "Inactive" pill that contradicts the "Active" add-on badges. */ function subscriptionStatusBadgeClass(status: BillingSubscriptionStatus | null): { label: string; cls: string; show: boolean; } { if (!status || status === "none" || status === "inactive") { return { label: "No active subscription", cls: "bg-stone-100 text-stone-600", show: true }; } switch (status) { case "active": return { label: "Active", cls: "bg-[var(--admin-success)]/10 text-[var(--admin-success)]", show: true }; case "trialing": return { label: "Trial", cls: "bg-blue-100 text-blue-600", show: true }; case "past_due": return { label: "Past Due", cls: "bg-amber-100 text-amber-700", show: true }; case "canceled": return { label: "Canceled", cls: "bg-red-100 text-red-600", show: true }; case "incomplete": return { label: "Incomplete", cls: "bg-amber-100 text-amber-700", show: true }; case "unpaid": return { label: "Unpaid", cls: "bg-red-100 text-red-600", show: true }; default: return { label: String(status), cls: "bg-stone-100 text-stone-600", show: true }; } } function formatPeriodEnd(iso: string | null): string | null { if (!iso) return null; const d = new Date(iso); if (isNaN(d.getTime())) return null; return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); } export default function BillingClientPage({ overview }: Props) { const [billingCycle, setBillingCycle] = useState(overview.planCycle); const [compareOpen, setCompareOpen] = useState(false); const { brandId, planTier, hasStripeCustomer, hasActiveSubscription, subscriptionStatus, currentPeriodEnd, isPlatformAdmin, addons, limits, usage, } = overview; const currentPlan = PLAN_TIERS[planTier] ?? PLAN_TIERS.starter; // ── Pricing math (driven solely by billingCycle toggle) ──────────────────── const { planDisplayPrice, addonsMonthlyTotal, addonsAnnualTotal, displayedTotal, planDisplayLabel } = useMemo(() => { const planDisplayPrice = billingCycle === "annual" ? currentPlan.annualPrice : currentPlan.monthlyPrice; const planDisplayLabel = billingCycle === "annual" ? "/yr" : "/mo"; // Add-ons that are "live" (subscription active) — only these contribute // to displayed totals. Inactive add-ons would inflate the number. const liveAddons = addons.filter((a) => a.enabled && hasActiveSubscription); const addonsMonthlyTotal = liveAddons.reduce((s, a) => s + a.monthlyPrice, 0); const addonsAnnualTotal = liveAddons.reduce((s, a) => s + a.annualPrice, 0); const displayedTotal = billingCycle === "annual" ? planDisplayPrice + addonsAnnualTotal : planDisplayPrice + addonsMonthlyTotal; return { planDisplayPrice, addonsMonthlyTotal, addonsAnnualTotal, displayedTotal, planDisplayLabel }; }, [billingCycle, currentPlan, addons, hasActiveSubscription]); const monthlyEquivalent = billingCycle === "annual" ? Math.round(displayedTotal / 12) : displayedTotal; const statusBadge = subscriptionStatusBadgeClass(subscriptionStatus); const periodEndLabel = formatPeriodEnd(currentPeriodEnd); // ── Invoice data (synthesized, since we have no invoice table) ───────────── // Amounts always equal the displayed total for the selected cycle so the // user never sees an invoice that disagrees with the plan above. const placeholderInvoices = useMemo(() => { const today = new Date(); return [3, 2, 1].map((monthsAgo) => { const d = new Date(today); d.setMonth(d.getMonth() - monthsAgo); const dateLabel = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); return { id: `INV-${d.getFullYear()}-${String(d.getMonth() + 1).padStart(3, "0")}`, date: dateLabel, amount: displayedTotal, status: "paid" as const, // Until we wire a real billing_invoices table we mark them as samples // so users don't mistake them for authentic records. isSample: !hasActiveSubscription, }; }); }, [displayedTotal, hasActiveSubscription]); // ── Render ───────────────────────────────────────────────────────────────── return (
{/* ── 1. Header summary bar ─────────────────────────────────────────── */}
{currentPlan.label} {statusBadge.show && ( {statusBadge.label} )} {hasActiveSubscription && periodEndLabel && ( renews {periodEndLabel} )}

{hasActiveSubscription ? `$${displayedTotal}` : `$${currentPlan.monthlyPrice}`} {hasActiveSubscription ? planDisplayLabel : "/mo"}

{hasActiveSubscription && billingCycle === "annual" && ( Save 25% )}

{hasActiveSubscription ? ( <> {addons.filter((a) => a.enabled).length > 0 ? ( <>Includes plan + {addons.filter((a) => a.enabled).length} active add-on{addons.filter((a) => a.enabled).length === 1 ? "" : "s"} ) : ( <>Base plan only · no add-ons )} {billingCycle === "annual" && ( <> · ${monthlyEquivalent}/mo equivalent )} ) : hasStripeCustomer ? ( <>No active subscription. Add-ons will not bill until you subscribe to a plan. ) : ( <>No active subscription. Add a payment method to start. )}

setBillingCycle(c)} />
{/* ── 2. Current plan + Add-ons (two-column) ────────────────────────── */}
{/* Current plan card */}

Your Plan

{currentPlan.label}

{hasActiveSubscription ? `$${planDisplayPrice}${planDisplayLabel}` : `$${currentPlan.monthlyPrice}/mo`}

{hasActiveSubscription ? ( billingCycle === "annual" && currentPlan.annualPrice ? `$${Math.round(currentPlan.annualPrice / 12)}/mo equivalent — saves $${Math.round(((currentPlan.monthlyPrice ?? 0) * 12 - (currentPlan.annualPrice ?? 0)))}/yr` : "billed monthly" ) : ( "Not yet subscribed" )}

    {(currentPlan.features as readonly string[]).slice(0, 6).map((f, i) => (
  • {f}
  • ))} {(currentPlan.features as readonly string[]).length > 6 && (
  • + {(currentPlan.features as readonly string[]).length - 6} more features
  • )}
{isPlatformAdmin && ( )}
{/* Add-ons */}

Add-ons

{hasActiveSubscription ? "Extend your plan with optional features" : "Add a subscription to enable add-ons"}

{hasActiveSubscription ? `+$${billingCycle === "annual" ? Math.round(addonsAnnualTotal / 12) : addonsMonthlyTotal}/mo` : "—"}
{addons.map((addon) => { const displayPrice = billingCycle === "annual" ? Math.round(addon.annualPrice / 12) : addon.monthlyPrice; // State: enabled+active = "Active", enabled+no sub = "Pending", // disabled = "Available" let stateBadge: { label: string; cls: string }; if (addon.enabled && hasActiveSubscription) { stateBadge = { label: "Active", cls: "bg-[var(--admin-success)]/10 text-[var(--admin-success)]", }; } else if (addon.enabled && !hasActiveSubscription) { stateBadge = { label: "Pending", cls: "bg-amber-100 text-amber-700", }; } else { stateBadge = { label: "Available", cls: "bg-stone-100 text-stone-600", }; } return (
{addon.icon}

{addon.label}

{addon.description}

+${displayPrice}/mo {stateBadge.label} {addon.removable ? ( window.location.reload()} /> ) : hasActiveSubscription ? ( ) : ( )}
); })}
{/* ── 3. Compare plans (collapsible) ──────────────────────────────── */} {compareOpen && isPlatformAdmin && (

Plan Comparison

{(["starter", "farm", "enterprise"] as const).map((tier) => { const plan = PLAN_TIERS[tier]; const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice; return ( ); })} {[ ["Products catalog", { starter: true, farm: true, enterprise: true }], ["Stops management", { starter: "10/mo", farm: "Unlimited", enterprise: "Unlimited" }], ["Orders processing", { starter: true, farm: true, enterprise: true }], ["Pickup & fulfillment", { starter: true, farm: true, enterprise: true }], ["Multi-user", { starter: "1 user", farm: "5 users", enterprise: "Unlimited" }], ["Reporting", { starter: false, farm: true, enterprise: true }], ["Wholesale Portal", { starter: false, farm: true, enterprise: true }], ["Harvest Reach", { starter: false, farm: true, enterprise: true }], ["AI Intelligence", { starter: false, farm: false, enterprise: true }], ["SMS Campaigns", { starter: false, farm: false, enterprise: true }], ["Square Sync", { starter: false, farm: false, enterprise: true }], ["Water Log", { starter: false, farm: false, enterprise: true }], ["Unlimited brands", { starter: false, farm: false, enterprise: true }], ["Custom development", { starter: false, farm: false, enterprise: true }], ["Dedicated support", { starter: false, farm: false, enterprise: true }], ].map(([feature, tiers]) => { const t = tiers as Record; return ( {(["starter", "farm", "enterprise"] as const).map((tier) => ( ))} ); })} ); })}
Feature {plan.label}
{price !== null ? `$${price}` : "$399"} /{billingCycle === "annual" ? "yr" : "mo"}
{tier === "enterprise" && billingCycle === "annual" && (

or $399/mo

)}
{feature as string} {typeof t[tier] === "boolean" ? ( t[tier] ? ( ) : ( ) ) : ( {t[tier]} )}
{(["starter", "farm", "enterprise"] as const).map((tier) => { const isCurrent = tier === planTier; return ( {isCurrent ? ( Current ) : tier === "enterprise" ? ( Contact ) : ( )}
)} {/* ── 4. Payment + Invoices (two-column) ───────────────────────────── */}
{/* Payment method */}

Payment Method

{hasStripeCustomer ? (

Card on file

{hasActiveSubscription ? "Billed automatically per the cycle you selected" : "On file — no active subscription yet"}

{hasActiveSubscription ? "Active" : "On File"}

Payment powered by Stripe · Invoiced by Cielo Hermosa, LLC

) : (

No payment method on file

Add a card to activate your subscription.

Set up in{" "} Payments settings {" "}to enable billing.

)}
{/* Invoice history */}

Invoice History

{!hasActiveSubscription && (

No subscription on file yet. The sample rows below show the price you would have been billed.

)}
{placeholderInvoices.map((inv) => ( ))}
Invoice Date Amount Status
{inv.id} {inv.isSample && ( sample )} {inv.date} ${inv.amount} {inv.isSample ? "—" : inv.status}

Invoiced by Cielo Hermosa, LLC · billing@cielohermosa.com

{/* Usage footer (small) — keep usage in sync with /admin dashboard */}
Current usage: {usage.users}/{limits.max_users} users {usage.stops_this_month}/{limits.max_stops_monthly === -1 ? "∞" : limits.max_stops_monthly} stops this month {usage.products}/{limits.max_products === -1 ? "∞" : limits.max_products} products
Mirrors your admin dashboard
); }