mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Auto-download desktop updates before prompting
This commit is contained in:
@@ -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<DesktopAppUpdateCheckResu
|
||||
|
||||
return {
|
||||
hasUpdate: result.hasUpdate === true,
|
||||
readyToInstall: result.readyToInstall === true,
|
||||
currentVersion: toStringOrNull(result.currentVersion),
|
||||
latestVersion: toStringOrNull(result.latestVersion),
|
||||
body: toStringOrNull(result.body),
|
||||
|
||||
@@ -55,7 +55,7 @@ export function UpdateBanner() {
|
||||
|
||||
function getSubtitle(): string {
|
||||
if (isInstalled) return "Restart to use the new version.";
|
||||
if (isInstalling) return "Downloading and installing...";
|
||||
if (isInstalling) return "Installing and restarting...";
|
||||
if (isError) return errorMessage ?? "Something went wrong.";
|
||||
return `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
checkDesktopAppUpdate,
|
||||
formatVersionWithPrefix,
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
export type DesktopAppUpdateStatus =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "pending"
|
||||
| "up-to-date"
|
||||
| "available"
|
||||
| "installing"
|
||||
| "installed"
|
||||
| "error";
|
||||
|
||||
const PENDING_RECHECK_MS = 10_000;
|
||||
|
||||
export interface UseDesktopAppUpdaterReturn {
|
||||
isDesktopApp: boolean;
|
||||
status: DesktopAppUpdateStatus;
|
||||
@@ -56,11 +59,15 @@ function formatStatusText(input: {
|
||||
return "App is up to date.";
|
||||
}
|
||||
|
||||
if (status === "pending") {
|
||||
return "We'll let you know when the update is ready.";
|
||||
}
|
||||
|
||||
if (status === "available") {
|
||||
if (availableUpdate?.latestVersion) {
|
||||
return `Update available: ${formatVersionWithPrefix(availableUpdate.latestVersion)}`;
|
||||
return `Update ready: ${formatVersionWithPrefix(availableUpdate.latestVersion)}`;
|
||||
}
|
||||
return "An app update is available.";
|
||||
return "An app update is ready to install.";
|
||||
}
|
||||
|
||||
if (status === "installed") {
|
||||
@@ -106,9 +113,12 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
setInstallMessage(null);
|
||||
setLastCheckedAt(Date.now());
|
||||
|
||||
if (result.hasUpdate) {
|
||||
if (result.readyToInstall) {
|
||||
setAvailableUpdate(result);
|
||||
setStatus("available");
|
||||
} else if (result.hasUpdate) {
|
||||
setAvailableUpdate(null);
|
||||
setStatus("pending");
|
||||
} else {
|
||||
setAvailableUpdate(null);
|
||||
setStatus("up-to-date");
|
||||
@@ -133,6 +143,20 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
[isDesktopApp],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
@@ -807,7 +807,7 @@ function DesktopAppUpdateRow() {
|
||||
<Text style={styles.aboutHintText}>{statusText}</Text>
|
||||
{availableUpdate?.latestVersion ? (
|
||||
<Text style={styles.aboutHintText}>
|
||||
New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||
</Text>
|
||||
) : null}
|
||||
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
||||
|
||||
@@ -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>): 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<AppUpdateCheckResult> {
|
||||
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<AppUpda
|
||||
|
||||
if (hasUpdate) {
|
||||
cachedUpdateInfo = info;
|
||||
downloading = !isReadyToInstallVersion(latestVersion);
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
hasUpdate: true,
|
||||
readyToInstall: isReadyToInstallVersion(latestVersion),
|
||||
info,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hasUpdate,
|
||||
cachedUpdateInfo = null;
|
||||
downloadedUpdateVersion = null;
|
||||
downloading = false;
|
||||
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
body: typeof info.releaseNotes === "string" ? info.releaseNotes : null,
|
||||
date: typeof info.releaseDate === "string" ? info.releaseDate : null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[auto-updater] Failed to check for updates:", error);
|
||||
return {
|
||||
hasUpdate: false,
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +187,6 @@ export async function downloadAndInstallUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
return {
|
||||
installed: false,
|
||||
version: currentVersion,
|
||||
message: "Update already in progress.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!cachedUpdateInfo) {
|
||||
return {
|
||||
installed: false,
|
||||
@@ -126,24 +197,35 @@ export async function downloadAndInstallUpdate(
|
||||
|
||||
configureAutoUpdater();
|
||||
|
||||
const readyVersion = cachedUpdateInfo.version;
|
||||
if (isReadyToInstallVersion(readyVersion)) {
|
||||
scheduleQuitAndInstall(onBeforeQuit);
|
||||
return {
|
||||
installed: true,
|
||||
version: readyVersion,
|
||||
message: "Update downloaded. The app will restart shortly.",
|
||||
};
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
return {
|
||||
installed: false,
|
||||
version: currentVersion,
|
||||
message: "Update is still being prepared. Try again in a moment.",
|
||||
};
|
||||
}
|
||||
|
||||
downloading = true;
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
// quitAndInstall restarts the app with the new version.
|
||||
// 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);
|
||||
downloadedUpdateVersion = readyVersion;
|
||||
downloading = false;
|
||||
scheduleQuitAndInstall(onBeforeQuit);
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
version: cachedUpdateInfo.version,
|
||||
version: readyVersion,
|
||||
message: "Update downloaded. The app will restart shortly.",
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user