Skip to main content

max / audiofiles

10.7 KB · 259 lines History Blame Raw
1 //! License activation screen, trial mode, and deactivation logic.
2
3 use audiofiles_browser::ui::theme;
4 use eframe::egui;
5
6 use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL};
7
8 impl AudioFilesApp {
9 /// Draw the license activation screen.
10 pub(crate) fn draw_activation_screen(&mut self, ui: &mut egui::Ui) {
11 // Poll async activation result (take from lock, then drop guard before mutating self)
12 let activation = self.activation_result.lock().take();
13 if let Some(result) = activation {
14 match result {
15 Ok(()) => {
16 let cache = super::license::LicenseCache {
17 key_code: self.license_key_input.trim().to_string(),
18 machine_id: self.machine_id.clone(),
19 activated_at: chrono::Utc::now().to_rfc3339(),
20 };
21 if let Err(e) = super::license::save_license(&self.config_dir, &cache) {
22 tracing::error!("Failed to save license: {e}");
23 }
24 self.license_cache = Some(cache);
25 self.activating = false;
26 self.activation_error = None;
27 // If vault registry already exists (e.g. deactivate/reactivate),
28 // go straight to browser. Otherwise show vault setup.
29 if self.vault_registry.is_some() {
30 self.activate_browser();
31 } else {
32 self.screen = AppScreen::VaultSetup;
33 }
34 return;
35 }
36 Err(e) => {
37 self.activation_error = Some(e);
38 self.activating = false;
39 }
40 }
41 }
42
43 egui::CentralPanel::default().show_inside(ui, |ui| {
44 let available = ui.available_size();
45
46 ui.add_space((available.y * 0.35).max(40.0));
47
48 ui.vertical_centered(|ui| {
49 ui.heading("audiofiles");
50 ui.add_space(theme::space::MD);
51 ui.label("Start a free trial, or activate a license key.");
52 ui.add_space(theme::space::SECTION);
53
54 // Trial entry (primary path for first-time users)
55 let trial_expired = matches!(
56 self.trial_state,
57 Some(ref t) if super::license::trial_days_remaining(t) <= 0
58 );
59 let trial_label = match self.trial_state {
60 Some(ref trial) => {
61 let days = super::license::trial_days_remaining(trial);
62 if days > 0 {
63 format!("Continue trial ({days} days left)")
64 } else {
65 "Trial expired".to_string()
66 }
67 }
68 None => "Start free trial — 30 days, no card".to_string(),
69 };
70 let trial_btn = egui::Button::new(egui::RichText::new(trial_label).strong());
71 if ui.add_enabled(!trial_expired, trial_btn).clicked() {
72 self.start_trial();
73 }
74 if trial_expired {
75 ui.add_space(theme::space::SM);
76 ui.label(
77 egui::RichText::new("Activate a license below to continue.")
78 .small()
79 .color(theme::text_secondary()),
80 );
81 }
82
83 ui.add_space(theme::space::XL);
84 ui.separator();
85 ui.add_space(theme::space::MD);
86
87 // License key entry
88 ui.label(
89 egui::RichText::new("Already have a license?")
90 .color(theme::text_secondary()),
91 );
92 ui.add_space(theme::space::SM);
93
94 let input_width = 360.0_f32.min(available.x - 40.0);
95 ui.allocate_ui(egui::vec2(input_width, 28.0), |ui| {
96 let response = ui.add_sized(
97 ui.available_size(),
98 egui::TextEdit::singleline(&mut self.license_key_input)
99 .hint_text("five-word-license-key-example"),
100 );
101 // Clear stale activation error as soon as the user edits the field.
102 if response.changed() {
103 self.activation_error = None;
104 }
105 // Submit on Enter
106 if response.lost_focus()
107 && ui.input(|i| i.key_pressed(egui::Key::Enter))
108 && !self.activating
109 && !self.license_key_input.trim().is_empty()
110 {
111 self.start_activation();
112 }
113 });
114
115 ui.add_space(theme::space::MD);
116
117 let can_activate = !self.activating && !self.license_key_input.trim().is_empty();
118 ui.horizontal(|ui| {
119 let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" };
120 if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() {
121 self.start_activation();
122 }
123 if self.activating {
124 ui.spinner();
125 }
126 });
127
128 if let Some(err) = self.activation_error.clone() {
129 ui.add_space(theme::space::MD);
130 ui.colored_label(theme::accent_red(), err.to_string());
131 ui.add_space(theme::space::SM);
132 // Per-class recovery affordance.
133 match err {
134 super::license::ActivationError::Network
135 | super::license::ActivationError::Server(_)
136 | super::license::ActivationError::Other(_) => {
137 if ui.button("Try again").clicked() && !self.activating {
138 self.start_activation();
139 }
140 }
141 super::license::ActivationError::InvalidKey => {
142 ui.hyperlink_to(
143 "Get a new license key",
144 "https://makenot.work/store/audiofiles",
145 );
146 }
147 super::license::ActivationError::MachineLimit => {
148 ui.hyperlink_to(
149 "Contact support",
150 "mailto:info@makenot.work?subject=License%20activation%20issue",
151 );
152 }
153 }
154 }
155
156 ui.add_space(theme::space::MD);
157 ui.hyperlink_to(
158 "Get a license key",
159 "https://makenot.work/store/audiofiles",
160 );
161
162 ui.add_space(theme::space::XL);
163 ui.horizontal(|ui| {
164 ui.add_space((ui.available_width() / 2.0 - 60.0).max(0.0));
165 if ui.small_button("About audiofiles").clicked() {
166 self.show_about = true;
167 }
168 });
169 });
170 });
171 }
172
173 /// Start or continue trial mode: create trial state if needed, then proceed.
174 pub(crate) fn start_trial(&mut self) {
175 if self.trial_state.is_none() {
176 let now = chrono::Utc::now().to_rfc3339();
177 let trial = super::license::TrialState {
178 first_launch_date: now.clone(),
179 last_seen_date: Some(now),
180 };
181 if let Err(e) = super::license::save_trial(&self.config_dir, &trial) {
182 tracing::error!("Failed to save trial state: {e}");
183 }
184 self.trial_state = Some(trial);
185 }
186 if self.vault_registry.is_some() {
187 self.activate_browser();
188 } else {
189 self.screen = AppScreen::VaultSetup;
190 }
191 }
192
193 /// Spawn the async activation request.
194 fn start_activation(&mut self) {
195 self.activating = true;
196 self.activation_error = None;
197 let slot = self.activation_result.clone();
198 let server_url = SYNC_SERVER_URL.to_string();
199 let key = self.license_key_input.trim().to_string();
200 let mid = self.machine_id.clone();
201 self._runtime.spawn(async move {
202 let result = super::license::activate_key(&server_url, &key, &mid).await;
203 *slot.lock() = Some(result);
204 });
205 }
206
207 /// Push license info into the browser settings state.
208 pub(crate) fn sync_license_to_browser(&mut self) {
209 if let Some(ref mut browser) = self.browser {
210 if let Some(ref cache) = self.license_cache {
211 browser.settings.license_key_masked = Some(mask_key(&cache.key_code));
212 browser.settings.trial_days_remaining = None;
213 } else if let Some(ref trial) = self.trial_state {
214 browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial));
215 }
216 let mid = &self.machine_id;
217 browser.settings.machine_id = Some(
218 if mid.len() > 12 {
219 format!("{}...{}", &mid[..8], &mid[mid.len()-4..])
220 } else {
221 mid.clone()
222 }
223 );
224 }
225 }
226
227 /// Deactivate the license: notify the server (best-effort), delete the
228 /// local cache, and return to the activation screen.
229 pub(crate) fn deactivate(&mut self) {
230 if let Some(ref cache) = self.license_cache {
231 let server_url = SYNC_SERVER_URL.to_string();
232 let key = cache.key_code.clone();
233 let mid = self.machine_id.clone();
234 self._runtime.spawn(async move {
235 if let Err(e) = super::license::deactivate_key(&server_url, &key, &mid).await {
236 tracing::warn!("Deactivation request failed (best-effort): {e}");
237 }
238 });
239 }
240 let _ = super::license::remove_license(&self.config_dir);
241 self.license_cache = None;
242 self.browser = None;
243 self.sync_manager = None;
244 self.screen = AppScreen::Activation;
245 self.license_key_input.clear();
246 self.activation_error = None;
247 }
248 }
249
250 /// Mask a 5-word key: show first word + ... + last word.
251 pub(crate) fn mask_key(key: &str) -> String {
252 let words: Vec<&str> = key.split('-').collect();
253 if words.len() >= 2 {
254 format!("{}-...-{}", words[0], words[words.len() - 1])
255 } else {
256 "***".to_string()
257 }
258 }
259