fix(updater): check for updates in Flatpak too and say how to update

"No Update Available" must only mean the server has nothing newer. Flatpak
installs were skipped before the check, so they always saw that alert.

Replace the installable flag with an install method (integrated | flatpak |
manual). Flatpak still never self-installs, but now gets a toast telling the
user to run `flatpak update`; manual installs keep the download link. The
X-Install-Mode header reports "flatpak" for those installs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-23 08:45:20 -07:00
co-authored by Claude Fable 5
parent 8a029db38a
commit 9c3a4eeb67
3 changed files with 96 additions and 47 deletions
+26 -16
View File
@@ -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 = (
<Button
@@ -198,8 +198,9 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
</Button>
);
if (!installable) {
// Nothing to reply to here; the backend only told us so we can point the user at the download
if (install !== "integrated") {
// Nothing to reply to here; the backend only told us so we can say how to update
const flatpak = install === "flatpak";
showToast({
id: UPDATE_TOAST_ID,
color: "info",
@@ -208,22 +209,31 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
<VStack>
<h2 className="font-semibold">Yaak {version} is available</h2>
<p className="text-text-subtle text-sm">
This install can&apos;t update itself. Download the new version to upgrade.
{flatpak ? (
<>
This install is managed by Flatpak. Update it with{" "}
<InlineCode>flatpak update</InlineCode> or your software center.
</>
) : (
"This install can't update itself. Download the new version to upgrade."
)}
</p>
</VStack>
),
action: () => (
<HStack space={1.5}>
<Button
size="xs"
color="info"
rightSlot={<Icon icon="external_link" />}
onClick={async () => {
await platform.openUrl("https://yaak.app/download");
}}
>
Download
</Button>
{!flatpak && (
<Button
size="xs"
color="info"
rightSlot={<Icon icon="external_link" />}
onClick={async () => {
await platform.openUrl("https://yaak.app/download");
}}
>
Download
</Button>
)}
{whatsNewButton}
</HStack>
),
+8 -3
View File
@@ -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";
+62 -28
View File
@@ -76,13 +76,6 @@ impl YaakUpdater {
auto_download: bool,
update_trigger: UpdateTrigger,
) -> Result<bool> {
// 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<R: Runtime>(
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<R: Runtime>(
}
}
/// 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<R: Runtime>(window: &WebviewWindow<R>, 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<R: Runtime>(
window: &WebviewWindow<R>,
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.