'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, Activity, Clock, ArrowRight } 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 }; }; }; } 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 [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('Failed to fetch billing status'); 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('Failed to fetch quotas'); return res.json(); }, refetchInterval: 30000, }); const quotas = usageData?.quotas; const billingEnabled = status?.billingEnabled ?? billingEnabledEnvFallback; const stripe = getStripePromise(billingEnabled); useEffect(() => { const params = new URLSearchParams(window.location.search); const sessionId = params.get('session_id'); if (sessionId) { 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('Failed to start checkout. Please try again.'); } finally { setCheckoutLoading(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 || 'Failed to open billing portal.'); return; } window.location.href = data.url; } catch (err) { console.error('[BillingPlans] portal error:', err); toast.error('Failed to open billing portal.'); } finally { setPortalLoading(false); } }; const handleCancelSubscription = async () => { const confirmMsg = t('billing.cancelConfirm') || "Êtes-vous sûr de vouloir résilier votre abonnement ? Vous conserverez vos accès Pro/Business jusqu'à la fin de la période en cours."; 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 || 'Failed to cancel subscription.'); return; } toast.success(t('billing.cancelSuccess') || "Votre abonnement a été résilié avec succès. Il prendra fin à la fin de la période de facturation en cours."); queryClient.invalidateQueries({ queryKey: ['billing', 'status'] }); queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }); } catch (err) { console.error('[BillingPlans] cancel error:', err); toast.error('Failed to cancel subscription.'); } 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 aiUsed = quotas?.semantic_search?.used ?? 0; const aiLimit = quotas?.semantic_search?.limit ?? 50; const aiPct = aiLimit > 0 ? (aiUsed / aiLimit) * 100 : 0; const plans = [ { id: 'free', name: t('billing.freePlan'), price: t('billing.freePrice') || 'Gratuit', period: '', description: t('billing.freeDescription') || 'Pour découvrir la magie de Memento.', features: [ t('billing.freeF1') || '100 Notes max', t('billing.freeF2') || '3 Carnets', t('billing.freeF3') || '50 crédits IA (Lifetime)', t('billing.freeF4') || 'Recherche sémantique', t('billing.freeF5') || 'Historique 7 jours', ], 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' ? '/mois' : '/an', description: t('billing.proDescription') || 'Pour les consultants et créateurs exigeants.', features: [ t('billing.proFeature1') || 'Notes illimitées', t('billing.proFeature2') || 'BYOK (OpenAI/Anthropic)', t('billing.proFeature3') || '200 recherches sémantiques', t('billing.proFeature4') || 'Agents (12 runs/mois)', t('billing.proFeature5') || 'Historique 30 jours', t('billing.proFeature6') || 'Support Email', ], 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' ? '/mois' : '/an', description: t('billing.businessDescription') || 'Pour les équipes et chefs de produit.', features: [ t('billing.businessFeature1') || '10 Collaborateurs inclus', t('billing.businessFeature2') || 'BYOK (13 fournisseurs)', t('billing.businessFeature3') || '1000 recherches sémantiques', t('billing.businessFeature4') || 'Agents (60 runs/mois)', t('billing.businessFeature5') || 'Brainstorm illimité', t('billing.businessFeature6') || 'Accès API', ], 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') || 'Quotas personnalisés, SSO, support prioritaire.', features: [ t('billing.enterpriseFeature1') || 'Quotas illimités', t('billing.enterpriseFeature2') || 'SSO / SAML', t('billing.enterpriseFeature3') || 'Support dédié', t('billing.enterpriseFeature4') || 'Facturation personnalisée', t('billing.enterpriseFeature5') || 'SLA garanti', ], 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; } }; // Sommes pour l'usage global agrégé (donut) let totalUsed = 0; let totalLimit = 0; if (quotas) { Object.entries(quotas as Record).forEach(([_, q]) => { if (q.limit > 0 && q.limit !== Infinity) { totalUsed += q.used; totalLimit += q.limit; } }); } const globalPct = totalLimit > 0 ? (totalUsed / totalLimit) * 100 : 0; // 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 */}
{Math.round(globalPct)}% {t('billing.used')}

{totalUsed} / {totalLimit} {t('billing.aiCredits') || 'crédits IA'}

{/* Usage breakdown per feature */}

{t('billing.usageThisPeriod') || 'Utilisation sur cette période'}

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

{!quotas && (
)} {quotas && Object.entries(quotas as Record).map(([key, q]) => { const pct = q.limit > 0 && q.limit !== Infinity ? (q.used / q.limit) * 100 : 0; const isUnlimited = q.limit === Infinity || q.limit <= 0; const featureLabels: Record = { semantic_search: t('usageMeter.featureSearch') || 'Recherche Sémantique', auto_tag: t('usageMeter.featureTags') || 'Tags Automatiques', auto_title: t('usageMeter.featureTitles') || 'Titres Automatiques', reformulate: t('usageMeter.featureReformulate') || 'Reformulation IA', chat: t('usageMeter.featureChat') || 'Chat IA', brainstorm_create: t('usageMeter.featureBrainstormCreate') || 'Création de Brainstorm', brainstorm_expand: t('usageMeter.featureBrainstormExpand') || 'Expansion de Brainstorm', brainstorm_enrich: t('usageMeter.featureBrainstormEnrich') || 'Enrichissement de Brainstorm', }; // Couleur de barre : normal (violet), warning >= 75% (ambre), exhausted >= 100% (rose) const barFillColor = pct >= 100 ? 'bg-rose-400' : pct >= 75 ? 'bg-amber-400' : 'bg-gradient-to-r from-violet-400 to-purple-400'; return (
{featureLabels[key] || key} {isUnlimited ? t('billing.unlimited') || 'Illimité' : `${q.used} / ${q.limit}`}
); })}
{/* 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')}

)} ); }