feat(upsell): activate "Get Base", honest Premium note, purchase path in Free, Account card text #362
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#362
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-346-purchase-cta"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #346
En édition gratuite, les écrans verrouillés affichaient « Obtenir {tier} » désactivé avec « Achat en ligne bientôt disponible ». Maintenant que #270 a livré
purchaseUrl, le bouton Base devient actif partout. Premium reçoit une note honnête, la Gratuite gagne un chemin d'achat et un lien vers la dernière version, et la carte Compte cesse de promettre des fonctionnalités inexistantes. Deuxième maillon deplanned-2026-09-24-paiements.Ce qui change
src/shared/upsellCta.ts,upsellCtaFor(tier)→{ purchasable, noteKey }. Base est achetable ; Premium reste désactivé avecupsell.premiumNotForSale(décision A1).PurchaseButton.tsxde l'issue) :PurchaseButton(« Obtenir Base ») etDownloadLatestButton(« Télécharger la dernière version ») ouvrentpurchaseUrldans le navigateur, sans routeur ;HaveKeyButtonne rend rien horsBrowserRouter, oùuseNavigatelèverait une erreur.UpsellGate: il utilise la règle et les CTA. Le lien d'achat n'appelle jamaisonNavigate, donc le modal hôte reste ouvert ; seul « J'ai déjà une clé » l'appelle.ProfileFormModal: il réutiliseUpsellGateau lieu de son panneau compact, garde son bouton Annuler (retour à la liste des profils) et se ferme après « J'ai déjà une clé » (onNavigate={onClose}).ProfileSwitcher(dans le routeur), il affiche « J'ai déjà une clé ».ProfileSelectionPage(hors routeur), il ne l'affiche pas, comme avant.settings.updates.notEntitledreformulé etdownloadLatestajouté ;docs.editions;architecture.md: ligneUpsellGate, CTA partagés,upsellCtaFor, ligne multi-profils, comptes (12 composants partagés) et liste desrc/shared/;Tests
upsellCta6,entitlements5)npm run buildPas de modification Rust, donc
check-rustne tourne pas.upsellCtaForest testée seule, puis sur toute la matrice. Budget, Ajustements, rapports avancés et multi-profils donnent un achat possible sans note ; le Bilan donne un bouton désactivé avec sa note, et cette note existe en FR comme en EN.entitlements.test.tsépingle le jeu exact de clésupsellpar langue, ce qui rattraperait le retour d'une clé périmée sans jamais la nommer. Il épingle aussi mot pour mot les textes décidés : note Premium, carte Compte, libellé de téléchargement, textenotEntitled.Preuve du rouge
Chaque mutation est appliquée depuis une copie de sauvegarde puis restaurée ; le diffstat est identique avant et après, et la suite revient à 27/27 verts.
upsellCtaForÀ noter
architecture.mdet les fichiers i18n. Selon l'ordre de merge, attendez-vous à des conflits textuels simples.🤖 Generated with Claude Code
- upsellCtaFor(tier) decides what a locked screen offers: Base is purchasable, Premium stays disabled with "Premium isn't sold online yet". Pure and tested on the whole entitlements matrix. - Shared CTAs, one component per file: PurchaseButton ("Get Base") and DownloadLatestButton open the localized product page (purchaseUrl) and need no router; HaveKeyButton renders only inside BrowserRouter. - UpsellGate uses them; the purchase link never calls onNavigate, so modal hosts stay open. ProfileFormModal reuses UpsellGate instead of its compact panel, keeps its Cancel button, and closes after "I already have a key" (onNavigate={onClose}). - In Free, the Updates card offers Get Base, I already have a key and Download the latest version; the error page (outside the router) offers Get Base and the download link. No update check, no network call. - i18n FR/EN: Account card text, Premium note, Updates notEntitled text, download label, editions guide step and tip; the "coming soon" note key is gone. Tests pin the upsell key set and the decided texts. - Docs: ADR 0017 amendment, architecture (UpsellGate row, shared CTAs, upsellCtaFor, multi-profile row, counts), user guide section 12; CHANGELOG FR/EN. Proven red: Premium made purchasable 2, balance sold as Base in the matrix 4, the old note key back in a locale 1, Account text altered 1, download label altered 1. Fixes #346 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>CI verte au head
57a0fbc, vérifiée dans les logs : run 429 (check-frontend),HEAD is now at 57a0fbc, build OK, 64 fichiers, 1257 tests vitest (comme en local).check-rustne s'est pas déclenché : aucun fichier soussrc-tauri/n'est touché.Verdict : APPROVE
Résumé. Le code fait ce que l'issue demande, et chaque critère d'acceptation se vérifie à la lecture. Les trois mutations annoncées se reproduisent au chiffre près. En revanche, le câblage des composants n'est gardé par aucun test, alors qu'il peut l'être sans jsdom : cinq mutations du relecteur sur six passent la suite sans un rouge, et une sonde de rendu en attrape cinq (suggestion 1).
Vérifié
57a0fbc, log lu.vite buildpasse, puis 64 fichiers et 1257 tests verts. En local, sur une copie extraite de la tête (git archive),npm run buildetvitest runsont verts, 1257/1257.ctaGetSoonremis dansen.json→ 1, le test du jeu de clés, qui ne nomme pas la clé.ProfileSelectionPage(App.tsx:97) etErrorPage(App.tsx:93,ErrorBoundary.tsx:29) sont rendues hors duBrowserRoutermonté enApp.tsx:109, etmain.tsxn'en monte aucun.HaveKeyButtonappelleuseInRouterContext()sans condition et ne monteuseNavigateque dans le sous-composant : le patron est correct.UpsellGatesuitupsellCtaFor;ProfileFormModaln'a plus de panneau propre, garde Annuler et passeonNavigate={onClose};ctaGetSoonn'apparaît plus nulle part (git grepvide).opener:defaultautorise les URLhttps, et l'appel est celui deLicenseCard.tsx:40.…_x64-setup.exe,…_amd64.deb). « Télécharger la dernière version » dit donc vrai.architecture.mdest exact : 12 composants danscomponents/shared/, contre 9 sur la base ;Fixes #346figure dans le message de commit.Suggestions (non bloquantes)
1. Le câblage des CTA n'est gardé par aucun test, et il peut l'être sans jsdom.
La prémisse « pas de jsdom, donc seules des fonctions pures se testent » vient de l'issue, et la PR la reprend. Elle ne tient pas :
renderToStaticMarkup(react-dom/server) rend dans l'environnement node de vitest ;FilterPanel.test.tsx(#274) appelle déjà un composant comme une fonction pour lire ses props.J'ai écrit une sonde de 6 tests sur une copie de la tête. Elle passe sur
57a0fbc: 1263/1263,tscpropre. Chaque mutation ci-dessous est appliquée puis restaurée, et le fichier est comparé octet par octet à la tête.UpsellGateignore la règle ({true ? (, l.57) : « Obtenir Base » actif sur le BilanuseInRouterContextretirée deHaveKeyButton(l.27)useNavigate() may be used only in the context of a <Router> component.PurchaseButtonouvrepurchaseUrl("fr")en dur (l.26)UpsellGatene rend plus la note Premium (l.69)UpsellGatene transmet plusonNavigateàHaveKeyButton(l.75)ProfileFormModalne passe plusonNavigate={onClose}(l.189)Deux lignes méritent un mot :
ProfileSelectionPage→ Gérer → Créer finirait sur l'ErrorBoundary.upsellest un état interne du modal. Elle reste à vérifier à la main dans l'app : ouvrir le modal depuisProfileSwitcher, cliquer « J'ai déjà une clé » ; le modal doit se fermer sur la carte Licence.Sonde proposée :
src/components/shared/UpsellCtas.render.test.tsx2. Le texte de la carte Compte est exact en pratique, pas dans le code.
Le texte D4 dit : « Les éditions Base et Premium viennent de la clé de licence, pas du compte. » Or
current_entitlementsaccorde encore Premium à un compte dont le cache signé portesubscription_status: "active"(license_commands.rs:290-296et:353-358). L'ADR 0017 le dit aussi en §Contexte : « ou depuis un abonnement Compte Maximus pour Premium ».Aucun abonnement n'est vendu, donc rien n'atteint ce chemin aujourd'hui. Le jour où un abonnement existera, le texte redeviendra faux en silence, comme celui de v0.7. Deux options :
3.
PurchaseButtonvend Base en dur.UpsellGaterend<PurchaseButton />dès quecta.purchasableest vrai, sans lui passer le palier. SiupsellCtaFor("premium")bascule un jour, l'écran du Bilan affichera « Obtenir Base » et ouvrira la page de Base : celui qui fera ce changement modifiera le test Premium, pas le bouton. Deux options :tierau bouton ;upsellCtaFor.4. Un
openUrlen échec ne laisse aucune trace.void openUrl(...)figure dansPurchaseButton.tsx:26etDownloadLatestButton.tsx:27, comme déjà dansLicenseCard.tsx:40. Si l'appel échoue, par exemple sous Linux sansxdg-open, le bouton ne fait rien.initLogCapturecapteconsole.errormais pasunhandledrejection, donc rien n'arrive dans Journaux. Un.catch((e) => logError("openUrl failed", e))suffit.5. Rédaction
docs.editions.steps[3]FR/EN : l'achat arrive en queue de « Les modules se déverrouillent immédiatement », donc après la saisie de la clé. Une étape à part, placée avant l'étape 3, suivrait l'ordre réel.account.title) ;Hors code
Le checkout du site reste en mode test tant que la-compagnie-maximus#141 et maximus-api#62 sont ouvertes. #141 porte déjà « release desktop re-gated » : aucune release ne doit embarquer ce bouton actif avant.
Suggestions de la revue appliquées :
170b22dLes cinq suggestions sont appliquées dans un seul commit,
fix(upsell): apply the review of #362.src/components/shared/UpsellCtas.render.test.tsx, étendue àDownloadLatestButtonet à la journalisation des échecs, soit 9 tests.check_account_edition, chemin dormant tant qu'aucun abonnement n'est vendu).PurchaseButton: un commentaire dansupsellCtaForsignale qu'avant de rendre Premium achetable, il faut donner au bouton le palier et une URL Premium. Le palier n'est pas passé au bouton dès maintenant, parce quepurchaseUrlne connaît qu'une page produit.openUrlen échec :.catchverslogErrordansPurchaseButton,DownloadLatestButtonetLicenseCard.LicenseCardn'a pas de test, car ses hooks empêchent de l'appeler comme une fonction.Mutations rejouées sur la tête finale
Worktree jetable à
170b22d,npm ci, suite complète pour chaque mutation, puis fichier restauré et comparé octet par octet. Base : 0 rouge sur 1266, code de sortie 0.upsellCtaFor)ctaGetSoonremis dansen.jsonUpsellGateignore la règleHaveKeyButtonProfileFormModalne passe plusonNavigate={onClose}PurchaseButton)onNavigatenon transmis àHaveKeyButtonopenUrlnon journalisé (PurchaseButton)openUrlnon journalisé (DownloadLatestButton)DownloadLatestButton)Chaque mutation rouge sort avec le code 1.
CI
Run 430 (
check-frontend) vert sur170b22d, log lu :HEAD is now at 170b22d, build OK, 65 fichiers, 1266 tests (1257 + 9).check-rustne s'est pas déclenché, à raison : aucun fichier soussrc-tauri/.Reste à vérifier à la main
En édition Gratuite, ouvrir le modal de création de profil depuis
ProfileSwitcheret cliquer « J'ai déjà une clé » : le modal doit se fermer sur la carte Licence.Avant toute release
Le checkout du site reste en mode test tant que la-compagnie-maximus#141 et maximus-api#62 sont ouvertes : aucun tag ne doit embarquer ce bouton actif avant. La contrainte est consignée dans
STATE.mdau merge.