| 260 |
260 |
|
let dir = tempfile::tempdir().unwrap();
|
| 261 |
261 |
|
let reg = Some(make_registry(dir.path()));
|
| 262 |
262 |
|
let status = license::LicenseStatus::Licensed(make_license_cache());
|
| 263 |
|
- |
assert_eq!(resolve_initial_screen(®, &status), AppScreen::Browser);
|
|
263 |
+ |
assert_eq!(resolve_initial_screen(®, &status, false), AppScreen::Browser);
|
| 264 |
264 |
|
}
|
| 265 |
265 |
|
|
| 266 |
266 |
|
#[test]
|
| 267 |
267 |
|
fn initial_screen_licensed_without_registry() {
|
| 268 |
268 |
|
let status = license::LicenseStatus::Licensed(make_license_cache());
|
| 269 |
|
- |
assert_eq!(resolve_initial_screen(&None, &status), AppScreen::VaultSetup);
|
|
269 |
+ |
assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::VaultSetup);
|
| 270 |
270 |
|
}
|
| 271 |
271 |
|
|
| 272 |
272 |
|
#[test]
|
| 274 |
274 |
|
let dir = tempfile::tempdir().unwrap();
|
| 275 |
275 |
|
let reg = Some(make_registry(dir.path()));
|
| 276 |
276 |
|
let status = license::LicenseStatus::Unlicensed;
|
| 277 |
|
- |
assert_eq!(resolve_initial_screen(®, &status), AppScreen::Activation);
|
|
277 |
+ |
assert_eq!(resolve_initial_screen(®, &status, false), AppScreen::Activation);
|
| 278 |
278 |
|
}
|
| 279 |
279 |
|
|
| 280 |
280 |
|
#[test]
|
| 281 |
281 |
|
fn initial_screen_unlicensed_without_registry() {
|
| 282 |
282 |
|
let status = license::LicenseStatus::Unlicensed;
|
| 283 |
|
- |
assert_eq!(resolve_initial_screen(&None, &status), AppScreen::Activation);
|
|
283 |
+ |
assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::Activation);
|
|
284 |
+ |
}
|
|
285 |
+ |
|
|
286 |
+ |
#[test]
|
|
287 |
+ |
fn initial_screen_trial_with_registry() {
|
|
288 |
+ |
let dir = tempfile::tempdir().unwrap();
|
|
289 |
+ |
let reg = Some(make_registry(dir.path()));
|
|
290 |
+ |
let status = license::LicenseStatus::Unlicensed;
|
|
291 |
+ |
assert_eq!(resolve_initial_screen(®, &status, true), AppScreen::Browser);
|
|
292 |
+ |
}
|
|
293 |
+ |
|
|
294 |
+ |
#[test]
|
|
295 |
+ |
fn initial_screen_trial_without_registry() {
|
|
296 |
+ |
let status = license::LicenseStatus::Unlicensed;
|
|
297 |
+ |
assert_eq!(resolve_initial_screen(&None, &status, true), AppScreen::VaultSetup);
|
| 284 |
298 |
|
}
|
| 285 |
299 |
|
|
| 286 |
300 |
|
// ── License migration ──
|
| 387 |
401 |
|
Browser,
|
| 388 |
402 |
|
}
|
| 389 |
403 |
|
|
| 390 |
|
- |
/// Determine the initial screen based on vault registry and license status.
|
|
404 |
+ |
/// Determine the initial screen based on vault registry, license status, and trial.
|
| 391 |
405 |
|
///
|
| 392 |
406 |
|
/// This is the pure decision logic extracted from `AudioFilesApp::new()` so it
|
| 393 |
407 |
|
/// can be tested without constructing the full app.
|
| 394 |
408 |
|
fn resolve_initial_screen(
|
| 395 |
409 |
|
vault_registry: &Option<VaultRegistry>,
|
| 396 |
410 |
|
license_status: &license::LicenseStatus,
|
|
411 |
+ |
has_trial: bool,
|
| 397 |
412 |
|
) -> AppScreen {
|
| 398 |
|
- |
match (vault_registry, license_status) {
|
| 399 |
|
- |
(Some(_), license::LicenseStatus::Licensed(_)) => AppScreen::Browser,
|
| 400 |
|
- |
(Some(_), license::LicenseStatus::Unlicensed) => AppScreen::Activation,
|
| 401 |
|
- |
(None, license::LicenseStatus::Licensed(_)) => AppScreen::VaultSetup,
|
| 402 |
|
- |
(None, license::LicenseStatus::Unlicensed) => AppScreen::Activation,
|
|
413 |
+ |
let licensed_or_trial = matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial;
|
|
414 |
+ |
match (vault_registry, licensed_or_trial) {
|
|
415 |
+ |
(Some(_), true) => AppScreen::Browser,
|
|
416 |
+ |
(Some(_), false) => AppScreen::Activation,
|
|
417 |
+ |
(None, true) => AppScreen::VaultSetup,
|
|
418 |
+ |
(None, false) => AppScreen::Activation,
|
| 403 |
419 |
|
}
|
| 404 |
420 |
|
}
|
| 405 |
421 |
|
|
| 435 |
451 |
|
activation_error: Option<String>,
|
| 436 |
452 |
|
activating: bool,
|
| 437 |
453 |
|
license_cache: Option<license::LicenseCache>,
|
|
454 |
+ |
trial_state: Option<license::TrialState>,
|
| 438 |
455 |
|
}
|
| 439 |
456 |
|
|
| 440 |
457 |
|
impl AudioFilesApp {
|
| 454 |
471 |
|
|
| 455 |
472 |
|
let machine_id = license::get_or_create_machine_id(&config_dir);
|
| 456 |
473 |
|
let license_status = license::load_license(&config_dir);
|
|
474 |
+ |
let trial_state = license::load_trial(&config_dir);
|
| 457 |
475 |
|
|
| 458 |
476 |
|
// Load (or create) the vault registry
|
| 459 |
477 |
|
let vault_registry = match vault::load_registry() {
|
| 464 |
482 |
|
}
|
| 465 |
483 |
|
};
|
| 466 |
484 |
|
|
| 467 |
|
- |
let screen = resolve_initial_screen(&vault_registry, &license_status);
|
|
485 |
+ |
let screen = resolve_initial_screen(&vault_registry, &license_status, trial_state.is_some());
|
|
486 |
+ |
|
|
487 |
+ |
let licensed_or_trial = matches!(&license_status, license::LicenseStatus::Licensed(_)) || trial_state.is_some();
|
| 468 |
488 |
|
|
| 469 |
489 |
|
let (data_dir, browser, error, sync_manager, license_cache) =
|
| 470 |
490 |
|
match (&vault_registry, &license_status) {
|
| 476 |
496 |
|
let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_name_for_path(reg, &data_dir));
|
| 477 |
497 |
|
(data_dir, browser, error, sync_manager, Some(cache.clone()))
|
| 478 |
498 |
|
}
|
|
499 |
+ |
// Registry exists, unlicensed but in trial → open the active vault (no sync)
|
|
500 |
+ |
(Some(reg), license::LicenseStatus::Unlicensed) if trial_state.is_some() => {
|
|
501 |
+ |
let data_dir = reg.active.clone();
|
|
502 |
+ |
let _ = std::fs::create_dir_all(&data_dir);
|
|
503 |
+ |
let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_name_for_path(reg, &data_dir));
|
|
504 |
+ |
(data_dir, browser, error, None, None)
|
|
505 |
+ |
}
|
| 479 |
506 |
|
// Registry exists but unlicensed (deactivated and reactivated)
|
| 480 |
507 |
|
(Some(reg), license::LicenseStatus::Unlicensed) => {
|
| 481 |
508 |
|
tracing::info!("No valid license, showing activation screen");
|
| 486 |
513 |
|
tracing::info!("Licensed but no vault registry, showing vault setup");
|
| 487 |
514 |
|
(default_vault.clone(), None, None, None, Some(cache.clone()))
|
| 488 |
515 |
|
}
|
| 489 |
|
- |
// No registry + unlicensed → activation first
|
|
516 |
+ |
// No registry + unlicensed → activation first (or vault setup if trial)
|
| 490 |
517 |
|
(None, license::LicenseStatus::Unlicensed) => {
|
| 491 |
|
- |
tracing::info!("No license, showing activation screen");
|
|
518 |
+ |
if licensed_or_trial {
|
|
519 |
+ |
tracing::info!("Trial mode, showing vault setup");
|
|
520 |
+ |
} else {
|
|
521 |
+ |
tracing::info!("No license, showing activation screen");
|
|
522 |
+ |
}
|
| 492 |
523 |
|
(default_vault.clone(), None, None, None, None)
|
| 493 |
524 |
|
}
|
| 494 |
525 |
|
};
|
| 516 |
547 |
|
activation_error: None,
|
| 517 |
548 |
|
activating: false,
|
| 518 |
549 |
|
license_cache,
|
|
550 |
+ |
trial_state,
|
| 519 |
551 |
|
};
|
| 520 |
552 |
|
app.sync_vault_list_to_browser();
|
| 521 |
553 |
|
app.sync_license_to_browser();
|
| 542 |
574 |
|
if let Some(ref mut browser) = self.browser {
|
| 543 |
575 |
|
if let Some(ref cache) = self.license_cache {
|
| 544 |
576 |
|
browser.settings.license_key_masked = Some(mask_key(&cache.key_code));
|
|
577 |
+ |
browser.settings.trial_days_remaining = None;
|
|
578 |
+ |
} else if let Some(ref trial) = self.trial_state {
|
|
579 |
+ |
browser.settings.trial_days_remaining = Some(license::trial_days_remaining(trial));
|
| 545 |
580 |
|
}
|
| 546 |
581 |
|
let mid = &self.machine_id;
|
| 547 |
582 |
|
browser.settings.machine_id = Some(
|
| 613 |
648 |
|
egui::CentralPanel::default().show(ctx, |ui| {
|
| 614 |
649 |
|
let available = ui.available_size();
|
| 615 |
650 |
|
|
| 616 |
|
- |
// Temporary alpha skip button — top-right corner
|
| 617 |
|
- |
ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
|
| 618 |
|
- |
ui.add_space(8.0);
|
| 619 |
|
- |
if ui.small_button("\u{2715}").on_hover_text("Skip activation (alpha)").clicked() {
|
| 620 |
|
- |
if self.vault_registry.is_some() {
|
| 621 |
|
- |
self.activate_browser();
|
| 622 |
|
- |
} else {
|
| 623 |
|
- |
self.screen = AppScreen::VaultSetup;
|
| 624 |
|
- |
}
|
| 625 |
|
- |
}
|
| 626 |
|
- |
});
|
| 627 |
|
- |
|
| 628 |
651 |
|
ui.add_space((available.y * 0.35).max(40.0));
|
| 629 |
652 |
|
|
| 630 |
653 |
|
ui.vertical_centered(|ui| {
|
| 669 |
692 |
|
"Get a license key",
|
| 670 |
693 |
|
"https://makenot.work/store/audiofiles",
|
| 671 |
694 |
|
);
|
|
695 |
+ |
|
|
696 |
+ |
// Trial button
|
|
697 |
+ |
ui.add_space(24.0);
|
|
698 |
+ |
ui.separator();
|
|
699 |
+ |
ui.add_space(8.0);
|
|
700 |
+ |
|
|
701 |
+ |
let trial_label = if let Some(ref trial) = self.trial_state {
|
|
702 |
+ |
let days = license::trial_days_remaining(trial);
|
|
703 |
+ |
if days > 0 {
|
|
704 |
+ |
format!("I am still testing the software ({days} days left)")
|
|
705 |
+ |
} else {
|
|
706 |
+ |
format!("I am still \"testing\" the software :) ({days} days)")
|
|
707 |
+ |
}
|
|
708 |
+ |
} else {
|
|
709 |
+ |
"I am still testing the software".to_string()
|
|
710 |
+ |
};
|
|
711 |
+ |
|
|
712 |
+ |
if ui.button(trial_label).clicked() {
|
|
713 |
+ |
self.start_trial();
|
|
714 |
+ |
}
|
| 672 |
715 |
|
});
|
| 673 |
716 |
|
});
|
| 674 |
717 |
|
}
|
| 675 |
718 |
|
|
|
719 |
+ |
/// Start or continue trial mode: create trial state if needed, then proceed.
|
|
720 |
+ |
fn start_trial(&mut self) {
|
|
721 |
+ |
if self.trial_state.is_none() {
|
|
722 |
+ |
let trial = license::TrialState {
|
|
723 |
+ |
first_launch_date: chrono::Utc::now().to_rfc3339(),
|
|
724 |
+ |
};
|
|
725 |
+ |
if let Err(e) = license::save_trial(&self.config_dir, &trial) {
|
|
726 |
+ |
tracing::error!("Failed to save trial state: {e}");
|
|
727 |
+ |
}
|
|
728 |
+ |
self.trial_state = Some(trial);
|
|
729 |
+ |
}
|
|
730 |
+ |
if self.vault_registry.is_some() {
|
|
731 |
+ |
self.activate_browser();
|
|
732 |
+ |
} else {
|
|
733 |
+ |
self.screen = AppScreen::VaultSetup;
|
|
734 |
+ |
}
|
|
735 |
+ |
}
|
|
736 |
+ |
|
| 676 |
737 |
|
/// Spawn the async activation request.
|
| 677 |
738 |
|
fn start_activation(&mut self) {
|
| 678 |
739 |
|
self.activating = true;
|