From 2683dae5f96c611a957202a4d7023ff97689ed12 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 9 Apr 2026 19:42:59 +0700 Subject: [PATCH] Auto-download desktop updates before prompting --- .../src/desktop/updates/desktop-updates.ts | 2 + .../app/src/desktop/updates/update-banner.tsx | 2 +- .../updates/use-desktop-app-updater.ts | 32 +++- packages/app/src/screens/settings-screen.tsx | 2 +- packages/desktop/src/features/auto-updater.ts | 172 +++++++++++++----- 5 files changed, 159 insertions(+), 51 deletions(-) diff --git a/packages/app/src/desktop/updates/desktop-updates.ts b/packages/app/src/desktop/updates/desktop-updates.ts index 04d429dec..36e97e9a1 100644 --- a/packages/app/src/desktop/updates/desktop-updates.ts +++ b/packages/app/src/desktop/updates/desktop-updates.ts @@ -4,6 +4,7 @@ import { invokeDesktopCommand } from "@/desktop/electron/invoke"; export interface DesktopAppUpdateCheckResult { hasUpdate: boolean; + readyToInstall: boolean; currentVersion: string | null; latestVersion: string | null; body: string | null; @@ -76,6 +77,7 @@ export async function checkDesktopAppUpdate(): Promise { + if (!isDesktopApp || status !== "pending") { + return undefined; + } + + const intervalId = setInterval(() => { + void checkForUpdates({ silent: true }); + }, PENDING_RECHECK_MS); + + return () => { + clearInterval(intervalId); + }; + }, [checkForUpdates, isDesktopApp, status]); + const installUpdate = useCallback(async () => { if (!isDesktopApp) { return null; diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index f6e7d2b7a..f87abed98 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -807,7 +807,7 @@ function DesktopAppUpdateRow() { {statusText} {availableUpdate?.latestVersion ? ( - New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)} + Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)} ) : null} {errorMessage ? {errorMessage} : null} diff --git a/packages/desktop/src/features/auto-updater.ts b/packages/desktop/src/features/auto-updater.ts index d1c1dca8f..61ecee1d9 100644 --- a/packages/desktop/src/features/auto-updater.ts +++ b/packages/desktop/src/features/auto-updater.ts @@ -7,6 +7,7 @@ import { autoUpdater, type UpdateInfo } from "electron-updater"; export type AppUpdateCheckResult = { hasUpdate: boolean; + readyToInstall: boolean; currentVersion: string; latestVersion: string; body: string | null; @@ -24,19 +25,84 @@ export type AppUpdateInstallResult = { // --------------------------------------------------------------------------- let cachedUpdateInfo: UpdateInfo | null = null; +let downloadedUpdateVersion: string | null = null; let downloading = false; +let autoUpdaterConfigured = false; // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- function configureAutoUpdater(): void { - // Don't auto-download — the user triggers install explicitly. - autoUpdater.autoDownload = false; + // Download updates in the background and only prompt once they are ready to install. + autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; // Suppress built-in dialogs; the renderer handles UI. autoUpdater.autoRunAppAfterInstall = true; + + if (autoUpdaterConfigured) { + return; + } + + autoUpdaterConfigured = true; + + autoUpdater.on("update-available", (info) => { + cachedUpdateInfo = info; + downloadedUpdateVersion = null; + downloading = true; + }); + + autoUpdater.on("update-downloaded", (info) => { + cachedUpdateInfo = info; + downloadedUpdateVersion = info.version; + downloading = false; + }); + + autoUpdater.on("update-not-available", () => { + cachedUpdateInfo = null; + downloadedUpdateVersion = null; + downloading = false; + }); + + autoUpdater.on("error", (error) => { + downloading = false; + console.error("[auto-updater] Updater event failed:", error); + }); +} + +function isReadyToInstallVersion(version: string): boolean { + return downloadedUpdateVersion === version; +} + +function buildCheckResult(input: { + currentVersion: string; + hasUpdate: boolean; + readyToInstall: boolean; + info?: UpdateInfo | null; +}): AppUpdateCheckResult { + const { currentVersion, hasUpdate, readyToInstall, info } = input; + + return { + hasUpdate, + readyToInstall, + currentVersion, + latestVersion: info?.version ?? currentVersion, + body: typeof info?.releaseNotes === "string" ? info.releaseNotes : null, + date: typeof info?.releaseDate === "string" ? info.releaseDate : null, + }; +} + +function scheduleQuitAndInstall(onBeforeQuit?: () => Promise): void { + // Use a short delay to allow the renderer to receive the response. + setTimeout(async () => { + try { + if (onBeforeQuit) await onBeforeQuit(); + autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true); + } catch (error) { + console.error("[auto-updater] quitAndInstall failed:", error); + } + }, 1500); } // --------------------------------------------------------------------------- @@ -45,28 +111,34 @@ function configureAutoUpdater(): void { export async function checkForAppUpdate(currentVersion: string): Promise { if (!app.isPackaged) { - return { - hasUpdate: false, + return buildCheckResult({ currentVersion, - latestVersion: currentVersion, - body: null, - date: null, - }; + hasUpdate: false, + readyToInstall: false, + }); } configureAutoUpdater(); + const cachedVersion = cachedUpdateInfo?.version ?? null; + if (cachedVersion && cachedVersion !== currentVersion) { + return buildCheckResult({ + currentVersion, + hasUpdate: true, + readyToInstall: isReadyToInstallVersion(cachedVersion), + info: cachedUpdateInfo, + }); + } + try { const result = await autoUpdater.checkForUpdates(); if (!result || !result.updateInfo) { - return { - hasUpdate: false, + return buildCheckResult({ currentVersion, - latestVersion: currentVersion, - body: null, - date: null, - }; + hasUpdate: false, + readyToInstall: false, + }); } const info = result.updateInfo; @@ -75,24 +147,31 @@ export async function checkForAppUpdate(currentVersion: string): Promise { - try { - if (onBeforeQuit) await onBeforeQuit(); - autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true); - } catch (error) { - console.error("[auto-updater] quitAndInstall failed:", error); - } - }, 1500); + downloadedUpdateVersion = readyVersion; + downloading = false; + scheduleQuitAndInstall(onBeforeQuit); return { installed: true, - version: cachedUpdateInfo.version, + version: readyVersion, message: "Update downloaded. The app will restart shortly.", }; } catch (error) {