//! License activation screen, trial mode, and deactivation logic. use audiofiles_browser::ui::theme; use eframe::egui; use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL}; impl AudioFilesApp { /// Draw the license activation screen. pub(crate) fn draw_activation_screen(&mut self, ui: &mut egui::Ui) { // Poll async activation result (take from lock, then drop guard before mutating self) let activation = self.activation_result.lock().take(); if let Some(result) = activation { match result { Ok(()) => { let cache = super::license::LicenseCache { key_code: self.license_key_input.trim().to_string(), machine_id: self.machine_id.clone(), activated_at: chrono::Utc::now().to_rfc3339(), }; if let Err(e) = super::license::save_license(&self.config_dir, &cache) { tracing::error!("Failed to save license: {e}"); } self.license_cache = Some(cache); self.activating = false; self.activation_error = None; // If vault registry already exists (e.g. deactivate/reactivate), // go straight to browser. Otherwise show vault setup. if self.vault_registry.is_some() { self.activate_browser(); } else { self.screen = AppScreen::VaultSetup; } return; } Err(e) => { self.activation_error = Some(e); self.activating = false; } } } egui::CentralPanel::default().show_inside(ui, |ui| { let available = ui.available_size(); ui.add_space((available.y * 0.35).max(40.0)); ui.vertical_centered(|ui| { ui.heading("audiofiles"); ui.add_space(theme::space::MD); ui.label("Start a free trial, or activate a license key."); ui.add_space(theme::space::SECTION); // Trial entry (primary path for first-time users) let trial_expired = matches!( self.trial_state, Some(ref t) if super::license::trial_days_remaining(t) <= 0 ); let trial_label = match self.trial_state { Some(ref trial) => { let days = super::license::trial_days_remaining(trial); if days > 0 { format!("Continue trial ({days} days left)") } else { "Trial expired".to_string() } } None => "Start free trial — 30 days, no card".to_string(), }; let trial_btn = egui::Button::new(egui::RichText::new(trial_label).strong()); if ui.add_enabled(!trial_expired, trial_btn).clicked() { self.start_trial(); } if trial_expired { ui.add_space(theme::space::SM); ui.label( egui::RichText::new("Activate a license below to continue.") .small() .color(theme::text_secondary()), ); } ui.add_space(theme::space::XL); ui.separator(); ui.add_space(theme::space::MD); // License key entry ui.label( egui::RichText::new("Already have a license?") .color(theme::text_secondary()), ); ui.add_space(theme::space::SM); let input_width = 360.0_f32.min(available.x - 40.0); ui.allocate_ui(egui::vec2(input_width, 28.0), |ui| { let response = ui.add_sized( ui.available_size(), egui::TextEdit::singleline(&mut self.license_key_input) .hint_text("five-word-license-key-example"), ); // Clear stale activation error as soon as the user edits the field. if response.changed() { self.activation_error = None; } // Submit on Enter if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !self.activating && !self.license_key_input.trim().is_empty() { self.start_activation(); } }); ui.add_space(theme::space::MD); let can_activate = !self.activating && !self.license_key_input.trim().is_empty(); ui.horizontal(|ui| { let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" }; if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() { self.start_activation(); } if self.activating { ui.spinner(); } }); if let Some(err) = self.activation_error.clone() { ui.add_space(theme::space::MD); ui.colored_label(theme::accent_red(), err.to_string()); ui.add_space(theme::space::SM); // Per-class recovery affordance. match err { super::license::ActivationError::Network | super::license::ActivationError::Server(_) | super::license::ActivationError::Other(_) => { if ui.button("Try again").clicked() && !self.activating { self.start_activation(); } } super::license::ActivationError::InvalidKey => { ui.hyperlink_to( "Get a new license key", "https://makenot.work/store/audiofiles", ); } super::license::ActivationError::MachineLimit => { ui.hyperlink_to( "Contact support", "mailto:info@makenot.work?subject=License%20activation%20issue", ); } } } ui.add_space(theme::space::MD); ui.hyperlink_to( "Get a license key", "https://makenot.work/store/audiofiles", ); ui.add_space(theme::space::XL); ui.horizontal(|ui| { ui.add_space((ui.available_width() / 2.0 - 60.0).max(0.0)); if ui.small_button("About audiofiles").clicked() { self.show_about = true; } }); }); }); } /// Start or continue trial mode: create trial state if needed, then proceed. pub(crate) fn start_trial(&mut self) { if self.trial_state.is_none() { let now = chrono::Utc::now().to_rfc3339(); let trial = super::license::TrialState { first_launch_date: now.clone(), last_seen_date: Some(now), }; if let Err(e) = super::license::save_trial(&self.config_dir, &trial) { tracing::error!("Failed to save trial state: {e}"); } self.trial_state = Some(trial); } if self.vault_registry.is_some() { self.activate_browser(); } else { self.screen = AppScreen::VaultSetup; } } /// Spawn the async activation request. fn start_activation(&mut self) { self.activating = true; self.activation_error = None; let slot = self.activation_result.clone(); let server_url = SYNC_SERVER_URL.to_string(); let key = self.license_key_input.trim().to_string(); let mid = self.machine_id.clone(); self._runtime.spawn(async move { let result = super::license::activate_key(&server_url, &key, &mid).await; *slot.lock() = Some(result); }); } /// Push license info into the browser settings state. pub(crate) fn sync_license_to_browser(&mut self) { if let Some(ref mut browser) = self.browser { if let Some(ref cache) = self.license_cache { browser.settings.license_key_masked = Some(mask_key(&cache.key_code)); browser.settings.trial_days_remaining = None; } else if let Some(ref trial) = self.trial_state { browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial)); } let mid = &self.machine_id; browser.settings.machine_id = Some( if mid.len() > 12 { format!("{}...{}", &mid[..8], &mid[mid.len()-4..]) } else { mid.clone() } ); } } /// Deactivate the license: notify the server (best-effort), delete the /// local cache, and return to the activation screen. pub(crate) fn deactivate(&mut self) { if let Some(ref cache) = self.license_cache { let server_url = SYNC_SERVER_URL.to_string(); let key = cache.key_code.clone(); let mid = self.machine_id.clone(); self._runtime.spawn(async move { if let Err(e) = super::license::deactivate_key(&server_url, &key, &mid).await { tracing::warn!("Deactivation request failed (best-effort): {e}"); } }); } let _ = super::license::remove_license(&self.config_dir); self.license_cache = None; self.browser = None; self.sync_manager = None; self.screen = AppScreen::Activation; self.license_key_input.clear(); self.activation_error = None; } } /// Mask a 5-word key: show first word + ... + last word. pub(crate) fn mask_key(key: &str) -> String { let words: Vec<&str> = key.split('-').collect(); if words.len() >= 2 { format!("{}-...-{}", words[0], words[words.len() - 1]) } else { "***".to_string() } }