'use client' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { updateBillingConfig } from '@/app/actions/admin-billing' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { CreditCard, Gauge, Coins, Package, Activity, CheckCircle2, XCircle, AlertTriangle, Users, BookOpen, ChevronDown, ChevronUp, } from 'lucide-react' import { cn } from '@/lib/utils' type PriceHealth = { key: string priceId: string configured: boolean source: string stripeAmount: number | null stripeCurrency: string | null stripeActive: boolean | null error: string | null } type BillingAdminData = { billingConfig: Record usageOverview: { period: string lastSyncedAt: string | null byFeature: Array<{ feature: string; requests: number; tokens: number; users: number }> topUsers: Array<{ userId: string; email: string; name: string | null; requests: number }> } tiers: string[] creditAllocations?: Array<{ tier: string monthlyCredits: number | null unlimited: boolean }> creditCosts?: Record creditPacks?: Array<{ id: string credits: number defaultDisplay: string }> stripeHealth?: { secretConfigured: boolean secretMode: 'test' | 'live' | 'missing' | 'placeholder' publishableConfigured: boolean webhookSecretConfigured: boolean billingEnabled: boolean trialDays: number prices: PriceHealth[] } subscriptionStats?: { byTier: Record byStatus: Record cancelAtPeriodEnd: number usersWithoutSub: number trialing: number pastDue: number paidActive: number recent: Array<{ email: string name: string | null tier: string status: string trialEndsAt: string | null currentPeriodEnd: string | null cancelAtPeriodEnd: boolean hasStripeSub: boolean updatedAt: string }> } } const SUBSCRIPTION_PRICE_KEYS = [ 'STRIPE_PRICE_PRO_MONTHLY', 'STRIPE_PRICE_PRO_ANNUAL', 'STRIPE_PRICE_BUSINESS_MONTHLY', 'STRIPE_PRICE_BUSINESS_ANNUAL', ] as const const PACK_PRICE_KEYS = [ 'STRIPE_PRICE_CREDITS_S', 'STRIPE_PRICE_CREDITS_M', 'STRIPE_PRICE_CREDITS_L', ] as const function formatStableUtc(iso: string): string { const d = new Date(iso) if (Number.isNaN(d.getTime())) return iso const pad = (n: number) => String(n).padStart(2, '0') return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} UTC` } function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) { if (ok) return if (warn) return return } export function BillingAdminClient({ initialData }: { initialData: BillingAdminData }) { const { t } = useLanguage() const [billingEnabled, setBillingEnabled] = useState(initialData.billingConfig.BILLING_ENABLED === 'true') const [isSavingBilling, setIsSavingBilling] = useState(false) const [showTestGuide, setShowTestGuide] = useState(true) const health = initialData.stripeHealth const stats = initialData.subscriptionStats const priceMap = Object.fromEntries((health?.prices ?? []).map((p) => [p.key, p])) const handleSaveBilling = async (formData: FormData) => { setIsSavingBilling(true) try { const data: Record = { BILLING_ENABLED: billingEnabled ? 'true' : 'false', } for (const key of [...SUBSCRIPTION_PRICE_KEYS, ...PACK_PRICE_KEYS]) { data[key] = String(formData.get(key) ?? '') } await updateBillingConfig(data) toast.success(t('admin.billing.configSaved')) } catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('admin.billing.configFailed')) } finally { setIsSavingBilling(false) } } const modeLabel = health?.secretMode === 'test' ? t('admin.billing.modeTest') : health?.secretMode === 'live' ? t('admin.billing.modeLive') : health?.secretMode === 'placeholder' ? t('admin.billing.modePlaceholder') : t('admin.billing.modeMissing') return (

{t('admin.billing.title')}

{t('admin.billing.description')}

{/* Stripe health */} {health && (

{t('admin.billing.healthTitle')}

{t('admin.billing.healthDescription')}

{t('admin.billing.healthSecret')}

{modeLabel}

{t('admin.billing.healthPublishable')}

{health.publishableConfigured ? t('admin.billing.configured') : t('admin.billing.missing')}

{t('admin.billing.healthWebhook')}

{health.webhookSecretConfigured ? t('admin.billing.configured') : t('admin.billing.missing')}

{t('admin.billing.healthBillingFlag')}

{health.billingEnabled ? t('admin.billing.enabled') : t('admin.billing.disabled')}

{t('admin.billing.healthTrial')}

{t('admin.billing.trialDaysValue', { days: health.trialDays })}

{t('admin.billing.priceStatusTitle')}

{(health.prices ?? []).map((row) => ( ))}
{t('admin.billing.colKey')} {t('admin.billing.colPriceId')} {t('admin.billing.colSource')} {t('admin.billing.colStripe')}
{t(`admin.billing.${row.key}`)} {row.configured ? row.priceId : '—'} {row.source} {!row.configured ? ( {t('admin.billing.missing')} ) : row.error ? ( {t('admin.billing.priceError')} ) : row.stripeAmount != null ? ( {row.stripeAmount.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} {row.stripeCurrency} {row.stripeActive === false ? ` (${t('admin.billing.inactive')})` : ''} ) : ( {t('admin.billing.notChecked')} )}
)} {/* Subscription stats */} {stats && (

{t('admin.billing.subsTitle')}

{t('admin.billing.subsDescription')}

{t('admin.billing.statPaid')}

{stats.paidActive}

{t('admin.billing.statTrialing')}

{stats.trialing}

{t('admin.billing.statPastDue')}

{stats.pastDue}

{t('admin.billing.statCanceling')}

{stats.cancelAtPeriodEnd}

{t('admin.billing.byTier')}

    {Object.entries(stats.byTier).map(([tier, count]) => (
  • {tier} {count}
  • ))}
  • {t('admin.billing.usersWithoutSub')} {stats.usersWithoutSub}

{t('admin.billing.byStatus')}

    {Object.keys(stats.byStatus).length === 0 ? (
  • {t('admin.billing.noSubs')}
  • ) : ( Object.entries(stats.byStatus).map(([status, count]) => (
  • {status} {count}
  • )) )}

{t('admin.billing.recentSubs')}

{stats.recent.length === 0 ? (

{t('admin.billing.noSubs')}

) : (
{stats.recent.map((row) => ( ))}
{t('admin.billing.colUser')} {t('admin.billing.colTier')} {t('admin.billing.colStatus')} {t('admin.billing.colPeriod')}
{row.email} {row.tier} {row.status} {row.cancelAtPeriodEnd ? ` · ${t('admin.billing.canceling')}` : ''} {!row.hasStripeSub ? ` · ${t('admin.billing.manualTier')}` : ''} {row.trialEndsAt ? t('admin.billing.trialUntil', { date: formatStableUtc(row.trialEndsAt) }) : row.currentPeriodEnd ? formatStableUtc(row.currentPeriodEnd) : '—'}
)}
)} {/* How to test Stripe */}
{showTestGuide && (
  1. {t('admin.billing.testStep1')}
  2. {t('admin.billing.testStep2')}
  3. stripe listen --forward-to localhost:3000/api/billing/webhook {' — '}{t('admin.billing.testStep3')}
  4. {t('admin.billing.testStep4')}
  5. {t('admin.billing.testStep5')}
  6. {t('admin.billing.testStep6')}

{t('admin.billing.testCardHint')}

)}

{t('admin.billing.stripeConfigTitle')}

{t('admin.billing.stripeConfigDescription')}

{ e.preventDefault(); handleSaveBilling(new FormData(e.currentTarget)) }} className="p-6 space-y-6">
setBillingEnabled(!!c)} />

{t('admin.billing.subscriptionPricesTitle')}

{SUBSCRIPTION_PRICE_KEYS.map((key) => { const meta = priceMap[key] return (
{meta?.stripeAmount != null && (

Stripe: {meta.stripeAmount.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} {meta.stripeCurrency}

)} {meta?.error && (

{meta.error}

)}
) })}

{t('admin.billing.packPricesTitle')}

{t('admin.billing.packPricesDescription')}

{PACK_PRICE_KEYS.map((key) => (
))}

{t('admin.billing.secretsNote')}

{t('admin.billing.creditsTitle')}

{t('admin.billing.creditsDescription')}

{(initialData.creditAllocations ?? []).map((row) => (

{row.tier}

{row.unlimited ? t('admin.billing.modeUnlimited') : row.monthlyCredits?.toLocaleString('fr-FR')}

{!row.unlimited && (

{t('admin.billing.creditsPerMonth')}

)}
))}
{(initialData.creditPacks?.length ?? 0) > 0 && (

{t('admin.billing.packsCatalogTitle')}

{(initialData.creditPacks ?? []).map((pack) => ( {pack.id} {pack.credits.toLocaleString('fr-FR')} cr. · {pack.defaultDisplay} ))}
)}

{t('admin.billing.creditsCostsNote')}

{t('admin.billing.creditsResetHint')}

{t('admin.billing.usageTitle')}

{t('admin.billing.usagePeriod', { period: initialData.usageOverview.period })} {initialData.usageOverview.lastSyncedAt ? ` · ${t('admin.billing.lastSync', { date: formatStableUtc(initialData.usageOverview.lastSyncedAt), })}` : ` · ${t('admin.billing.notSynced')}`}

{t('admin.billing.byFeature')}

{initialData.usageOverview.byFeature.length === 0 ? (

{t('admin.billing.noUsageData')}

) : (
    {initialData.usageOverview.byFeature.map((row) => (
  • {row.feature} {row.requests} req · {row.tokens} tok
  • ))}
)}

{t('admin.billing.topUsers')}

{initialData.usageOverview.topUsers.length === 0 ? (

{t('admin.billing.noUsageData')}

) : (
    {initialData.usageOverview.topUsers.map((row) => (
  • {row.email} {row.requests} req
  • ))}
)}
) }