Skip to content

Commit 1a29b99

Browse files
committed
updates smooth
1 parent 37eb313 commit 1a29b99

7 files changed

Lines changed: 215 additions & 169 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "restbro",
3-
"version": "1.0.9",
3+
"version": "1.0.10",
44
"description": "A modern API testing tool similar to Postman and Insomnia",
55
"main": "dist/main/main/index.js",
66
"scripts": {

src/main/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class ApiCourierApp {
2525
ipcManager.initialize();
2626
updateManager.initialize();
2727
this.createWindow();
28+
updateManager.notifyIfJustUpdated();
2829
this.setupEventHandlers();
2930
}
3031

@@ -68,6 +69,10 @@ class ApiCourierApp {
6869
await storeManager.flush();
6970
storeManager.stopAutoBackup();
7071
await aiEngine.flush();
72+
// If an update was downloaded, install it on quit so next launch is updated
73+
if (updateManager.isUpdateReady()) {
74+
updateManager.installOnQuitIfReady();
75+
}
7176
updateManager.destroy();
7277
console.log('Database flushed successfully');
7378
}

src/main/modules/update-manager.ts

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { autoUpdater, UpdateInfo } from 'electron-updater';
22
import { app, BrowserWindow } from 'electron';
3+
import { join } from 'path';
4+
import { existsSync, readFileSync, writeFileSync } from 'fs';
35

46
/**
57
* UpdateManager — wraps electron-updater with safe defaults.
68
*
79
* - Only runs in packaged (production) builds; no-ops in dev mode.
810
* - Checks for updates immediately on app start and again every 4 hours.
9-
* - Downloads updates silently; prompts the user before installing.
10-
* - Forwards progress/status to the renderer via the focused window so the
11-
* UI can surface a non-intrusive update badge.
11+
* - Downloads updates completely silently in the background.
12+
* - Only notifies renderer when download is complete (header button).
13+
* - Auto-installs pending updates on quit.
14+
* - Detects post-update launch and notifies renderer to show a popup.
1215
*/
1316
class UpdateManager {
1417
private checkInterval: NodeJS.Timeout | null = null;
18+
private updateReady = false;
1519
private readonly CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
1620

1721
initialize(): void {
@@ -20,7 +24,6 @@ class UpdateManager {
2024
return;
2125
}
2226

23-
// Disable auto-downloading; we want to show a prompt first on macOS.
2427
autoUpdater.autoDownload = false;
2528
autoUpdater.autoInstallOnAppQuit = true;
2629

@@ -34,9 +37,66 @@ class UpdateManager {
3437
);
3538
}
3639

40+
/**
41+
* Check if the app was just updated by comparing current version to
42+
* the last stored version. Sends 'update:just-updated' to the renderer
43+
* if a version change is detected.
44+
*/
45+
notifyIfJustUpdated(): void {
46+
const currentVersion = app.getVersion();
47+
const lastVersionPath = join(app.getPath('userData'), '.last-version');
48+
49+
let justUpdated = false;
50+
if (existsSync(lastVersionPath)) {
51+
try {
52+
const lastVersion = readFileSync(lastVersionPath, 'utf-8').trim();
53+
if (lastVersion && lastVersion !== currentVersion) {
54+
justUpdated = true;
55+
}
56+
} catch {
57+
// ignore read errors
58+
}
59+
}
60+
61+
writeFileSync(lastVersionPath, currentVersion, 'utf-8');
62+
63+
if (justUpdated) {
64+
// Delay slightly so the renderer is ready to receive the event
65+
setTimeout(() => {
66+
this.send('update:just-updated', { version: currentVersion });
67+
}, 2000);
68+
}
69+
}
70+
71+
/** Returns true if an update has been downloaded and is ready to install. */
72+
isUpdateReady(): boolean {
73+
return this.updateReady;
74+
}
75+
76+
/** Installs the pending update and restarts the app. */
77+
installAndRestart(): void {
78+
autoUpdater.quitAndInstall(false, true);
79+
}
80+
81+
/**
82+
* Called during graceful shutdown. If an update is downloaded,
83+
* install it so the next launch gets the new version.
84+
*/
85+
installOnQuitIfReady(): void {
86+
if (this.updateReady) {
87+
autoUpdater.quitAndInstall(true, true);
88+
}
89+
}
90+
91+
destroy(): void {
92+
if (this.checkInterval) {
93+
clearInterval(this.checkInterval);
94+
this.checkInterval = null;
95+
}
96+
}
97+
3798
private checkForUpdates(): void {
3899
autoUpdater.checkForUpdates().catch((err) => {
39-
// Update checks failing (e.g. no network) is non-fatal — log silently.
40100
console.error('[updater] Update check failed:', err?.message ?? err);
41101
});
42102
}
@@ -52,10 +112,7 @@ class UpdateManager {
52112
private registerListeners(): void {
53113
autoUpdater.on('update-available', (info: UpdateInfo) => {
54114
console.log(`[updater] Update available: ${info.version}`);
55-
this.send('update:available', { version: info.version });
56-
57-
// Ask the user before downloading via the renderer (or auto-download here).
58-
// For now we trigger download immediately and let the renderer show progress.
115+
// Download silently — no UI shown to user
59116
autoUpdater.downloadUpdate().catch((err) => {
60117
console.error('[updater] Download failed:', err?.message ?? err);
61118
});
@@ -66,37 +123,21 @@ class UpdateManager {
66123
});
67124

68125
autoUpdater.on('download-progress', (progress) => {
69-
this.send('update:download-progress', {
70-
percent: Math.round(progress.percent),
71-
bytesPerSecond: progress.bytesPerSecond,
72-
transferred: progress.transferred,
73-
total: progress.total,
74-
});
126+
// Silent — no UI shown to user
127+
console.log(`[updater] Download progress: ${Math.round(progress.percent)}%`);
75128
});
76129

77130
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
78131
console.log(`[updater] Update downloaded: ${info.version}`);
79-
// Notify the renderer so it can show an "Install & restart" prompt.
132+
this.updateReady = true;
133+
// Notify the renderer to show the header restart button
80134
this.send('update:downloaded', { version: info.version });
81135
});
82136

83137
autoUpdater.on('error', (err: Error) => {
84138
console.error('[updater] Error:', err?.message ?? err);
85-
this.send('update:error', { message: err?.message ?? String(err) });
86139
});
87140
}
88-
89-
/** Called by the renderer (via IPC) when the user accepts the update. */
90-
installAndRestart(): void {
91-
autoUpdater.quitAndInstall(false, true);
92-
}
93-
94-
destroy(): void {
95-
if (this.checkInterval) {
96-
clearInterval(this.checkInterval);
97-
this.checkInterval = null;
98-
}
99-
}
100141
}
101142

102143
export const updateManager = new UpdateManager();

src/preload/index.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,8 @@ const IPC_CHANNELS = {
8282

8383
// Auto-updater channels
8484
UPDATE_INSTALL: 'update:install',
85-
UPDATE_AVAILABLE: 'update:available',
86-
UPDATE_DOWNLOAD_PROGRESS: 'update:download-progress',
8785
UPDATE_DOWNLOADED: 'update:downloaded',
88-
UPDATE_ERROR: 'update:error',
86+
UPDATE_JUST_UPDATED: 'update:just-updated',
8987
} as const;
9088

9189
// Define types inline to avoid import issues
@@ -630,41 +628,23 @@ const apiCourierAPI = {
630628
update: {
631629
install: (): Promise<void> =>
632630
ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL),
633-
onAvailable: (
631+
onDownloaded: (
634632
callback: (data: { version: string }) => void
635633
): (() => void) => {
636-
ipcRenderer.on(IPC_CHANNELS.UPDATE_AVAILABLE, (_, data) =>
637-
callback(data)
638-
);
639-
return () =>
640-
ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_AVAILABLE);
641-
},
642-
onDownloadProgress: (
643-
callback: (data: {
644-
percent: number;
645-
bytesPerSecond: number;
646-
transferred: number;
647-
total: number;
648-
}) => void
649-
): (() => void) => {
650-
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOAD_PROGRESS, (_, data) =>
634+
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, (_, data) =>
651635
callback(data)
652636
);
653637
return () =>
654-
ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_DOWNLOAD_PROGRESS);
638+
ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_DOWNLOADED);
655639
},
656-
onDownloaded: (
640+
onJustUpdated: (
657641
callback: (data: { version: string }) => void
658642
): (() => void) => {
659-
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, (_, data) =>
643+
ipcRenderer.on(IPC_CHANNELS.UPDATE_JUST_UPDATED, (_, data) =>
660644
callback(data)
661645
);
662646
return () =>
663-
ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_DOWNLOADED);
664-
},
665-
onError: (callback: (data: { message: string }) => void): (() => void) => {
666-
ipcRenderer.on(IPC_CHANNELS.UPDATE_ERROR, (_, data) => callback(data));
667-
return () => ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_ERROR);
647+
ipcRenderer.removeAllListeners(IPC_CHANNELS.UPDATE_JUST_UPDATED);
668648
},
669649
},
670650
};

