Skip to content

Commit c0a63f5

Browse files
committed
Licensing: Remove supporter tier
- Remove `Supporter` variant from `AppStatus` and `LicenseType` enums (Rust), `LicenseStatus` union (TS), and all match/switch arms across 32 files - Legacy supporter keys gracefully map to `Personal` at runtime (no crash) - Remove supporter pricing card from website, shrink grid to 3 columns - Simplify pricing page checkout JS: all purchases now go through org-name modal (no more direct-to-checkout branch) - `getLicenseTypeFromPriceId` now returns `null` for unknown price IDs instead of silently defaulting to `commercial_subscription` - Remove `PRICE_ID_SUPPORTER` from license server bindings, wrangler config, setup script, and `.env.example` - Update ADR 016 with historical note, update 6 CLAUDE.md files and 3 spec docs
1 parent 2af7ee8 commit c0a63f5

32 files changed

Lines changed: 58 additions & 228 deletions

apps/desktop/src-tauri/src/commands/licensing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::licensing;
44

5-
/// Get the current app status (personal, supporter, commercial, or expired).
5+
/// Get the current app status (personal, commercial, or expired).
66
#[tauri::command]
77
pub fn get_license_status(app: tauri::AppHandle) -> licensing::AppStatus {
88
licensing::get_app_status(&app)

apps/desktop/src-tauri/src/licensing/CLAUDE.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ License keys are self-contained: `base64(JSON payload).base64(Ed25519 signature)
1818

1919
```
2020
Personal { show_commercial_reminder } — no license
21-
Supporter { show_commercial_reminder } — personal badge
2221
Commercial { license_type, organization_name, expires_at }
2322
Expired { organization_name, expired_at, show_modal }
2423
```
2524

26-
`LicenseType`: `Supporter`, `CommercialSubscription`, `CommercialPerpetual`.
25+
`LicenseType`: `CommercialSubscription`, `CommercialPerpetual`.
2726

2827
## Two-layer caching
2928

@@ -49,7 +48,7 @@ return VerifyResult ← LicenseInfo + full_key + short_code (nothing sto
4948
Frontend: validateLicenseWithServer(transactionId)
5049
| ↑ passed explicitly since key isn't stored yet
5150
v
52-
Server says active/supporter → commitLicense(fullKey, shortCode) → persist + onSuccess
51+
Server says active → commitLicense(fullKey, shortCode) → persist + onSuccess
5352
Server says expired → commitLicense(fullKey, shortCode) → persist + show error
5453
Server says invalid → DON'T commit. Show error. Nothing stored.
5554
Network error → commitLicense(fullKey, shortCode) → persist + fallback
@@ -69,7 +68,7 @@ Legacy `activate_license`/`activate_license_async` wrappers still exist for back
6968
- Short codes: `CMDR-XXXX-XXXX-XXXX` (3 segments × 4 alphanumeric chars after CMDR prefix).
7069
- Key format: `base64(JSON).base64(signature)` — split on single `.`.
7170
- Public key embedded at compile time as hex in `verification.rs` (`PUBLIC_KEY_HEX`).
72-
- Mock values (`CMDR_MOCK_LICENSE`): `personal`, `personal_reminder`, `supporter`, `supporter_reminder`, `commercial`, `perpetual`, `expired`, `expired_no_modal`.
71+
- Mock values (`CMDR_MOCK_LICENSE`): `personal`, `personal_reminder`, `commercial`, `perpetual`, `expired`, `expired_no_modal`.
7372
- Key gen: see [license server CLAUDE.md](../../../../apps/license-server/CLAUDE.md) and
7473
[README.md](../../../../apps/license-server/README.md#first-time-setup) for the full setup.
7574

@@ -103,7 +102,7 @@ Legacy `activate_license`/`activate_license_async` wrappers still exist for back
103102
**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.
104103

105104
**Decision**: `CMDR_MOCK_LICENSE` env var bypasses all license logic including server calls.
106-
**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.
105+
**Why**: License UX testing requires seeing every state (personal, 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.
107106

108107
## Gotchas
109108

apps/desktop/src-tauri/src/licensing/app_status.rs

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Application license status and validation.
22
//!
33
//! This module handles:
4-
//! - License status checking (personal, supporter, commercial)
4+
//! - License status checking (personal, commercial)
55
//! - Server-side validation for subscription status
66
//! - Caching for offline use (30-day grace period)
77
//! - Mock mode for local testing
@@ -31,7 +31,6 @@ const STORE_KEY_REMINDER_LAST_DISMISSED: &str = "commercial_reminder_last_dismis
3131
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3232
#[serde(rename_all = "snake_case")]
3333
pub enum LicenseType {
34-
Supporter,
3534
CommercialSubscription,
3635
CommercialPerpetual,
3736
}
@@ -46,12 +45,6 @@ pub enum AppStatus {
4645
/// Whether to show the commercial license reminder modal.
4746
show_commercial_reminder: bool,
4847
},
49-
/// Supporter license (personal use with badge).
50-
#[serde(rename_all = "camelCase")]
51-
Supporter {
52-
/// Whether to show the commercial license reminder modal.
53-
show_commercial_reminder: bool,
54-
},
5548
/// Active commercial license.
5649
#[serde(rename_all = "camelCase")]
5750
Commercial {
@@ -211,9 +204,10 @@ fn response_to_app_status(
211204
}
212205

213206
/// Convert string to LicenseType.
207+
///
208+
/// Legacy "supporter" keys are treated as personal (returns `None`).
214209
pub fn string_to_license_type(s: &str) -> Option<LicenseType> {
215210
match s {
216-
"supporter" => Some(LicenseType::Supporter),
217211
"commercial_subscription" => Some(LicenseType::CommercialSubscription),
218212
"commercial_perpetual" => Some(LicenseType::CommercialPerpetual),
219213
_ => None,
@@ -230,9 +224,6 @@ fn to_app_status(
230224
) -> AppStatus {
231225
match status {
232226
"active" => match license_type {
233-
Some(LicenseType::Supporter) => AppStatus::Supporter {
234-
show_commercial_reminder: should_show_commercial_reminder(app),
235-
},
236227
Some(lt) => AppStatus::Commercial {
237228
license_type: lt,
238229
organization_name,
@@ -418,7 +409,6 @@ pub fn update_cached_status(
418409
pub fn get_window_title(status: &AppStatus) -> String {
419410
match status {
420411
AppStatus::Personal { .. } => "Cmdr – Personal use only".to_string(),
421-
AppStatus::Supporter { .. } => "Cmdr – Personal".to_string(),
422412
AppStatus::Commercial { .. } => "Cmdr".to_string(),
423413
AppStatus::Expired { .. } => "Cmdr – Personal use only".to_string(),
424414
}
@@ -453,8 +443,6 @@ fn current_timestamp() -> u64 {
453443
/// Set CMDR_MOCK_LICENSE to one of:
454444
/// - "personal" - No license (no reminder)
455445
/// - "personal_reminder" - No license (shows commercial reminder modal)
456-
/// - "supporter" - Supporter badge (no reminder)
457-
/// - "supporter_reminder" - Supporter badge (shows commercial reminder modal)
458446
/// - "commercial" - Active commercial subscription
459447
/// - "perpetual" - Active perpetual license
460448
/// - "expired" - Expired subscription (shows modal)
@@ -470,10 +458,11 @@ fn get_mock_status(_app: &tauri::AppHandle) -> Option<AppStatus> {
470458
"personal_reminder" => Some(AppStatus::Personal {
471459
show_commercial_reminder: true,
472460
}),
473-
"supporter" => Some(AppStatus::Supporter {
461+
// Legacy: treat "supporter" / "supporter_reminder" as Personal
462+
"supporter" => Some(AppStatus::Personal {
474463
show_commercial_reminder: false,
475464
}),
476-
"supporter_reminder" => Some(AppStatus::Supporter {
465+
"supporter_reminder" => Some(AppStatus::Personal {
477466
show_commercial_reminder: true,
478467
}),
479468
"commercial" => Some(AppStatus::Commercial {
@@ -520,14 +509,6 @@ mod tests {
520509
assert_eq!(get_window_title(&status), "Cmdr – Personal use only");
521510
}
522511

523-
#[test]
524-
fn test_get_window_title_supporter() {
525-
let status = AppStatus::Supporter {
526-
show_commercial_reminder: false,
527-
};
528-
assert_eq!(get_window_title(&status), "Cmdr – Personal");
529-
}
530-
531512
#[test]
532513
fn test_get_window_title_commercial() {
533514
let status = AppStatus::Commercial {
@@ -560,7 +541,6 @@ mod tests {
560541

561542
#[test]
562543
fn test_license_type_serialization() {
563-
assert_eq!(serde_json::to_string(&LicenseType::Supporter).unwrap(), "\"supporter\"");
564544
assert_eq!(
565545
serde_json::to_string(&LicenseType::CommercialSubscription).unwrap(),
566546
"\"commercial_subscription\""
@@ -581,16 +561,6 @@ mod tests {
581561
assert!(json.contains("\"showCommercialReminder\":true"));
582562
}
583563

584-
#[test]
585-
fn test_app_status_supporter_serialization() {
586-
let status = AppStatus::Supporter {
587-
show_commercial_reminder: false,
588-
};
589-
let json = serde_json::to_string(&status).unwrap();
590-
assert!(json.contains("\"type\":\"supporter\""));
591-
assert!(json.contains("\"showCommercialReminder\":false"));
592-
}
593-
594564
#[test]
595565
fn test_app_status_commercial_serialization() {
596566
let status = AppStatus::Commercial {

apps/desktop/src-tauri/src/licensing/validation_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const LICENSE_SERVER_URL: &str = "https://license.getcmdr.com";
1717
pub struct ValidationResponse {
1818
pub status: String, // "active", "expired", "invalid"
1919
#[serde(rename = "type")]
20-
pub license_type: Option<String>, // "supporter", "commercial_subscription", "commercial_perpetual"
20+
pub license_type: Option<String>, // "commercial_subscription", "commercial_perpetual"
2121
#[serde(rename = "organizationName")]
2222
pub organization_name: Option<String>,
2323
#[serde(rename = "expiresAt")]

apps/desktop/src/lib/licensing/AboutWindow.svelte

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@
4545
return 'No license – only personal use allowed'
4646
}
4747
switch (status.type) {
48-
case 'supporter':
49-
return 'Personal license – thanks for your support ❤️'
5048
case 'commercial':
5149
if (status.licenseType === 'commercial_perpetual') {
5250
return `Perpetual commercial license for ${status.organizationName || 'your organization'}`
@@ -61,27 +59,14 @@
6159
}
6260
}
6361
64-
// Determine if we should show the license purchase/upgrade link
62+
// Determine if we should show the license purchase link
6563
function shouldShowLicenseLink(): boolean {
6664
if (!status) return true
6765
if (status.type === 'personal') return true
6866
if (status.type === 'expired') return true
69-
// Supporter shows "upgrade" to commercial
70-
if (status.type === 'supporter') return true
7167
return false
7268
}
7369
74-
// Label varies by license state: "Get a license" for unlicensed, "Upgrade" for supporters
75-
function getLicenseLinkLabel(): string {
76-
if (status?.type === 'supporter') return 'Upgrade'
77-
return 'Get a license'
78-
}
79-
80-
// Determine if we should show the commercial upgrade prompt (for supporters)
81-
function shouldShowCommercialPrompt(): boolean {
82-
return status?.type === 'supporter'
83-
}
84-
8570
function handleLinkClick(url: string) {
8671
return (event: MouseEvent) => {
8772
event.preventDefault()
@@ -117,14 +102,6 @@
117102

118103
<div class="license-info">
119104
<p class="license-description">{getLicenseDescription()}</p>
120-
{#if shouldShowCommercialPrompt()}
121-
<p class="commercial-prompt">
122-
Also using Cmdr for work? You must <a
123-
href="https://getcmdr.com/pricing"
124-
onclick={handleLinkClick('https://getcmdr.com/pricing')}>upgrade to a commercial license</a
125-
>.
126-
</p>
127-
{/if}
128105
</div>
129106

130107
<p class="ai-attribution">AI powered by Falcon-H1R-7B by Technology Innovation Institute (TII)</p>
@@ -134,7 +111,7 @@
134111
{#if shouldShowLicenseLink()}
135112
<span class="separator">•</span>
136113
<a href="https://getcmdr.com/pricing" onclick={handleLinkClick('https://getcmdr.com/pricing')}
137-
>{getLicenseLinkLabel()}</a
114+
>Get a license</a
138115
>
139116
{/if}
140117
<span class="separator">•</span>
@@ -221,22 +198,6 @@
221198
margin: 0;
222199
}
223200
224-
.commercial-prompt {
225-
color: var(--color-text-secondary);
226-
font-size: var(--font-size-md);
227-
line-height: 1.5;
228-
margin: var(--spacing-md) 0 0;
229-
}
230-
231-
.commercial-prompt a {
232-
color: var(--color-accent);
233-
text-decoration: underline;
234-
}
235-
236-
.commercial-prompt a:hover {
237-
color: var(--color-accent-hover);
238-
}
239-
240201
.ai-attribution {
241202
color: var(--color-text-tertiary);
242203
font-size: var(--font-size-sm);

apps/desktop/src/lib/licensing/CLAUDE.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ for offline validation (no server call needed after activation).
99

1010
- `licensing-store.svelte.ts` — state management, validation trigger
1111
- `LicenseKeyDialog.svelte` — license key entry + details view (key-value display, "Use a different key" reset flow)
12-
- `CommercialReminderModal.svelte` — 30-day reminder for personal/supporter users
12+
- `CommercialReminderModal.svelte` — 30-day reminder for personal users
1313
- `ExpirationModal.svelte` — shown when commercial license expires
1414
- `AboutWindow.svelte` — displays current license status
1515

@@ -23,7 +23,6 @@ for offline validation (no server call needed after activation).
2323
## License types
2424

2525
- **Personal** — free forever. Shows "Personal use only" in title bar. Commercial reminder every 30 days.
26-
- **Supporter** — $10 one-time. Same as Personal but with a badge in About window. Commercial reminder every 30 days.
2726
- **Commercial subscription** — $59/year. Server validation every 7 days. 14-day grace on network failure.
2827
- **Commercial perpetual** — $199 one-time. No periodic validation. 3 years of updates.
2928
- **Expired** — subscription expired. Shows modal once, then reverts to Personal behavior.
@@ -69,7 +68,7 @@ dismiss). The About window and modals read the cached value on mount.
6968
- **`handleActivate` uses verify/commit split** — calls `verifyLicense()` first (nothing stored), then
7069
`validateLicenseWithServer(transactionId)` passing the transaction ID explicitly, then decides whether to call
7170
`commitLicense()`. Four outcomes:
72-
1. Server confirms active (commercial/supporter) → `commitLicense()` + `onSuccess()`.
71+
1. Server confirms active (commercial) → `commitLicense()` + `onSuccess()`.
7372
2. Server says expired → `commitLicense()` + inline error with expiry date (key IS valid, just expired).
7473
3. Server says invalid (returns `personal` type) → DON'T commit. Nothing stored. Tracks `serverInvalidRetryCount`
7574
for escalating messaging. Cancel and X just close (no cleanup needed).

apps/desktop/src/lib/licensing/LicenseKeyDialog.svelte

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
function getLicenseTypeLabel(licenseType: string | null | undefined): string | null {
6767
if (licenseType === 'commercial_perpetual') return 'Commercial perpetual'
6868
if (licenseType === 'commercial_subscription') return 'Commercial subscription'
69-
if (licenseType === 'supporter') return 'Supporter'
7069
return null
7170
}
7271
@@ -164,9 +163,6 @@
164163
expiresAt: null,
165164
}
166165
}
167-
if (info.licenseType === 'supporter') {
168-
return { type: 'supporter', showCommercialReminder: false }
169-
}
170166
return null
171167
}
172168
@@ -226,7 +222,7 @@
226222
}
227223
228224
// Step 3: Decide whether to commit (persist) the key based on server response.
229-
if (newStatus?.type === 'commercial' || newStatus?.type === 'supporter') {
225+
if (newStatus?.type === 'commercial') {
230226
await commitLicense(verifyResult.fullKey, verifyResult.shortCode)
231227
setPendingVerification(false)
232228
setCachedStatus(newStatus)

apps/desktop/src/lib/licensing/licensing-store.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function loadLicenseStatus(): Promise<LicenseStatus> {
2929
licenseState.cachedStatus = status
3030

3131
// Derive pending verification: license exists and is non-personal, but server has never confirmed it
32-
if (status.type === 'commercial' || status.type === 'supporter') {
32+
if (status.type === 'commercial') {
3333
const validated = await hasLicenseBeenValidated()
3434
licenseState.pendingVerification = !validated
3535
} else {

apps/desktop/src/lib/settings/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ percentage-based, not pixel-based.
6868
radio group, or combobox — anything that benefits from consistent horizontal alignment.
6969

7070
**When NOT to use `split`:**
71+
7172
- Switches (too small; 50-50 wastes space and doesn't improve alignment)
7273
- Toggle groups (multi-button controls that may not fit in 50% width at narrow window sizes)
7374
- Full-width custom layouts (keyboard shortcuts table, license card, advanced auto-generated rows)

apps/desktop/src/lib/settings/sections/LicenseSection.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@
6464
<span
6565
class="info-value"
6666
class:status-expired={licenseStatus?.type === 'expired'}
67-
class:status-active={licenseStatus?.type === 'commercial' ||
68-
licenseStatus?.type === 'supporter'}>{statusText}</span
67+
class:status-active={licenseStatus?.type === 'commercial'}>{statusText}</span
6968
>
7069
</div>
7170
{/if}

0 commit comments

Comments
 (0)