import type { Metadata } from "next"; import { headers } from "next/headers"; import { auth } from "@/lib/auth/server"; import { db, users, tierDefinitions, tierEnum, userUsage, eq, and } from "@epicure/db"; import { getRecipeCount, getStorageUsedMb, UNLIMITED } from "@/lib/tiers"; import { BillingPlanCards } from "@/components/settings/billing-plan-cards"; import { ManageBillingButton } from "@/components/settings/manage-billing-button"; import { UsageQuotaSection } from "@/components/settings/usage-quota-section"; import { Badge } from "@/components/ui/badge"; export const metadata: Metadata = {}; const TIER_NAMES: Record = { free: "Free", pro: "Pro", family: "Family" }; function describeLimit(n: number, unit: string): string { return n === UNLIMITED ? `Unlimited ${unit}` : `${n} ${unit}`; } export default async function BillingPage({ searchParams }: { searchParams: Promise<{ checkout?: string }> }) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; const { checkout } = await searchParams; const currentMonth = new Date().toISOString().slice(0, 7); const [dbUser, allTierDefs, usage, recipeCount, storageUsedMb] = await Promise.all([ db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { tier: true, subscriptionStatus: true, currentPeriodEnd: true, stripeCustomerId: true }, }), db.select().from(tierDefinitions), db.query.userUsage.findFirst({ where: and(eq(userUsage.userId, session.user.id), eq(userUsage.month, currentMonth)) }), getRecipeCount(session.user.id), getStorageUsedMb(session.user.id), ]); const currentTier = dbUser?.tier ?? "free"; const currentTierDef = allTierDefs.find((t) => t.tier === currentTier); const asLimit = (n: number | undefined) => (n === undefined || n === UNLIMITED ? null : n); const usageMetrics = [ { label: "AI calls", used: usage?.aiCallsUsed ?? 0, limit: asLimit(currentTierDef?.aiCallsPerMonth), unlimitedLabel: "Unlimited" }, { label: "Recipes created", used: recipeCount, limit: asLimit(currentTierDef?.maxRecipes), unlimitedLabel: "Unlimited" }, { label: "Storage", used: storageUsedMb, limit: asLimit(currentTierDef?.storageMb), unit: " MB", unlimitedLabel: "Unlimited" }, ]; const plans = tierEnum.enumValues.map((tier) => { const def = allTierDefs.find((t) => t.tier === tier); return { tier, name: TIER_NAMES[tier] ?? tier, monthlyPrice: tier === "free" ? "€0" : "Contact for pricing", yearlyPrice: null, features: def ? [ describeLimit(def.maxRecipes, "recipes"), describeLimit(def.aiCallsPerMonth, "AI calls/month"), describeLimit(def.storageMb, "MB storage"), ] : [], purchasable: tier === "free" || !!(def?.stripePriceIdMonthly), }; }); return (
{checkout === "success" && (
Payment received — your plan updates within a few seconds as Stripe confirms it. Refresh if it doesn't show up right away.
)} {checkout === "canceled" && (
Checkout canceled — no changes made.
)}

Current plan: {TIER_NAMES[currentTier] ?? currentTier}

{dbUser?.currentPeriodEnd && (

Renews {dbUser.currentPeriodEnd.toLocaleDateString()}

)}
{dbUser?.subscriptionStatus === "past_due" && ( Payment failed — update your card )}
{dbUser?.stripeCustomerId && }

Usage this month

AI calls reset monthly. Recipes and storage are lifetime totals.

Plans

); }