//! The store's Tauri commands for every app that sets [`crate::app::AppConfig::store`]: the //! `waybetter://install` deep link, dropped files, the signed wayBetter Store and the install //! screen's commands ([`crate::commands::STORE`]). Every command is shell-only; site tabs and web //! pages have no way to install anything. A deep link only *proposes* an install: the shell shows //! the confirmation screen and nothing is written until the owner confirms there. //! //! The rules, the catalog format and the install state are [`crate::store`]'s; this module only //! wraps them. An app without a store configuration registers nothing and every command here //! answers that it is not available. use crate::app::AppConfig; use crate::host::{emit_shell, eval_in_site_tabs, shell_only}; use crate::plugins::ScriptPlugin; use crate::policy; use crate::store::{InstallOutcome, InstallPreview, InstallRecords, LinkEvent, Store, StoreItemRef, StoreView, EVENT_LINK}; use tauri::{AppHandle, Manager, State, Webview}; use tauri_plugin_deep_link::DeepLinkExt; /// Makes the [`Store`] for the app's configuration and listens for install links (launch argument, /// open-URL event, a second launch forwarded by single-instance). pub(crate) fn init(app: &AppHandle, config: &AppConfig) { let Some(store) = config.store else { return }; let dir = app.path().app_data_dir().unwrap_or_default(); app.manage(Store::new(store, config.plugins, dir)); // Windows installs register the scheme in the NSIS installer and macOS in Info.plist; a Linux // AppImage and a Windows development build register it here (HKCU / xdg, current user only). #[cfg(any(target_os = "linux", all(windows, debug_assertions)))] if let Err(error) = app.deep_link().register_all() { eprintln!("{}: deep link registration failed: {error}", config.title); } let handle = app.clone(); app.deep_link().on_open_url(move |event| { for url in event.urls() { received(&handle, url.as_str()); } }); // Launched by a link (Windows/Linux pass it as an argument; macOS as an open-URL event). if let Ok(Some(urls)) = app.deep_link().get_current() { for url in urls { received(app, url.as_str()); } } } /// A `waybetter://` link reached the app: validate it, queue it for the shell and bring the window /// forward. Nothing is downloaded or installed until the shell's screen asks for it. fn received(app: &AppHandle, raw: &str) { let Some(store) = app.try_state::() else { return }; emit_shell(app, EVENT_LINK, store.receive_link(raw)); if let Some(window) = app.get_window(policy::MAIN_LABEL) { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } fn store(app: &AppHandle) -> Result, String> { app.try_state::().ok_or_else(|| "UNAVAILABLE: this app has no store".to_owned()) } fn eval_in_site(app: &AppHandle, script: &str) { eval_in_site_tabs(app, crate::app::config(app).site, script); } /// Links that arrived before the shell listened (drained). #[tauri::command] pub(crate) async fn install_pending(webview: Webview, app: AppHandle) -> Result, String> { shell_only(&webview)?; Ok(store(&app)?.take_pending()) } /// Downloads what a validated install link points at and stages it for the confirmation screen. #[tauri::command] pub(crate) async fn install_stage_link(webview: Webview, app: AppHandle, kind: String, url: String) -> Result { shell_only(&webview)?; store(&app)?.stage_link(&kind, &url).await } /// A file dropped on the window (the shell read it with the File API; native code validates it). #[tauri::command] pub(crate) async fn install_stage_file(webview: Webview, app: AppHandle, name: String, text: String) -> Result { shell_only(&webview)?; store(&app)?.stage_file(&name, text) } #[tauri::command] pub(crate) async fn install_cancel(webview: Webview, app: AppHandle, token: String) -> Result<(), String> { shell_only(&webview)?; store(&app)?.cancel(&token); Ok(()) } /// The owner confirmed the screen for `token`. Plugins are written (validated again) and, when /// `enable` (or they were enabled before an update), started in every site tab. Themes are handed /// back for the shell to add under `theme_id`. #[tauri::command] pub(crate) async fn install_commit( webview: Webview, app: AppHandle, token: String, enable: bool, theme_id: Option, ) -> Result { shell_only(&webview)?; store(&app)?.commit(&token, enable, theme_id, |script| eval_in_site(&app, script)) } /// Deletes a plugin (stopped in every site tab first). Shell-only, like every plugin command. #[tauri::command] pub(crate) async fn plugin_remove(webview: Webview, app: AppHandle, file: String) -> Result, String> { shell_only(&webview)?; store(&app)?.remove_plugin(&file, |script| eval_in_site(&app, script)) } /// Where installed plugins and themes came from (for badges and update checks). #[tauri::command] pub(crate) async fn install_records(webview: Webview, app: AppHandle) -> Result { shell_only(&webview)?; Ok(store(&app)?.records()) } /// A theme was removed in Appearance: forget where it came from. #[tauri::command] pub(crate) async fn install_forget(webview: Webview, app: AppHandle, kind: String, name: String) -> Result<(), String> { shell_only(&webview)?; store(&app)?.forget(&kind, &name) } /// The signed catalog, this app's items only (fetched at most every 10 minutes unless `refresh`), /// with what is installed. #[tauri::command] pub(crate) async fn store_catalog(webview: Webview, app: AppHandle, refresh: bool) -> Result { shell_only(&webview)?; Ok(store(&app)?.catalog(refresh).await) } /// Downloads a store item, checks it against the signed catalog (size + sha256) and stages it. #[tauri::command] pub(crate) async fn store_stage(webview: Webview, app: AppHandle, item: StoreItemRef) -> Result { shell_only(&webview)?; store(&app)?.stage_item(&item).await } /// The store's commands ([`crate::commands::STORE`]). pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![ install_pending, install_stage_link, install_stage_file, install_cancel, install_commit, plugin_remove, install_records, install_forget, store_catalog, store_stage ] }