"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { API_BASE } from "@/lib/config"; import { Crown, Zap, Sparkles, Building2, Rocket, BadgeCheck, ArrowRight, AlertTriangle, CheckCircle2, XCircle, BarChart3, FileText, Layers, Brain, CreditCard, RefreshCw, ExternalLink, ChevronRight, Info, TrendingUp, Calendar, Gauge } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { cn } from "@/lib/utils"; /* ───────────────────────────────────────────── Types ───────────────────────────────────────────── */ interface UserInfo { id: string; email: string; name: string; plan: string; subscription_status: string; docs_translated_this_month: number; pages_translated_this_month: number; api_calls_this_month: number; extra_credits: number; subscription_ends_at?: string; cancel_at_period_end?: boolean; } interface UsageLimits { plan: string; docs_used: number; docs_limit: number; pages_used: number; pages_limit: number; api_calls_used: number; api_calls_limit: number; can_translate: boolean; upgrade_required: boolean; extra_credits: number; } interface Plan { id: string; name: string; price_monthly: number; price_yearly: number; docs_per_month: number; max_pages_per_doc: number; max_file_size_mb: number; features: string[]; ai_translation: boolean; ai_tier?: string; api_access: boolean; priority_processing: boolean; team_seats?: number; popular?: boolean; badge?: string; description?: string; } /* ───────────────────────────────────────────── Helpers ───────────────────────────────────────────── */ const PLAN_ICONS: Record = { free: Sparkles, starter: Zap, pro: Crown, business: Building2, enterprise: Rocket, }; const PLAN_COLORS: Record = { free: "from-slate-600 to-slate-700", starter: "from-blue-600 to-blue-700", pro: "from-violet-600 to-violet-700", business: "from-emerald-600 to-emerald-700", enterprise: "from-amber-600 to-amber-700", }; const PLAN_LABELS: Record = { free: "Gratuit", starter: "Starter", pro: "Pro", business: "Business", enterprise: "Entreprise", }; function pct(used: number, limit: number) { if (limit === -1) return 0; return Math.min(100, Math.round((used / limit) * 100)); } function fmtLimit(val: number) { return val === -1 ? "Illimité" : String(val); } function UsageBar({ label, used, limit, icon, }: { label: string; used: number; limit: number; icon: React.ReactNode }) { const p = pct(used, limit); const isUnlimited = limit === -1; return (
{icon} {label}
= 90 ? "text-destructive" : p >= 70 ? "text-warning" : "text-muted-foreground" )}> {isUnlimited ? "∞" : `${used} / ${limit}`}
{!isUnlimited && (
= 90 ? "bg-destructive" : p >= 70 ? "bg-warning" : "bg-accent" )} style={{ width: `${p}%` }} />
)}
); } /* ───────────────────────────────────────────── Main component ───────────────────────────────────────────── */ export default function SubscriptionPage() { const router = useRouter(); const searchParams = useSearchParams(); const targetPlan = searchParams.get("plan"); const [user, setUser] = useState(null); const [usage, setUsage] = useState(null); const [plans, setPlans] = useState([]); const [isYearly, setIsYearly] = useState(false); const [loadingPortal, setLoadingPortal] = useState(false); const [cancelConfirm, setCancelConfirm] = useState(false); const [statusMsg, setStatusMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null); const [loading, setLoading] = useState(true); const [isProcessingCheckout, setIsProcessingCheckout] = useState(false); const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; const authHeaders = { Authorization: `Bearer ${token}` }; const fetchData = useCallback(async () => { if (!token) { router.push("/auth/login?redirect=/settings/subscription"); return; } try { const [meRes, usageRes, plansRes] = await Promise.all([ fetch(`${API_BASE}/api/v1/auth/me`, { headers: authHeaders }), fetch(`${API_BASE}/api/v1/auth/usage`, { headers: authHeaders }), fetch(`${API_BASE}/api/v1/auth/plans`), ]); if (meRes.ok) { const j = await meRes.json(); setUser(j.data ?? j); } if (usageRes.ok) { const j = await usageRes.json(); setUsage(j.data ?? j); } if (plansRes.ok) { const j = await plansRes.json(); const d = j.data ?? j; if (Array.isArray(d.plans)) setPlans(d.plans); } } catch { // ignore } finally { setLoading(false); } }, [token]); useEffect(() => { fetchData(); }, [fetchData]); const handleBillingPortal = async () => { setLoadingPortal(true); try { const res = await fetch(`${API_BASE}/api/v1/auth/billing-portal`, { headers: authHeaders }); const j = await res.json(); const url = j.data?.url ?? j.url; if (url) window.open(url, "_blank"); else setStatusMsg({ type: "err", text: "Portail de facturation non disponible pour le moment." }); } catch { setStatusMsg({ type: "err", text: "Impossible d'accéder au portail de facturation." }); } finally { setLoadingPortal(false); } }; const handleCancel = async () => { if (!cancelConfirm) { setCancelConfirm(true); return; } try { const res = await fetch(`${API_BASE}/api/v1/auth/cancel-subscription`, { method: "POST", headers: authHeaders, }); if (res.ok) { setStatusMsg({ type: "ok", text: "Abonnement annulé. Vous conservez l'accès jusqu'à la fin de la période en cours." }); setCancelConfirm(false); fetchData(); } else { setStatusMsg({ type: "err", text: "Erreur lors de l'annulation. Réessayez ou contactez le support." }); } } catch { setStatusMsg({ type: "err", text: "Erreur réseau." }); } }; const handleSubscribe = async (planId: string) => { if (planId === "enterprise") { window.location.href = "mailto:contact@votre-domaine.com?subject=Offre Enterprise"; return; } setStatusMsg({ type: "ok", text: `Création de votre session de paiement pour le forfait ${PLAN_LABELS[planId] ?? planId}…`, }); try { const res = await fetch(`${API_BASE}/api/v1/auth/create-checkout`, { method: "POST", headers: { ...authHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ plan: planId, billing_period: isYearly ? "yearly" : "monthly" }), }); const data = await res.json(); if (!res.ok) { const msg = data.message ?? data.error ?? "Erreur lors de la création de la session de paiement."; setStatusMsg({ type: "err", text: msg }); return; } const url = data.data?.url ?? data.url; if (url) { window.location.replace(url); } else { await handleBillingPortal(); } } catch { setStatusMsg({ type: "err", text: "Erreur réseau lors de la création de la session de paiement." }); } finally { setIsProcessingCheckout(false); } }; // Handle targetPlan from URL query param automatically (MUST be after handleSubscribe declaration) useEffect(() => { if (targetPlan && plans.length > 0 && !loading && !isProcessingCheckout) { const p = plans.find(plan => plan.id === targetPlan); if (p && user?.plan !== targetPlan) { setIsProcessingCheckout(true); // Clean URL to prevent infinite re-triggering router.replace("/settings/subscription", { scroll: false }); handleSubscribe(targetPlan); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [targetPlan, plans, loading, user]); const handleCreditsCheckout = async (packageIndex: number) => { setStatusMsg({ type: "ok", text: "Préparation de l'achat de crédits...", }); try { const res = await fetch(`${API_BASE}/api/v1/auth/create-credits-checkout`, { method: "POST", headers: { ...authHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ package_index: packageIndex }), }); const data = await res.json(); const url = data.data?.url ?? data.url; if (url) { window.location.replace(url); } else { setTimeout(() => handleBillingPortal(), 1000); } } catch { setStatusMsg({ type: "err", text: "Erreur lors de la création de la session de paiement." }); } }; if (loading || isProcessingCheckout) { return (
); } const currentPlanId = user?.plan ?? "free"; const currentPlanLabel = PLAN_LABELS[currentPlanId] ?? currentPlanId; const Icon = PLAN_ICONS[currentPlanId] ?? Sparkles; const gradient = PLAN_COLORS[currentPlanId] ?? PLAN_COLORS.free; const currentPlanData = plans.find((p) => p.id === currentPlanId); const otherPlans = plans.filter((p) => p.id !== currentPlanId); const upgradePlans = otherPlans.filter((p) => { const order = ["free", "starter", "pro", "business", "enterprise"]; return order.indexOf(p.id) > order.indexOf(currentPlanId); }); const downgradePlans = otherPlans.filter((p) => { const order = ["free", "starter", "pro", "business", "enterprise"]; return order.indexOf(p.id) < order.indexOf(currentPlanId); }); return (

Mon abonnement

Gérez votre forfait, votre usage et votre facturation.

{/* Status message */} {statusMsg && (
{statusMsg.type === "ok" ? : } {statusMsg.text}
)} {/* ── Current plan card ── */}

Forfait {currentPlanLabel}

{user?.subscription_status && ( {user.subscription_status === "active" ? "Actif" : user.subscription_status === "trialing" ? "Essai" : user.subscription_status === "canceled" ? "Annulé" : user.subscription_status} )}
{user?.subscription_ends_at && (

{user.cancel_at_period_end ? "Expire le " : "Renouvellement le "} {new Date(user.subscription_ends_at).toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric" })}

)}
{currentPlanId !== "free" && ( )} {upgradePlans.length > 0 && ( )}
{/* ── Usage this month ── */} {usage && ( Utilisation ce mois Remise à zéro chaque 1er du mois } /> } /> {usage.api_calls_limit !== 0 && ( } /> )} {usage.extra_credits > 0 && (
{usage.extra_credits} crédit{usage.extra_credits > 1 ? "s" : ""} supplémentaire{usage.extra_credits > 1 ? "s" : ""} disponible{usage.extra_credits > 1 ? "s" : ""}
)} {usage.upgrade_required && (
Quota atteint. Achetez des crédits ou upgradez votre forfait pour continuer.
)}
)} {/* ── Plan features recap ── */} {currentPlanData && ( Inclus dans votre forfait
{currentPlanData.features.map((f, i) => (
{f}
))}
)} {/* ── Upgrade options ── */} {upgradePlans.length > 0 && (

Passer à un forfait supérieur

{/* Billing toggle */}
{upgradePlans.map((plan) => { const PIcon = PLAN_ICONS[plan.id] ?? Zap; const grad = PLAN_COLORS[plan.id] ?? PLAN_COLORS.starter; const price = plan.price_monthly === -1 ? null : isYearly ? (plan.price_yearly / 12).toFixed(2) : plan.price_monthly.toFixed(2); return (
{plan.badge && (
{plan.badge}
)}
{plan.name}
{price === null ? ( Sur devis ) : ( <> {price} € /mois )}
{plan.features.slice(0, 4).map((f, i) => (
{f}
))} {plan.features.length > 4 && (

+{plan.features.length - 4} autres avantages…

)}
); })}
)} {/* ── Buy credits ── */} Crédits supplémentaires

1 crédit = 1 page traduite. Utilisables sans expiration.

{[ { credits: 50, price: 5 }, { credits: 150, price: 12, popular: true }, { credits: 500, price: 35 }, { credits: 1000, price: 60 }, ].map((pkg, i) => (
{pkg.popular && (
Meilleure valeur
)}
{pkg.credits}
crédits
{pkg.price} €
{((pkg.price / pkg.credits) * 100).toFixed(0)} cts/crédit
))}
{/* ── Downgrade / Cancel ── */} {currentPlanId !== "free" && ( Zone de danger {downgradePlans.length > 0 && (

Rétrograder vers un forfait inférieur :

{downgradePlans.map((p) => ( ))}
)}
{!cancelConfirm ? (

Annuler mon abonnement

Vous conservez l'accès jusqu'à la fin de la période payée.

) : (

⚠️ Confirmer l'annulation ?

Votre abonnement sera annulé et vous reviendrez au forfait Gratuit à la fin de la période en cours. Vos documents traduits resteront accessibles pendant 30 jours.

)}
)} {/* Link to full pricing */}
); }