feat(desktop): wire auto-updater events into recovery store
- All updater events update recoveryStore (drives tray/menu UI) - Native notification fires on update-downloaded when window unfocused - Notification click calls quitAndInstall() directly (force-kill fix) - Preserves existing renderer IPC channels (in-app banner unaffected) - Graceful degradation when electron-updater module unavailable
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
recoveryStore,
|
recoveryStore,
|
||||||
attachRecoveryHandlers,
|
attachRecoveryHandlers,
|
||||||
|
extractErrorCode,
|
||||||
|
setAutoUpdater,
|
||||||
setMainWindow,
|
setMainWindow,
|
||||||
setOnQuitRequested,
|
setOnQuitRequested,
|
||||||
handleRendererReady,
|
handleRendererReady,
|
||||||
@@ -639,49 +641,86 @@ function initAutoUpdater(): void {
|
|||||||
autoUpdater.autoDownload = true;
|
autoUpdater.autoDownload = true;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
// Track whether an update was confirmed to exist — only show error toast
|
setAutoUpdater(autoUpdater);
|
||||||
// if the download fails after we know an update is available. Errors from
|
|
||||||
// the check itself (e.g. private repo, no releases, network) are silent.
|
|
||||||
let updateConfirmed = false;
|
let updateConfirmed = false;
|
||||||
|
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
recoveryStore.update({ updateState: 'checking', lastCheckResult: null });
|
||||||
|
updateConfirmed = false;
|
||||||
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info: { version: string }) => {
|
autoUpdater.on('update-available', (info: { version: string }) => {
|
||||||
updateConfirmed = true;
|
updateConfirmed = true;
|
||||||
|
recoveryStore.update({ updateState: 'downloading', updateVersion: info.version });
|
||||||
mainWindow?.webContents.send('update-available', { version: info.version });
|
mainWindow?.webContents.send('update-available', { version: info.version });
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
updateConfirmed = false;
|
updateConfirmed = false;
|
||||||
|
recoveryStore.update({ updateState: 'idle', lastCheckResult: 'up-to-date' });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (recoveryStore.get().lastCheckResult === 'up-to-date') {
|
||||||
|
recoveryStore.update({ lastCheckResult: null });
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info: { version: string }) => {
|
autoUpdater.on('update-downloaded', (info: { version: string }) => {
|
||||||
mainWindow?.webContents.send('update-downloaded', { version: info.version });
|
const version = info.version.slice(0, 32);
|
||||||
|
recoveryStore.update({ updateState: 'downloaded', updateVersion: version });
|
||||||
|
mainWindow?.webContents.send('update-downloaded', { version });
|
||||||
|
|
||||||
|
// Symmetric focus-based suppression: if the user is looking at the
|
||||||
|
// window, the in-app banner (normal mode) or recovery Restart button
|
||||||
|
// (recovery mode) is visible — no need to also fire a native toast.
|
||||||
|
if (!mainWindow?.isFocused()) {
|
||||||
|
showNotification(
|
||||||
|
'Backspace update ready',
|
||||||
|
`Click to restart and install version ${version}.`,
|
||||||
|
() => autoUpdater.quitAndInstall(),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('error', (err: Error) => {
|
autoUpdater.on('error', (err: unknown) => {
|
||||||
// Only notify renderer if we already confirmed an update exists but the
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
// download/install failed. Check-phase errors (auth, 404, network) are
|
const code = extractErrorCode(err);
|
||||||
// silently ignored — there's nothing actionable for the user.
|
recoveryStore.update({
|
||||||
|
updateState: 'error',
|
||||||
|
lastUpdateError: { message, code, at: Date.now() },
|
||||||
|
lastCheckResult: 'failed',
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
if (recoveryStore.get().lastCheckResult === 'failed') {
|
||||||
|
recoveryStore.update({ lastCheckResult: null });
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
// Existing behavior preserved: only push renderer error IPC after
|
||||||
|
// confirmed update. Check-phase errors stay silent.
|
||||||
if (updateConfirmed) {
|
if (updateConfirmed) {
|
||||||
mainWindow?.webContents.send('update-error', {
|
mainWindow?.webContents.send('update-error', {
|
||||||
message: err.message,
|
message,
|
||||||
releaseUrl: 'https://github.com/TheZwiss/backspace/releases/latest',
|
releaseUrl: 'https://github.com/TheZwiss/backspace/releases/latest',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial check with 10s delay
|
// Initial check with 10s delay (existing behavior)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateConfirmed = false;
|
updateConfirmed = false;
|
||||||
autoUpdater.checkForUpdates().catch(() => {});
|
autoUpdater.checkForUpdates().catch(() => {});
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
// Periodic check every 4 hours
|
// Periodic check every 4 hours (existing behavior)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
updateConfirmed = false;
|
updateConfirmed = false;
|
||||||
autoUpdater.checkForUpdates().catch(() => {});
|
autoUpdater.checkForUpdates().catch(() => {});
|
||||||
}, 4 * 60 * 60 * 1000);
|
}, 4 * 60 * 60 * 1000);
|
||||||
} catch {
|
} catch {
|
||||||
// Graceful degradation — no update URL configured or electron-updater not available
|
// Auto-updater unavailable — recovery surface still functions, just
|
||||||
|
// without update affordances. autoUpdaterRef stays null; recovery action
|
||||||
|
// handlers no-op on update-related actions.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user