diff --git a/package-lock.json b/package-lock.json index 958b66f..3dc42b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "clipless", - "version": "1.7.6", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clipless", - "version": "1.7.6", + "version": "1.8.0", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.1", diff --git a/package.json b/package.json index a87caad..74529b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clipless", - "version": "1.7.6", + "version": "1.8.0", "description": "An Electron application with React and TypeScript", "main": "./out/main/index.js", "author": "Daniel Essig", diff --git a/src/main/app/index.ts b/src/main/app/index.ts index 1b20e03..56dc9f6 100644 --- a/src/main/app/index.ts +++ b/src/main/app/index.ts @@ -3,7 +3,7 @@ import { electronApp, optimizer } from '@electron-toolkit/utils'; import { storage } from '../storage'; import { hotkeyManager } from '../hotkeys'; import { getTray, setIsQuitting } from '../tray'; -import { configureAutoUpdater, setupAutoUpdaterEvents } from '../updater'; +import { configureAutoUpdater, setupAutoUpdaterEvents, runAutomaticUpdateCheck } from '../updater'; import { setupMainIPC } from '../ipc'; import { initializeWindowSystem, getMainWindow, getWindowBounds } from '../window'; import { applyWindowSettings } from '../window/settings'; @@ -47,6 +47,11 @@ export async function initializeApp(): Promise { } catch (error) { console.error('Failed to apply auto-start setting on startup:', error); } + + // Silently check for updates in the background. Errors are swallowed + // inside the function so unsupported platforms (e.g. unsigned macOS + // builds) never surface failures to the user. + runAutomaticUpdateCheck(getMainWindow()); }); // Apply window bounds if available after initialization diff --git a/src/main/storage/defaults.ts b/src/main/storage/defaults.ts index 146a1b2..2cd999a 100644 --- a/src/main/storage/defaults.ts +++ b/src/main/storage/defaults.ts @@ -48,6 +48,7 @@ export const DEFAULT_SETTINGS: UserSettings = { alwaysOnTop: false, rememberWindowPosition: true, showNotifications: false, + automaticUpdates: true, hotkeys: DEFAULT_HOTKEY_SETTINGS, }; diff --git a/src/main/updater/index.ts b/src/main/updater/index.ts index 2003e1e..fbba99f 100644 --- a/src/main/updater/index.ts +++ b/src/main/updater/index.ts @@ -1,5 +1,7 @@ import { autoUpdater, type UpdateInfo } from 'electron-updater'; import { is } from '@electron-toolkit/utils'; +import { dialog, type BrowserWindow } from 'electron'; +import { storage } from '../storage'; // Helper function to check for updates with timeout and retry export async function checkForUpdatesWithRetry( @@ -67,20 +69,19 @@ export async function checkForUpdatesWithRetry( export function configureAutoUpdater(): void { if (!is.dev) { - // Configure auto-updater settings - autoUpdater.autoDownload = false; // Don't auto-download, let user control it - autoUpdater.autoInstallOnAppQuit = false; // Don't auto-install on quit - - // Delay auto-updater check to not block startup - // Check for updates 10 seconds after startup to avoid blocking UI - setTimeout(() => { - autoUpdater.checkForUpdatesAndNotify(); - }, 10000); + // Manual flow (UpdaterControl) controls its own download; automatic flow + // sets autoDownload = true just before invoking the check. + autoUpdater.autoDownload = false; + // If an update is downloaded and the user defers the restart, install it + // automatically the next time they quit. + autoUpdater.autoInstallOnAppQuit = true; } } export function setupAutoUpdaterEvents(): void { - // Auto-updater events + // Auto-updater events — logging only. Lifecycle decisions (download, + // install/restart) are owned by the manual UpdaterControl flow and by + // runAutomaticUpdateCheck so the user is never restarted without consent. autoUpdater.on('checking-for-update', () => { console.log('Checking for update...'); }); @@ -103,7 +104,63 @@ export function setupAutoUpdaterEvents(): void { autoUpdater.on('update-downloaded', (info) => { console.log('Update downloaded:', info); - // Auto-install and restart - autoUpdater.quitAndInstall(); }); } + +// Runs at app startup: silently checks for an update and, if one is available, +// downloads it and shows a native dialog asking the user to restart now or +// later. All failures are swallowed silently so unsupported platforms (e.g. +// unsigned macOS builds) never surface errors to the user. +export async function runAutomaticUpdateCheck(parentWindow: BrowserWindow | null): Promise { + if (is.dev) return; + + let enabled = true; + try { + const settings = await storage.getSettings(); + enabled = settings.automaticUpdates ?? true; + } catch { + return; + } + if (!enabled) return; + + const onError = (): void => { + autoUpdater.off('update-downloaded', onDownloaded); + autoUpdater.off('error', onError); + }; + + const onDownloaded = async (): Promise => { + autoUpdater.off('update-downloaded', onDownloaded); + autoUpdater.off('error', onError); + const options: Electron.MessageBoxOptions = { + type: 'info', + buttons: ['Restart Now', 'Later'], + defaultId: 0, + cancelId: 1, + title: 'Update Ready', + message: 'Clipless has been updated.', + detail: 'Restart now to use the new version, or wait until the next time you quit.', + }; + try { + const promise = parentWindow + ? dialog.showMessageBox(parentWindow, options) + : dialog.showMessageBox(options); + const { response } = await promise; + if (response === 0) { + autoUpdater.quitAndInstall(); + } + } catch { + // Silent: never bother the user with auto-update errors. + } + }; + + autoUpdater.once('update-downloaded', onDownloaded); + autoUpdater.once('error', onError); + + try { + autoUpdater.autoDownload = true; + await autoUpdater.checkForUpdates(); + } catch { + autoUpdater.off('update-downloaded', onDownloaded); + autoUpdater.off('error', onError); + } +} diff --git a/src/renderer/src/components/settings/UpdaterControl.tsx b/src/renderer/src/components/settings/UpdaterControl.tsx index d9b92b3..6375a05 100644 --- a/src/renderer/src/components/settings/UpdaterControl.tsx +++ b/src/renderer/src/components/settings/UpdaterControl.tsx @@ -114,8 +114,8 @@ function UpdaterControl(): React.JSX.Element { {/* Helper Text */}

