Rust (license_commands.rs)
- LicenseError gains InvalidPrefix, InvalidSignature, Malformed and
UnknownEdition. validate_with_key returns it (ExpiredSignature maps
to LicenseExpired), and store_license, read_license and
validate_license_key reject with the same JSON as the network
commands. store_license_in and read_license_in take a directory, so
the Storage mapping is tested at the source.
- D12: the edition comes from the license key alone.
resolve_license_entitlements no longer takes the activation token,
and current_entitlements reads only license.key
(entitlements_from_dir). A token that had expired (2-year server
lifetime) or came from another machine (restored data) used to drop
a valid key to free, while a key copied without its token kept its
edition. get_activation_status and persist_activation_token keep the
strict token validation, and the key keeps its strict validation
(CWE-613).
Front
- LicenseCard names the load error, and LicenseKeyForm (extracted so a
static render reaches it) names a refused key, through
parseLicenseError: no raw backend string reaches the card.
- D13: a license_revoked answer is recorded in LicenseContext for the
whole session, because the card unmounts when the user leaves
Settings > Users. Only a different key clears it. Card-level
message, alert icon, Activate disabled through selectCanActivate.
- Machine counter n/limit, "Not activated", activating and
deactivating labels, dates in fr-CA/en-CA, and "No expiry date"
beyond 50 years (licenseExpiry.ts, for the 100-year keys of
maximus-api#63).
- license.removeLicense and license.machines.activateError removed;
the price authFailed message points to activating the machine.
Tests: 15 new Rust tests (resolution from a data directory, named
variants, the 100-year key of #363, a key without exp) replace 3
machine-binding tests; vitest covers the card and form renders, the
context revocation, the hook wiring and the locales.
test: reproduces #347 (an expired token of this machine resolved a
valid Base key to free on 5d549d6).
Docs: CHANGELOG FR/EN, guide section 12 and in-app docs,
architecture.md, ADR 0017 amendment for #347, qa-update-cycle.md,
api-contract-prices.md.
Fixes#347Fixes#360
Refs #363
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Render tests for the upsell CTAs (UpsellCtas.render.test.tsx): the
reviewer's probe, which needs no jsdom (react-dom/server in the node
environment, plus the FilterPanel.test.tsx pattern of calling a
component as a function). It pins the Base/Premium CTA on UpsellGate,
the router guard of HaveKeyButton, the localized product URL, the
Premium note and the onNavigate forwarding, none of which the suite
guarded before.
- A failure to open the browser (openUrl) is now logged through
logError in PurchaseButton, DownloadLatestButton and LicenseCard,
instead of vanishing as an unhandled rejection that never reaches the
in-app logs. Tested for both shared buttons.
- upsellCtaFor: comment the trap in making Premium purchasable, since
PurchaseButton sells Base only.
- ADR 0017 amendment: the Account card text holds in practice only;
current_entitlements still grants Premium to an active subscription,
a dormant path until a subscription is sold.
- User guide and in-app docs: the purchase step now comes before the
key entry, and the Balance button is named "Get Premium".
- CHANGELOG: "Maximus Account" in English, "every screen that requires
it" in both files (the Balance screen sells nothing), and the French
elision before a guillemet.
Refs #346
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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>
Document the edition-gating work (#297-#301):
- ADR 0017 (accepted): tier->features matrix, signed features[] override
fail-closed in Free (CWE-863), UI-only enforcement as an assumed GPL
soft-paywall (server-enforced price fetching stays the only hard gate),
non-destructive downgrade, dev-override behind an explicit Cargo
feature (CWE-489), rejected alternatives.
- architecture.md: new 'Gating par edition' section (entitlements matrix,
LicenseContext, useEntitlement, RequireFeature/UpsellGate, NavLock,
profileGate, Rust side), rewritten entitlements.rs section (auto-update
now Base+, stale 'open to free' note removed), gated routes listed in
the routing section, hooks table updated (useLicense removed in #297 ->
useEntitlement/useIsPremium), ADR index + header refreshed.
- guide-utilisateur.md + docs.editions.* i18n keys (FR/EN) wired into
DocsContent: new 'Editions' section with the Free/Base/Premium table,
unlock flow and non-destructive locking tips.
- CHANGELOG.md + CHANGELOG.fr.md: one global [Unreleased] entry listing
the modules now gated Base (Budget, Adjustments, advanced reports,
multi-profile, auto-update) and Premium (Balance), the visible-but-
locked upsell with disabled 'coming soon' purchase CTA, and the
data-preserving behaviour.
Resolves#302
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>