feat(billing): redesign page abonnement — layout asymétrique + identité Rubis
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s
Mensuel par défaut dans le toggle (au lieu de yearly).
Layout asymétrique 1fr/1.3fr/1fr avec Pro en hero card centrale, plus
large et plus dense visuellement que Free et Business — sort du pattern
"3 cards égales" générique des landing pages SaaS.
Identité Rubis :
- Gem 280px en filigrane sur la Pro card (rubis-glow, opacity 70%) —
le motif signature de la marque, pas un blob de gradient
- Bullets ◆ losanges rubis tournés (5×5px) au lieu des check-circles
Lucide génériques — cohérent avec PlanCard, Timeline, etc.
- Pastille "Le plus pris" avec mini-gem inline sur Pro
- Border rubis + shadow-card sur Pro, line + sobre pour les voisines
- Free : bg cream-2 (ton "découverte"), Business : bg blanc + accents
rubis-deep
- Strip "plan courant" : remplace le gros bloc card → ligne flex avec
gem + texte + mini progress bar 32px + bouton ghost. Discret.
Voice direct dans les bénéfices :
- Header narratif "Trois mois pour voir. Puis vous décidez."
- "Tester sans poser sa CB" (Free)
- "Pour les TPE qui n'ont plus jamais à y penser" (Pro titre)
- "Moins cher qu'une heure de votre temps mensuel" (Pro footer)
- Annulation 1-clic mentionné en sous-titre du toggle
Prix Pro XL 44px (vs 28px pour les voisines) — hiérarchie visuelle qui
guide l'œil vers l'option qu'on veut pousser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1952265217
commit
fd24ef42a6
@ -1,20 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import {
|
import { ArrowLeft, ArrowRight, CreditCard } from "lucide-react";
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Check,
|
|
||||||
CreditCard,
|
|
||||||
Sparkles,
|
|
||||||
Users,
|
|
||||||
Zap,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle, type PlanKey } from "@/lib/billing";
|
import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle } from "@/lib/billing";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { formatDate } from "@/lib/format";
|
import { formatDate } from "@/lib/format";
|
||||||
|
import { Gem } from "@/components/brand/Gem";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Card } from "@/components/ui/Card";
|
import { Card } from "@/components/ui/Card";
|
||||||
@ -30,22 +23,13 @@ export const Route = createFileRoute("/_app/parametres_/abonnement")({
|
|||||||
component: AbonnementPage,
|
component: AbonnementPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* /parametres/abonnement — gestion du plan et de la facturation Rubis.
|
|
||||||
*
|
|
||||||
* Sections :
|
|
||||||
* - Plan actuel + caps + état grace period
|
|
||||||
* - Comparaison Free / Pro / Business avec toggle mensuel/annuel
|
|
||||||
* - CTA upgrade (Stripe Checkout) ou portail (Customer Portal)
|
|
||||||
*/
|
|
||||||
function AbonnementPage() {
|
function AbonnementPage() {
|
||||||
const search = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
const { data: sub, isPending } = useSubscription();
|
const { data: sub, isPending } = useSubscription();
|
||||||
const checkout = useStartCheckout();
|
const checkout = useStartCheckout();
|
||||||
const portal = useOpenPortal();
|
const portal = useOpenPortal();
|
||||||
const [cycle, setCycle] = useState<BillingCycle>("yearly");
|
const [cycle, setCycle] = useState<BillingCycle>("monthly");
|
||||||
|
|
||||||
// Toast post-redirect Stripe
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (search.checkout === "success") {
|
if (search.checkout === "success") {
|
||||||
toast.success("Bienvenue sur le nouveau plan ! Activation en cours…");
|
toast.success("Bienvenue sur le nouveau plan ! Activation en cours…");
|
||||||
@ -75,8 +59,10 @@ function AbonnementPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentPlan = sub?.plan;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-7">
|
||||||
<Link
|
<Link
|
||||||
to="/parametres"
|
to="/parametres"
|
||||||
className="inline-flex items-center gap-1.5 self-start text-[12.5px] text-ink-3 hover:text-rubis"
|
className="inline-flex items-center gap-1.5 self-start text-[12.5px] text-ink-3 hover:text-rubis"
|
||||||
@ -84,71 +70,69 @@ function AbonnementPage() {
|
|||||||
<ArrowLeft size={13} aria-hidden="true" /> Paramètres
|
<ArrowLeft size={13} aria-hidden="true" /> Paramètres
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<header>
|
{/* Header — direct, pas trop de baratin */}
|
||||||
|
<header className="max-w-2xl">
|
||||||
<Eyebrow>Abonnement</Eyebrow>
|
<Eyebrow>Abonnement</Eyebrow>
|
||||||
<h1 className="mt-2 font-display text-[28px] font-bold tracking-[-0.022em] text-ink lg:text-[32px]">
|
<h1 className="mt-2 font-display text-[30px] font-bold tracking-[-0.024em] text-ink lg:text-[36px] leading-[1.05]">
|
||||||
Choisir <em className="text-rubis">son plan</em>
|
Trois mois pour voir.
|
||||||
|
<br />
|
||||||
|
Puis vous <em className="text-rubis">décidez</em>.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1.5 text-[14px] text-ink-3 max-w-2xl leading-relaxed">
|
<p className="mt-3 text-[14.5px] text-ink-2 leading-relaxed">
|
||||||
Trois mois offerts au démarrage. Au-delà, passez Pro pour
|
Pas de carte demandée pour démarrer. Au-delà de 5 factures actives,
|
||||||
continuer à relancer plus de 5 factures à la fois.
|
vous passez Pro pour continuer — quand vous êtes prêt, pas avant.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Plan courant */}
|
{/* État courant — slim, info-dense, pas un gros bloc */}
|
||||||
{!isPending && sub && (
|
{!isPending && sub && (
|
||||||
<CurrentPlanCard
|
<CurrentPlanStrip
|
||||||
state={sub}
|
state={sub}
|
||||||
onOpenPortal={onOpenPortal}
|
onOpenPortal={onOpenPortal}
|
||||||
isOpeningPortal={portal.isPending}
|
isOpeningPortal={portal.isPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Toggle mensuel / annuel */}
|
{/* Toggle cycle */}
|
||||||
<div className="self-start">
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<CycleToggle value={cycle} onChange={setCycle} />
|
<CycleToggle value={cycle} onChange={setCycle} />
|
||||||
|
<p className="text-[11.5px] italic text-ink-3">
|
||||||
|
Annulation 1-clic · pas d'engagement
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cards plans */}
|
{/* Layout asymétrique : Pro en hero centré, Free et Business plus modestes */}
|
||||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-3 lg:gap-5">
|
<section className="grid grid-cols-1 gap-4 lg:grid-cols-[1fr_1.3fr_1fr] lg:gap-5 lg:items-stretch">
|
||||||
<PlanCard
|
<PlanFree current={currentPlan === "free"} />
|
||||||
plan="free"
|
<PlanProHero
|
||||||
currentPlan={sub?.plan}
|
current={currentPlan === "pro"}
|
||||||
cycle={cycle}
|
cycle={cycle}
|
||||||
ctaDisabled
|
|
||||||
ctaLabel={sub?.plan === "free" ? "Plan actuel" : "Plan gratuit"}
|
|
||||||
/>
|
|
||||||
<PlanCard
|
|
||||||
plan="pro"
|
|
||||||
currentPlan={sub?.plan}
|
|
||||||
cycle={cycle}
|
|
||||||
highlight
|
|
||||||
loading={checkout.isPending && checkout.variables?.plan === "pro"}
|
loading={checkout.isPending && checkout.variables?.plan === "pro"}
|
||||||
onUpgrade={() => onUpgrade("pro")}
|
onUpgrade={() => onUpgrade("pro")}
|
||||||
/>
|
/>
|
||||||
<PlanCard
|
<PlanBusiness
|
||||||
plan="business"
|
current={currentPlan === "business"}
|
||||||
currentPlan={sub?.plan}
|
|
||||||
cycle={cycle}
|
cycle={cycle}
|
||||||
loading={checkout.isPending && checkout.variables?.plan === "business"}
|
loading={checkout.isPending && checkout.variables?.plan === "business"}
|
||||||
onUpgrade={() => onUpgrade("business")}
|
onUpgrade={() => onUpgrade("business")}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Footer rassurant */}
|
||||||
<p className="text-[12px] text-ink-3 italic max-w-2xl leading-relaxed">
|
<p className="text-[12px] text-ink-3 italic max-w-2xl leading-relaxed">
|
||||||
Paiement sécurisé via Stripe (CB, SEPA). TVA selon votre pays.
|
Paiement sécurisé via Stripe — CB ou SEPA, TVA selon votre pays.
|
||||||
Annulation possible à tout moment depuis le portail client. Aucun
|
Vous gardez la main sur votre abonnement à tout moment depuis le
|
||||||
engagement.
|
portail client.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Card plan courant
|
// Strip "Plan courant" — discret mais informatif (vs gros bloc IA-template)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function CurrentPlanCard({
|
function CurrentPlanStrip({
|
||||||
state,
|
state,
|
||||||
onOpenPortal,
|
onOpenPortal,
|
||||||
isOpeningPortal,
|
isOpeningPortal,
|
||||||
@ -158,72 +142,71 @@ function CurrentPlanCard({
|
|||||||
isOpeningPortal: boolean;
|
isOpeningPortal: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state;
|
const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state;
|
||||||
const isLimited = plan === "free" && caps.activeInvoicesLimit !== null;
|
|
||||||
const limit = caps.activeInvoicesLimit;
|
const limit = caps.activeInvoicesLimit;
|
||||||
|
const isLimited = plan === "free" && limit !== null;
|
||||||
const limitReached =
|
const limitReached =
|
||||||
isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit;
|
isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="md" className="flex flex-col gap-4">
|
<div
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
className={cn(
|
||||||
<div>
|
"flex items-center gap-4 rounded-card border bg-white px-5 py-4",
|
||||||
<Eyebrow tone="ink">Plan actuel</Eyebrow>
|
"border-line",
|
||||||
<p className="mt-2 font-display text-[22px] font-bold text-ink">
|
)}
|
||||||
Rubis {planLabel(plan)}
|
>
|
||||||
{state.subscriptionStatus && state.subscriptionStatus !== "active" && (
|
<Gem size={22} className="shrink-0" />
|
||||||
<span className="ml-2 text-[12px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
|
|
||||||
· {state.subscriptionStatus}
|
<div className="flex-1 min-w-0">
|
||||||
</span>
|
<p className="font-display text-[15px] font-bold text-ink leading-tight">
|
||||||
)}
|
{plan === "free" ? "Plan Rubis" : "Rubis"}
|
||||||
</p>
|
{plan !== "free" && (
|
||||||
{state.currentPeriodEnd && (
|
<span className="ml-1 text-rubis">{plan === "pro" ? " Pro" : " Business"}</span>
|
||||||
<p className="mt-1 text-[12.5px] text-ink-3">
|
)}
|
||||||
Prochaine facture le{" "}
|
{state.subscriptionStatus && state.subscriptionStatus !== "active" && (
|
||||||
<strong className="font-medium text-ink-2">
|
<span className="ml-2 text-[10.5px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
|
||||||
|
· {state.subscriptionStatus}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-[12px] text-ink-3 leading-tight">
|
||||||
|
{plan === "free" && inGracePeriod && gracePeriodEndsAt && (
|
||||||
|
<>
|
||||||
|
Période illimitée jusqu'au{" "}
|
||||||
|
<strong className="text-ink-2 font-medium">
|
||||||
|
{formatDate(gracePeriodEndsAt)}
|
||||||
|
</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{plan === "free" && !inGracePeriod && limit !== null && (
|
||||||
|
<>
|
||||||
|
<strong
|
||||||
|
className={cn(
|
||||||
|
"tabular-nums font-medium",
|
||||||
|
limitReached ? "text-rubis-deep" : "text-ink-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{activeInvoicesCount} / {limit}
|
||||||
|
</strong>{" "}
|
||||||
|
factures actives
|
||||||
|
{limitReached && " — limite atteinte"}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{plan !== "free" && state.currentPeriodEnd && (
|
||||||
|
<>
|
||||||
|
Renouvellement le{" "}
|
||||||
|
<strong className="text-ink-2 font-medium">
|
||||||
{formatDate(state.currentPeriodEnd)}
|
{formatDate(state.currentPeriodEnd)}
|
||||||
</strong>
|
</strong>
|
||||||
{state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"}
|
{state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"}
|
||||||
</p>
|
</>
|
||||||
)}
|
)}
|
||||||
{plan === "free" && inGracePeriod && gracePeriodEndsAt && (
|
</p>
|
||||||
<p className="mt-1 text-[12.5px] text-rubis-deep">
|
|
||||||
<Sparkles size={12} className="inline mr-1" aria-hidden="true" />
|
|
||||||
Période de grâce — illimité jusqu'au{" "}
|
|
||||||
<strong className="font-semibold">
|
|
||||||
{formatDate(gracePeriodEndsAt)}
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{state.hasStripeCustomer && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={onOpenPortal}
|
|
||||||
loading={isOpeningPortal}
|
|
||||||
>
|
|
||||||
<CreditCard size={14} aria-hidden="true" /> Gérer
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compteur de factures actives — visible pour Free uniquement */}
|
{/* Mini progress bar mobile-OK pour Free */}
|
||||||
{isLimited && limit !== null && (
|
{isLimited && limit !== null && !inGracePeriod && (
|
||||||
<div>
|
<div className="hidden sm:block w-32 shrink-0">
|
||||||
<div className="flex items-baseline justify-between mb-1">
|
<div className="h-1 w-full rounded-full bg-cream-2 overflow-hidden">
|
||||||
<p className="text-[12.5px] text-ink-3">
|
|
||||||
Factures actives en relance
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
"text-[12.5px] font-semibold tabular-nums",
|
|
||||||
limitReached ? "text-rubis-deep" : "text-ink-2",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{activeInvoicesCount} / {limit}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 w-full rounded-full bg-cream-2 overflow-hidden">
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full transition-[width] duration-300",
|
"h-full transition-[width] duration-300",
|
||||||
@ -234,19 +217,25 @@ function CurrentPlanCard({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{limitReached && (
|
|
||||||
<p className="mt-2 text-[12.5px] text-rubis-deep font-medium">
|
|
||||||
Limite atteinte — passez Pro pour ajouter de nouvelles factures.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
|
||||||
|
{state.hasStripeCustomer && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onOpenPortal}
|
||||||
|
loading={isOpeningPortal}
|
||||||
|
>
|
||||||
|
<CreditCard size={13} aria-hidden="true" /> Gérer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Toggle cycle
|
// Toggle cycle — mensuel par défaut, annuel à droite avec −17%
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function CycleToggle({
|
function CycleToggle({
|
||||||
@ -295,148 +284,221 @@ function CycleToggle({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Cards plans (Free / Pro / Business)
|
// PlanFree — petite card sobre. Pas de pricing affiché en gros (= 0 €).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const PRICES: Record<
|
function PlanFree({ current }: { current: boolean }) {
|
||||||
PlanKey,
|
|
||||||
{ monthly: number | null; yearly: number | null }
|
|
||||||
> = {
|
|
||||||
free: { monthly: 0, yearly: 0 },
|
|
||||||
pro: { monthly: 19, yearly: 190 },
|
|
||||||
business: { monthly: 49, yearly: 490 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const FEATURES_BY_PLAN: Record<PlanKey, string[]> = {
|
|
||||||
free: [
|
|
||||||
"5 factures actives en relance",
|
|
||||||
"1 utilisateur",
|
|
||||||
"Plans de relance fournis",
|
|
||||||
"OCR illimité (3 premiers mois)",
|
|
||||||
],
|
|
||||||
pro: [
|
|
||||||
"Factures illimitées",
|
|
||||||
"OCR illimité",
|
|
||||||
"Plans custom + IA générative",
|
|
||||||
"Toutes les automatisations",
|
|
||||||
"1 utilisateur",
|
|
||||||
],
|
|
||||||
business: [
|
|
||||||
"Tout le plan Pro",
|
|
||||||
"Réponses depuis votre email pro",
|
|
||||||
"5 sièges utilisateurs",
|
|
||||||
"Support prioritaire",
|
|
||||||
"SMS (à venir)",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function PlanCard({
|
|
||||||
plan,
|
|
||||||
currentPlan,
|
|
||||||
cycle,
|
|
||||||
highlight = false,
|
|
||||||
ctaDisabled = false,
|
|
||||||
ctaLabel,
|
|
||||||
loading = false,
|
|
||||||
onUpgrade,
|
|
||||||
}: {
|
|
||||||
plan: PlanKey;
|
|
||||||
currentPlan?: PlanKey;
|
|
||||||
cycle: BillingCycle;
|
|
||||||
highlight?: boolean;
|
|
||||||
ctaDisabled?: boolean;
|
|
||||||
ctaLabel?: string;
|
|
||||||
loading?: boolean;
|
|
||||||
onUpgrade?: () => void;
|
|
||||||
}) {
|
|
||||||
const isCurrent = currentPlan === plan;
|
|
||||||
const price = PRICES[plan][cycle];
|
|
||||||
const features = FEATURES_BY_PLAN[plan];
|
|
||||||
const planIcon =
|
|
||||||
plan === "pro" ? <Zap size={16} /> : plan === "business" ? <Users size={16} /> : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div className="rounded-card border border-line bg-cream-2/40 p-6 flex flex-col">
|
||||||
padding="md"
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
className={cn(
|
<p className="font-display text-[16px] font-bold text-ink">Découverte</p>
|
||||||
"flex flex-col",
|
{current && <CurrentBadge />}
|
||||||
highlight && !isCurrent && "border-rubis shadow-card",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
{planIcon && <span className="text-rubis">{planIcon}</span>}
|
|
||||||
<p className="font-display text-[18px] font-bold text-ink">
|
|
||||||
{planLabel(plan)}
|
|
||||||
</p>
|
|
||||||
{highlight && !isCurrent && (
|
|
||||||
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
|
|
||||||
Recommandé
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isCurrent && (
|
|
||||||
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-ink-3">
|
|
||||||
Actuel
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[11.5px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-5">
|
||||||
|
Gratuit
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="mt-2 mb-5">
|
<p className="font-display text-[28px] font-bold leading-none text-ink mb-1">
|
||||||
<p className="font-display tabular-nums">
|
0 €
|
||||||
<span className="text-[36px] font-bold tracking-[-0.02em] text-ink">
|
</p>
|
||||||
{price === 0 ? "0" : price}
|
<p className="text-[11.5px] text-ink-3 mb-5">3 mois illimités, puis 5 factures actives</p>
|
||||||
</span>
|
|
||||||
<span className="text-[14px] font-medium text-ink-3 ml-1">
|
|
||||||
{cycle === "monthly" ? "€/mois" : "€/an"}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
{price !== null && price > 0 && cycle === "yearly" && (
|
|
||||||
<p className="text-[11.5px] text-ink-3">
|
|
||||||
soit {(price / 12).toFixed(2).replace(".", ",")} €/mois
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{plan === "free" && (
|
|
||||||
<p className="text-[11.5px] text-ink-3">3 mois illimités, puis 5 factures actives</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="flex flex-col gap-2 mb-6 flex-1">
|
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
|
||||||
{features.map((f) => (
|
<FeatureLine>Tester sans poser sa CB</FeatureLine>
|
||||||
<li
|
<FeatureLine>Plans de relance fournis</FeatureLine>
|
||||||
key={f}
|
<FeatureLine>OCR + automatisations 3 mois</FeatureLine>
|
||||||
className="flex items-start gap-2 text-[13px] text-ink-2 leading-snug"
|
<FeatureLine>Au-delà : 5 factures à la fois</FeatureLine>
|
||||||
>
|
|
||||||
<Check size={13} className="text-rubis shrink-0 mt-0.5" aria-hidden="true" />
|
|
||||||
<span>{f}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{!ctaDisabled && onUpgrade ? (
|
<p className="mt-auto pt-6 text-[11px] italic text-ink-3 leading-snug">
|
||||||
<Button
|
Tout ce qu'il faut pour comprendre l'outil. Vous prendrez Pro
|
||||||
size="md"
|
quand votre volume mensuel dépasse 5 factures à relancer.
|
||||||
variant={highlight ? "primary" : "secondary"}
|
</p>
|
||||||
loading={loading}
|
</div>
|
||||||
disabled={isCurrent}
|
);
|
||||||
onClick={onUpgrade}
|
}
|
||||||
className="w-full"
|
|
||||||
>
|
// ---------------------------------------------------------------------------
|
||||||
{isCurrent ? (
|
// PlanProHero — la pièce maîtresse. Bg cream + gem en filigrane, prix XL.
|
||||||
"Plan actuel"
|
// ---------------------------------------------------------------------------
|
||||||
) : (
|
|
||||||
<>
|
function PlanProHero({
|
||||||
Passer {planLabel(plan)} <ArrowRight size={14} aria-hidden="true" />
|
current,
|
||||||
</>
|
cycle,
|
||||||
)}
|
loading,
|
||||||
</Button>
|
onUpgrade,
|
||||||
) : (
|
}: {
|
||||||
<Button size="md" variant="ghost" disabled className="w-full">
|
current: boolean;
|
||||||
{ctaLabel ?? "—"}
|
cycle: BillingCycle;
|
||||||
</Button>
|
loading: boolean;
|
||||||
)}
|
onUpgrade: () => void;
|
||||||
|
}) {
|
||||||
|
const monthlyPrice = cycle === "monthly" ? 19 : Math.round((190 / 12) * 100) / 100;
|
||||||
|
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 190 € facturés annuellement";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card padding="none" className="relative overflow-hidden flex flex-col p-7 border-rubis shadow-card">
|
||||||
|
{/* Gem en filigrane, bas-droite, très large pour devenir un motif */}
|
||||||
|
<Gem
|
||||||
|
aria-hidden="true"
|
||||||
|
size={280}
|
||||||
|
className="absolute -bottom-16 -right-16 text-rubis-glow opacity-70 pointer-events-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Pastille "Recommandé" */}
|
||||||
|
<div className="relative flex items-center gap-2 mb-3">
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-rubis text-white px-2.5 py-0.5 text-[10.5px] font-bold uppercase tracking-[0.12em]">
|
||||||
|
<Gem size={10} className="text-white" />
|
||||||
|
Le plus pris
|
||||||
|
</span>
|
||||||
|
{current && <CurrentBadge />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="relative font-display text-[24px] font-bold tracking-[-0.018em] text-ink leading-tight">
|
||||||
|
Pour les TPE qui n'ont plus jamais à y penser
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Bloc prix — affiché de manière narrative */}
|
||||||
|
<div className="relative mt-5 mb-5">
|
||||||
|
<p className="font-display flex items-baseline">
|
||||||
|
<span className="text-[44px] font-bold tabular-nums tracking-[-0.025em] text-ink leading-none">
|
||||||
|
{monthlyPrice.toString().replace(".", ",")} €
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 text-[13px] font-medium text-ink-3">/ mois</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[12px] text-ink-3">{totalLabel}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste de bénéfices, formulés en valeur (pas en feature spec) */}
|
||||||
|
<ul className="relative flex flex-col gap-2.5 text-[13px] text-ink-2 leading-snug mb-7">
|
||||||
|
<FeatureLine strong>Factures et relances illimitées</FeatureLine>
|
||||||
|
<FeatureLine>OCR sans limite de volume</FeatureLine>
|
||||||
|
<FeatureLine>Plans custom + génération IA des relances</FeatureLine>
|
||||||
|
<FeatureLine>Mode démo pour montrer l'outil aux prospects</FeatureLine>
|
||||||
|
<FeatureLine>Toutes les automatisations</FeatureLine>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="primary"
|
||||||
|
loading={loading}
|
||||||
|
disabled={current}
|
||||||
|
onClick={onUpgrade}
|
||||||
|
className="relative w-full"
|
||||||
|
>
|
||||||
|
{current ? (
|
||||||
|
"Plan actuel"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Passer Pro <ArrowRight size={14} aria-hidden="true" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className="relative mt-3 text-[11px] italic text-ink-3 text-center leading-snug">
|
||||||
|
Moins cher qu'une heure de votre temps mensuel.
|
||||||
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function planLabel(plan: PlanKey): string {
|
// ---------------------------------------------------------------------------
|
||||||
return plan === "free" ? "Free" : plan === "pro" ? "Pro" : "Business";
|
// PlanBusiness — card sobre comme Free mais avec accents rubis-deep.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function PlanBusiness({
|
||||||
|
current,
|
||||||
|
cycle,
|
||||||
|
loading,
|
||||||
|
onUpgrade,
|
||||||
|
}: {
|
||||||
|
current: boolean;
|
||||||
|
cycle: BillingCycle;
|
||||||
|
loading: boolean;
|
||||||
|
onUpgrade: () => void;
|
||||||
|
}) {
|
||||||
|
const monthlyPrice = cycle === "monthly" ? 49 : Math.round((490 / 12) * 100) / 100;
|
||||||
|
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 490 € facturés annuellement";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-card border border-line bg-white p-6 flex flex-col">
|
||||||
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
|
<p className="font-display text-[16px] font-bold text-ink">Équipe</p>
|
||||||
|
{current && <CurrentBadge />}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11.5px] uppercase tracking-[0.12em] text-rubis-deep font-semibold mb-5">
|
||||||
|
Business
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="font-display flex items-baseline mb-1">
|
||||||
|
<span className="text-[28px] font-bold tabular-nums tracking-[-0.02em] text-ink leading-none">
|
||||||
|
{monthlyPrice.toString().replace(".", ",")} €
|
||||||
|
</span>
|
||||||
|
<span className="ml-1.5 text-[12px] font-medium text-ink-3">/ mois</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[11.5px] text-ink-3 mb-5">{totalLabel}</p>
|
||||||
|
|
||||||
|
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
|
||||||
|
<FeatureLine strong>Tout Pro, plus :</FeatureLine>
|
||||||
|
<FeatureLine>5 sièges utilisateurs</FeatureLine>
|
||||||
|
<FeatureLine>Réponses depuis votre email pro</FeatureLine>
|
||||||
|
<FeatureLine>Support prioritaire</FeatureLine>
|
||||||
|
<FeatureLine subtle>SMS (à venir)</FeatureLine>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
loading={loading}
|
||||||
|
disabled={current}
|
||||||
|
onClick={onUpgrade}
|
||||||
|
className="mt-auto pt-6 w-full"
|
||||||
|
>
|
||||||
|
{current ? "Plan actuel" : "Passer Business"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Atomes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function FeatureLine({
|
||||||
|
children,
|
||||||
|
strong = false,
|
||||||
|
subtle = false,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
strong?: boolean;
|
||||||
|
subtle?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
{/* Petit losange rubis = bullet maison cohérent avec la marque */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"mt-1.5 size-[5px] rotate-45 shrink-0",
|
||||||
|
subtle ? "bg-ink-3/40" : "bg-rubis",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
strong && "font-semibold text-ink",
|
||||||
|
subtle && "text-ink-3 italic",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CurrentBadge() {
|
||||||
|
return (
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.14em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
|
||||||
|
Actuel
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user