feat(gating): garde UI — RequireFeature + UpsellGate + i18n #304

Closed
maximus wants to merge 0 commits from issue-298-require-feature into main
Owner

Composants de garde réutilisables au-dessus du socle #297.

  • UpsellGate (src/components/shared/) : écran verrouillé plein (cadenas, titre « Fonctionnalité », description par feature). 2 CTA : « Obtenir » visible mais désactivé avec la note « Achat en ligne bientôt disponible » (décision de planification — #270 l'activera), et « J'ai déjà une clé » → navigate("/settings/users") (route vérifiée : App.tsx:125, LicenseCard montée dans UsersSettingsPage.tsx:34). Libellé du tier via les clés existantes license.editions.*.
  • RequireFeature : loader neutre tant que !ready (aucun flash upsell au boot), puis children si allowed, sinon UpsellGate. Rend <Outlet/> quand children est omis → utilisable en layout-route groupant les routes d'une même feature (les deux formes marchent pour l'issue #299).
  • requiredTierFor() (helper pur additif dans src/shared/entitlements.ts) : dérive le tier minimal débloquant depuis l'appartenance à la matrice (pas l'ordre du tableau) — testable sans jsdom, réutilisable par les tooltips de #299.
  • i18n : upsell.* (titre, descriptions par feature, CTA) + nav.locked dans les deux locales. nav.locked sans interpolation (robuste pour tous les call-sites Sidebar/profils).
  • Tests (+6) : mapping/minimalité de requiredTierFor + couverture i18n upsell.features.<FeatureKey> fr+en (clé manquante = clé brute à l'écran) + pas de clé stale. Les composants eux-mêmes ne sont pas testables sans jsdom (précédent accepté) — vérif runtime déléguée à la review.

Validation : 858 vitest verts (852 + 6), npm run build (tsc + vite) propre. Pas de CHANGELOG (centralisé dans #302).

Resolves #298
Generated autonomously by /autopilot run of 2026-07-20

Composants de garde réutilisables au-dessus du socle #297. - `UpsellGate` (`src/components/shared/`) : écran verrouillé plein (cadenas, titre « Fonctionnalité <tier> », description par feature). 2 CTA : « Obtenir <tier> » **visible mais désactivé** avec la note « Achat en ligne bientôt disponible » (décision de planification — #270 l'activera), et « J'ai déjà une clé » → `navigate("/settings/users")` (route vérifiée : `App.tsx:125`, LicenseCard montée dans `UsersSettingsPage.tsx:34`). Libellé du tier via les clés existantes `license.editions.*`. - `RequireFeature` : loader neutre tant que `!ready` (aucun flash upsell au boot), puis `children` si `allowed`, sinon `UpsellGate`. Rend `<Outlet/>` quand `children` est omis → utilisable en layout-route groupant les routes d'une même feature (les deux formes marchent pour l'issue #299). - `requiredTierFor()` (helper pur additif dans `src/shared/entitlements.ts`) : dérive le tier minimal débloquant depuis l'appartenance à la matrice (pas l'ordre du tableau) — testable sans jsdom, réutilisable par les tooltips de #299. - i18n : `upsell.*` (titre, descriptions par feature, CTA) + `nav.locked` dans **les deux** locales. `nav.locked` sans interpolation (robuste pour tous les call-sites Sidebar/profils). - Tests (+6) : mapping/minimalité de `requiredTierFor` + couverture i18n `upsell.features.<FeatureKey>` fr+en (clé manquante = clé brute à l'écran) + pas de clé stale. Les composants eux-mêmes ne sont pas testables sans jsdom (précédent accepté) — vérif runtime déléguée à la review. Validation : 858 vitest verts (852 + 6), `npm run build` (tsc + vite) propre. Pas de CHANGELOG (centralisé dans #302). Resolves #298 Generated autonomously by /autopilot run of 2026-07-20
maximus added 1 commit 2026-07-21 02:05:26 +00:00
feat(gating): UI guard — RequireFeature + UpsellGate + i18n
All checks were successful
PR Check / rust (pull_request) Successful in 21m44s
PR Check / frontend (pull_request) Successful in 2m28s
554373e7d8
Add the reusable gating guard components on top of the #297 foundation:

- UpsellGate: full locked screen (lock icon, tier title, per-feature
  description). Two CTAs: "Get <tier>" rendered VISIBLE but DISABLED with
  an "online purchase coming soon" note (per planning decision — #270 will
  activate it), and "I already have a key" navigating to /settings/users
  (LicenseCard).
- RequireFeature: renders a neutral loader while the license is not ready
  (no upsell flash at boot), then children or UpsellGate. Renders <Outlet/>
  when children are omitted so it also works as a layout route grouping
  all routes of one feature.
- requiredTierFor() pure helper in shared/entitlements.ts derives the
  minimum unlocking tier from matrix membership (not array order).
- i18n: upsell.* (title, per-feature descriptions, CTAs) + nav.locked in
  BOTH locales; tier labels reuse the existing license.editions.* keys.
- Tests: requiredTierFor mapping/minimality + upsell i18n coverage for
  every FeatureKey in fr and en (the components themselves are not
  testable without jsdom).

Resolves #298

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maximus added the
status:review
type:feature
source:human
autopilot:pending-human
labels 2026-07-21 02:05:34 +00:00
Author
Owner

Review — APPROVE

Maillon 2 de la pile feature-gating, correctement basé sur main (socle #303/#297 mergé). Le code implémente fidèlement le contrat du socle et les décisions de la spec ; tous les claims du body ont été vérifiés contre le code réel de la branche. CI verte (rust + frontend), 6 tests pertinents, aucun point bloquant.

Vérifications

  • Contrat anti-flash : RequireFeature rend un loader neutre tant que status !== "ready" — jamais l'upsell pendant boot/erreur IPC ; la boucle de retry (backoff cappé 30s) vit bien dans LicenseProvider. Double sûreté : allowed est fail-closed pendant le boot (édition par défaut free), le contenu gated ne fuite jamais.
  • requiredTierFor correct et total : la matrice ne contient que des features gated (les features Free n'ont pas de clé), donc includes("base") ? "base" : "premium" donne le tier minimal exact pour les 5 clés ; le tier passé à license.editions.${tier} ne peut jamais être free → la clé résout toujours.
  • Route du CTA : /settings/users confirmée (App.tsx:125) et LicenseCard montée dans src/pages/settings/UsersSettingsPage.tsx:34. CTA « Obtenir » désactivé + note « bientôt » = conforme à la décision de planification (#270 l'activera).
  • i18n symétrique : upsell.* + nav.locked identiques en structure fr/en, {{tier}} des deux côtés, license.editions.* préexistantes confirmées.
  • Tests : invariant de minimalité robuste aux évolutions de la matrice ; la couverture i18n échouera si une future FeatureKey arrive sans traduction ou si une clé stale traîne. Pas de skip/only.
  • Sécurité : pas de secrets, pas d'injection (clés i18n dérivées de littéraux typés, échappement i18next/React), fail-closed CWE-863 du socle non contourné.
  • Data : aucune migration.
  • CI : PR Check / rust + PR Check / frontend success sur le head. Rappel pile : #305-#308 (bases non-main) ne seront pas CI-validées → valider le tip cumulé en local avant merge.

Suggestions (non bloquantes)

  1. requiredTierFor n'est pas défensif sur une clé hors matrice (ENTITLEMENTS[f].includes crasherait sur un cast inconnu), là où isEntitled gère le drift du namespace partagé avec Rust. Aucun risque aux call-sites actuels (littéraux FeatureKey) ; à durcir seulement si le helper reçoit un jour une valeur du namespace JWT.
  2. Le spinner de RequireFeature n'a pas de role="status"/aria-label — pattern cohérent avec les loaders existants d'App.tsx ; à harmoniser si un chantier accessibilité s'ouvre.
  3. nav.locked livré sans consommateur dans ce maillon — préparation assumée pour #299 (PR #305), déjà testée ; à confirmer consommée lors de la review de #305.

Review générée par /pr-review

## Review — APPROVE Maillon 2 de la pile feature-gating, correctement basé sur `main` (socle #303/#297 mergé). Le code implémente fidèlement le contrat du socle et les décisions de la spec ; tous les claims du body ont été vérifiés contre le code réel de la branche. CI verte (rust + frontend), 6 tests pertinents, aucun point bloquant. ### Vérifications - **Contrat anti-flash** : `RequireFeature` rend un loader neutre tant que `status !== "ready"` — jamais l'upsell pendant boot/erreur IPC ; la boucle de retry (backoff cappé 30s) vit bien dans `LicenseProvider`. Double sûreté : `allowed` est fail-closed pendant le boot (édition par défaut `free`), le contenu gated ne fuite jamais. - **`requiredTierFor` correct et total** : la matrice ne contient que des features gated (les features Free n'ont pas de clé), donc `includes("base") ? "base" : "premium"` donne le tier minimal exact pour les 5 clés ; le tier passé à `license.editions.${tier}` ne peut jamais être `free` → la clé résout toujours. - **Route du CTA** : `/settings/users` confirmée (`App.tsx:125`) et `LicenseCard` montée dans `src/pages/settings/UsersSettingsPage.tsx:34`. CTA « Obtenir » désactivé + note « bientôt » = conforme à la décision de planification (#270 l'activera). - **i18n symétrique** : `upsell.*` + `nav.locked` identiques en structure fr/en, `{{tier}}` des deux côtés, `license.editions.*` préexistantes confirmées. - **Tests** : invariant de minimalité robuste aux évolutions de la matrice ; la couverture i18n échouera si une future `FeatureKey` arrive sans traduction ou si une clé stale traîne. Pas de `skip`/`only`. - **Sécurité** : pas de secrets, pas d'injection (clés i18n dérivées de littéraux typés, échappement i18next/React), fail-closed CWE-863 du socle non contourné. - **Data** : aucune migration. - **CI** : `PR Check / rust` + `PR Check / frontend` **success** sur le head. Rappel pile : #305-#308 (bases non-`main`) ne seront pas CI-validées → valider le tip cumulé en local avant merge. ### Suggestions (non bloquantes) 1. `requiredTierFor` n'est pas défensif sur une clé hors matrice (`ENTITLEMENTS[f].includes` crasherait sur un cast inconnu), là où `isEntitled` gère le drift du namespace partagé avec Rust. Aucun risque aux call-sites actuels (littéraux `FeatureKey`) ; à durcir seulement si le helper reçoit un jour une valeur du namespace JWT. 2. Le spinner de `RequireFeature` n'a pas de `role="status"`/`aria-label` — pattern cohérent avec les loaders existants d'`App.tsx` ; à harmoniser si un chantier accessibilité s'ouvre. 3. `nav.locked` livré sans consommateur dans ce maillon — préparation assumée pour #299 (PR #305), déjà testée ; à confirmer consommée lors de la review de #305. *Review générée par `/pr-review`*
Author
Owner

Mergé en fast-forward sur main (pile #304→#308), voir commit 554373e. Verdict /pr-review : APPROVE. Fermeture manuelle : le merge local ff n est pas détecté comme merged par Forgejo.

Mergé en fast-forward sur main (pile #304→#308), voir commit 554373e. Verdict /pr-review : APPROVE. Fermeture manuelle : le merge local ff n est pas détecté comme *merged* par Forgejo.
maximus closed this pull request 2026-07-22 00:23:43 +00:00
All checks were successful
PR Check / rust (pull_request) Successful in 21m44s
PR Check / frontend (pull_request) Successful in 2m28s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#304
No description provided.