# Spec Decisions — Gating des fonctionnalités par édition de licence > Date: 2026-07-19 > Projet: simpl-resultat > Statut: Draft > Slug: feature-gating ## Contexte Simpl'Résultat n'applique aujourd'hui **aucun gating de fonctionnalités** : un utilisateur Free a accès à tout, sauf la récupération des cours (seul gate dur, vérifié côté serveur via maximus-api). L'infrastructure de licence est pourtant déjà là — 3 éditions signées Ed25519 (`free`/`base`/`premium`), table `FEATURE_TIERS`, champ `features[]` signé — mais seuls 3 call-sites de gate existent (auto-update, laissé ouvert ; cours, Premium). Max veut définir un vrai périmètre d'abonnement en verrouillant des modules selon le tier. ## Objectif Appliquer une matrice d'entitlements Free/Base/Premium à travers l'app : chaque module hors du tier de l'utilisateur reste **visible mais verrouillé**, avec un écran d'upsell. Le gating est piloté par un hook central côté React, adossé à l'édition de licence déjà vérifiée, avec un override par-licence optionnel. Aucune donnée n'est supprimée — le durcissement bloque l'accès, il ne détruit rien. ## Scope ### IN - Matrice d'entitlements Free/Base/Premium (ci-dessous). - Hook central `useEntitlement(feature)` + garde de navigation (Sidebar) et de route. - Écran/zone d'upsell « verrouillé » (cadenas, CTA upgrade), i18n FR/EN. - Enrichir `FEATURE_TIERS` (Rust) + câbler l'override par-licence (`features[]` consulté par `is_feature_allowed`). - Gating **par onglet** des rapports (`/reports/*`) : Free = Tendance uniquement. - Absorption du re-gate auto-update (#271) : l'auto-update devient Base+. - Durcissement sec **non destructif** : blocage d'accès immédiat, données conservées, récupérables par upgrade. ### OUT (explicitement exclu) - Le champ `product` explicite (#270) — reste dans spec-paiements. - Le go-live Stripe / le flux d'achat en ligne (l'upsell pointe vers l'achat, dont l'URL localisée relève de #270). - Un vrai système de codes d'invitation (l'émission de clés reste manuelle via l'endpoint admin `generate`). - Tout gate « dur » server-enforced au-delà de l'existant (cours) — le gating local est un soft paywall assumé (GPL). - Un 4e tier « admin » : l'accès total de Max = une licence Premium auto-émise. - Mode lecture-seule des features verrouillées (écarté : durcissement sec, pas de distinction voir/éditer). ## Matrice d'entitlements | Module | Free | Base | Premium | |---|:--:|:--:|:--:| | Dashboard | ✅ | ✅ | ✅ | | Import CSV | ✅ | ✅ | ✅ | | Transactions | ✅ | ✅ | ✅ | | Catégories (+ guide, migration) | ✅ | ✅ | ✅ | | Rapport **Tendance** | ✅ | ✅ | ✅ | | Autres rapports (Highlights, Comparaison, Catégorie, Cartes) | ❌ | ✅ | ✅ | | Budget | ❌ | ✅ | ✅ | | Multi-profils | ❌ | ✅ | ✅ | | Auto-update | ❌ | ✅ | ✅ | | **Bilan complet** (patrimoine + cours) | ❌ | ❌ | ✅ | | Export/Import chiffré | ✅ | ✅ | ✅ | | Changelog / Docs | ✅ | ✅ | ✅ | Admin (Max) = licence **Premium** auto-émise (superset). ## Décisions prises | Question | Décision | Raison | |---|---|---| | UX du gate (feature hors tier) | **Verrouillé + upsell** : entrée visible avec cadenas ; clic → écran « Passez à Base/Premium » | Meilleure conversion — l'utilisateur voit ce qu'il rate | | Données existantes lors du durcissement | **Durcissement sec non destructif** : blocage d'accès (vue + édition) immédiat, mais **données conservées**, jamais supprimées, récupérables par upgrade | Simple à coder (pas de mode lecture-seule) ; ne détruit aucune donnée financière (clarifié par Max) | | Modèle d'entitlements | **Statique + override licence** : map `tier → features` en dur, PLUS `license.features[]` consulté en additif (`allowed = feature ∈ tier OU feature ∈ license.features`) | Matrice centralisée et simple, + débloque le champ `features[]` déjà signé pour des licences spéciales sans créer de tier | | Coordination spec-paiements | **Absorber #271** (re-gate auto-update) ici ; **#270** (`product`) reste séparé | L'auto-update est dans la matrice → gaté ici, #271 redondant ; `product`/URL d'achat = go-live, hors scope | | Enforcement | **UI-only** (gardes React : nav + route). `current_edition()` (Rust) reste la source de vérité lue ; les commandes Tauri ne refusent pas (sauf l'existant : cours server-enforced) | Cohérent avec le soft-paywall GPL assumé — un gate Rust serait aussi retirable par un fork, sans gain de sécurité | | Écran d'upsell | **Composant générique paramétré** (feature + tier requis), i18n FR/EN ; CTA « Obtenir Base/Premium » (lien d'achat, dépend de #270) + « J'ai déjà une clé » → Réglages → Licence | Un seul composant réutilisable, pas un écran par feature | | Admin (accès total de Max) | **Licence Premium auto-émise** (endpoint admin `generate`), pas de 4e édition ni mode dev | Premium = superset ; réutilise l'infra existante, zéro code neuf | | Free multi-profils existants | **Profil actif conservé** ; profils supplémentaires verrouillés → upsell (données conservées) | Cohérent avec durcissement sec + conservation des données | ## Contraintes - **GPL-3.0 + privacy-first** : le gating local est un soft paywall **assumé** (contournable par fork). Seules les features adossées à maximus-api (les cours, aujourd'hui) sont dures. Aucune donnée financière ne quitte l'appareil. - **Séquencement** : le gating est livrable et testable **indépendamment de Stripe** (pas encore en live) via des clés admin-émises. Corollaire : sans ce chantier, distribuer des clés n'a aucun effet visible (tout est déjà ouvert). ## Références | Source | Pertinence | |---|---| | `spec-monetisation.md` | Modèle des 3 tiers, contraintes GPL / soft-paywall, format de licence | | `src-tauri/src/commands/entitlements.rs` | `FEATURE_TIERS` + `is_feature_allowed` — table à enrichir, override `features[]` à câbler | | `src-tauri/src/commands/license_commands.rs` | `current_edition()` (source de vérité de l'édition) + `LicenseClaims.features` (champ signé à consulter) | | `src/services/licenseService.ts`, `src/hooks/useLicense.ts`, `src/hooks/useIsPremium.ts` | Base du futur hook `useEntitlement` | | `src/App.tsx` (routes `/reports/*`), `src/components/layout/Sidebar` | Points de garde nav + route |