"use client"; import { useState, useEffect } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { Check, CheckCircle2, X, Zap, Building2, Crown, Sparkles, ArrowRight, ArrowLeft, ChevronLeft, Star, Shield, Rocket, Users, Headphones, Lock, Globe, Clock, ChevronDown, ChevronUp, Cpu, BarChart3, Infinity, FileText, Layers, Brain, BadgeCheck, Gauge, Activity, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; import { API_BASE } from "@/lib/config"; import { ANNUAL_DISCOUNT_PERCENT } from "@/lib/pricing"; import { useI18n } from "@/lib/i18n"; // Enterprise contact — single place to update the sales address. const SUPPORT_EMAIL = "contact@wordly.art"; /* ───────────────────────────────────────────── Types ───────────────────────────────────────────── */ 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; max_chars_per_month: number; features: string[]; providers: string[]; ai_translation: boolean; ai_tier?: string; api_access: boolean; priority_processing: boolean; team_seats?: number; popular?: boolean; highlight?: string; description?: string; badge?: string; } interface CreditPackage { credits: number; price: number; price_per_credit: number; popular?: boolean; } /* ───────────────────────────────────────────── Static plan data (fallback + SSR-friendly) Names, descriptions, features, badges use i18n keys resolved via t() in the render layer. ───────────────────────────────────────────── */ const STATIC_PLANS: Plan[] = [ { id: "free", name: "pricing.plans.free.name", price_monthly: 0, price_yearly: 0, docs_per_month: 5, max_pages_per_doc: 15, max_file_size_mb: 5, max_chars_per_month: 50_000, providers: ["google"], features: [ "pricing.plans.free.feat1", "pricing.plans.free.feat2", "pricing.plans.free.feat3", "pricing.plans.free.feat4", "pricing.plans.free.feat5", ], ai_translation: false, api_access: false, priority_processing: false, popular: false, description: "pricing.plans.free.description", }, { id: "starter", name: "pricing.plans.starter.name", price_monthly: 9.00, price_yearly: 86.40, docs_per_month: 50, max_pages_per_doc: 50, max_file_size_mb: 10, max_chars_per_month: 500_000, providers: ["google"], features: [ "pricing.plans.starter.feat1", "pricing.plans.starter.feat2", "pricing.plans.starter.feat3", "pricing.plans.starter.feat4", "pricing.plans.starter.feat5", "pricing.plans.starter.feat6", ], ai_translation: false, api_access: false, priority_processing: false, popular: false, description: "pricing.plans.starter.description", }, { id: "pro", name: "pricing.plans.pro.name", price_monthly: 19.00, price_yearly: 182.40, docs_per_month: 200, max_pages_per_doc: 200, max_file_size_mb: 25, max_chars_per_month: 2_000_000, providers: ["google", "openrouter"], features: [ "pricing.plans.pro.feat1", "pricing.plans.pro.feat2", "pricing.plans.pro.feat3", "pricing.plans.pro.feat4", "pricing.plans.pro.feat5", "pricing.plans.pro.feat6", "pricing.plans.pro.feat7", "pricing.plans.pro.feat8", ], ai_translation: true, ai_tier: "essential", api_access: false, priority_processing: true, popular: true, highlight: "pricing.plans.pro.highlight", description: "pricing.plans.pro.description", badge: "pricing.plans.pro.badge", }, { id: "business", name: "pricing.plans.business.name", price_monthly: 49.00, price_yearly: 470.40, docs_per_month: 1000, max_pages_per_doc: 500, max_file_size_mb: 50, max_chars_per_month: 10_000_000, providers: ["google", "openrouter", "openrouter_premium", "openai"], features: [ "pricing.plans.business.feat1", "pricing.plans.business.feat2", "pricing.plans.business.feat3", "pricing.plans.business.feat4", "pricing.plans.business.feat5", "pricing.plans.business.feat6", "pricing.plans.business.feat7", "pricing.plans.business.feat8", "pricing.plans.business.feat9", "pricing.plans.business.feat10", ], ai_translation: true, ai_tier: "premium", api_access: true, priority_processing: true, team_seats: 5, popular: false, description: "pricing.plans.business.description", }, { id: "enterprise", name: "pricing.plans.enterprise.name", price_monthly: -1, price_yearly: -1, docs_per_month: -1, max_pages_per_doc: -1, max_file_size_mb: -1, max_chars_per_month: -1, providers: ["all"], features: [ "pricing.plans.enterprise.feat1", "pricing.plans.enterprise.feat2", "pricing.plans.enterprise.feat3", "pricing.plans.enterprise.feat4", "pricing.plans.enterprise.feat5", "pricing.plans.enterprise.feat6", "pricing.plans.enterprise.feat7", "pricing.plans.enterprise.feat8", ], ai_translation: true, ai_tier: "custom", api_access: true, priority_processing: true, team_seats: -1, popular: false, description: "pricing.plans.enterprise.description", badge: "pricing.plans.enterprise.badge", }, ]; const STATIC_CREDITS: CreditPackage[] = [ { credits: 50, price: 5, price_per_credit: 0.10 }, { credits: 150, price: 12, price_per_credit: 0.08, popular: true }, { credits: 500, price: 35, price_per_credit: 0.07 }, { credits: 1000, price: 60, price_per_credit: 0.06 }, ]; /* ───────────────────────────────────────────── Visual config by plan — editorial design ───────────────────────────────────────────── */ const PLAN_ICONS: Record = { free: Star, starter: Zap, pro: Crown, business: Globe, enterprise: Shield, }; /** Avoids flash of static prices before the API responds on refresh. */ function PricingDataSkeleton() { return ( <>
{Array.from({ length: 5 }).map((_, i) => (
))}
{Array.from({ length: 4 }).map((_, i) => (
))}
); } const FAQS = [ { q: "pricing.faq.q1", a: "pricing.faq.a1" }, { q: "pricing.faq.q2", a: "pricing.faq.a2" }, { q: "pricing.faq.q3", a: "pricing.faq.a3" }, { q: "pricing.faq.q4", a: "pricing.faq.a4" }, { q: "pricing.faq.q5", a: "pricing.faq.a5" }, { q: "pricing.faq.q6", a: "pricing.faq.a6" }, { q: "pricing.faq.q7", a: "pricing.faq.a7" }, ]; /* ───────────────────────────────────────────── Main component ───────────────────────────────────────────── */ export default function PricingPage() { const { t } = useI18n(); const router = useRouter(); const searchParams = useSearchParams(); const planFromUrl = searchParams.get("plan"); const billingFromUrl = searchParams.get("billing"); const [isYearly, setIsYearly] = useState(billingFromUrl === "yearly"); const [plans, setPlans] = useState(STATIC_PLANS); const [credits, setCredits] = useState(STATIC_CREDITS); const [currentPlan, setCurrentPlan] = useState(null); const [openFAQ, setOpenFAQ] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(false); const [loadingPlanId, setLoadingPlanId] = useState(null); const [loadingCreditIdx, setLoadingCreditIdx] = useState(null); const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT); /** Until false: don't show STATIC_PLANS (avoids flash of stale prices on refresh). */ const [pricingLoaded, setPricingLoaded] = useState(false); /** Plan awaiting explicit confirmation before any Stripe redirect. */ const [confirmPlan, setConfirmPlan] = useState(null); useEffect(() => { // Fetch live plans — backend returns prices set by admin (no browser cache) const plansUrl = `${API_BASE}/api/v1/auth/plans`; fetch(plansUrl, { cache: "no-store" }) .then(async (r) => { if (!r.ok) { throw new Error(`HTTP ${r.status}`); } return r.json(); }) .then((json) => { const d = json.data ?? json; const meta = (json.meta ?? d.meta) as { annual_discount_percent?: number } | undefined; if (typeof meta?.annual_discount_percent === "number") { setAnnualDiscountPercent(meta.annual_discount_percent); } if (Array.isArray(d.plans) && d.plans.length) setPlans(d.plans); if (Array.isArray(d.credit_packages)) setCredits(d.credit_packages); }) .catch((err) => { if (process.env.NODE_ENV === "development") { console.warn("[pricing] Cannot load /api/v1/auth/plans — showing default prices.", err); } }) .finally(() => { setPricingLoaded(true); }); // Fetch current user plan (if logged in) const token = localStorage.getItem("token"); if (token) { setIsLoggedIn(true); fetch(`${API_BASE}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` } }) .then((r) => r.json()) .then((json) => { const user = json.data ?? json; if (user?.plan) setCurrentPlan(user.plan); }) .catch(() => {}); } }, []); // ?plan= in the URL pre-opens the confirmation dialog — never a silent checkout. useEffect(() => { if (!pricingLoaded) return; if (planFromUrl && isLoggedIn && plans.length > 0 && currentPlan !== null) { const targetPlan = plans.find(p => p.id === planFromUrl); if (targetPlan && targetPlan.price_monthly > 0 && currentPlan !== planFromUrl) { setConfirmPlan(targetPlan); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [planFromUrl, isLoggedIn, plans, currentPlan, pricingLoaded]); const displayPrice = (plan: Plan) => { if (plan.price_monthly === -1) return null; if (plan.price_monthly === 0) return 0; const price = isYearly ? (plan.price_yearly / 12) : plan.price_monthly; return Number.isInteger(price) ? price.toString() : price.toFixed(2); }; const handleSubscribe = async (planId: string) => { const token = localStorage.getItem("token"); if (!token) { router.push(`/auth/login?redirect=/pricing`); return; } if (planId === "enterprise") { window.location.href = `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(t('pricing.enterprise.subject'))}`; return; } if (planId === currentPlan) return; setLoadingPlanId(planId); setToastMsg(null); try { const res = await fetch(`${API_BASE}/api/v1/auth/create-checkout`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ plan: planId, billing_period: isYearly ? "yearly" : "monthly", }), }); // Safe JSON parse — backend may return plain text on 500 let data: any = {}; try { data = await res.json(); } catch { /* ignore */ } const url = data.data?.url ?? data.url; const backendMessage = data.message ?? data.error ?? data.data?.error; if (!res.ok) { throw new Error(backendMessage || t('pricing.error.server', { status: res.status })); } if (url) { window.location.replace(url); } else { // Demo mode: Stripe not yet configured setToastMsg({ type: 'ok', text: t('pricing.toast.demo', { planId }), }); } } catch (error) { const message = error instanceof Error ? error.message : t('pricing.toast.networkError'); setToastMsg({ type: 'err', text: message }); } finally { setLoadingPlanId(null); } }; const handleBuyCredits = async (packageIndex: number) => { const token = localStorage.getItem("token"); if (!token) { router.push(`/auth/login?redirect=/pricing`); return; } setLoadingCreditIdx(packageIndex); setToastMsg(null); try { const res = await fetch(`${API_BASE}/api/v1/auth/create-credits-checkout`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ package_index: packageIndex }), }); // Safe JSON parse let data: any = {}; try { data = await res.json(); } catch { /* ignore */ } const url = data.data?.url ?? data.url; if (!res.ok) { throw new Error(data.message ?? data.error ?? t('pricing.error.server', { status: res.status })); } if (url) { window.location.replace(url); } else { setToastMsg({ type: 'ok', text: t('pricing.toast.demo', { planId: 'credits' }) }); } } catch (error) { const message = error instanceof Error ? error.message : t('pricing.toast.networkError'); setToastMsg({ type: 'err', text: message }); } finally { setLoadingCreditIdx(null); } }; return (
{/* ── Top navigation — breadcrumb bar ── */}
{t('pricing.dashboard')} {isLoggedIn && ( {t('pricing.nav.mySubscription')} )}
{/* ── Checkout confirmation — no Stripe redirect without an explicit confirm ── */} { if (!open) setConfirmPlan(null); }}> {t('pricing.confirm.title')} {t('pricing.confirm.subtitle')} {confirmPlan && (
{t(confirmPlan.name)} {isYearly ? `${Number(confirmPlan.price_yearly.toFixed(2))} € / ${t('pricing.confirm.year')}` : `${Number(confirmPlan.price_monthly.toFixed(2))} € / ${t('pricing.confirm.month')}`}
{isYearly && confirmPlan.price_yearly > 0 && (

{t('pricing.confirm.monthlyEquivalent', { price: (confirmPlan.price_yearly / 12).toFixed(2) })}

)}

{t('pricing.confirm.secureNote')}

)}
{/* ── Toast notification ── */} {toastMsg && (
{toastMsg.type === 'ok' ? t('pricing.okSymbol') : t('pricing.errSymbol')}

{toastMsg.text}

)} {/* ── Header ── */}

{t('pricing.header.titleBase')}{" "} {t('pricing.header.titleAccent')}

{t('pricing.header.subtitle')}

{/* ── Monthly / Yearly toggle ── */}
{/* ── Plan cards (skeleton until API responds) ── */}
{!pricingLoaded ? ( ) : ( <> {/* Essai gratuit : une ligne discrète au-dessus du choix payant */}

{t('pricing.freeBand.text')}

{t('pricing.freeBand.cta')}
{plans.filter((p) => !['free', 'enterprise'].includes(p.id)).map((plan) => { const Icon = PLAN_ICONS[plan.id] ?? Sparkles; const price = displayPrice(plan); const isCurrent = currentPlan === plan.id; const isEnterprise = plan.id === "enterprise"; const isFree = plan.id === "free"; return (
{/* ── Header (editorial) ── */}
{(plan.popular || isCurrent) && (
{plan.badge && ( {t(plan.badge)} )} {isCurrent && ( {t('pricing.card.myPlan')} )}
)} {!plan.popular && !isCurrent && plan.badge && ( {t(plan.badge)} )} {/* Icon + plan name */}
{t(plan.name)}
{/* Price */}
{isEnterprise ? (

{t('pricing.card.onRequest')}

) : price === 0 ? (

{t('pricing.card.free')}

) : ( <>

{price} €

{t('pricing.card.perMonth')} )}
{/* Yearly billing note */} {isYearly && plan.price_yearly > 0 && (
{t('pricing.card.billedYearly', { price: plan.price_yearly.toFixed(2) })}
)} {/* Description */}

{t(plan.description || '')}

{/* ── Content section ── */}
{/* Metrics */}
{t('pricing.card.documents')}
{plan.docs_per_month === -1 ? t('pricing.card.unlimited') : `${plan.docs_per_month} ${t('pricing.card.perMonthStat')}`}
{t('pricing.card.pagesMax')}
{plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`}
{plan.ai_translation && (
{t('pricing.card.aiTranslation')}
{plan.ai_tier === "essential" ? t('pricing.card.aiEssential') : plan.ai_tier === "premium" ? t('pricing.card.aiEssentialPremium') : t('pricing.card.aiCustom')}
)}
{/* Features list */}
    {plan.features.map((feat, i) => (
  • {t(feat)}
  • ))}
{/* CTA */} {isCurrent ? ( {t('pricing.card.managePlan')} ) : isFree && !currentPlan ? ( {t('pricing.card.startFree')} ) : ( )}
); })}
{/* Sur mesure : une ligne discrète sous le choix payant */}

{t('pricing.enterpriseBand.text')}

{t('pricing.enterpriseBand.cta')}
{/* ── Feature comparison table ── */}

{t('pricing.comparison.title')}

{t('pricing.comparison.subtitle')}

{plans.slice(0, 4).map((p) => ( ))} {[ { label: t('pricing.comparison.docsPerMonth'), vals: plans.slice(0,4).map(p => p.docs_per_month === -1 ? "∞" : String(p.docs_per_month)) }, { label: t('pricing.comparison.pagesMaxPerDoc'), vals: plans.slice(0,4).map(p => p.max_pages_per_doc === -1 ? "∞" : String(p.max_pages_per_doc)) }, { label: t('pricing.comparison.maxFileSize'), vals: plans.slice(0,4).map(p => p.max_file_size_mb === -1 ? "∞" : `${p.max_file_size_mb} ${t('pricing.comparison.mb')}`) }, { label: t('pricing.comparison.googleTranslation'), vals: plans.slice(0,4).map(() => true) }, { label: t('pricing.comparison.aiEssential'), vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "essential" || p.ai_tier === "premium" || p.ai_tier === "custom")) }, { label: t('pricing.comparison.aiPremium'), vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "premium" || p.ai_tier === "custom")) }, { label: t('pricing.comparison.apiAccess'), vals: plans.slice(0,4).map(p => p.api_access) }, { label: t('pricing.comparison.priorityProcessing'), vals: plans.slice(0,4).map(p => p.priority_processing) }, { label: t('pricing.comparison.support'), vals: [t('pricing.comparison.support.community'), t('pricing.comparison.support.email'), t('pricing.comparison.support.priority'), t('pricing.comparison.support.dedicated')] }, ].map((row, i) => ( {row.vals.map((val, j) => ( ))} ))}
{t('pricing.comparison.feature')} {t(p.name)}
{row.label} {typeof val === "boolean" ? ( val ? : ) : ( {val} )}
{/* ── Credits ── */}

{t('pricing.credits.title')}

{t('pricing.credits.subtitle')} {t('pricing.credits.perPage')}

{credits.map((pkg, i) => (
{pkg.popular && (
{t('pricing.credits.bestValue')}
)}
{pkg.credits}
{t('pricing.credits.unit')}
{pkg.price} €
{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}
))}
)} {/* ── Trust signals ── */}
{[ { icon: , title: t('pricing.trust.encryption.title'), sub: t('pricing.trust.encryption.sub') }, { icon: , title: t('pricing.trust.languages.title'), sub: t('pricing.trust.languages.sub') }, { icon: , title: t('pricing.trust.parallel.title'), sub: t('pricing.trust.parallel.sub') }, { icon: , title: t('pricing.trust.availability.title'), sub: t('pricing.trust.availability.sub') }, ].map((signal, i) => (
{signal.icon}
{signal.title}
{signal.sub}
))}
{/* ── AI Models info ── */}

{t('pricing.aiModels.title')}

{t('pricing.aiModels.essential.title')} {t('pricing.aiModels.essential.plan')}
{t('pricing.aiModels.essential.descPrefix')} {t('pricing.aiModels.essential.modelName')} {t('pricing.aiModels.essential.descSuffix')}
{t('pricing.aiModels.essential.context')} $0.25/$0.38 per 1M {t('pricing.aiModels.essential.value')}
{t('pricing.aiModels.premium.title')} {t('pricing.aiModels.premium.plan')}
{t('pricing.aiModels.premium.descPrefix')} Claude Sonnet 4.6 {t('pricing.aiModels.premium.descSuffix')}
{t('pricing.aiModels.premium.context')} $3.00/$15.00 per 1M {t('pricing.aiModels.premium.precision')}
{/* ── FAQ ── */}

{t('pricing.faq.title')}

{FAQS.map((faq, i) => (
{openFAQ === i && (
{t(faq.a)}
)}
))}
{/* ── CTA bottom ── */}

{t('pricing.cta.title')}

{t('pricing.cta.subtitle')}

); }