const { autoUpdater } = require('electron-updater'); const log = require('electron-log'); autoUpdater.logger = log; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true; // Remembered across the process so the main window can ask for it // once the renderer is ready. Cleared on successful download so we // don't mislead about a pending install. let lastCheckResult = { hasUpdate: false, required: false, latestVersion: null, currentVersion: null, releaseNotes: null, downloading: false, downloaded: false, progress: 0, error: null, }; const statusListeners = new Set(); function emitStatus() { for (const cb of statusListeners) { try { cb({ ...lastCheckResult }); } catch (err) { log.warn('update status listener threw', err); } } } function onStatus(cb) { statusListeners.add(cb); return () => statusListeners.delete(cb); } function getStatus() { return { ...lastCheckResult }; } // A release is "required" when its notes start with the `[REQUIRED]` // marker. Keeping the signal in release notes means no new feed file // or schema change — publishers just prefix the message. function isRequired(info) { const notes = typeof info?.releaseNotes === 'string' ? info.releaseNotes : ''; return /^\s*\[REQUIRED\]/i.test(notes); } // Splash-phase check. Resolves once we know whether to open the main // window (optional or no update) or to force an install (required). // - No update / error → resolve true (main window should open) // - Optional update → resolve true (main window should open; header // icon surfaces the update inside the app) // - Required update → download + quitAndInstall; never resolves function checkForUpdates(splashWindow) { return new Promise((resolve) => { function sendToSplash(js) { if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.webContents.executeJavaScript(js).catch(() => {}); } } autoUpdater.on('checking-for-update', () => { sendToSplash('setStatus("Checking for updates...")'); }); autoUpdater.on('update-available', (info) => { const required = isRequired(info); lastCheckResult = { hasUpdate: true, required, latestVersion: info?.version ?? null, currentVersion: autoUpdater.currentVersion?.version ?? null, releaseNotes: typeof info?.releaseNotes === 'string' ? info.releaseNotes : null, downloading: required, downloaded: false, progress: 0, error: null, }; emitStatus(); if (required) { sendToSplash('setStatus("Downloading required update...")'); autoUpdater.downloadUpdate().catch((err) => { log.error('downloadUpdate (required) failed:', err); sendToSplash('setStatus("Update failed — opening anyway")'); lastCheckResult.error = err?.message || 'download failed'; emitStatus(); setTimeout(() => resolve(true), 1500); }); } else { // Optional — fall through to open main window. The header // icon will surface the offer inside the app. sendToSplash('setStatus("Update available — continuing")'); setTimeout(() => resolve(true), 400); } }); autoUpdater.on('download-progress', (progress) => { const percent = Math.round(progress.percent || 0); lastCheckResult.progress = percent; lastCheckResult.downloading = true; emitStatus(); if (lastCheckResult.required) { sendToSplash(`setProgress(${percent})`); sendToSplash(`setStatus("Downloading required update... ${percent}%")`); } }); autoUpdater.on('update-downloaded', () => { lastCheckResult.downloading = false; lastCheckResult.downloaded = true; lastCheckResult.progress = 100; emitStatus(); if (lastCheckResult.required) { sendToSplash('setStatus("Installing update...")'); sendToSplash('setProgress(100)'); setTimeout(() => { autoUpdater.quitAndInstall(); }, 1200); } }); autoUpdater.on('update-not-available', () => { lastCheckResult = { hasUpdate: false, required: false, latestVersion: null, currentVersion: autoUpdater.currentVersion?.version ?? null, releaseNotes: null, downloading: false, downloaded: false, progress: 0, error: null, }; emitStatus(); sendToSplash('setStatus("Up to date!")'); sendToSplash('hideProgress()'); setTimeout(() => resolve(true), 500); }); autoUpdater.on('error', (err) => { log.error('Auto-updater error:', err); lastCheckResult.error = err?.message || String(err); lastCheckResult.downloading = false; emitStatus(); sendToSplash('setStatus("Update check failed")'); sendToSplash('hideProgress()'); setTimeout(() => resolve(true), 1500); }); autoUpdater.checkForUpdates().catch((err) => { log.error('checkForUpdates failed:', err); lastCheckResult.error = err?.message || String(err); emitStatus(); sendToSplash('setStatus("Update check failed")'); sendToSplash('hideProgress()'); setTimeout(() => resolve(true), 1500); }); }); } // Start downloading an optional update from the renderer. The // splash flow handles required updates itself so the renderer only // ever reaches here for optional ones. async function downloadAndInstall() { if (!lastCheckResult.hasUpdate) return { ok: false, error: 'no update' }; if (lastCheckResult.downloaded) { autoUpdater.quitAndInstall(); return { ok: true }; } if (lastCheckResult.downloading) return { ok: true }; try { lastCheckResult.downloading = true; lastCheckResult.progress = 0; lastCheckResult.error = null; emitStatus(); await autoUpdater.downloadUpdate(); // The 'update-downloaded' handler bumps status; install triggers // the quit-and-install flow the next tick. autoUpdater.quitAndInstall(); return { ok: true }; } catch (err) { log.error('downloadAndInstall failed:', err); lastCheckResult.downloading = false; lastCheckResult.error = err?.message || String(err); emitStatus(); return { ok: false, error: lastCheckResult.error }; } } module.exports = { checkForUpdates, getStatus, onStatus, downloadAndInstall, };