From fd24ef42a6a27cac96cfa838ca8582e6526e3ada Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Thu, 7 May 2026 16:34:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(billing):=20redesign=20page=20abonnement?= =?UTF-8?q?=20=E2=80=94=20layout=20asym=C3=A9trique=20+=20identit=C3=A9=20?= =?UTF-8?q?Rubis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../routes/_app/parametres_.abonnement.tsx | 554 ++++++++++-------- 1 file changed, 308 insertions(+), 246 deletions(-) diff --git a/apps/web/src/routes/_app/parametres_.abonnement.tsx b/apps/web/src/routes/_app/parametres_.abonnement.tsx index ede1345..64e5740 100644 --- a/apps/web/src/routes/_app/parametres_.abonnement.tsx +++ b/apps/web/src/routes/_app/parametres_.abonnement.tsx @@ -1,20 +1,13 @@ import { useEffect, useState } from "react"; import { createFileRoute, Link } from "@tanstack/react-router"; -import { - ArrowLeft, - ArrowRight, - Check, - CreditCard, - Sparkles, - Users, - Zap, -} from "lucide-react"; +import { ArrowLeft, ArrowRight, CreditCard } from "lucide-react"; import { toast } from "sonner"; 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 { formatDate } from "@/lib/format"; +import { Gem } from "@/components/brand/Gem"; import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; @@ -30,22 +23,13 @@ export const Route = createFileRoute("/_app/parametres_/abonnement")({ 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() { const search = Route.useSearch(); const { data: sub, isPending } = useSubscription(); const checkout = useStartCheckout(); const portal = useOpenPortal(); - const [cycle, setCycle] = useState("yearly"); + const [cycle, setCycle] = useState("monthly"); - // Toast post-redirect Stripe useEffect(() => { if (search.checkout === "success") { toast.success("Bienvenue sur le nouveau plan ! Activation en cours…"); @@ -75,8 +59,10 @@ function AbonnementPage() { }); }; + const currentPlan = sub?.plan; + return ( -
+
Paramètres -
+ {/* Header — direct, pas trop de baratin */} +
Abonnement -

- Choisir son plan +

+ Trois mois pour voir. +
+ Puis vous décidez.

-

- Trois mois offerts au démarrage. Au-delà, passez Pro pour - continuer à relancer plus de 5 factures à la fois. +

+ Pas de carte demandée pour démarrer. Au-delà de 5 factures actives, + vous passez Pro pour continuer — quand vous êtes prêt, pas avant.

- {/* Plan courant */} + {/* État courant — slim, info-dense, pas un gros bloc */} {!isPending && sub && ( - )} - {/* Toggle mensuel / annuel */} -
+ {/* Toggle cycle */} +
+

+ Annulation 1-clic · pas d'engagement +

- {/* Cards plans */} -
- + + - onUpgrade("pro")} /> - onUpgrade("business")} />
+ {/* Footer rassurant */}

- Paiement sécurisé via Stripe (CB, SEPA). TVA selon votre pays. - Annulation possible à tout moment depuis le portail client. Aucun - engagement. + Paiement sécurisé via Stripe — CB ou SEPA, TVA selon votre pays. + Vous gardez la main sur votre abonnement à tout moment depuis le + portail client.

); } // --------------------------------------------------------------------------- -// Card plan courant +// Strip "Plan courant" — discret mais informatif (vs gros bloc IA-template) // --------------------------------------------------------------------------- -function CurrentPlanCard({ +function CurrentPlanStrip({ state, onOpenPortal, isOpeningPortal, @@ -158,72 +142,71 @@ function CurrentPlanCard({ isOpeningPortal: boolean; }) { const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state; - const isLimited = plan === "free" && caps.activeInvoicesLimit !== null; const limit = caps.activeInvoicesLimit; + const isLimited = plan === "free" && limit !== null; const limitReached = isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit; return ( - -
-
- Plan actuel -

- Rubis {planLabel(plan)} - {state.subscriptionStatus && state.subscriptionStatus !== "active" && ( - - · {state.subscriptionStatus} - - )} -

