fix(parametres): teaser banking l'emporte sur upsell + section démo admin-only
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 42s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m26s
Build & Deploy API / build-and-deploy (push) Successful in 2m10s

Deux bugs visibles dans /parametres :

1. **Banque** — un user Free voyait la carte "Plan Pro ou Business
   requis" alors que la feature est gated derrière BANKING_ENABLED=false
   en prod (Powens KYC en cours). L'upgrade n'aurait rien débloqué.
   Fix : la branche `comingSoon` court-circuite l'upsell, et le titre +
   description de la SettingsSection bascule en mode teaser ("Bientôt :
   votre banque connectée à Rubis") pour rester cohérent avec la carte
   "Bientôt disponible" en dessous.

2. **Démonstration** — la section apparaissait pour tous les users,
   alors que c'est un outil de prospection commerciale réservé aux
   admins Rubis (horloge virtuelle + capture des emails). Déroutant
   pour un user lambda.
   Fix : section gated sur `user.isAdmin` côté UI, et split des routes
   /demo côté API :
   - GET /demo/state reste accessible à tous les users authed (sinon le
     DemoClock dans AppLayout spam des 403 sur chaque page). Un user
     normal reçoit `{active: false}` — pas de leak.
   - GET /demo/inbox + POST /demo/start, /end, /tick : auth + admin.
     Mutations et lecture des emails capturés.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-14 13:57:37 +02:00
parent e449b708f3
commit b81bc2609b
3 changed files with 65 additions and 26 deletions

View File

@ -365,20 +365,36 @@ router
.use(middleware.auth()) .use(middleware.auth())
/** /**
* Demo auth requise. Mode démo opt-in par org (cf. CLAUDE.md * Demo réservé aux admins Rubis (outil de prospection commerciale).
* Architecture). Routes opérantes seulement si `org.demo_mode = true`. * Gating fin :
* - GET /state : auth seul, parce que `DemoClock` dans `AppLayout`
* fetch sur chaque page pour tous les users. Un non-admin reçoit
* `{ active: false }` (son org n'est jamais en mode démo) pas
* de leak, juste un payload neutre.
* - GET /inbox + POST /start, /end, /tick : auth + admin. Mutations
* et lecture des emails capturés (potentiellement sensibles).
*
* UI : la section "Démonstration" dans /parametres est aussi gated
* sur `user.isAdmin` (cf. parametres.tsx).
*/ */
router
.group(() => {
router.get('state', [controllers.Demo, 'state']).as('state')
})
.prefix('demo')
.as('demo')
.use(middleware.auth())
router router
.group(() => { .group(() => {
router.post('start', [controllers.Demo, 'start']).as('start') router.post('start', [controllers.Demo, 'start']).as('start')
router.post('end', [controllers.Demo, 'end']).as('end') router.post('end', [controllers.Demo, 'end']).as('end')
router.post('tick', [controllers.Demo, 'tick']).as('tick') router.post('tick', [controllers.Demo, 'tick']).as('tick')
router.get('state', [controllers.Demo, 'state']).as('state')
router.get('inbox', [controllers.Demo, 'inbox']).as('inbox') router.get('inbox', [controllers.Demo, 'inbox']).as('inbox')
}) })
.prefix('demo') .prefix('demo')
.as('demo') .as('demo.admin')
.use(middleware.auth()) .use([middleware.auth(), middleware.admin()])
/** /**
* Dashboard auth requise. Calculs agrégés on-the-fly (pas de cache V1). * Dashboard auth requise. Calculs agrégés on-the-fly (pas de cache V1).

View File

@ -81,16 +81,20 @@ export function BankingSection({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [callbackStatus, callbackReason]); }, [callbackStatus, callbackReason]);
if (!isPaid) { // Order matters : `comingSoon` doit gagner sur `isPaid`. La feature n'est
return <UpsellCard />; // pas dispo pour personne pendant la fenêtre KYC Powens, donc inciter un
} // Free à passer Pro pour "connecter sa banque" serait trompeur (il
// tomberait sur "Bientôt disponible" après l'upgrade). On annonce la
// Banking pas encore activé mais teaser ON → afficher "Bientôt disponible" // feature à venir à tout le monde ; l'upsell reviendra automatiquement
// pour annoncer la feature aux Pro/Business pendant la fenêtre KYC Powens. // une fois `BANKING_ENABLED=true` en prod.
if (status?.comingSoon) { if (status?.comingSoon) {
return <ComingSoonCard />; return <ComingSoonCard />;
} }
if (!isPaid) {
return <UpsellCard />;
}
return ( return (
<BankingPaidView <BankingPaidView
isLoading={connectionsQuery.isLoading} isLoading={connectionsQuery.isLoading}

View File

@ -13,6 +13,7 @@ import { Button } from "@rubis/ui";
import { Card } from "@rubis/ui"; import { Card } from "@rubis/ui";
import { useSubscription } from "@/lib/billing"; import { useSubscription } from "@/lib/billing";
import { useBankingStatus } from "@/lib/banking"; import { useBankingStatus } from "@/lib/banking";
import { useAuth } from "@/lib/auth";
/** /**
* Search params optionnels : * Search params optionnels :
@ -52,6 +53,12 @@ function ParametresPage() {
const { data: bankingStatus } = useBankingStatus(); const { data: bankingStatus } = useBankingStatus();
const showBanking = const showBanking =
bankingStatus?.enabled === true || bankingStatus?.comingSoon === true; bankingStatus?.enabled === true || bankingStatus?.comingSoon === true;
// Mode démo réservé aux admins Rubis (outil de prospection en démo
// commerciale). Un user lambda n'a aucune raison d'y avoir accès — c'est
// une horloge virtuelle + capture des emails, donc complètement déroutant
// si l'user croit que c'est une feature produit.
const { user } = useAuth();
const isAdmin = !!user?.isAdmin;
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -188,11 +195,21 @@ function ParametresPage() {
<SettingsSection <SettingsSection
eyebrow="Banque" eyebrow="Banque"
title={ title={
bankingStatus?.comingSoon ? (
<>
Bientôt : votre <em className="text-rubis">banque</em> connectée à Rubis
</>
) : (
<> <>
Connecter votre <em className="text-rubis">banque</em> Connecter votre <em className="text-rubis">banque</em>
</> </>
)
}
description={
bankingStatus?.comingSoon
? "Nous finalisons notre agrément AISP avec Powens. Une fois ouvert, Rubis lira vos virements entrants pour détecter automatiquement les factures payées — en lecture seule, sans déplacement de fonds."
: "Rubis lit vos virements entrants pour détecter automatiquement les factures payées. Lecture seule, aucun déplacement de fonds. Disponible sur les plans Pro et Business."
} }
description="Rubis lit vos virements entrants pour détecter automatiquement les factures payées. Lecture seule, aucun déplacement de fonds. Disponible sur les plans Pro et Business."
> >
<BankingSection <BankingSection
callbackStatus={search.banking} callbackStatus={search.banking}
@ -201,6 +218,7 @@ function ParametresPage() {
</SettingsSection> </SettingsSection>
)} )}
{isAdmin && (
<SettingsSection <SettingsSection
eyebrow="Démonstration" eyebrow="Démonstration"
title={ title={
@ -212,6 +230,7 @@ function ParametresPage() {
> >
<DemoToggle /> <DemoToggle />
</SettingsSection> </SettingsSection>
)}
<SettingsSection <SettingsSection
eyebrow="Zone danger" eyebrow="Zone danger"