Add auto-update and display scale features

Integrates electron-updater for automatic app updates, including IPC and renderer APIs for update status and controls. Adds display scale (zoom) setting to settings UI, persists user preference in localStorage, and applies it on startup. Updates OAuth popup allowlist logic and upgrades Electron to v39.2.7.
This commit is contained in:
2025-12-27 22:23:14 +13:00
parent c864ca187c
commit 43ebed0ade
7 changed files with 320 additions and 29 deletions
+149 -15
View File
@@ -1,4 +1,5 @@
const { app, BrowserWindow, ipcMain, session, screen, shell, dialog, Menu, clipboard, webContents } = require('electron');
const { autoUpdater } = require('electron-updater');
const { pathToFileURL } = require('url');
const fs = require('fs');
const path = require('path');
@@ -140,14 +141,28 @@ function createWindow(startUrl) {
perfMarks.browserWindow_instantiated = performance.now();
// Intercept window.open() requests and route them into the existing window as a new tab
// instead of spawning separate BrowserWindows. We still allow a small OAuth allowlist
// (accounts.google.com, login.microsoftonline.com, oauth, sso) to open real popups if
// the flow depends on window.opener relationships. Everything else becomes a new tab.
// instead of spawning separate BrowserWindows. We allow a small list of specific OAuth
// domains to open real popups if the flow depends on window.opener relationships.
// Everything else becomes a new tab.
win.webContents.setWindowOpenHandler((details) => {
const { url } = details;
if (!/^https?:\/\//i.test(url)) return { action: 'deny' };
// OAuth / SSO allowlist heuristic
if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) {
// OAuth / SSO allowlist - only allow specific authentication provider domains
// Be restrictive to prevent normal links from opening in new windows
const oauthDomains = [
'accounts.google.com',
'login.microsoftonline.com',
'appleid.apple.com',
'github.com/login',
'auth0.com',
'okta.com',
'login.live.com',
'facebook.com/dialog',
'api.twitter.com/oauth',
'discord.com/oauth2'
];
const isOAuthDomain = oauthDomains.some(domain => url.toLowerCase().includes(domain.toLowerCase()));
if (isOAuthDomain) {
return { action: 'allow' }; // preserve popup semantics for complex auth flows
}
// Forward to renderer to open as tab
@@ -166,18 +181,31 @@ function createWindow(startUrl) {
// above now governs popup behavior.
// ensure all embedded <webview> tags behave predictably without heavy injections
win.webContents.on('did-attach-webview', (event, webContents) => {
win.webContents.on('did-attach-webview', (event, webviewContents) => {
// Route <webview> window.open() calls to tabs unless OAuth allowlist matched
webContents.setWindowOpenHandler((details) => {
webviewContents.setWindowOpenHandler((details) => {
const { url } = details;
if (!/^https?:\/\//i.test(url)) return { action: 'deny' };
if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) {
// OAuth / SSO allowlist - only allow specific authentication provider domains
const oauthDomains = [
'accounts.google.com',
'login.microsoftonline.com',
'appleid.apple.com',
'github.com/login',
'auth0.com',
'okta.com',
'login.live.com',
'facebook.com/dialog',
'api.twitter.com/oauth',
'discord.com/oauth2'
];
const isOAuthDomain = oauthDomains.some(domain => url.toLowerCase().includes(domain.toLowerCase()));
if (isOAuthDomain) {
return { action: 'allow' }; // keep popup for auth
}
// Send to the owning window (embedder) to open a new tab
// Send to main window's webContents to open a new tab
try {
const host = webContents.hostWebContents || webContents;
host.send('open-url-new-tab', url);
win.webContents.send('open-url-new-tab', url);
} catch {}
return { action: 'deny' };
});
@@ -364,6 +392,61 @@ app.whenReady().then(() => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
// --- Auto-Updater Setup ---
// Configure auto-updater logging
autoUpdater.logger = require('electron-updater').autoUpdater.logger;
if (autoUpdater.logger) autoUpdater.logger.transports.file.level = 'info';
// Check for updates after a short delay to not block startup
setTimeout(() => {
autoUpdater.checkForUpdatesAndNotify().catch(err => {
console.log('[AutoUpdater] Update check failed:', err.message);
});
}, 3000);
// Auto-updater event handlers
autoUpdater.on('checking-for-update', () => {
console.log('[AutoUpdater] Checking for updates...');
broadcastToAll('update-status', { status: 'checking' });
});
autoUpdater.on('update-available', (info) => {
console.log('[AutoUpdater] Update available:', info.version);
broadcastToAll('update-status', { status: 'available', version: info.version });
});
autoUpdater.on('update-not-available', (info) => {
console.log('[AutoUpdater] No update available. Current version:', app.getVersion());
broadcastToAll('update-status', { status: 'not-available', currentVersion: app.getVersion() });
});
autoUpdater.on('download-progress', (progress) => {
console.log(`[AutoUpdater] Download progress: ${progress.percent.toFixed(1)}%`);
broadcastToAll('update-status', { status: 'downloading', progress: progress.percent });
});
autoUpdater.on('update-downloaded', (info) => {
console.log('[AutoUpdater] Update downloaded:', info.version);
broadcastToAll('update-status', { status: 'downloaded', version: info.version });
// Optionally prompt user to restart
dialog.showMessageBox({
type: 'info',
title: 'Update Ready',
message: `Nebula ${info.version} has been downloaded.`,
detail: 'The update will be installed when you restart the app.',
buttons: ['Restart Now', 'Later']
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
autoUpdater.on('error', (err) => {
console.error('[AutoUpdater] Error:', err.message);
broadcastToAll('update-status', { status: 'error', message: err.message });
});
});
// Quit when all windows are closed.
@@ -373,6 +456,33 @@ app.on('window-all-closed', () => {
// ipcMain handlers
// --- Auto-Update IPC handlers ---
ipcMain.handle('check-for-updates', async () => {
try {
const result = await autoUpdater.checkForUpdates();
return { success: true, updateInfo: result?.updateInfo };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('download-update', async () => {
try {
await autoUpdater.downloadUpdate();
return { success: true };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall();
});
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
// --- window control handlers (only registered once now)
ipcMain.handle('window-minimize', event => {
BrowserWindow.fromWebContents(event.sender).minimize();
@@ -560,6 +670,30 @@ ipcMain.handle('zoom-out', event => {
return z;
});
ipcMain.handle('get-display-scale', async (event) => {
// Try to read from localStorage data (user data path)
const userDataPath = app.getPath('userData');
const storageFile = path.join(userDataPath, 'localStorage');
try {
// Try to get from electron store or persistent storage
// For now, we'll just return a default and let the app set it
// The display scale is stored in localStorage on the client side
return 100; // Default to 100%
} catch (err) {
return 100; // Default to 100%
}
});
ipcMain.handle('set-zoom-factor', (event, zoomFactor) => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents;
if (wc && typeof wc.setZoomFactor === 'function') {
wc.setZoomFactor(zoomFactor);
return true;
}
return false;
});
// allow renderer to pop a tab into its own window
ipcMain.handle('open-tab-in-new-window', (event, url) => {
createWindow(url);
@@ -750,7 +884,7 @@ ipcMain.handle('get-electron-versions', async (event, buildType = 'stable') => {
});
ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => {
const { execFile } = require('child_process');
const { exec } = require('child_process');
const packageName = buildType === 'nightly' ? 'electron-nightly' : 'electron';
return new Promise((resolve) => {
@@ -758,10 +892,10 @@ ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => {
const otherPackage = buildType === 'nightly' ? 'electron' : 'electron-nightly';
// Run npm install to upgrade the package
const args = ['install', '--save-dev', packageName + '@latest'];
const command = `npm install --save-dev ${packageName}@latest`;
execFile('npm', args,
{ cwd: __dirname, shell: true, maxBuffer: 10 * 1024 * 1024 },
exec(command,
{ cwd: __dirname, maxBuffer: 10 * 1024 * 1024 },
(error, stdout, stderr) => {
if (error) {
console.error('Upgrade failed:', error);