Commit graph

6 commits

Author SHA1 Message Date
le king fu
cdf4df85f7 fix(license): apply the second review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m15s
- Key entry and machine activation can no longer overlap. An activation
  in flight writes the token of the key it read: a key stored meanwhile
  ended up next to the previous key's token, the dead end 3f90635
  closes. "Enter a key" and the form's submit are disabled while an
  activation runs (LicenseKeyForm gains a busy prop), and "Activate"
  while a key is being stored.
- The "Activated" badge is hidden for a revoked key: the server
  deactivated every machine of the license.
- ADR 0017: the #347 amendment also points at section 1, which still
  mentions the machine-binding downgrade.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 20:44:51 -04:00
le king fu
3f90635298 fix(license): apply the review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
PR Check — Rust / rust (pull_request) Successful in 10m35s
- store_license_in drops activation.token when the stored key changes.
  The token names the previous license to the price server (its sub is
  the license id, the key carries the email), so the card showed the
  machine as activated while prices were refused for the old license.
  The same key entered again keeps its token; a refused key changes
  nothing.
- get_activation_status goes through activation_status_in(dir,
  machine_id, key), tested in a temporary directory: a missing,
  expired, foreign-machine, foreign-key or unreadable token reads as
  not activated. It alone decides "Not activated" and "Activate".
- LicenseCard is keyed by licenseIdentity(info): another key remounts
  the body, which re-reads the activation and drops the previous key's
  machine list.
- invalidSignature asks to check the copy first. jsonwebtoken checks
  the signature before decoding the payload, so a damaged copy often
  reads as a signature failure (characterization test). User guide and
  changelogs aligned.
- Render tests: the other errors stay visible under Machines, and the
  edition's green check gives way to an alert for a revoked key.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 19:58:22 -04:00
le king fu
d2ded515fd fix(license): name key-entry errors, keep the edition off the activation token, flag revoked keys
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m11s
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 #347
Fixes #360
Refs #363

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:39:01 -04:00
le king fu
170b22d356 fix(upsell): apply the review of #362
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m39s
- 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>
2026-09-27 20:47:04 -04:00
le king fu
57a0fbc2f7 feat(upsell): activate "Get Base", honest Premium note, purchase path in Free, Account card text
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s
- 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>
2026-09-27 15:50:45 -04:00
le king fu
01da65c215 docs(gating): ADR 0017 + architecture + user guide + CHANGELOG
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>
2026-07-20 22:48:55 -04:00