'use client'; import { useState, useEffect, useCallback } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { loadStripe } from '@stripe/stripe-js'; import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js'; import { Check, Shield, Zap, Crown, X, ExternalLink, Loader2, CheckCircle2, ArrowRight, Coins } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useLanguage } from '@/lib/i18n'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { motion } from 'motion/react'; import { BillingHistory } from './billing-history'; type Tier = 'PRO' | 'BUSINESS'; type Interval = 'month' | 'year'; interface BillingStatus { tier: string; effectiveTier: string; status: string; currentPeriodStart: string | null; currentPeriodEnd: string | null; cancelAtPeriodEnd: boolean; hasStripeSubscription: boolean; billingEnabled?: boolean; prices?: { PRO: { month: { display: string; amount: number; currency: string }; year: { display: string; amount: number; currency: string }; }; BUSINESS: { month: { display: string; amount: number; currency: string }; year: { display: string; amount: number; currency: string }; }; }; creditPacks?: Array<{ id: 'S' | 'M' | 'L'; credits: number; display: string; amount: number; currency: string; configured: boolean; }>; } const billingEnabledEnvFallback = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' || process.env.NODE_ENV === 'development'; let stripePromise: ReturnType | null = null; function getStripePromise(enabled: boolean) { if (!enabled) return null; if (!stripePromise && process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) { stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); } return stripePromise; } export function BillingPlans() { const { t } = useLanguage(); const queryClient = useQueryClient(); const [interval, setInterval] = useState('month'); const [checkoutClientSecret, setCheckoutClientSecret] = useState(null); const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); const [checkoutLoading, setCheckoutLoading] = useState(null); const [packLoading, setPackLoading] = useState<'S' | 'M' | 'L' | null>(null); const [portalLoading, setPortalLoading] = useState(false); const [cancelLoading, setCancelLoading] = useState(false); const [successBanner, setSuccessBanner] = useState(null); const { data: status, isLoading } = useQuery({ queryKey: ['billing', 'status'], queryFn: async () => { const search = typeof window !== 'undefined' ? window.location.search : ''; const res = await fetch(`/api/billing/status${search}`); if (!res.ok) throw new Error(t('billing.fetchStatusFailed')); return res.json(); }, }); const { data: usageData } = useQuery({ queryKey: ['usage', 'current'], queryFn: async () => { const res = await fetch('/api/usage/current'); if (!res.ok) throw new Error(t('billing.fetchQuotasFailed')); return res.json(); }, refetchInterval: 30000, }); const quotas = usageData?.quotas; const balance = usageData?.balance as | { totalRemaining: number | null subscriptionGranted: number | null purchasedRemaining: number spentThisPeriod: number unlimited: boolean } | undefined const breakdown = (usageData?.breakdown ?? []) as Array<{ feature: string creditsUsed: number actionsCount: number }> const billingEnabled = status?.billingEnabled ?? billingEnabledEnvFallback; const stripe = getStripePromise(billingEnabled); useEffect(() => { const params = new URLSearchParams(window.location.search); const sessionId = params.get('session_id'); const isPack = params.get('pack') === '1'; if (sessionId) { if (isPack) { setSuccessBanner(t('billing.packCheckoutSuccess')); } else { const tier = status?.effectiveTier ?? 'Pro'; setSuccessBanner(t('billing.checkoutSuccessBody').replace('{tier}', tier)); } queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }); queryClient.invalidateQueries({ queryKey: ['billing', 'status'] }); window.history.replaceState({}, '', '/settings/billing'); } }, [status, t, queryClient]); useEffect(() => { if (successBanner) { const timer = setTimeout(() => { setSuccessBanner(null); }, 5000); return () => clearTimeout(timer); } }, [successBanner]); const handleCheckout = async (tier: Tier) => { if (!billingEnabled) return; setCheckoutLoading(tier); try { const res = await fetch('/api/billing/create-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tier, interval }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error ?? 'Failed to create checkout'); if (data.clientSecret) { setCheckoutClientSecret(data.clientSecret); setIsCheckoutOpen(true); } else if (data.url) { window.location.href = data.url; } } catch (err) { console.error('[BillingPlans] checkout error:', err); toast.error(t('billing.checkoutFailed')); } finally { setCheckoutLoading(null); } }; const handleBuyPack = async (packId: 'S' | 'M' | 'L') => { if (!billingEnabled) return; setPackLoading(packId); try { const res = await fetch('/api/billing/create-pack-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ packId }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error ?? 'Failed to create pack checkout'); if (data.url) { window.location.href = data.url; } else { throw new Error('No checkout URL'); } } catch (err) { console.error('[BillingPlans] pack checkout error:', err); toast.error(t('billing.packCheckoutFailed')); } finally { setPackLoading(null); } }; const handlePortal = async (action: 'portal' | 'cancel' | React.MouseEvent = 'portal') => { const actualAction = typeof action === 'string' ? action : 'portal'; setPortalLoading(true); try { const res = await fetch('/api/billing/portal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: actualAction }), }); const data = await res.json(); if (!res.ok) { toast.error(data.error || t('billing.portalFailed')); return; } window.location.href = data.url; } catch (err) { console.error('[BillingPlans] portal error:', err); toast.error(t('billing.portalFailed')); } finally { setPortalLoading(false); } }; const handleCancelSubscription = async () => { const confirmMsg = t('billing.cancelConfirm'); if (!window.confirm(confirmMsg)) { return; } setCancelLoading(true); try { const res = await fetch('/api/billing/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); if (!res.ok) { toast.error(data.error || t('billing.cancelFailed')); return; } toast.success(t('billing.cancelSuccess')); queryClient.invalidateQueries({ queryKey: ['billing', 'status'] }); queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }); } catch (err) { console.error('[BillingPlans] cancel error:', err); toast.error(t('billing.cancelFailed')); } finally { setCancelLoading(false); } }; const handleCheckoutComplete = useCallback(() => { setIsCheckoutOpen(false); setCheckoutClientSecret(null); const tier = status?.effectiveTier ?? 'Pro'; setSuccessBanner(t('billing.checkoutSuccessBody').replace('{tier}', tier)); queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }); queryClient.invalidateQueries({ queryKey: ['billing', 'status'] }); }, [status, t, queryClient]); if (isLoading) { return (
); } const effectiveTier = status?.effectiveTier ?? 'BASIC'; const isPaid = effectiveTier !== 'BASIC'; const plans = [ { id: 'free', name: t('billing.freePlan'), price: t('billing.freePrice') || 'Gratuit', period: '', description: t('billing.freeDescription') || 'Pour découvrir Memento.', features: [ t('billing.freeF1'), t('billing.freeF2'), t('billing.freeF3'), t('billing.freeF4'), t('billing.freeF5'), ], current: effectiveTier === 'BASIC', buttonText: effectiveTier === 'BASIC' ? (t('billing.currentPlan') || 'Plan Actuel') : t('billing.startCheckout'), buttonClass: effectiveTier === 'BASIC' ? 'bg-paper text-concrete cursor-default' : 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95', onClick: () => {}, }, { id: 'pro', name: t('billing.proPlan'), price: status?.prices?.PRO?.[interval]?.display ?? (interval === 'month' ? (t('billing.proPrice') || '9,90€') : (t('billing.proAnnualPrice') || '99€')), period: interval === 'month' ? t('billing.perMonth') : t('billing.perYear'), description: t('billing.proDescription') || 'Pour les consultants et créateurs exigeants.', features: [ t('billing.proFeature1'), t('billing.proFeature2'), t('billing.proFeature3'), t('billing.proFeature4'), t('billing.proFeature5'), t('billing.proFeature6'), ], current: effectiveTier === 'PRO', popular: true, buttonText: effectiveTier === 'PRO' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.proCta') || 'Passer au Plan Pro'), buttonClass: effectiveTier === 'PRO' ? 'bg-paper text-concrete cursor-default' : 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95', onClick: () => handleCheckout('PRO'), }, { id: 'business', name: t('billing.businessPlan'), price: status?.prices?.BUSINESS?.[interval]?.display ?? (interval === 'month' ? (t('billing.businessPrice') || '29,90€') : (t('billing.businessAnnualPrice') || '299€')), period: interval === 'month' ? t('billing.perMonth') : t('billing.perYear'), features: [ t('billing.businessFeature1'), t('billing.businessFeature2'), t('billing.businessFeature3'), t('billing.businessFeature4'), t('billing.businessFeature5'), t('billing.businessFeature6'), ], current: effectiveTier === 'BUSINESS', buttonText: effectiveTier === 'BUSINESS' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.businessCta') || 'Choisir Plan Business'), buttonClass: effectiveTier === 'BUSINESS' ? 'bg-paper text-concrete cursor-default' : 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95', onClick: () => handleCheckout('BUSINESS'), }, { id: 'enterprise', name: t('billing.enterpriseTitle') || 'Enterprise', price: t('billing.contactSales') || 'Sur devis', period: '', description: t('billing.enterpriseDescription') || 'Crédits illimités ou pool dédié, SSO, support prioritaire.', features: [ t('billing.enterpriseFeature1'), t('billing.enterpriseFeature2'), t('billing.enterpriseFeature3'), t('billing.enterpriseFeature4'), t('billing.enterpriseFeature5'), ], current: effectiveTier === 'ENTERPRISE', buttonText: effectiveTier === 'ENTERPRISE' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.contactSales') || 'Contact Sales'), buttonClass: effectiveTier === 'ENTERPRISE' ? 'bg-paper text-concrete cursor-default' : 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95', onClick: () => { window.location.href = 'mailto:sales@memento-note.com'; }, }, ]; const plansToShow = isPaid ? plans.filter((p) => p.id !== 'free') : plans; const formatDate = (dateStr: string | null | undefined) => { if (!dateStr) return '—'; try { const date = new Date(dateStr); const locale = typeof window !== 'undefined' ? window.navigator.language : 'fr-FR'; return new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'long', year: 'numeric', }).format(date); } catch (e) { return dateStr; } }; // Solde crédits global (modèle A) — pas la somme des anciens seaux const unlimitedCredits = !!balance?.unlimited const totalUsed = balance?.spentThisPeriod ?? 0 const remainingCredits = unlimitedCredits ? null : (balance?.totalRemaining ?? 0) const granted = unlimitedCredits ? null : (balance?.subscriptionGranted ?? 0) + (balance?.purchasedRemaining ?? 0) // pot ≈ restants + déjà utilisés sur la fenêtre courante const totalLimit = unlimitedCredits ? 0 : Math.max(granted ?? 0, totalUsed + (remainingCredits ?? 0)) const globalPct = unlimitedCredits || totalLimit <= 0 ? 0 : Math.min(100, (totalUsed / totalLimit) * 100) // SVG Donut Config const radius = 28; const strokeWidth = 5; const circumference = 2 * Math.PI * radius; const strokeDashoffset = circumference - (Math.min(globalPct, 100) / 100) * circumference; const donutColor = globalPct >= 100 ? '#f43f5e' : globalPct >= 75 ? '#f59e0b' : '#a78bfa'; // Rose, Ambre, Violet return ( {/* Success Banner */} {successBanner && (

