feat(auto-updater, ui): implement update notification and installation prompt in desktop application

This commit is contained in:
Anish Sarkar 2026-05-25 23:24:26 +05:30
parent 74fff64779
commit c0fefa4db1
7 changed files with 155 additions and 18 deletions

View file

@ -2,6 +2,8 @@ export const IPC_CHANNELS = {
OPEN_EXTERNAL: 'open-external',
GET_APP_VERSION: 'get-app-version',
DEEP_LINK: 'deep-link',
UPDATE_DOWNLOADED: 'update:downloaded',
UPDATE_INSTALL_NOW: 'update:install-now',
QUICK_ASK_TEXT: 'quick-ask-text',
SET_QUICK_ASK_MODE: 'set-quick-ask-mode',
GET_QUICK_ASK_MODE: 'get-quick-ask-mode',

View file

@ -51,6 +51,7 @@ import {
stopAgentFilesystemTreeWatch,
type AgentFilesystemTreeWatchOptions,
} from '../modules/agent-filesystem-tree-watcher';
import { installDownloadedUpdate } from '../modules/auto-updater';
let authTokens: { bearer: string; refresh: string } | null = null;
@ -70,6 +71,10 @@ export function registerIpcHandlers(): void {
return app.getVersion();
});
ipcMain.handle(IPC_CHANNELS.UPDATE_INSTALL_NOW, () => {
installDownloadedUpdate();
});
ipcMain.handle(IPC_CHANNELS.GET_PERMISSIONS_STATUS, () => {
return getPermissionsStatus();
});

View file

@ -1,4 +1,5 @@
import { app, dialog } from 'electron';
import { app, BrowserWindow, dialog } from 'electron';
import { IPC_CHANNELS } from '../ipc/channels';
import { trackEvent } from './analytics';
const SEMVER_RE = /^\d+\.\d+\.\d+/;
@ -17,6 +18,7 @@ type UpdateInfo = {
};
let listenersRegistered = false;
let manualUpdateCheckInProgress = false;
function getAutoUpdater(): AutoUpdater {
const { autoUpdater } = require('electron-updater');
@ -45,20 +47,9 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void {
current_version: version,
new_version: info.version,
});
dialog.showMessageBox({
type: 'info',
buttons: ['Restart', 'Later'],
defaultId: 0,
title: 'Update Ready',
message: `Version ${info.version} has been downloaded. Restart to apply the update.`,
}).then(({ response }: { response: number }) => {
if (response === 0) {
trackEvent('desktop_update_install_accepted', { new_version: info.version });
autoUpdater.quitAndInstall();
} else {
trackEvent('desktop_update_install_deferred', { new_version: info.version });
}
});
if (!manualUpdateCheckInProgress) {
notifyRenderersUpdateDownloaded(info);
}
});
autoUpdater.on('error', (err: Error) => {
@ -69,6 +60,39 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void {
});
}
function notifyRenderersUpdateDownloaded(info: UpdateInfo): void {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) {
win.webContents.send(IPC_CHANNELS.UPDATE_DOWNLOADED, {
version: info.version,
});
}
}
}
async function showNativeInstallDialog(autoUpdater: AutoUpdater, info: UpdateInfo): Promise<void> {
const { response } = await dialog.showMessageBox({
type: 'info',
buttons: ['Restart', 'Later'],
defaultId: 0,
title: 'Update Ready',
message: `Version ${info.version} has been downloaded. Restart to apply the update.`,
});
if (response === 0) {
trackEvent('desktop_update_install_accepted', { new_version: info.version });
autoUpdater.quitAndInstall();
} else {
trackEvent('desktop_update_install_deferred', { new_version: info.version });
}
}
export function installDownloadedUpdate(): void {
const autoUpdater = getAutoUpdater();
trackEvent('desktop_update_install_accepted', { source: 'renderer_prompt' });
autoUpdater.quitAndInstall();
}
export function setupAutoUpdater(): void {
if (!app.isPackaged) return;
@ -108,25 +132,31 @@ export async function checkForUpdatesManually(): Promise<void> {
configureAutoUpdater(autoUpdater);
try {
const result = await new Promise<'available' | 'not-available'>((resolve, reject) => {
manualUpdateCheckInProgress = true;
const result = await new Promise<'not-available' | 'downloaded'>((resolve, reject) => {
const cleanup = () => {
manualUpdateCheckInProgress = false;
autoUpdater.removeListener('update-available', onAvailable);
autoUpdater.removeListener('update-not-available', onNotAvailable);
autoUpdater.removeListener('update-downloaded', onDownloaded);
autoUpdater.removeListener('error', onError);
};
const onAvailable = (info: UpdateInfo) => {
cleanup();
void dialog.showMessageBox({
type: 'info',
title: 'Update Available',
message: `Version ${info.version} is available and will download in the background.`,
});
resolve('available');
};
const onNotAvailable = () => {
cleanup();
resolve('not-available');
};
const onDownloaded = (info: UpdateInfo) => {
cleanup();
void showNativeInstallDialog(autoUpdater, info);
resolve('downloaded');
};
const onError = (err: Error) => {
cleanup();
reject(err);
@ -134,6 +164,7 @@ export async function checkForUpdatesManually(): Promise<void> {
autoUpdater.once('update-available', onAvailable);
autoUpdater.once('update-not-available', onNotAvailable);
autoUpdater.once('update-downloaded', onDownloaded);
autoUpdater.once('error', onError);
autoUpdater.checkForUpdates().catch((err: Error) => {
cleanup();
@ -149,6 +180,7 @@ export async function checkForUpdatesManually(): Promise<void> {
});
}
} catch (err) {
manualUpdateCheckInProgress = false;
await dialog.showMessageBox({
type: 'error',
title: 'Update Check Failed',

View file

@ -10,6 +10,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
openExternal: (url: string) => ipcRenderer.send(IPC_CHANNELS.OPEN_EXTERNAL, url),
getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION),
onUpdateDownloaded: (callback: (data: { version: string }) => void) => {
const listener = (_event: unknown, data: { version: string }) => callback(data);
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, listener);
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.UPDATE_DOWNLOADED, listener);
};
},
installUpdateNow: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL_NOW),
onDeepLink: (callback: (url: string) => void) => {
const listener = (_event: unknown, url: string) => callback(url);
ipcRenderer.on(IPC_CHANNELS.DEEP_LINK, listener);