src/renderer/components/update-notification-manager.ts

Lines changed: 51 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,88 @@
11
/**
22
* UpdateNotificationManager
33
*
4-
* Listens for auto-update events from the main process and shows
5-
* a non-intrusive banner at the bottom of the app.
6-
*
7-
* States: hidden → "downloading vX.Y.Z" → "ready to install" → (user clicks) → restart
4+
* - When an update is downloaded, shows a header button "Updates installed, please restart"
5+
* styled like the Time Machine / Theme buttons.
6+
* - On post-update launch, shows a centered popup: "RestBro is now up to date" + version.
87
*/
98
export class UpdateNotificationManager {
10-
private banner: HTMLElement | null = null;
119
private cleanups: (() => void)[] = [];
1210

1311
initialize(): void {
1412
const api = window.apiCourier?.update;
1513
if (!api) return;
1614

17-
this.cleanups.push(
18-
api.onAvailable(({ version }) => {
19-
this.showBanner('downloading', version);
20-
})
21-
);
22-
23-
this.cleanups.push(
24-
api.onDownloadProgress(({ percent }) => {
25-
this.updateProgress(percent);
26-
})
27-
);
28-
2915
this.cleanups.push(
3016
api.onDownloaded(({ version }) => {
31-
this.showBanner('ready', version);
17+
this.showHeaderButton(version);
3218
})
3319
);
3420

3521
this.cleanups.push(
36-
api.onError(() => {
37-
this.hideBanner();
22+
api.onJustUpdated(({ version }) => {
23+
this.showUpdatedPopup(version);
3824
})
3925
);
4026
}
4127

4228
destroy(): void {
4329
this.cleanups.forEach((fn) => fn());
4430
this.cleanups = [];
45-
this.hideBanner();
4631
}
4732

48-
private showBanner(state: 'downloading' | 'ready', version: string): void {
49-
this.ensureBanner();
50-
if (!this.banner) return;
33+
private showHeaderButton(version: string): void {
34+
// Don't add duplicate
35+
if (document.getElementById('update-restart-btn')) return;
5136

52-
this.banner.className = 'update-banner';
53-
this.banner.classList.add(`update-banner--${state}`);
37+
const headerRight = document.querySelector('.header-right');
38+
if (!headerRight) return;
5439

55-
if (state === 'downloading') {
56-
this.banner.innerHTML = `
57-
<span class="update-banner__text">Downloading update v${this.escapeHtml(version)}…</span>
58-
<span class="update-banner__progress">0%</span>
59-
`;
60-
} else {
61-
this.banner.innerHTML = `
62-
<span class="update-banner__text">RestBro v${this.escapeHtml(version)} is ready</span>
63-
<button class="update-banner__action" id="update-install-btn">Restart to update</button>
64-
<button class="update-banner__dismiss" id="update-dismiss-btn" title="Dismiss">✕</button>
65-
`;
66-
document
67-
.getElementById('update-install-btn')
68-
?.addEventListener('click', () => {
69-
window.apiCourier.update.install();
70-
});
71-
document
72-
.getElementById('update-dismiss-btn')
73-
?.addEventListener('click', () => {
74-
this.hideBanner();
75-
});
76-
}
40+
const btn = document.createElement('button');
41+
btn.id = 'update-restart-btn';
42+
btn.className = 'update-restart-button';
43+
btn.title = `RestBro v${this.escapeHtml(version)} is ready to install`;
44+
btn.textContent = 'Updates installed, please restart';
45+
btn.addEventListener('click', () => {
46+
window.apiCourier.update.install();
47+
});
7748

78-
this.banner.style.display = 'flex';
49+
// Insert before the first child (left of Time Machine)
50+
headerRight.insertBefore(btn, headerRight.firstChild);
7951
}
8052

81-
private updateProgress(percent: number): void {
82-
if (!this.banner) return;
83-
const el = this.banner.querySelector('.update-banner__progress');
84-
if (el) el.textContent = `${Math.round(percent)}%`;
85-
}
53+
private showUpdatedPopup(version: string): void {
54+
const overlay = document.createElement('div');
55+
overlay.className = 'update-popup-overlay';
8656

87-
private hideBanner(): void {
88-
if (this.banner) this.banner.style.display = 'none';
89-
}
57+
const popup = document.createElement('div');
58+
popup.className = 'update-popup';
59+
popup.innerHTML = `
60+
<div class="update-popup__title">RestBro is now up to date</div>
61+
<div class="update-popup__version">v${this.escapeHtml(version)}</div>
62+
<button class="update-popup__ok" id="update-popup-ok">Okay</button>
63+
`;
64+
65+
overlay.appendChild(popup);
66+
document.body.appendChild(overlay);
67+
68+
// Trigger reflow then add visible class for animation
69+
requestAnimationFrame(() => {
70+
overlay.classList.add('update-popup-overlay--visible');
71+
});
9072

91-
private ensureBanner(): void {
92-
if (this.banner) return;
73+
const dismiss = () => {
74+
overlay.classList.remove('update-popup-overlay--visible');
75+
overlay.addEventListener('transitionend', () => overlay.remove(), {
76+
once: true,
77+
});
78+
};
9379

94-
this.banner = document.createElement('div');
95-
this.banner.className = 'update-banner';
96-
this.banner.style.display = 'none';
97-
document.body.appendChild(this.banner);
80+
popup
81+
.querySelector('#update-popup-ok')
82+
?.addEventListener('click', dismiss);
83+
overlay.addEventListener('click', (e) => {
84+
if (e.target === overlay) dismiss();
85+
});
9886
}
9987

10088
private escapeHtml(str: string): string {

0 commit comments

Comments
 (0)