- {state.currentPeriodEnd && ( -

- Prochaine facture le{" "} - +

+ + +
+

+ {plan === "free" ? "Plan Rubis" : "Rubis"} + {plan !== "free" && ( + {plan === "pro" ? " Pro" : " Business"} + )} + {state.subscriptionStatus && state.subscriptionStatus !== "active" && ( + + · {state.subscriptionStatus} + + )} +

+

+ {plan === "free" && inGracePeriod && gracePeriodEndsAt && ( + <> + Période illimitée jusqu'au{" "} + + {formatDate(gracePeriodEndsAt)} + + + )} + {plan === "free" && !inGracePeriod && limit !== null && ( + <> + + {activeInvoicesCount} / {limit} + {" "} + factures actives + {limitReached && " — limite atteinte"} + + )} + {plan !== "free" && state.currentPeriodEnd && ( + <> + Renouvellement le{" "} + {formatDate(state.currentPeriodEnd)} {state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"} -

+ )} - {plan === "free" && inGracePeriod && gracePeriodEndsAt && ( -

-

- )} -
- {state.hasStripeCustomer && ( - - )} +

- {/* Compteur de factures actives — visible pour Free uniquement */} - {isLimited && limit !== null && ( -
-
-

- Factures actives en relance -

-

- {activeInvoicesCount} / {limit} -

-
-
+ {/* Mini progress bar mobile-OK pour Free */} + {isLimited && limit !== null && !inGracePeriod && ( +
+
- {limitReached && ( -

- Limite atteinte — passez Pro pour ajouter de nouvelles factures. -

- )}
)} - + + {state.hasStripeCustomer && ( + + )} +
); } // --------------------------------------------------------------------------- -// Toggle cycle +// Toggle cycle — mensuel par défaut, annuel à droite avec −17% // --------------------------------------------------------------------------- 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< - 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 = { - 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" ? : plan === "business" ? : null; - +function PlanFree({ current }: { current: boolean }) { return ( - -
- {planIcon && {planIcon}} -

- {planLabel(plan)} -

- {highlight && !isCurrent && ( - - Recommandé - - )} - {isCurrent && ( - - Actuel - - )} +
+
+

Découverte

+ {current && }
+

+ Gratuit +

-
-

- - {price === 0 ? "0" : price} - - - {cycle === "monthly" ? "€/mois" : "€/an"} - -

- {price !== null && price > 0 && cycle === "yearly" && ( -

- soit {(price / 12).toFixed(2).replace(".", ",")} €/mois -

- )} - {plan === "free" && ( -

3 mois illimités, puis 5 factures actives

- )} -
+

+ 0 € +

+

3 mois illimités, puis 5 factures actives

-
    - {features.map((f) => ( -
  • -
  • - ))} +
      + Tester sans poser sa CB + Plans de relance fournis + OCR + automatisations 3 mois + Au-delà : 5 factures à la fois
    - {!ctaDisabled && onUpgrade ? ( - - ) : ( - - )} +

    + Tout ce qu'il faut pour comprendre l'outil. Vous prendrez Pro + quand votre volume mensuel dépasse 5 factures à relancer. +

    +
+ ); +} + +// --------------------------------------------------------------------------- +// PlanProHero — la pièce maîtresse. Bg cream + gem en filigrane, prix XL. +// --------------------------------------------------------------------------- + +function PlanProHero({ + current, + cycle, + loading, + onUpgrade, +}: { + current: boolean; + cycle: BillingCycle; + 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 ( + + {/* Gem en filigrane, bas-droite, très large pour devenir un motif */} + ); } -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 ( +
+
+

Équipe

+ {current && } +
+

+ Business +

+ +

+ + {monthlyPrice.toString().replace(".", ",")} € + + / mois +

+

{totalLabel}

+ +
    + Tout Pro, plus : + 5 sièges utilisateurs + Réponses depuis votre email pro + Support prioritaire + SMS (à venir) +
+ + +
+ ); +} + +// --------------------------------------------------------------------------- +// Atomes +// --------------------------------------------------------------------------- + +function FeatureLine({ + children, + strong = false, + subtle = false, +}: { + children: React.ReactNode; + strong?: boolean; + subtle?: boolean; +}) { + return ( +
  • + {/* Petit losange rubis = bullet maison cohérent avec la marque */} +
  • + ); +} + +function CurrentBadge() { + return ( + + Actuel + + ); }