diff --git a/apps/yaak-client/lib/initGlobalListeners.tsx b/apps/yaak-client/lib/initGlobalListeners.tsx index a4dbb130..23a0fb1c 100644 --- a/apps/yaak-client/lib/initGlobalListeners.tsx +++ b/apps/yaak-client/lib/initGlobalListeners.tsx @@ -12,7 +12,7 @@ import type { UpdateResponse, YaakNotification, } from "@yaakapp-internal/tauri-client"; -import { HStack, Icon, VStack } from "@yaakapp-internal/ui"; +import { HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui"; import { openSettings } from "../commands/openSettings"; import { Button } from "../components/core/Button"; import { ButtonInfiniteLoading } from "../components/core/ButtonInfiniteLoading"; @@ -180,9 +180,9 @@ function showUpdateInstalledToast(version: string) { async function showUpdateAvailableToast(updateInfo: UpdateInfo) { const UPDATE_TOAST_ID = "update-info"; - const { version, replyEventId, downloaded, installable } = updateInfo; + const { version, replyEventId, downloaded, install } = updateInfo; - jotaiStore.set(updateAvailableAtom, { version, downloaded, installable }); + jotaiStore.set(updateAvailableAtom, { version, downloaded, install }); const whatsNewButton = ( + {!flatpak && ( + + )} {whatsNewButton} ), diff --git a/crates-tauri/yaak-app-client/bindings/index.ts b/crates-tauri/yaak-app-client/bindings/index.ts index afbdee32..6fb02079 100644 --- a/crates-tauri/yaak-app-client/bindings/index.ts +++ b/crates-tauri/yaak-app-client/bindings/index.ts @@ -9,12 +9,17 @@ export type UpdateInfo = { version: string; downloaded: boolean; /** - * Whether the app can download and install this update itself. When false, the - * user has to go download the new version (e.g. Linux .deb/.rpm installs). + * How this update gets applied. Anything but `Integrated` means the app can't do it + * itself and the user is told how to update instead. */ - installable: boolean; + install: UpdateInstall; }; +/** + * How an update can be applied to this install. + */ +export type UpdateInstall = "integrated" | "flatpak" | "manual"; + export type UpdateResponse = { type: "ack" } | { type: "action"; action: UpdateResponseAction }; export type UpdateResponseAction = "install" | "skip"; diff --git a/crates-tauri/yaak-app-client/src/updates.rs b/crates-tauri/yaak-app-client/src/updates.rs index 884b2160..37512008 100644 --- a/crates-tauri/yaak-app-client/src/updates.rs +++ b/crates-tauri/yaak-app-client/src/updates.rs @@ -76,13 +76,6 @@ impl YaakUpdater { auto_download: bool, update_trigger: UpdateTrigger, ) -> Result { - // Flatpak installs (e.g. FlatPark) are updated by flatpak from their remote; the - // in-app updater can't write inside the sandbox and must not try - #[cfg(target_os = "linux")] - if std::env::var_os("FLATPAK_ID").is_some() { - return Ok(false); - } - let settings = window.db().get_settings(); let update_key = format!("{:x}", md5::compute(settings.id)); self.last_check = Some(Instant::now()); @@ -129,11 +122,15 @@ impl YaakUpdater { Some(update) => { let w = window.clone(); tauri::async_runtime::spawn(async move { - // This install can't apply the update itself (e.g. a .deb or .rpm), so - // just point the user at the download instead of failing to install it - if !can_install_update(&update) { - info!("{} available but must be installed manually", update.version); - notify_manual_update(&w, &update); + // Only hand the artifact to the updater plugin when this install can + // apply it itself; otherwise tell the user how to update instead + let install = update_install_method(&update); + if install != UpdateInstall::Integrated { + info!( + "{} available, but this install updates via {install:?}", + update.version + ); + notify_external_update(&w, &update, install); return; } @@ -214,9 +211,23 @@ struct UpdateInfo { reply_event_id: String, version: String, downloaded: bool, - /// Whether the app can download and install this update itself. When false, the - /// user has to go download the new version (e.g. Linux .deb/.rpm installs). - installable: bool, + /// How this update gets applied. Anything but `Integrated` means the app can't do it + /// itself and the user is told how to update instead. + install: UpdateInstall, +} + +/// How an update can be applied to this install. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Default, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export, export_to = "index.ts")] +enum UpdateInstall { + /// The app downloads and installs it itself + #[default] + Integrated, + /// Flatpak install: updated by `flatpak update` from its remote (e.g. FlatPark) + Flatpak, + /// Nothing can install it in-app (distro package, Nix, unknown); download by hand + Manual, } #[derive(Debug, Clone, PartialEq, Deserialize, TS)] @@ -285,7 +296,7 @@ async fn start_integrated_update( let info = UpdateInfo { version: update.version.to_string(), downloaded, - installable: true, + install: UpdateInstall::Integrated, reply_event_id: reply_id, }; window @@ -320,13 +331,17 @@ async fn start_integrated_update( } } -/// Tell the frontend about an update this install can't apply itself, so the user can go -/// download it. Unlike the integrated flow, there is nothing to reply to. -fn notify_manual_update(window: &WebviewWindow, update: &Update) { +/// Tell the frontend about an update this install can't apply itself, so the user can be +/// told how to get it. Unlike the integrated flow, there is nothing to reply to. +fn notify_external_update( + window: &WebviewWindow, + update: &Update, + install: UpdateInstall, +) { let info = UpdateInfo { version: update.version.to_string(), downloaded: false, - installable: false, + install, reply_event_id: generate_id(), }; if let Err(e) = window.emit_to(window.label(), "update_available", &info) { @@ -404,7 +419,19 @@ fn detect_install_mode() -> Option<&'static str> { return Some("nsis"); } #[allow(unreachable_code)] - if cfg!(target_os = "linux") { linux_installer() } else { None } + if !cfg!(target_os = "linux") { + None + } else if is_flatpak() { + Some("flatpak") + } else { + linux_installer() + } +} + +/// Flatpak installs (e.g. FlatPark) are updated by flatpak from their remote; the in-app +/// updater can't write inside the sandbox and must not try. +fn is_flatpak() -> bool { + std::env::var_os("FLATPAK_ID").is_some() } /// How Yaak was installed on Linux, as far as the updater plugin can install into it. @@ -442,15 +469,22 @@ fn package_manager_owns_exe(cmd: &str, query_arg: &str) -> bool { .unwrap_or(false) } -/// Whether the updater plugin can install the artifact the server returned. On Linux the -/// server may hand back a different package format than the one installed (it currently -/// serves the AppImage for every Linux install), and unknown install methods (distro -/// packages, Nix, ...) can't be updated in-app at all. -fn can_install_update(update: &Update) -> bool { +/// How the artifact the server returned can be applied to this install. On Linux the +/// server may hand back a different package format than the one installed, Flatpak can't +/// be written from inside the sandbox, and unknown install methods (distro packages, +/// Nix, ...) can't be updated in-app at all. +fn update_install_method(update: &Update) -> UpdateInstall { if !cfg!(target_os = "linux") { - return true; + return UpdateInstall::Integrated; + } + if is_flatpak() { + return UpdateInstall::Flatpak; + } + if artifact_matches_installer(linux_installer(), update.download_url.path()) { + UpdateInstall::Integrated + } else { + UpdateInstall::Manual } - artifact_matches_installer(linux_installer(), update.download_url.path()) } /// Whether the artifact at `url_path` is in the package format `installer` can install.