You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Split activation into verify (read-only) → server validate → commit (persist), so invalid keys never touch disk
- Replace `Result<_, String>` with `LicenseActivationError` typed enum across Rust + TypeScript (frontend switches on `code`, not substrings)
- Distinguish `Success`/`UpstreamError`/`NetworkError` in validation so Paddle outages don't overwrite cached "active" status
- License server returns 502 on Paddle failures and strips multi-seat transaction ID suffixes
- `LicenseKeyDialog` shows key-value details view with pending verification state, server-invalid warnings, reset flow, and success toasts
- Embed `shortCode` in signed license payload, add `success` toast level, allow `reset_license` in release builds
- Paddle.js loaded async with proper init sequencing on pricing page
- Updated CLAUDE.md files for licensing (Rust + Svelte), UI, and license server
- Add new toast notification type ("success")
The frontend uses `license_type` to construct a fallback `LicenseStatus` when the server is unavailable.
62
+
63
+
Legacy `activate_license`/`activate_license_async` wrappers still exist for backward compatibility — they call
64
+
`commit_license` internally (verify + commit in one call).
65
+
48
66
## Key patterns
49
67
50
68
- Short codes: `CMDR-XXXX-XXXX-XXXX` (3 segments × 4 alphanumeric chars after CMDR prefix).
@@ -71,6 +89,18 @@ update LICENSE_CACHE ← in-memory fast path
71
89
**Decision**: Short codes (`CMDR-XXXX-XXXX-XXXX`) exchanged server-side for full crypto keys, rather than being directly verifiable.
72
90
**Why**: Short codes are human-friendly for typing and sharing, but too short to embed a full Ed25519 signature. The server maps short codes to full keys, so users get a nice entry experience while the app still gets a cryptographically verifiable key for offline use.
73
91
92
+
**Decision**: `LicenseActivationError` typed enum instead of `Result<_, String>` for activation errors.
93
+
**Why**: The frontend was pattern-matching English substrings to decide which error message to show. A tagged enum (`#[serde(tag = "code")]`) serializes as `{ code: "badSignature" }` so the frontend can `switch` on the code. Follows the same pattern as `MtpConnectionError`. The enum lives in `verification.rs` and is used by `validation_client.rs` too.
94
+
95
+
**Decision**: Verify/commit split — `verify_license_async` (read-only) + `commit_license` (persist), instead of a single `activate_license_internal` that does both.
96
+
**Why**: The old flow stored the key before server validation. If the server said "invalid" and the user force-quit (or the app crashed), the invalid key persisted to disk. Now the frontend verifies first, validates with the server, and only commits on success or network fallback. Invalid keys never touch disk, eliminating the need for defensive `resetLicense()` cleanup in error handlers.
97
+
98
+
**Decision**: `VerifyResult` is a separate struct from `LicenseInfo`.
99
+
**Why**: `VerifyResult` carries `full_key` (needed to call `commit_license` later) and `short_code`. These fields shouldn't leak to the frontend via `get_license_info` — they're only meaningful during the activation flow. Keeping them separate means `LicenseInfo` stays clean for its primary use case (displaying license details).
100
+
101
+
**Decision**: `validate_license_async` accepts an optional `transaction_id` parameter.
102
+
**Why**: During activation, the key isn't stored yet, so the function can't read the transaction ID from the store. The frontend passes it explicitly. For periodic re-validation (7-day cycle), the parameter is `None` and the function falls back to reading from the stored license. This avoids storing the key just to read the transaction ID back.
103
+
74
104
**Decision**: `CMDR_MOCK_LICENSE` env var bypasses all license logic including server calls.
75
105
**Why**: License UX testing requires seeing every state (personal, supporter, commercial, expired, with/without modals). Without mocking, you'd need real license keys for each variant and a running license server. The mock skips network entirely, making UI development fast.
76
106
@@ -82,6 +112,12 @@ update LICENSE_CACHE ← in-memory fast path
82
112
**Gotcha**: `ValidationResponse` uses manual `#[serde(rename)]` on individual fields instead of `#[serde(rename_all)]` on the struct.
83
113
**Why**: The license server API returns a mix of naming conventions — `status` is lowercase, but `organizationName` and `expiresAt` are camelCase, and `type` is a Rust keyword requiring rename. The struct matches the API as-is rather than imposing a consistent naming convention that would break deserialization.
84
114
115
+
**Gotcha**: `validate_with_server` returns `ValidationOutcome` (an enum with `Success`/`UpstreamError`/`NetworkError`), not `Option<ValidationResponse>`.
116
+
**Why**: The license server returns HTTP 502 when it can't reach Paddle (upstream error) vs HTTP 200 with `status: "invalid"` when Paddle actively says the transaction is unknown. The old code collapsed both into `None`, causing `validate_license_async` to trust a stale "invalid" response from a transient Paddle outage and overwrite the cached "active" status. Now `UpstreamError` and `NetworkError` both fall back to cached status without overwriting, while `Success` (even with `status: "invalid"`) is treated as definitive and cached.
117
+
118
+
**Gotcha**: `validate_license_async` returns `Result<AppStatus, String>`, not bare `AppStatus`.
119
+
**Why**: The Tauri command must propagate network/upstream errors to the frontend so it can distinguish "server actively rejected the key" (`Ok(Personal)`) from "couldn't reach the server" (`Err`). Without this, the frontend's catch block never fires — Tauri's `invoke` only throws on `Err` — and stale cached `Personal` status gets misinterpreted as a server rejection.
0 commit comments