Skip to content

Commit 9a8a819

Browse files
authored
Merge pull request #41 from t3rr11/bugfix/fix-macos-updater-failing
Fix MacOS updater failing silently
2 parents ea9c5d4 + 29bfaae commit 9a8a819

8 files changed

Lines changed: 115 additions & 32 deletions

File tree

apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "requesto-electron",
3-
"version": "1.5.3",
3+
"version": "1.5.4",
44
"description": "Requesto - API Client",
55
"homepage": "https://requesto.com.au",
66
"main": "dist/main.js",

apps/electron/src/ipcHandlers.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcMain, dialog, BrowserWindow } from 'electron';
1+
import { ipcMain, dialog, shell, BrowserWindow } from 'electron';
22
import { autoUpdater } from 'electron-updater';
33
import { SIMULATE_UPDATE_AVAILABLE } from './constants';
44
import { state } from './state';
@@ -40,15 +40,43 @@ export function registerIpcHandlers(): void {
4040
return autoUpdater.downloadUpdate();
4141
});
4242

43-
ipcMain.handle('update:install', () => {
44-
if (SIMULATE_UPDATE_AVAILABLE) return;
43+
ipcMain.handle('update:install', async () => {
44+
if (process.platform === 'darwin') {
45+
// macOS requires code signing for silent in-place updates. Since this app
46+
// is unsigned, quitAndInstall() will silently fail. Instead, show the user
47+
// where the downloaded file is and direct them to install it manually.
48+
const downloadedFile = state.downloadedUpdatePath;
49+
const buttons = downloadedFile
50+
? ['Show in Finder', 'Open Releases Page', 'Cancel']
51+
: ['Open Releases Page', 'Cancel'];
52+
53+
const { response } = await dialog.showMessageBox({
54+
type: 'info',
55+
title: 'Manual Installation Required',
56+
message: 'Automatic updates require the app to be signed.',
57+
detail: downloadedFile
58+
? 'The update has been downloaded. Open it in Finder, drag the new Requesto.app into your Applications folder, and relaunch.'
59+
: 'Please download the latest version from the releases page and drag it into your Applications folder.',
60+
buttons,
61+
defaultId: 0,
62+
cancelId: buttons.length - 1,
63+
});
64+
65+
if (downloadedFile && response === 0) {
66+
shell.showItemInFolder(downloadedFile);
67+
} else if (response === (downloadedFile ? 1 : 0)) {
68+
shell.openExternal('https://github.com/t3rr11/Requesto/releases/latest');
69+
}
70+
return;
71+
}
72+
4573
autoUpdater.quitAndInstall();
4674
});
4775

4876
// OAuth: open a child BrowserWindow for the OAuth flow, intercept the redirect
4977
// back to our callback URL, and return the full callback URL to the renderer.
5078
ipcMain.handle('oauth:open-window', (_event, authUrl: string, callbackUrlPrefix: string): Promise<string | null> => {
51-
return new Promise<string | null>((resolve) => {
79+
return new Promise<string | null>(resolve => {
5280
const oauthWindow = new BrowserWindow({
5381
width: 520,
5482
height: 720,

apps/electron/src/main.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ function setupAutoUpdater(): void {
1313
autoUpdater.autoDownload = false;
1414
autoUpdater.autoInstallOnAppQuit = false;
1515

16-
autoUpdater.on('update-available', (info) => {
16+
autoUpdater.on('update-available', info => {
1717
state.mainWindow?.webContents.send('update:available', {
1818
version: info.version,
1919
releaseNotes: info.releaseNotes ?? null,
2020
});
2121
});
2222

23-
autoUpdater.on('download-progress', (progress) => {
23+
autoUpdater.on('download-progress', progress => {
2424
state.mainWindow?.webContents.send('update:progress', {
2525
percent: progress.percent,
2626
bytesPerSecond: progress.bytesPerSecond,
@@ -29,11 +29,12 @@ function setupAutoUpdater(): void {
2929
});
3030
});
3131

32-
autoUpdater.on('update-downloaded', () => {
32+
autoUpdater.on('update-downloaded', info => {
33+
state.downloadedUpdatePath = info.downloadedFile ?? null;
3334
state.mainWindow?.webContents.send('update:downloaded');
3435
});
3536

36-
autoUpdater.on('error', (err) => {
37+
autoUpdater.on('error', err => {
3738
state.mainWindow?.webContents.send('update:error', err.message);
3839
});
3940
}
@@ -84,7 +85,7 @@ app.whenReady().then(async () => {
8485
});
8586
}, 2000);
8687
} else {
87-
autoUpdater.checkForUpdates().catch((err) => {
88+
autoUpdater.checkForUpdates().catch(err => {
8889
console.error('Update check failed:', err);
8990
});
9091
}

apps/electron/src/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ export const state: {
66
splashWindow: BrowserWindow | null;
77
backendProcess: ChildProcess | null;
88
isQuitting: boolean;
9+
downloadedUpdatePath: string | null;
910
} = {
1011
mainWindow: null,
1112
splashWindow: null,
1213
backendProcess: null,
1314
isQuitting: false,
15+
downloadedUpdatePath: null,
1416
};

apps/frontend/src/App.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,22 @@ function App() {
2020
const { loadWorkspaces } = useWorkspaceStore();
2121
const { checkGit } = useGitStore();
2222
const { isDarkMode } = useThemeStore();
23-
const { isOpen: alertOpen, title: alertTitle, message: alertMessage, variant: alertVariant, closeAlert } = useAlertStore();
24-
const { dialogOpen: updateDialogOpen, setAvailable, setDownloading, setProgress, setDownloaded, setError, setDialogOpen } = useUpdateStore();
23+
const {
24+
isOpen: alertOpen,
25+
title: alertTitle,
26+
message: alertMessage,
27+
variant: alertVariant,
28+
closeAlert,
29+
} = useAlertStore();
30+
const {
31+
dialogOpen: updateDialogOpen,
32+
setAvailable,
33+
setDownloading,
34+
setProgress,
35+
setDownloaded,
36+
setError,
37+
setDialogOpen,
38+
} = useUpdateStore();
2539

2640
useEffect(() => {
2741
loadWorkspaces().then(() => {
@@ -50,10 +64,19 @@ function App() {
5064
useEffect(() => {
5165
const api = window.electronAPI?.update;
5266
if (!api) return;
53-
const unsubAvailable = api.onAvailable((info) => { setAvailable(info); setDialogOpen(true); });
54-
const unsubProgress = api.onProgress((p) => { setDownloading(); setProgress(p); });
67+
const unsubAvailable = api.onAvailable(info => {
68+
setAvailable(info);
69+
const dismissed = localStorage.getItem('update-dismissed-version');
70+
if (dismissed !== info.version) {
71+
setDialogOpen(true);
72+
}
73+
});
74+
const unsubProgress = api.onProgress(p => {
75+
setDownloading();
76+
setProgress(p);
77+
});
5578
const unsubDownloaded = api.onDownloaded(() => setDownloaded());
56-
const unsubError = api.onError((msg) => setError(msg));
79+
const unsubError = api.onError(msg => setError(msg));
5780
return () => {
5881
unsubAvailable();
5982
unsubProgress();

apps/frontend/src/components/UpdateDialog.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ function formatBytes(bytes: number): string {
1515
export function UpdateDialog({ isOpen, onClose }: UpdateDialogProps) {
1616
const { status, version, releaseNotes, progress, errorMessage, setDownloading, setError } = useUpdateStore();
1717

18+
function handleClose() {
19+
if (status === 'available' && version) {
20+
localStorage.setItem('update-dismissed-version', version);
21+
}
22+
onClose();
23+
}
24+
1825
async function handleDownload() {
1926
setDownloading();
2027
try {
@@ -30,21 +37,29 @@ export function UpdateDialog({ isOpen, onClose }: UpdateDialogProps) {
3037
}
3138

3239
return (
33-
<Dialog isOpen={isOpen} onClose={onClose} title="Update Available" size="sm">
40+
<Dialog isOpen={isOpen} onClose={handleClose} title="Update Available" size="sm">
3441
{status === 'available' && (
3542
<>
3643
<p className="text-sm text-gray-600 dark:text-gray-300">
3744
Version <span className="font-semibold text-gray-900 dark:text-gray-100">{version}</span> is available.
3845
</p>
3946
{releaseNotes && (
4047
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg max-h-40 overflow-y-auto">
41-
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wide mb-1">Release Notes</p>
42-
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{releaseNotes}</p>
48+
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wide mb-1">
49+
Release Notes
50+
</p>
51+
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
52+
{releaseNotes.replace(/<[^>]+>/g, '').trim()}
53+
</p>
4354
</div>
4455
)}
4556
<div className="flex justify-end gap-2 mt-5">
46-
<Button onClick={onClose} variant="secondary" size="md">Later</Button>
47-
<Button onClick={handleDownload} variant="primary" size="md">Download Update</Button>
57+
<Button onClick={handleClose} variant="secondary" size="md">
58+
Later
59+
</Button>
60+
<Button onClick={handleDownload} variant="primary" size="md">
61+
Download Update
62+
</Button>
4863
</div>
4964
</>
5065
)}
@@ -60,23 +75,31 @@ export function UpdateDialog({ isOpen, onClose }: UpdateDialogProps) {
6075
</div>
6176
{progress && (
6277
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
63-
{formatBytes(progress.transferred)} / {formatBytes(progress.total)} &mdash; {formatBytes(progress.bytesPerSecond)}/s
78+
{formatBytes(progress.transferred)} / {formatBytes(progress.total)} &mdash;{' '}
79+
{formatBytes(progress.bytesPerSecond)}/s
6480
</p>
6581
)}
6682
<div className="flex justify-end mt-5">
67-
<Button disabled variant="primary" size="md">Downloading…</Button>
83+
<Button disabled variant="primary" size="md">
84+
Downloading…
85+
</Button>
6886
</div>
6987
</>
7088
)}
7189

7290
{status === 'downloaded' && (
7391
<>
7492
<p className="text-sm text-gray-600 dark:text-gray-300">
75-
Version <span className="font-semibold text-gray-900 dark:text-gray-100">{version}</span> is ready to install. The app will restart automatically.
93+
Version <span className="font-semibold text-gray-900 dark:text-gray-100">{version}</span> is ready to
94+
install. The app will restart automatically.
7695
</p>
7796
<div className="flex justify-end gap-2 mt-5">
78-
<Button onClick={onClose} variant="secondary" size="md">Later</Button>
79-
<Button onClick={handleInstall} variant="primary" size="md">Restart &amp; Install</Button>
97+
<Button onClick={onClose} variant="secondary" size="md">
98+
Later
99+
</Button>
100+
<Button onClick={handleInstall} variant="primary" size="md">
101+
Restart &amp; Install
102+
</Button>
80103
</div>
81104
</>
82105
)}
@@ -85,11 +108,17 @@ export function UpdateDialog({ isOpen, onClose }: UpdateDialogProps) {
85108
<>
86109
<p className="text-sm text-gray-600 dark:text-gray-300 mb-1">The update could not be downloaded.</p>
87110
{errorMessage && (
88-
<p className="text-xs text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded p-2">{errorMessage}</p>
111+
<p className="text-xs text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded p-2">
112+
{errorMessage}
113+
</p>
89114
)}
90115
<div className="flex justify-end gap-2 mt-5">
91-
<Button onClick={onClose} variant="secondary" size="md">Close</Button>
92-
<Button onClick={handleDownload} variant="primary" size="md">Retry</Button>
116+
<Button onClick={onClose} variant="secondary" size="md">
117+
Close
118+
</Button>
119+
<Button onClick={handleDownload} variant="primary" size="md">
120+
Retry
121+
</Button>
93122
</div>
94123
</>
95124
)}

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "requesto",
3-
"version": "1.5.3",
3+
"version": "1.5.4",
44
"author": "Matthew Allen",
55
"description": "A lightweight, self-hosted API client",
66
"private": true,

0 commit comments

Comments
 (0)