{t('billing.checkoutSuccessTitle') || 'Abonnement activé !'}

{successBanner}

)} {/* Current Plan Card & Usage Ring */}

{t('billing.currentPlan')}

{effectiveTier === 'BASIC' ? t('billing.freePlan') : effectiveTier === 'PRO' ? t('billing.proPlan') : effectiveTier === 'BUSINESS' ? t('billing.businessPlan') : t('billing.enterprisePlan')}

{isPaid && (
{status?.status ? t(`billing.${status.status.toLowerCase()}`) || status.status : t('billing.active')}
)}
{isPaid && (
{t('billing.billingPeriod')}

{status?.currentPeriodStart && status?.currentPeriodEnd ? ( `${formatDate(status.currentPeriodStart)} – ${formatDate(status.currentPeriodEnd)}` ) : ( '—' )}

{status?.cancelAtPeriodEnd ? t('billing.expiresOn') : t('billing.nextBillingDate')}

{status?.currentPeriodEnd ? formatDate(status.currentPeriodEnd) : '—'}

)} {isPaid && (
{status?.hasStripeSubscription && !status?.cancelAtPeriodEnd && ( )}
)}
{/* Global Usage Donut */}
{/* Background circle */} {/* Foreground circle */}
{unlimitedCredits ? '∞' : `${Math.round(globalPct)}%`} {t('billing.used')}

