diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index d140a1a2a..b318b737d 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -17,8 +17,32 @@ type UpdateInfo = { version: string; }; +type UpdateMenuState = + | { status: 'idle' } + | { status: 'downloading'; version: string } + | { status: 'ready'; version: string }; + let listenersRegistered = false; -let manualUpdateCheckInProgress = false; +let updateMenuState: UpdateMenuState = { status: 'idle' }; +const updateMenuStateListeners = new Set<(state: UpdateMenuState) => void>(); + +export function getUpdateMenuState(): UpdateMenuState { + return updateMenuState; +} + +export function onUpdateMenuStateChange(listener: (state: UpdateMenuState) => void): () => void { + updateMenuStateListeners.add(listener); + return () => { + updateMenuStateListeners.delete(listener); + }; +} + +function setUpdateMenuState(state: UpdateMenuState): void { + updateMenuState = state; + for (const listener of updateMenuStateListeners) { + listener(state); + } +} function getAutoUpdater(): AutoUpdater { const { autoUpdater } = require('electron-updater'); @@ -35,6 +59,7 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.on('update-available', (info: UpdateInfo) => { console.log(`Update available: ${info.version}`); + setUpdateMenuState({ status: 'downloading', version: info.version }); trackEvent('desktop_update_available', { current_version: version, new_version: info.version, @@ -43,16 +68,20 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.on('update-downloaded', (info: UpdateInfo) => { console.log(`Update downloaded: ${info.version}`); + setUpdateMenuState({ status: 'ready', version: info.version }); trackEvent('desktop_update_downloaded', { current_version: version, new_version: info.version, }); - if (!manualUpdateCheckInProgress) { - notifyRenderersUpdateDownloaded(info); - } + notifyRenderersUpdateDownloaded(info); + }); + + autoUpdater.on('update-not-available', () => { + setUpdateMenuState({ status: 'idle' }); }); autoUpdater.on('error', (err: Error) => { + setUpdateMenuState({ status: 'idle' }); console.log('Auto-updater: update check skipped —', err.message?.split('\n')[0]); trackEvent('desktop_update_error', { message: err.message?.split('\n')[0], @@ -92,6 +121,13 @@ export function setupAutoUpdater(): void { } export async function checkForUpdatesManually(): Promise { + const currentState = getUpdateMenuState(); + if (currentState.status === 'ready') { + installDownloadedUpdate(); + return; + } + if (currentState.status === 'downloading') return; + if (!app.isPackaged) { await dialog.showMessageBox({ type: 'info', @@ -115,29 +151,20 @@ export async function checkForUpdatesManually(): Promise { configureAutoUpdater(autoUpdater); try { - 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) => { - void dialog.showMessageBox({ - type: 'info', - title: 'Update Available', - message: `Version ${info.version} is available and will download in the background.`, - }); - }; + const onAvailable = () => {}; const onNotAvailable = () => { cleanup(); resolve('not-available'); }; - const onDownloaded = (info: UpdateInfo) => { + const onDownloaded = () => { cleanup(); - notifyRenderersUpdateDownloaded(info); resolve('downloaded'); }; const onError = (err: Error) => { @@ -151,6 +178,7 @@ export async function checkForUpdatesManually(): Promise { autoUpdater.once('error', onError); autoUpdater.checkForUpdates().catch((err: Error) => { cleanup(); + setUpdateMenuState({ status: 'idle' }); reject(err); }); }); @@ -163,7 +191,7 @@ export async function checkForUpdatesManually(): Promise { }); } } catch (err) { - manualUpdateCheckInProgress = false; + setUpdateMenuState({ status: 'idle' }); await dialog.showMessageBox({ type: 'error', title: 'Update Check Failed', diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 40e19398c..2753aaacd 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -1,12 +1,39 @@ import { app, Menu, shell } from 'electron'; -import { checkForUpdatesManually } from './auto-updater'; +import { + checkForUpdatesManually, + getUpdateMenuState, + installDownloadedUpdate, + onUpdateMenuStateChange, +} from './auto-updater'; -const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { - label: 'Check for Updates...', - click: () => { - void checkForUpdatesManually(); - }, -}; +let updateMenuListenerRegistered = false; + +function getUpdateMenuItem(): Electron.MenuItemConstructorOptions { + const state = getUpdateMenuState(); + + if (state.status === 'downloading') { + return { + label: 'Downloading...', + enabled: false, + }; + } + + if (state.status === 'ready') { + return { + label: 'Install and Restart', + click: () => { + installDownloadedUpdate(); + }, + }; + } + + return { + label: 'Check for Updates...', + click: () => { + void checkForUpdatesManually(); + }, + }; +} const privacyPolicyItem: Electron.MenuItemConstructorOptions = { label: 'Privacy Policy', @@ -23,14 +50,22 @@ const termsOfServiceItem: Electron.MenuItemConstructorOptions = { }; export function setupMenu(): void { + if (!updateMenuListenerRegistered) { + updateMenuListenerRegistered = true; + onUpdateMenuStateChange(() => { + setupMenu(); + }); + } + const isMac = process.platform === 'darwin'; + const updateMenuItem = getUpdateMenuItem(); const template: Electron.MenuItemConstructorOptions[] = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' as const }, - checkForUpdatesItem, + updateMenuItem, { type: 'separator' as const }, { role: 'services' as const }, { type: 'separator' as const }, @@ -51,7 +86,7 @@ export function setupMenu(): void { submenu: [ ...(!isMac ? [ - checkForUpdatesItem, + updateMenuItem, { type: 'separator' as const }, ] : []),