From 26fe4d74934f7026b0eaa5fba291dc039a914ebd Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 21:44:31 +0530 Subject: [PATCH] feat(auto-updater, menu): enhance update management and add menu options for updates and policies --- surfsense_desktop/.env.example | 2 +- surfsense_desktop/src/modules/auto-updater.ts | 119 ++++++++++++++++-- surfsense_desktop/src/modules/menu.ts | 36 +++++- surfsense_desktop/src/modules/window.ts | 2 +- 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/surfsense_desktop/.env.example b/surfsense_desktop/.env.example index 2d9de7561..f4e797250 100644 --- a/surfsense_desktop/.env.example +++ b/surfsense_desktop/.env.example @@ -3,7 +3,7 @@ # The hosted web frontend URL. Used to intercept OAuth redirects and keep them # inside the desktop app. Set to your production frontend domain. -HOSTED_FRONTEND_URL=https://surfsense.net +HOSTED_FRONTEND_URL=https://surfsense.com # Runtime override for the above (read at app start, no rebuild required). # Useful for self-hosters whose backend NEXT_FRONTEND_URL differs from the diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index e323abe53..c0fea6634 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -3,20 +3,35 @@ import { trackEvent } from './analytics'; const SEMVER_RE = /^\d+\.\d+\.\d+/; -export function setupAutoUpdater(): void { - if (!app.isPackaged) return; +type AutoUpdater = { + autoDownload: boolean; + on(event: string, listener: (...args: any[]) => void): void; + once(event: string, listener: (...args: any[]) => void): void; + removeListener(event: string, listener: (...args: any[]) => void): void; + checkForUpdates(): Promise; + quitAndInstall(): void; +}; - const version = app.getVersion(); - if (!SEMVER_RE.test(version)) { - console.log(`Auto-updater: skipping — "${version}" is not valid semver`); - return; - } +type UpdateInfo = { + version: string; +}; +let listenersRegistered = false; + +function getAutoUpdater(): AutoUpdater { const { autoUpdater } = require('electron-updater'); + return autoUpdater as AutoUpdater; +} +function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.autoDownload = true; - autoUpdater.on('update-available', (info: { version: string }) => { + if (listenersRegistered) return; + listenersRegistered = true; + + const version = app.getVersion(); + + autoUpdater.on('update-available', (info: UpdateInfo) => { console.log(`Update available: ${info.version}`); trackEvent('desktop_update_available', { current_version: version, @@ -24,7 +39,7 @@ export function setupAutoUpdater(): void { }); }); - autoUpdater.on('update-downloaded', (info: { version: string }) => { + autoUpdater.on('update-downloaded', (info: UpdateInfo) => { console.log(`Update downloaded: ${info.version}`); trackEvent('desktop_update_downloaded', { current_version: version, @@ -52,6 +67,92 @@ export function setupAutoUpdater(): void { message: err.message?.split('\n')[0], }); }); +} + +export function setupAutoUpdater(): void { + if (!app.isPackaged) return; + + const version = app.getVersion(); + if (!SEMVER_RE.test(version)) { + console.log(`Auto-updater: skipping - "${version}" is not valid semver`); + return; + } + + const autoUpdater = getAutoUpdater(); + configureAutoUpdater(autoUpdater); autoUpdater.checkForUpdates().catch(() => {}); } + +export async function checkForUpdatesManually(): Promise { + if (!app.isPackaged) { + await dialog.showMessageBox({ + type: 'info', + title: 'Updates Unavailable', + message: 'Updates are only available in packaged builds.', + }); + return; + } + + const version = app.getVersion(); + if (!SEMVER_RE.test(version)) { + await dialog.showMessageBox({ + type: 'info', + title: 'Updates Unavailable', + message: `Version "${version}" is not a valid release version, so updates cannot be checked.`, + }); + return; + } + + const autoUpdater = getAutoUpdater(); + configureAutoUpdater(autoUpdater); + + try { + const result = await new Promise<'available' | 'not-available'>((resolve, reject) => { + const cleanup = () => { + autoUpdater.removeListener('update-available', onAvailable); + autoUpdater.removeListener('update-not-available', onNotAvailable); + 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 onError = (err: Error) => { + cleanup(); + reject(err); + }; + + autoUpdater.once('update-available', onAvailable); + autoUpdater.once('update-not-available', onNotAvailable); + autoUpdater.once('error', onError); + autoUpdater.checkForUpdates().catch((err: Error) => { + cleanup(); + reject(err); + }); + }); + + if (result === 'not-available') { + await dialog.showMessageBox({ + type: 'info', + title: 'No Updates Available', + message: "You're up to date.", + }); + } + } catch (err) { + await dialog.showMessageBox({ + type: 'error', + title: 'Update Check Failed', + message: err instanceof Error ? err.message : String(err), + }); + } +} diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 128a73a21..067a6e461 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -1,13 +1,45 @@ -import { Menu } from 'electron'; +import { app, Menu } from 'electron'; +import { checkForUpdatesManually } from './auto-updater'; + +const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { + label: 'Check for Updates...', + click: () => { + void checkForUpdatesManually(); + }, +}; export function setupMenu(): void { const isMac = process.platform === 'darwin'; const template: Electron.MenuItemConstructorOptions[] = [ - ...(isMac ? [{ role: 'appMenu' as const }] : []), + ...(isMac + ? [{ + label: app.name, + submenu: [ + { role: 'about' as const }, + checkForUpdatesItem, + { type: 'separator' as const }, + { role: 'services' as const }, + { type: 'separator' as const }, + { role: 'hide' as const }, + { role: 'hideOthers' as const }, + { role: 'unhide' as const }, + { type: 'separator' as const }, + { role: 'quit' as const }, + ], + }] + : []), { role: 'fileMenu' as const }, { role: 'editMenu' as const }, { role: 'viewMenu' as const }, { role: 'windowMenu' as const }, + ...(!isMac + ? [{ + role: 'help' as const, + submenu: [ + checkForUpdatesItem, + ], + }] + : []), ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index e80bf7879..bcae1799c 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -13,7 +13,7 @@ function getHostedFrontendUrl(): string { return ( process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE || process.env.HOSTED_FRONTEND_URL || - 'https://surfsense.net' + 'https://surfsense.com' ); }