{unlimitedCredits ? ( {t('billing.unlimited')} ) : ( <> {totalUsed}{' '} / {totalLimit} {t('billing.aiCredits') || 'crédits IA'} )}

{!unlimitedCredits && remainingCredits != null && (

{remainingCredits} {t('billing.creditsRemaining')?.toLowerCase() || 'restants'}

)} {!unlimitedCredits && (balance?.purchasedRemaining ?? 0) > 0 && (

{balance?.purchasedRemaining} {t('billing.creditsFromPacks')?.toLowerCase() || 'de packs'}

)}
{/* Packs de crédits (one-shot) */} {!unlimitedCredits && billingEnabled && (status?.creditPacks?.length ?? 0) > 0 && (

{t('billing.packsTitle')}

{t('billing.packsDescription')}

{(status?.creditPacks ?? []).map((pack) => (

{t(`billing.pack${pack.id}Name`) || `Pack ${pack.id}`}

{pack.credits.toLocaleString()} {t('billing.aiCredits') || 'crédits'}

{pack.display}

))}
)} {/* Répartition crédits par fonction */}

{t('billing.creditsWhereTitle') || t('billing.usageThisPeriod')}

{t('billing.creditsUsageLegend')}

{!usageData && (
)} {usageData && (() => { const featureLabels: Record = { semantic_search: t('usageMeter.featureSearch'), auto_tag: t('usageMeter.featureTags'), auto_title: t('usageMeter.featureTitles'), reformulate: t('usageMeter.featureReformulate'), chat: t('usageMeter.featureChat'), brainstorm_create: t('usageMeter.featureBrainstormSessions'), brainstorm_expand: t('usageMeter.featureBrainstormExpand'), brainstorm_enrich: t('usageMeter.featureBrainstormEnrich'), suggest_charts: t('usageMeter.featureCharts'), publish_enhance: t('usageMeter.featurePublishEnhance'), ai_flashcard: t('usageMeter.featureFlashcards'), voice_transcribe: t('usageMeter.featureVoice'), slide_generate: t('usageMeter.featureSlides'), excalidraw_generate: t('usageMeter.featureDiagrams'), } const byFeature = new Map(breakdown.map((r) => [r.feature, r])) return Object.keys(featureLabels).map((key) => { const row = byFeature.get(key) const used = row?.creditsUsed ?? 0 const pct = totalUsed > 0 && used > 0 ? (used / totalUsed) * 100 : 0 const barFillColor = pct >= 40 ? 'bg-gradient-to-r from-violet-400 to-purple-400' : 'bg-gradient-to-r from-violet-300/80 to-purple-300/80' return (
{featureLabels[key]} {used > 0 ? `${used} cr.` : '0'}
) }) })()}
{/* Billing History (Only for paid users) */} {isPaid && } {/* Interval Toggle & Plan Cards */} {!isPaid && (

{t('billing.upgradePlan') || 'Changer de plan'}

{billingEnabled ? (
) : (

{t('billing.disabledByAdmin')}

)}
{plansToShow.map((plan) => (
{plan.popular && (
{t('billing.recommended') || 'Recommandé'}
)}

{plan.name}

{plan.price} {plan.period}

{plan.description}

{plan.features.map((feature, i) => (
{feature}
))}
))}
)} {/* Footer Info */}
{t('billing.secureTransactions') || 'Transactions sécurisées'}

{t('billing.secureDesc') || 'Paiement via Stripe. Annulez à tout moment, sans engagement.'}

{t('billing.instantActivation') || 'Activation instantanée'}
{t('billing.satisfactionGuarantee') || 'Garantie satisfait'}
{/* Embedded Checkout Modal */} {isCheckoutOpen && checkoutClientSecret && (

{t('billing.upgradeTitle')}

)} ); }