Skip to content

Commit 29eb6fe

Browse files
committed
Feat: Licensing: test, handle address, tax ID, etc
Website: - Refactor org name form, get it to a working state - Add pricing page tests App frontend: - Make local app connect to local license server, not live one - Make license details dialog different if there is a license - Customize links in About window App backend: - Add license shortcodes to make it all work
1 parent 542b491 commit 29eb6fe

18 files changed

Lines changed: 531 additions & 132 deletions

File tree

apps/desktop/src-tauri/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,17 @@ pub fn run() {
126126
// Load persisted settings to initialize menu with correct state
127127
let saved_settings = settings::load_settings(app.handle());
128128

129+
// Check if there's an existing license (for menu text)
130+
let has_existing_license = licensing::get_license_info(app.handle()).is_some();
131+
129132
// Build and set the application menu with persisted showHiddenFiles
130133
// Note: view mode is per-pane and managed by frontend, so we default to Brief here
131-
let menu_items = menu::build_menu(app.handle(), saved_settings.show_hidden_files, ViewMode::Brief)?;
134+
let menu_items = menu::build_menu(
135+
app.handle(),
136+
saved_settings.show_hidden_files,
137+
ViewMode::Brief,
138+
has_existing_license,
139+
)?;
132140
app.set_menu(menu_items.menu)?;
133141

134142
// Store the CheckMenuItem references in app state

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ use serde::{Deserialize, Serialize};
1919
#[derive(Debug, Clone, Serialize, Deserialize)]
2020
pub struct LicenseData {
2121
pub email: String,
22+
#[serde(rename = "transactionId")]
2223
pub transaction_id: String,
24+
#[serde(rename = "issuedAt")]
2325
pub issued_at: String,
2426
#[serde(rename = "type")]
2527
pub license_type: Option<String>,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66

77
/// License server URL (configured at compile time).
88
#[cfg(debug_assertions)]
9-
const LICENSE_SERVER_URL: &str = "https://license.getcmdr.com"; // Use same URL in debug
9+
const LICENSE_SERVER_URL: &str = "http://localhost:8787";
1010

1111
#[cfg(not(debug_assertions))]
1212
const LICENSE_SERVER_URL: &str = "https://license.getcmdr.com";

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tauri_plugin_store::StoreExt;
1414
const PUBLIC_KEY_HEX: &str = "c3b18e765fc5c74f9fb7f3a9869d14c6bdeda1f28ec85aa6182de78113930d26";
1515

1616
const STORE_KEY_LICENSE: &str = "license_key";
17+
const STORE_KEY_SHORT_CODE: &str = "license_short_code";
1718

1819
/// Information about the current license.
1920
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -23,53 +24,74 @@ pub struct LicenseInfo {
2324
pub transaction_id: String,
2425
pub issued_at: String,
2526
pub organization_name: Option<String>,
27+
/// The short code used to activate (if available)
28+
pub short_code: Option<String>,
2629
}
2730

2831
/// Activate a license key. Returns the license info if valid.
29-
pub fn activate_license(app: &tauri::AppHandle, license_key: &str) -> Result<LicenseInfo, String> {
32+
/// The `short_code` parameter is the original short code if this was activated via short code exchange.
33+
fn activate_license_internal(
34+
app: &tauri::AppHandle,
35+
license_key: &str,
36+
short_code: Option<&str>,
37+
) -> Result<LicenseInfo, String> {
3038
// Validate the license key
3139
let data = validate_license_key(license_key)?;
3240

33-
// Store the license key
41+
// Store the license key and optionally the short code
3442
let store = app
3543
.store("license.json")
3644
.map_err(|e| format!("Failed to open store: {}", e))?;
3745

3846
store.set(STORE_KEY_LICENSE, serde_json::json!(license_key));
47+
if let Some(code) = short_code {
48+
store.set(STORE_KEY_SHORT_CODE, serde_json::json!(code));
49+
}
3950

4051
Ok(LicenseInfo {
4152
email: data.email,
4253
transaction_id: data.transaction_id,
4354
issued_at: data.issued_at,
4455
organization_name: data.organization_name,
56+
short_code: short_code.map(|s| s.to_string()),
4557
})
4658
}
4759

60+
/// Activate a license key (full key, not short code). Returns the license info if valid.
61+
pub fn activate_license(app: &tauri::AppHandle, license_key: &str) -> Result<LicenseInfo, String> {
62+
activate_license_internal(app, license_key, None)
63+
}
64+
4865
/// Activate a license key or short code (async version).
4966
/// If the input is a short code (CMDR-XXXX-XXXX-XXXX), it first exchanges it for the full key.
5067
pub async fn activate_license_async(app: &tauri::AppHandle, input: &str) -> Result<LicenseInfo, String> {
51-
let full_key = if is_short_code(input) {
68+
let (full_key, short_code) = if is_short_code(input) {
5269
// Exchange short code for full key via server
53-
activate_short_code(input).await?
70+
let key = activate_short_code(input).await?;
71+
(key, Some(input))
5472
} else {
5573
// Already a full key
56-
input.to_string()
74+
(input.to_string(), None)
5775
};
5876

59-
// Now activate with the full key
60-
activate_license(app, &full_key)
77+
// Now activate with the full key (and store the short code if we have one)
78+
activate_license_internal(app, &full_key, short_code)
6179
}
6280

6381
/// Get stored license info, if any.
6482
pub fn get_license_info(app: &tauri::AppHandle) -> Option<LicenseInfo> {
6583
let store = app.store("license.json").ok()?;
6684
let license_key = store.get(STORE_KEY_LICENSE)?.as_str()?.to_string();
85+
let short_code = store
86+
.get(STORE_KEY_SHORT_CODE)
87+
.and_then(|v| v.as_str().map(|s| s.to_string()));
6788

6889
validate_license_key(&license_key).ok().map(|data| LicenseInfo {
6990
email: data.email,
7091
transaction_id: data.transaction_id,
7192
issued_at: data.issued_at,
7293
organization_name: data.organization_name,
94+
short_code,
7395
})
7496
}
7597

@@ -114,8 +136,16 @@ fn validate_license_key_with_public_key(license_key: &str, public_key_hex: &str)
114136
.map_err(|_| "Invalid license key: signature verification failed")?;
115137

116138
// Parse payload
117-
let data: LicenseData =
118-
serde_json::from_slice(&payload_bytes).map_err(|_| "Invalid license key: bad payload data")?;
139+
let data: LicenseData = serde_json::from_slice(&payload_bytes).map_err(|e| {
140+
log::info!(
141+
"License payload parse error: {}. Raw payload: {}",
142+
e,
143+
String::from_utf8_lossy(&payload_bytes)
144+
);
145+
"Invalid license key: bad payload data"
146+
})?;
147+
148+
log::info!("License validated successfully for: {}", data.email);
119149

120150
Ok(data)
121151
}

apps/desktop/src-tauri/src/menu.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub fn build_menu<R: Runtime>(
9191
app: &AppHandle<R>,
9292
show_hidden_files: bool,
9393
view_mode: ViewMode,
94+
has_existing_license: bool,
9495
) -> tauri::Result<MenuItems<R>> {
9596
// Start with the default menu (includes app menu with Quit, Hide, etc.)
9697
let menu = Menu::default(app)?;
@@ -113,11 +114,16 @@ pub fn build_menu<R: Runtime>(
113114
submenu.remove(pred)?;
114115
submenu.insert(&about_item, 0)?;
115116

116-
// Add "Enter License Key..." after About
117+
// Add license menu item after About - text depends on license status
118+
let license_menu_text = if has_existing_license {
119+
"See license details..."
120+
} else {
121+
"Enter license key..."
122+
};
117123
let enter_license_key_item = tauri::menu::MenuItem::with_id(
118124
app,
119125
ENTER_LICENSE_KEY_ID,
120-
"Enter license key...",
126+
license_menu_text,
121127
true,
122128
None::<&str>,
123129
)?;

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@
6767
}
6868
}
6969
70+
// Determine if we should show the Upgrade link
71+
function shouldShowUpgradeLink(): boolean {
72+
if (!status) return true // No license - show upgrade
73+
if (status.type === 'personal') return true
74+
if (status.type === 'expired') return true
75+
// Supporter and commercial don't show the generic upgrade link
76+
return false
77+
}
78+
79+
// Determine if we should show the commercial upgrade prompt (for supporters)
80+
function shouldShowCommercialPrompt(): boolean {
81+
return status?.type === 'supporter'
82+
}
83+
7084
function handleKeydown(event: KeyboardEvent) {
7185
// Stop propagation to prevent file explorer from handling keys while modal is open
7286
event.stopPropagation()
@@ -110,14 +124,24 @@
110124

111125
<div class="license-info">
112126
<p class="license-description">{getLicenseDescription()}</p>
127+
{#if shouldShowCommercialPrompt()}
128+
<p class="commercial-prompt">
129+
Also using Cmdr for work? You must <a
130+
href="https://getcmdr.com/pricing"
131+
onclick={handleLinkClick('https://getcmdr.com/pricing')}>upgrade to a commercial license</a
132+
>.
133+
</p>
134+
{/if}
113135
</div>
114136

115137
<div class="links">
116138
<a href="https://getcmdr.com" onclick={handleLinkClick('https://getcmdr.com')}>Website</a>
117-
<span class="separator">•</span>
118-
<a href="https://getcmdr.com/pricing" onclick={handleLinkClick('https://getcmdr.com/pricing')}
119-
>Upgrade</a
120-
>
139+
{#if shouldShowUpgradeLink()}
140+
<span class="separator">•</span>
141+
<a href="https://getcmdr.com/pricing" onclick={handleLinkClick('https://getcmdr.com/pricing')}
142+
>Upgrade</a
143+
>
144+
{/if}
121145
<span class="separator">•</span>
122146
<a href="https://github.com/vdavid/cmdr" onclick={handleLinkClick('https://github.com/vdavid/cmdr')}
123147
>GitHub</a
@@ -227,6 +251,22 @@
227251
margin: 0;
228252
}
229253
254+
.commercial-prompt {
255+
color: var(--color-text-secondary, #aaa);
256+
font-size: 13px;
257+
line-height: 1.5;
258+
margin: 12px 0 0;
259+
}
260+
261+
.commercial-prompt a {
262+
color: var(--color-accent);
263+
text-decoration: underline;
264+
}
265+
266+
.commercial-prompt a:hover {
267+
color: var(--color-accent-hover, #6eb5ff);
268+
}
269+
230270
.links {
231271
margin-bottom: 16px;
232272
}

0 commit comments

Comments
 (0)