authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-03 21:46:28-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-03 22:31:45-08:00
log32d89806d7b17dbcffa4f02b46b24b410bf54b5e
tree634b843f15891c1a0e304ce49617b0d9a07ce1b1
parent2fd4e06b345e6edbe12f1de456bb3f057c2b14f0

Extract desktop update state helpers and improve update UX

- Add `updateState` helpers for auto-update eligibility reasons and progress throttling - Show a user-facing dialog when manual update checks are unavailable - Ensure manual update checks can open a window when none exist - Add tests for disable-reason logic and 10% download progress bucketing

3 files changed, 158 insertions(+), 17 deletions(-)

apps/desktop/src/main.ts+38-17
......@@ -15,6 +15,7 @@ import { NetService } from "@t3tools/shared/Net";
1515import { RotatingFileSink } from "@t3tools/shared/logging";
1616import { showDesktopConfirmDialog } from "./confirmDialog";
1717import { fixPath } from "./fixPath";
18import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
1819
1920fixPath();
2021
......@@ -425,11 +426,28 @@ function dispatchMenuAction(action: string): void {
425426}
426427
427428function handleCheckForUpdatesMenuClick(): void {
428 if (!shouldEnableAutoUpdates()) {
429 const disabledReason = getAutoUpdateDisabledReason({
430 isDevelopment,
431 isPackaged: app.isPackaged,
432 platform: process.platform,
433 appImage: process.env.APPIMAGE,
434 disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1",
435 });
436 if (disabledReason) {
429437 console.info("[desktop-updater] Manual update check requested, but updates are disabled.");
438 void dialog.showMessageBox({
439 type: "info",
440 title: "Updates unavailable",
441 message: "Automatic updates are not available right now.",
442 detail: disabledReason,
443 buttons: ["OK"],
444 });
430445 return;
431446 }
432447
448 if (!BrowserWindow.getAllWindows().length) {
449 mainWindow = createWindow();
450 }
433451 void checkForUpdates("menu");
434452}
435453
......@@ -562,17 +580,15 @@ function setUpdateState(patch: Partial<DesktopUpdateState>): void {
562580}
563581
564582function shouldEnableAutoUpdates(): boolean {
565 if (isDevelopment || !app.isPackaged) {
566 return false;
567 }
568 if (process.env.T3CODE_DISABLE_AUTO_UPDATE === "1") {
569 return false;
570 }
571 if (process.platform === "linux" && !process.env.APPIMAGE) {
572 // electron-updater only supports Linux auto-updates when running from AppImage.
573 return false;
574 }
575 return true;
583 return (
584 getAutoUpdateDisabledReason({
585 isDevelopment,
586 isPackaged: app.isPackaged,
587 platform: process.platform,
588 appImage: process.env.APPIMAGE,
589 disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1",
590 }) === null
591 );
576592}
577593
578594async function checkForUpdates(reason: string): Promise<void> {
......@@ -719,11 +735,16 @@ function configureAutoUpdater(): void {
719735 });
720736 autoUpdater.on("download-progress", (progress) => {
721737 const percent = Math.floor(progress.percent);
722 setUpdateState({
723 status: "downloading",
724 downloadPercent: progress.percent,
725 message: null,
726 });
738 if (
739 shouldBroadcastDownloadProgress(updateState, progress.percent) ||
740 updateState.message !== null
741 ) {
742 setUpdateState({
743 status: "downloading",
744 downloadPercent: progress.percent,
745 message: null,
746 });
747 }
727748 const milestone = percent - (percent % 10);
728749 if (milestone > lastLoggedDownloadMilestone) {
729750 lastLoggedDownloadMilestone = milestone;
apps/desktop/src/updateState.test.ts created+82
......@@ -0,0 +1,82 @@
1import { describe, expect, it } from "vitest";
2import type { DesktopUpdateState } from "@t3tools/contracts";
3
4import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
5
6const baseState: DesktopUpdateState = {
7 enabled: true,
8 status: "idle",
9 currentVersion: "1.0.0",
10 availableVersion: null,
11 downloadedVersion: null,
12 downloadPercent: null,
13 checkedAt: null,
14 message: null,
15};
16
17describe("shouldBroadcastDownloadProgress", () => {
18 it("broadcasts the first downloading progress update", () => {
19 expect(
20 shouldBroadcastDownloadProgress(
21 { ...baseState, status: "downloading", downloadPercent: null },
22 1,
23 ),
24 ).toBe(true);
25 });
26
27 it("skips progress updates within the same 10% bucket", () => {
28 expect(
29 shouldBroadcastDownloadProgress(
30 { ...baseState, status: "downloading", downloadPercent: 11.2 },
31 18.7,
32 ),
33 ).toBe(false);
34 });
35
36 it("broadcasts progress updates when a new 10% bucket is reached", () => {
37 expect(
38 shouldBroadcastDownloadProgress(
39 { ...baseState, status: "downloading", downloadPercent: 19.9 },
40 20.1,
41 ),
42 ).toBe(true);
43 });
44});
45
46describe("getAutoUpdateDisabledReason", () => {
47 it("reports development builds as disabled", () => {
48 expect(
49 getAutoUpdateDisabledReason({
50 isDevelopment: true,
51 isPackaged: false,
52 platform: "darwin",
53 appImage: undefined,
54 disabledByEnv: false,
55 }),
56 ).toContain("packaged production builds");
57 });
58
59 it("reports env-disabled auto updates", () => {
60 expect(
61 getAutoUpdateDisabledReason({
62 isDevelopment: false,
63 isPackaged: true,
64 platform: "darwin",
65 appImage: undefined,
66 disabledByEnv: true,
67 }),
68 ).toContain("T3CODE_DISABLE_AUTO_UPDATE");
69 });
70
71 it("reports linux non-AppImage builds as disabled", () => {
72 expect(
73 getAutoUpdateDisabledReason({
74 isDevelopment: false,
75 isPackaged: true,
76 platform: "linux",
77 appImage: undefined,
78 disabledByEnv: false,
79 }),
80 ).toContain("AppImage");
81 });
82});
apps/desktop/src/updateState.ts created+38
......@@ -0,0 +1,38 @@
1import type { DesktopUpdateState } from "@t3tools/contracts";
2
3export function shouldBroadcastDownloadProgress(
4 currentState: DesktopUpdateState,
5 nextPercent: number,
6): boolean {
7 if (currentState.status !== "downloading") {
8 return true;
9 }
10
11 const currentPercent = currentState.downloadPercent;
12 if (currentPercent === null) {
13 return true;
14 }
15
16 const previousStep = Math.floor(currentPercent / 10);
17 const nextStep = Math.floor(nextPercent / 10);
18 return nextStep > previousStep || nextPercent === 100;
19}
20
21export function getAutoUpdateDisabledReason(args: {
22 isDevelopment: boolean;
23 isPackaged: boolean;
24 platform: NodeJS.Platform;
25 appImage?: string | undefined;
26 disabledByEnv: boolean;
27}): string | null {
28 if (args.isDevelopment || !args.isPackaged) {
29 return "Automatic updates are only available in packaged production builds.";
30 }
31 if (args.disabledByEnv) {
32 return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting.";
33 }
34 if (args.platform === "linux" && !args.appImage) {
35 return "Automatic updates on Linux require running the AppImage build.";
36 }
37 return null;
38}