Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/main/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,6 +47,11 @@ export async function initializeApp(): Promise<void> {
} 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
Expand Down
1 change: 1 addition & 0 deletions src/main/storage/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
alwaysOnTop: false,
rememberWindowPosition: true,
showNotifications: false,
automaticUpdates: true,
hotkeys: DEFAULT_HOTKEY_SETTINGS,
};

Expand Down
81 changes: 69 additions & 12 deletions src/main/updater/index.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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...');
});
Expand All @@ -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<void> {
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<void> => {
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);
}
}
4 changes: 2 additions & 2 deletions src/renderer/src/components/settings/UpdaterControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ function UpdaterControl(): React.JSX.Element {

{/* Helper Text */}
<p className={classNames(styles.helperText, { [styles.light]: isLight })}>
Updates are automatically downloaded. You&apos;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.
</p>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ export const ApplicationSettings: React.FC<ApplicationSettingsProps> = ({
/>
</SettingItem>

{/* Automatic Updates Setting */}
<SettingItem
label="Automatic Updates"
description="Check for updates in the background when Clipless starts. You'll be prompted to restart when a new version is downloaded."
>
<ToggleSwitch
checked={settings.automaticUpdates ?? true}
onChange={(checked) => onSettingChange('automaticUpdates', checked)}
disabled={saving}
label="Automatic Updates"
/>
</SettingItem>

{/* Theme Setting */}
<SettingItem label="Theme" description="Choose your preferred color scheme" htmlFor="theme">
<select
Expand Down
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface UserSettings {
alwaysOnTop?: boolean;
rememberWindowPosition?: boolean;
showNotifications?: boolean;
automaticUpdates?: boolean;
}

/**
Expand Down
Loading