1use std::path::PathBuf;
46use std::sync::Arc;
47use std::time::Duration;
48
49use moonbase::{
50 DeviceIdFallback, DeviceIdResolver, FileLicenseStore, HttpTransport, Licensing, LicensingOptions,
51 MoonbaseDeviceIdResolver, MoonbaseDeviceIdResolverOptions,
52};
53use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
54use tauri::webview::PageLoadEvent;
55use tauri::{Manager, RunEvent, Runtime, WindowEvent};
56
57mod commands;
58mod controller;
59mod error;
60mod models;
61mod ui;
62
63pub use crate::controller::{Moonbase, STATE_CHANGED_EVENT};
64pub use crate::error::{Error, Result};
65pub use crate::models::{
66 AddOnsPhase, AddOnsState, DeviceInfo, Failure, LicenseState, LicenseView, LockReason, NotCovered,
67 PendingActivation, Status, UpdateHint, UpdateInfo,
68};
69pub use crate::ui::{Presentation, Screen, TrialFeature, UiConfig, UiStrings, WINDOW_LABEL};
70pub use moonbase;
71
72pub const VERSION: &str = env!("CARGO_PKG_VERSION");
74
75pub trait MoonbaseExt<R: Runtime> {
78 fn moonbase(&self) -> &Moonbase<R>;
79}
80
81impl<R: Runtime, T: Manager<R>> MoonbaseExt<R> for T {
82 fn moonbase(&self) -> &Moonbase<R> {
83 self.state::<Moonbase<R>>().inner()
84 }
85}
86
87#[must_use]
89pub struct Builder {
90 options: LicensingOptions,
91 license_file: Option<PathBuf>,
92 device_id_resolver: Option<Arc<dyn DeviceIdResolver>>,
93 transport: Option<Arc<dyn HttpTransport>>,
94 allow_device_name_fallback: bool,
95 open_browser: bool,
96 ui: UiConfig,
97}
98
99impl Builder {
100 pub fn new(endpoint: impl Into<String>, product_id: impl Into<String>, public_key: impl Into<String>) -> Self {
103 Self {
104 options: LicensingOptions::new(endpoint, product_id, public_key),
105 license_file: None,
106 device_id_resolver: None,
107 transport: None,
108 allow_device_name_fallback: false,
109 open_browser: true,
110 ui: UiConfig::default(),
111 }
112 }
113
114 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
116 self.options.account_id = Some(account_id.into());
117 self
118 }
119
120 pub fn application_version(mut self, version: impl Into<String>) -> Self {
124 self.options.application_version = Some(version.into());
125 self
126 }
127
128 pub fn enforce_release_access(mut self, enforce: bool) -> Self {
133 self.options.enforce_release_access = enforce;
134 self
135 }
136
137 pub fn auto_activate_sub_products(mut self, auto_activate: bool) -> Self {
143 self.options.auto_activate_sub_products = auto_activate;
144 self
145 }
146
147 pub fn license_file(mut self, path: impl Into<PathBuf>) -> Self {
150 self.license_file = Some(path.into());
151 self
152 }
153
154 pub fn grace_period(mut self, grace: Duration) -> Self {
157 self.options.online_validation_grace_period = grace;
158 self
159 }
160
161 pub fn check_interval(mut self, interval: Duration) -> Self {
163 self.options.online_validation_min_interval = interval;
164 self
165 }
166
167 pub fn http_timeouts(mut self, connect: Duration, request: Duration) -> Self {
168 self.options = self.options.http_timeouts(connect, request);
169 self
170 }
171
172 pub fn client_info(mut self, client_info: impl Into<String>) -> Self {
175 self.options.client_info = Some(client_info.into());
176 self
177 }
178
179 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
181 self.options = self.options.metadata(key, value);
182 self
183 }
184
185 pub fn device_id_resolver(mut self, resolver: impl DeviceIdResolver + 'static) -> Self {
190 self.device_id_resolver = Some(Arc::new(resolver));
191 self
192 }
193
194 pub fn transport(mut self, transport: impl HttpTransport + 'static) -> Self {
198 self.transport = Some(Arc::new(transport));
199 self
200 }
201
202 pub fn allow_device_name_fallback(mut self, allow: bool) -> Self {
206 self.allow_device_name_fallback = allow;
207 self
208 }
209
210 pub fn open_browser(mut self, open: bool) -> Self {
213 self.open_browser = open;
214 self
215 }
216
217 pub fn ui(mut self, ui: UiConfig) -> Self {
220 self.ui = ui;
221 self
222 }
223
224 pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
225 let overlay_script = (self.ui.enabled && self.ui.overlay_window().is_some()).then(ui::overlay_script);
228 let mut plugin = PluginBuilder::new("moonbase");
229 if let Some(script) = overlay_script {
230 plugin = plugin.js_init_script(script);
231 }
232 plugin
233 .invoke_handler(tauri::generate_handler![
234 commands::state,
235 commands::activate,
236 commands::cancel_activation,
237 commands::refresh,
238 commands::deactivate,
239 commands::forget,
240 commands::device_token,
241 commands::activate_offline,
242 commands::dismiss_error,
243 commands::device,
244 commands::check_update,
245 commands::update_download_url,
246 commands::present,
247 commands::close_ui,
248 commands::ui_ready,
249 commands::ui_resize,
250 commands::save_machine_file,
251 commands::download_update,
252 commands::reveal_update,
253 commands::skip_update,
254 commands::open_link,
255 commands::load_add_ons,
256 commands::refresh_add_ons,
257 commands::activate_sub_product,
258 commands::request_sub_product_trial,
259 commands::buy_sub_product,
260 commands::upgrade_license,
261 ])
262 .register_uri_scheme_protocol(ui::SCHEME, |context, request| ui::serve(context.app_handle(), &request))
263 .setup(move |app, _api| {
264 let mut options = self.options;
265 if options.application_version.is_none() {
266 options.application_version = Some(app.package_info().version.to_string());
267 }
268 let segment =
269 format!("tauri-plugin-moonbase/{VERSION} (Tauri {}; {})", tauri::VERSION, std::env::consts::OS);
270 options.client_info = Some(match options.client_info.take() {
271 Some(outer) => format!("{segment} {outer}"),
272 None => segment,
273 });
274
275 let license_file = match self.license_file {
276 Some(path) => path,
277 None => app.path().app_data_dir()?.join("license.mb"),
278 };
279 let store = FileLicenseStore::new(license_file);
280 let resolver: Arc<dyn DeviceIdResolver> = self.device_id_resolver.unwrap_or_else(|| {
281 Arc::new(MoonbaseDeviceIdResolver::with_options(MoonbaseDeviceIdResolverOptions {
282 fallback: if self.allow_device_name_fallback {
283 DeviceIdFallback::DeviceName
284 } else {
285 DeviceIdFallback::None
286 },
287 ..Default::default()
288 }))
289 });
290
291 let settings = controller::Settings {
292 grace_period: options.online_validation_grace_period,
293 check_interval: options.online_validation_min_interval,
294 application_version: options.application_version.clone(),
295 open_browser: self.open_browser,
296 auto_activate_sub_products: options.auto_activate_sub_products,
297 };
298 let mut licensing = Licensing::builder(options).store(store.clone()).device_id_resolver(resolver);
299 if let Some(transport) = self.transport {
300 licensing = licensing.transport(transport);
301 }
302 let licensing = licensing.build()?;
303
304 let ui =
305 self.ui.enabled.then(|| ui::Ui::new(self.ui, licensing.options().endpoint.clone(), store.path()));
306 let moonbase = Moonbase::new(app.clone(), licensing, store, settings, ui);
307 moonbase.start();
308 app.manage(moonbase);
309 Ok(())
310 })
311 .on_page_load(|webview, payload| {
312 if payload.event() == PageLoadEvent::Finished
313 && let Some(moonbase) = webview.try_state::<Moonbase<R>>()
314 {
315 moonbase.on_page_load(webview.label());
316 }
317 })
318 .on_event(|app, event| {
319 let Some(moonbase) = app.try_state::<Moonbase<R>>() else {
320 return;
321 };
322 match event {
323 RunEvent::Exit => moonbase.shut_down(),
324 RunEvent::WindowEvent { event: WindowEvent::Focused(focused), .. } => {
326 moonbase.on_focus_changed(*focused);
327 }
328 _ => {}
329 }
330 })
331 .build()
332 }
333}
334
335impl std::fmt::Debug for Builder {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 f.debug_struct("Builder")
338 .field("endpoint", &self.options.endpoint)
339 .field("product_id", &self.options.product_id)
340 .field("license_file", &self.license_file)
341 .finish_non_exhaustive()
342 }
343}