- Updates are automatically downloaded. You'll be notified when a restart is required to - complete the installation. + Updates are checked automatically when Clipless starts (configurable in Application + Settings). Use this button to check manually.

); diff --git a/src/renderer/src/components/settings/usersettings/ApplicationSettings.test.tsx b/src/renderer/src/components/settings/usersettings/ApplicationSettings.test.tsx index 10fc044..65e5078 100644 --- a/src/renderer/src/components/settings/usersettings/ApplicationSettings.test.tsx +++ b/src/renderer/src/components/settings/usersettings/ApplicationSettings.test.tsx @@ -152,6 +152,7 @@ describe('ApplicationSettings - other settings', () => { expect(screen.getByRole('combobox')).toBeDisabled(); expect(screen.getByRole('checkbox', { name: 'Show Notifications' })).toBeDisabled(); expect(screen.getByRole('checkbox', { name: 'Code Detection & Highlighting' })).toBeDisabled(); + expect(screen.getByRole('checkbox', { name: 'Automatic Updates' })).toBeDisabled(); }); it('forwards Start Minimized toggle changes', () => { @@ -193,4 +194,17 @@ describe('ApplicationSettings - other settings', () => { expect(screen.getByRole('checkbox', { name: 'Show Notifications' })).not.toBeChecked(); expect(screen.getByRole('checkbox', { name: 'Code Detection & Highlighting' })).toBeChecked(); }); + + it('renders Automatic Updates toggle defaulted on when undefined', () => { + renderComponent({ + overrides: { automaticUpdates: undefined as unknown as boolean }, + }); + expect(screen.getByRole('checkbox', { name: 'Automatic Updates' })).toBeChecked(); + }); + + it('forwards Automatic Updates toggle changes', () => { + const { onSettingChange } = renderComponent({ overrides: { automaticUpdates: true } }); + fireEvent.click(screen.getByRole('checkbox', { name: 'Automatic Updates' })); + expect(onSettingChange).toHaveBeenCalledWith('automaticUpdates', false); + }); }); diff --git a/src/renderer/src/components/settings/usersettings/ApplicationSettings.tsx b/src/renderer/src/components/settings/usersettings/ApplicationSettings.tsx index b4d4003..ba0bda7 100644 --- a/src/renderer/src/components/settings/usersettings/ApplicationSettings.tsx +++ b/src/renderer/src/components/settings/usersettings/ApplicationSettings.tsx @@ -97,6 +97,19 @@ export const ApplicationSettings: React.FC = ({ /> + {/* Automatic Updates Setting */} + + onSettingChange('automaticUpdates', checked)} + disabled={saving} + label="Automatic Updates" + /> + + {/* Theme Setting */}