Host: the store's commands (install links, dropped files, the signed Store, the install screen) are served for every app that sets AppConfig::with_store; commands::STORE, testkit::assert_shell_only_group and assert_store_guards

This commit is contained in:
CRTkafa 2026-10-01 03:40:37 +07:00
parent d67d55886d
commit 005a953162
5 changed files with 230 additions and 6 deletions

View file

@ -0,0 +1,158 @@
//! 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::<Store>() 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<State<'_, Store>, String> {
app.try_state::<Store>().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<Vec<LinkEvent>, 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<InstallPreview, String> {
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<InstallPreview, String> {
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<String>,
) -> Result<InstallOutcome, String> {
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<Vec<ScriptPlugin>, 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<InstallRecords, String> {
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<StoreView, String> {
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<InstallPreview, String> {
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<tauri::Wry>) -> 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
]
}