Skip to content

Commit fa540d0

Browse files
authored
feat: check for updates (#233)
1 parent 30088ba commit fa540d0

File tree

6 files changed

+321
-70
lines changed

6 files changed

+321
-70
lines changed

apps/array/src/main/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ function createWindow(): void {
117117
submenu: [
118118
{ role: "about" },
119119
{ type: "separator" },
120+
{
121+
label: "Check for Updates...",
122+
click: () => {
123+
mainWindow?.webContents.send("check-for-updates-menu");
124+
},
125+
},
126+
{ type: "separator" },
120127
{
121128
label: "Settings...",
122129
accelerator: "CmdOrCtrl+,",

apps/array/src/main/preload.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,18 @@ contextBridge.exposeInMainWorld("electronAPI", {
322322
createVoidIpcListener("updates:ready", listener),
323323
installUpdate: (): Promise<{ installed: boolean }> =>
324324
ipcRenderer.invoke("updates:install"),
325+
checkForUpdates: (): Promise<{
326+
success: boolean;
327+
error?: string;
328+
}> => ipcRenderer.invoke("updates:check"),
329+
onUpdateStatus: (
330+
listener: IpcEventListener<{
331+
checking?: boolean;
332+
upToDate?: boolean;
333+
}>,
334+
): (() => void) => createIpcListener("updates:status", listener),
335+
onCheckForUpdatesMenu: (listener: () => void): (() => void) =>
336+
createVoidIpcListener("check-for-updates-menu", listener),
325337
shellCreate: (
326338
sessionId: string,
327339
cwd?: string,

apps/array/src/main/services/updates.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
1010
const DISABLE_FLAG = "ELECTRON_DISABLE_AUTO_UPDATE";
1111
const UPDATE_READY_CHANNEL = "updates:ready";
1212
const INSTALL_UPDATE_CHANNEL = "updates:install";
13+
const CHECK_FOR_UPDATES_CHANNEL = "updates:check";
14+
const UPDATE_STATUS_CHANNEL = "updates:status";
1315

1416
let updateReady = false;
1517
let pendingNotification = false;
18+
let checkingForUpdates = false;
1619

1720
function isAutoUpdateSupported(): boolean {
1821
return process.platform === "darwin" || process.platform === "win32";
@@ -54,6 +57,60 @@ export function registerAutoUpdater(
5457
return { installed: true };
5558
});
5659

60+
ipcMain.removeHandler(CHECK_FOR_UPDATES_CHANNEL);
61+
ipcMain.handle(CHECK_FOR_UPDATES_CHANNEL, async () => {
62+
if (!isAutoUpdateSupported()) {
63+
return {
64+
success: false,
65+
error: "Auto updates are only supported on macOS and Windows",
66+
};
67+
}
68+
69+
if (!app.isPackaged) {
70+
return {
71+
success: false,
72+
error: "Updates are only available in packaged builds",
73+
};
74+
}
75+
76+
if (checkingForUpdates) {
77+
return {
78+
success: false,
79+
error: "Already checking for updates",
80+
};
81+
}
82+
83+
try {
84+
checkingForUpdates = true;
85+
const window = getWindow();
86+
if (window) {
87+
window.webContents.send(UPDATE_STATUS_CHANNEL, {
88+
checking: true,
89+
});
90+
}
91+
92+
await checkForUpdates();
93+
94+
return {
95+
success: true,
96+
};
97+
} catch (error) {
98+
log.error("Manual update check failed", error);
99+
return {
100+
success: false,
101+
error: error instanceof Error ? error.message : "Unknown error",
102+
};
103+
} finally {
104+
checkingForUpdates = false;
105+
const window = getWindow();
106+
if (window) {
107+
window.webContents.send(UPDATE_STATUS_CHANNEL, {
108+
checking: false,
109+
});
110+
}
111+
}
112+
});
113+
57114
if (process.env[DISABLE_FLAG]) {
58115
log.info("Auto updates disabled via environment flag");
59116
return;
@@ -98,6 +155,13 @@ export function registerAutoUpdater(
98155

99156
autoUpdater.on("update-not-available", () => {
100157
log.info("No updates available");
158+
const window = getWindow();
159+
if (window && checkingForUpdates) {
160+
window.webContents.send(UPDATE_STATUS_CHANNEL, {
161+
checking: false,
162+
upToDate: true,
163+
});
164+
}
101165
});
102166

103167
autoUpdater.on("update-downloaded", () => {
Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Dialog, Flex, Text } from "@radix-ui/themes";
1+
import { Button, Dialog, Flex, Spinner, Text } from "@radix-ui/themes";
22
import { logger } from "@renderer/lib/logger";
33
import { useCallback, useEffect, useState } from "react";
44

@@ -8,6 +8,11 @@ export function UpdatePrompt() {
88
const [open, setOpen] = useState(false);
99
const [isInstalling, setIsInstalling] = useState(false);
1010
const [errorMessage, setErrorMessage] = useState<string | null>(null);
11+
const [checkDialogOpen, setCheckDialogOpen] = useState(false);
12+
const [checkingForUpdates, setCheckingForUpdates] = useState(false);
13+
const [checkResultMessage, setCheckResultMessage] = useState<string | null>(
14+
null,
15+
);
1116

1217
useEffect(() => {
1318
const unsubscribe = window.electronAPI?.onUpdateReady(() => {
@@ -20,6 +25,55 @@ export function UpdatePrompt() {
2025
};
2126
}, []);
2227

28+
useEffect(() => {
29+
const unsubscribeStatus = window.electronAPI?.onUpdateStatus((status) => {
30+
if (status.checking === false && status.upToDate) {
31+
setCheckingForUpdates(false);
32+
setCheckResultMessage(
33+
`Array is up to date (version ${window.electronAPI ? "" : "unknown"})`,
34+
);
35+
} else if (status.checking === false) {
36+
setCheckingForUpdates(false);
37+
} else if (status.checking === true) {
38+
setCheckingForUpdates(true);
39+
setCheckResultMessage(null);
40+
}
41+
});
42+
43+
return () => {
44+
unsubscribeStatus?.();
45+
};
46+
}, []);
47+
48+
useEffect(() => {
49+
const handleMenuCheck = async () => {
50+
setCheckDialogOpen(true);
51+
setCheckingForUpdates(true);
52+
setCheckResultMessage(null);
53+
54+
try {
55+
const result = await window.electronAPI?.checkForUpdates();
56+
57+
if (!result?.success) {
58+
setCheckingForUpdates(false);
59+
setCheckResultMessage(result?.error || "Failed to check for updates");
60+
}
61+
} catch (error) {
62+
log.error("Failed to check for updates:", error);
63+
setCheckingForUpdates(false);
64+
setCheckResultMessage("An unexpected error occurred");
65+
}
66+
};
67+
68+
const unsubscribeMenuCheck = window.electronAPI?.onCheckForUpdatesMenu(() =>
69+
handleMenuCheck(),
70+
);
71+
72+
return () => {
73+
unsubscribeMenuCheck?.();
74+
};
75+
}, []);
76+
2377
const handleRestart = useCallback(async () => {
2478
if (!window.electronAPI || isInstalling) {
2579
return;
@@ -44,44 +98,77 @@ export function UpdatePrompt() {
4498
}
4599
}, [isInstalling]);
46100

47-
if (!open) {
48-
return null;
49-
}
50-
51101
return (
52-
<Dialog.Root open={open} onOpenChange={setOpen}>
53-
<Dialog.Content maxWidth="360px">
54-
<Flex direction="column" gap="3">
55-
<Dialog.Title className="mb-0">Update ready</Dialog.Title>
56-
<Dialog.Description>
57-
A new version of Array has finished downloading. Restart now to
58-
install it or choose Later to keep working and update next time.
59-
</Dialog.Description>
60-
{errorMessage ? (
61-
<Text size="2" color="red">
62-
{errorMessage}
63-
</Text>
64-
) : null}
65-
<Flex justify="end" gap="3" mt="2">
66-
<Button
67-
type="button"
68-
variant="soft"
69-
color="gray"
70-
onClick={() => setOpen(false)}
71-
disabled={isInstalling}
72-
>
73-
Later
74-
</Button>
75-
<Button
76-
type="button"
77-
onClick={handleRestart}
78-
disabled={isInstalling}
79-
>
80-
{isInstalling ? "Restarting…" : "Restart now"}
81-
</Button>
82-
</Flex>
83-
</Flex>
84-
</Dialog.Content>
85-
</Dialog.Root>
102+
<>
103+
{/* Update ready dialog */}
104+
{open && (
105+
<Dialog.Root open={open} onOpenChange={setOpen}>
106+
<Dialog.Content maxWidth="360px">
107+
<Flex direction="column" gap="3">
108+
<Dialog.Title className="mb-0">Update ready</Dialog.Title>
109+
<Dialog.Description>
110+
A new version of Array has finished downloading. Restart now to
111+
install it or choose Later to keep working and update next time.
112+
</Dialog.Description>
113+
{errorMessage ? (
114+
<Text size="2" color="red">
115+
{errorMessage}
116+
</Text>
117+
) : null}
118+
<Flex justify="end" gap="3" mt="2">
119+
<Button
120+
type="button"
121+
variant="soft"
122+
color="gray"
123+
onClick={() => setOpen(false)}
124+
disabled={isInstalling}
125+
>
126+
Later
127+
</Button>
128+
<Button
129+
type="button"
130+
onClick={handleRestart}
131+
disabled={isInstalling}
132+
>
133+
{isInstalling ? "Restarting…" : "Restart now"}
134+
</Button>
135+
</Flex>
136+
</Flex>
137+
</Dialog.Content>
138+
</Dialog.Root>
139+
)}
140+
141+
{/* Check for updates dialog (menu-triggered) */}
142+
{checkDialogOpen && (
143+
<Dialog.Root open={checkDialogOpen} onOpenChange={setCheckDialogOpen}>
144+
<Dialog.Content maxWidth="360px">
145+
<Flex direction="column" gap="3">
146+
<Dialog.Title className="mb-0">Check for Updates</Dialog.Title>
147+
<Dialog.Description>
148+
{checkingForUpdates ? (
149+
<Flex align="center" gap="2">
150+
<Spinner />
151+
<Text>Checking for updates...</Text>
152+
</Flex>
153+
) : checkResultMessage ? (
154+
<Text>{checkResultMessage}</Text>
155+
) : (
156+
<Text>Ready to check for updates</Text>
157+
)}
158+
</Dialog.Description>
159+
<Flex justify="end" mt="2">
160+
<Button
161+
type="button"
162+
onClick={() => setCheckDialogOpen(false)}
163+
disabled={checkingForUpdates}
164+
>
165+
OK
166+
</Button>
167+
</Flex>
168+
</Flex>
169+
</Dialog.Content>
170+
</Dialog.Root>
171+
)}
172+
</>
86173
);
87174
}

0 commit comments

Comments
 (0)