From 7a86e1b09e6fde25f65b9e40f047b8ed6666bcd5 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:05:43 +0530 Subject: [PATCH 1/8] feat: add update preference helpers --- src/lib/updatePreferences.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/lib/updatePreferences.ts diff --git a/src/lib/updatePreferences.ts b/src/lib/updatePreferences.ts new file mode 100644 index 00000000..c588a322 --- /dev/null +++ b/src/lib/updatePreferences.ts @@ -0,0 +1,34 @@ +const DISMISSED_VERSION_KEY = "openscreen_dismissed_update_version"; +const CHECKS_DISABLED_KEY = "openscreen_update_checks_disabled"; + +export function getDismissedUpdateVersion(): string | null { + try { + return localStorage.getItem(DISMISSED_VERSION_KEY); + } catch { + return null; + } +} + +export function saveDismissedUpdateVersion(version: string): void { + try { + localStorage.setItem(DISMISSED_VERSION_KEY, version); + } catch { + // localStorage may be unavailable (e.g. private browsing quota exceeded) + } +} + +export function getUpdateChecksDisabled(): boolean { + try { + return localStorage.getItem(CHECKS_DISABLED_KEY) === "true"; + } catch { + return false; + } +} + +export function setUpdateChecksDisabled(disabled: boolean): void { + try { + localStorage.setItem(CHECKS_DISABLED_KEY, disabled ? "true" : "false"); + } catch { + // localStorage may be unavailable + } +} From 3d837a45f7a085c279000eb73ef78dcdd8be1755 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:07:21 +0530 Subject: [PATCH 2/8] feat: add update check module --- src/lib/checkForUpdate.ts | 86 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/lib/checkForUpdate.ts diff --git a/src/lib/checkForUpdate.ts b/src/lib/checkForUpdate.ts new file mode 100644 index 00000000..5748c35b --- /dev/null +++ b/src/lib/checkForUpdate.ts @@ -0,0 +1,86 @@ +import { toast } from "sonner"; +import { + getDismissedUpdateVersion, + getUpdateChecksDisabled, + saveDismissedUpdateVersion, + setUpdateChecksDisabled, +} from "./updatePreferences"; + +const RELEASES_API = "https://api.github.com/repos/siddharthvaddem/openscreen/releases/latest"; + +export interface LatestRelease { + version: string; + htmlUrl: string; +} + +export async function fetchLatestRelease(): Promise { + try { + const response = await fetch(RELEASES_API, { + headers: { Accept: "application/vnd.github+json" }, + }); + if (!response.ok) return null; + const data = (await response.json()) as { tag_name?: unknown; html_url?: unknown }; + if (typeof data.tag_name !== "string" || typeof data.html_url !== "string") return null; + const version = data.tag_name.replace(/^v/, ""); + if (version.includes("-")) return null; + return { version, htmlUrl: data.html_url }; + } catch { + return null; + } +} + +export function isNewer(latest: string, current: string): boolean { + if (latest.includes("-") || current.includes("-")) return false; + const latestParts = latest.split(".").map((n) => Number.parseInt(n, 10)); + const currentParts = current.split(".").map((n) => Number.parseInt(n, 10)); + if (latestParts.some(Number.isNaN) || currentParts.some(Number.isNaN)) return false; + const length = Math.max(latestParts.length, currentParts.length); + for (let i = 0; i < length; i++) { + const a = latestParts[i] ?? 0; + const b = currentParts[i] ?? 0; + if (a > b) return true; + if (a < b) return false; + } + return false; +} + +function showUpdateAvailableToast(release: LatestRelease, currentVersion: string): void { + toast(`OpenScreen ${release.version} is available`, { + duration: Number.POSITIVE_INFINITY, + description: `You're on ${currentVersion}`, + closeButton: true, + onDismiss: () => saveDismissedUpdateVersion(release.version), + action: { + label: "Download", + onClick: () => window.electronAPI.openExternalUrl(release.htmlUrl), + }, + cancel: { + label: "Don't remind again", + onClick: () => setUpdateChecksDisabled(true), + }, + }); +} + +export async function maybeShowUpdateToast(currentVersion: string): Promise { + if (getUpdateChecksDisabled()) return; + const release = await fetchLatestRelease(); + if (!release) return; + if (!isNewer(release.version, currentVersion)) return; + if (getDismissedUpdateVersion() === release.version) return; + showUpdateAvailableToast(release, currentVersion); +} + +export async function forceCheckForUpdate(currentVersion: string): Promise { + const release = await fetchLatestRelease(); + if (!release) { + toast.error("Couldn't check for updates", { + description: "Check your connection and try again.", + }); + return; + } + if (!isNewer(release.version, currentVersion)) { + toast.success("OpenScreen is up to date", { description: `You're on ${currentVersion}` }); + return; + } + showUpdateAvailableToast(release, currentVersion); +} From 3339fa7a98b30316d05d9fb7174eba7e66feea9d Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:08:00 +0530 Subject: [PATCH 3/8] feat: expose app version via IPC --- electron/electron-env.d.ts | 1 + electron/ipc/handlers.ts | 2 ++ electron/preload.ts | 3 +++ 3 files changed, 6 insertions(+) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 85d82946..53f258f9 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -73,6 +73,7 @@ interface Window { }>; onStopRecordingFromTray: (callback: () => void) => () => void; openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>; + getAppVersion: () => Promise; saveExportedVideo: ( videoData: ArrayBuffer, fileName: string, diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 95ed797e..fd9023f0 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -790,6 +790,8 @@ export function registerIpcHandlers( } }); + ipcMain.handle("get-app-version", () => app.getVersion()); + ipcMain.handle("open-external-url", async (_, url: string) => { try { const ALLOWED_SCHEMES = ["http:", "https:", "mailto:"]; diff --git a/electron/preload.ts b/electron/preload.ts index 46e16f04..d02d4460 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -68,6 +68,9 @@ contextBridge.exposeInMainWorld("electronAPI", { openExternalUrl: (url: string) => { return ipcRenderer.invoke("open-external-url", url); }, + getAppVersion: () => { + return ipcRenderer.invoke("get-app-version"); + }, saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { return ipcRenderer.invoke("save-exported-video", videoData, fileName); }, From a01408c0bab345bb87348b4c0026632d47f5a644 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:08:27 +0530 Subject: [PATCH 4/8] feat: check for updates on launcher start --- src/App.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 4045b5dc..8748ee4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,8 +7,11 @@ import { TooltipProvider } from "./components/ui/tooltip"; import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; import VideoEditor from "./components/video-editor/VideoEditor"; import { ShortcutsProvider } from "./contexts/ShortcutsContext"; +import { maybeShowUpdateToast } from "./lib/checkForUpdate"; import { loadAllCustomFonts } from "./lib/customFonts"; +const UPDATE_CHECK_DELAY_MS = 3000; + export default function App() { const [windowType, setWindowType] = useState( () => new URLSearchParams(window.location.search).get("windowType") || "", @@ -34,6 +37,15 @@ export default function App() { }); }, []); + useEffect(() => { + if (windowType !== "") return; + const id = setTimeout(async () => { + const version = await window.electronAPI.getAppVersion(); + maybeShowUpdateToast(version); + }, UPDATE_CHECK_DELAY_MS); + return () => clearTimeout(id); + }, [windowType]); + const content = (() => { switch (windowType) { case "hud-overlay": From 332076aa4937b6fb8bff9e2ae10cbfab95630407 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:09:24 +0530 Subject: [PATCH 5/8] feat: add Help menu Check for Updates entry --- electron/electron-env.d.ts | 1 + electron/main.ts | 24 ++++++++++++++++++++++++ electron/preload.ts | 5 +++++ src/App.tsx | 17 ++++++++++++++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 53f258f9..92e17d0b 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -129,6 +129,7 @@ interface Window { onMenuLoadProject: (callback: () => void) => () => void; onMenuSaveProject: (callback: () => void) => () => void; onMenuSaveProjectAs: (callback: () => void) => () => void; + onMenuCheckForUpdates: (callback: () => void) => () => void; getPlatform: () => Promise; revealInFolder: ( filePath: string, diff --git a/electron/main.ts b/electron/main.ts index ad0a33fc..ec93501d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -116,6 +116,21 @@ function sendEditorMenuAction( targetWindow.webContents.send(channel); } +function sendCheckForUpdates() { + const target = BrowserWindow.getFocusedWindow() ?? mainWindow; + if (target && !target.isDestroyed()) { + target.webContents.send("menu-check-for-updates"); + return; + } + showMainWindow(); + const dispatchTarget = mainWindow; + if (!dispatchTarget || dispatchTarget.isDestroyed()) return; + dispatchTarget.webContents.once("did-finish-load", () => { + if (dispatchTarget.isDestroyed()) return; + dispatchTarget.webContents.send("menu-check-for-updates"); + }); +} + function setupApplicationMenu() { const isMac = process.platform === "darwin"; const template: Electron.MenuItemConstructorOptions[] = []; @@ -191,6 +206,15 @@ function setupApplicationMenu() { ? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] : [{ role: "minimize" }, { role: "close" }], }, + { + role: "help", + submenu: [ + { + label: mainT("common", "actions.checkForUpdates") || "Check for Updates…", + click: () => sendCheckForUpdates(), + }, + ], + }, ); const menu = Menu.buildFromTemplate(template); diff --git a/electron/preload.ts b/electron/preload.ts index d02d4460..d4513c0c 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -119,6 +119,11 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.on("menu-save-project-as", listener); return () => ipcRenderer.removeListener("menu-save-project-as", listener); }, + onMenuCheckForUpdates: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-check-for-updates", listener); + return () => ipcRenderer.removeListener("menu-check-for-updates", listener); + }, getPlatform: () => { return ipcRenderer.invoke("get-platform"); }, diff --git a/src/App.tsx b/src/App.tsx index 8748ee4a..7c82ae6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { TooltipProvider } from "./components/ui/tooltip"; import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; import VideoEditor from "./components/video-editor/VideoEditor"; import { ShortcutsProvider } from "./contexts/ShortcutsContext"; -import { maybeShowUpdateToast } from "./lib/checkForUpdate"; +import { forceCheckForUpdate, maybeShowUpdateToast } from "./lib/checkForUpdate"; import { loadAllCustomFonts } from "./lib/customFonts"; const UPDATE_CHECK_DELAY_MS = 3000; @@ -46,6 +46,21 @@ export default function App() { return () => clearTimeout(id); }, [windowType]); + useEffect(() => { + if ( + windowType === "hud-overlay" || + windowType === "source-selector" || + windowType === "countdown-overlay" + ) { + return; + } + const unsubscribe = window.electronAPI.onMenuCheckForUpdates(async () => { + const version = await window.electronAPI.getAppVersion(); + forceCheckForUpdate(version); + }); + return unsubscribe; + }, [windowType]); + const content = (() => { switch (windowType) { case "hud-overlay": From c498981f1388623fa4f02b831eead170e4cb9fc8 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:12:02 +0530 Subject: [PATCH 6/8] test: cover update check flows --- src/lib/checkForUpdate.test.ts | 185 +++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 src/lib/checkForUpdate.test.ts diff --git a/src/lib/checkForUpdate.test.ts b/src/lib/checkForUpdate.test.ts new file mode 100644 index 00000000..81f040ce --- /dev/null +++ b/src/lib/checkForUpdate.test.ts @@ -0,0 +1,185 @@ +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; + +vi.mock("sonner", () => { + const fn = vi.fn(); + const toast = Object.assign(fn, { + success: vi.fn(), + error: vi.fn(), + }); + return { toast }; +}); + +const localStorageStore: Record = {}; +Object.defineProperty(globalThis, "localStorage", { + value: { + getItem: (k: string) => localStorageStore[k] ?? null, + setItem: (k: string, v: string) => { + localStorageStore[k] = v; + }, + removeItem: (k: string) => { + delete localStorageStore[k]; + }, + clear: () => { + for (const k of Object.keys(localStorageStore)) delete localStorageStore[k]; + }, + }, + configurable: true, +}); + +const openExternalUrl = vi.fn().mockResolvedValue({ success: true }); + +Object.defineProperty(window, "electronAPI", { + value: { openExternalUrl }, + configurable: true, + writable: true, +}); + +import { toast } from "sonner"; +import { forceCheckForUpdate, isNewer, maybeShowUpdateToast } from "./checkForUpdate"; + +const toastMock = toast as unknown as Mock; +const toastSuccess = toast.success as Mock; +const toastError = toast.error as Mock; + +function mockReleaseResponse(tagName: string, htmlUrl = "https://example.com/release") { + (global.fetch as Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ tag_name: tagName, html_url: htmlUrl }), + }); +} + +describe("isNewer", () => { + it("detects a higher minor version", () => { + expect(isNewer("1.3.0", "1.2.0")).toBe(true); + }); + + it("compares numerically rather than lexicographically", () => { + expect(isNewer("1.10.0", "1.2.0")).toBe(true); + }); + + it("returns false for equal versions", () => { + expect(isNewer("1.2.0", "1.2.0")).toBe(false); + }); + + it("returns false when current is newer", () => { + expect(isNewer("1.2.0", "1.3.0")).toBe(false); + }); + + it("rejects pre-release tags", () => { + expect(isNewer("1.3.0-beta.1", "1.2.0")).toBe(false); + }); +}); + +describe("maybeShowUpdateToast", () => { + beforeEach(() => { + localStorage.clear(); + global.fetch = vi.fn() as unknown as typeof fetch; + toastMock.mockClear(); + toastSuccess.mockClear(); + toastError.mockClear(); + openExternalUrl.mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("fires the update toast when a newer release exists", async () => { + mockReleaseResponse("v1.3.0"); + await maybeShowUpdateToast("1.2.0"); + expect(toastMock).toHaveBeenCalledTimes(1); + const [title, options] = toastMock.mock.calls[0]; + expect(title).toBe("OpenScreen 1.3.0 is available"); + expect(options.action.label).toBe("Download"); + expect(options.cancel.label).toBe("Don't remind again"); + expect(options.closeButton).toBe(true); + }); + + it("skips entirely when checks are disabled", async () => { + localStorage.setItem("openscreen_update_checks_disabled", "true"); + await maybeShowUpdateToast("1.2.0"); + expect(global.fetch).not.toHaveBeenCalled(); + expect(toastMock).not.toHaveBeenCalled(); + }); + + it("does not show toast when this version was dismissed", async () => { + localStorage.setItem("openscreen_dismissed_update_version", "1.3.0"); + mockReleaseResponse("v1.3.0"); + await maybeShowUpdateToast("1.2.0"); + expect(global.fetch).toHaveBeenCalled(); + expect(toastMock).not.toHaveBeenCalled(); + }); + + it("stays silent on fetch failure", async () => { + (global.fetch as Mock).mockRejectedValueOnce(new Error("network down")); + await maybeShowUpdateToast("1.2.0"); + expect(toastMock).not.toHaveBeenCalled(); + expect(toastError).not.toHaveBeenCalled(); + }); + + it("download action opens the release URL", async () => { + mockReleaseResponse("v1.3.0", "https://example.com/release/1.3.0"); + await maybeShowUpdateToast("1.2.0"); + const [, options] = toastMock.mock.calls[0]; + options.action.onClick(); + expect(openExternalUrl).toHaveBeenCalledWith("https://example.com/release/1.3.0"); + }); + + it("cancel action persists checks-disabled flag", async () => { + mockReleaseResponse("v1.3.0"); + await maybeShowUpdateToast("1.2.0"); + const [, options] = toastMock.mock.calls[0]; + options.cancel.onClick(); + expect(localStorage.getItem("openscreen_update_checks_disabled")).toBe("true"); + }); + + it("close button dismissal persists per-version cache", async () => { + mockReleaseResponse("v1.3.0"); + await maybeShowUpdateToast("1.2.0"); + const [, options] = toastMock.mock.calls[0]; + options.onDismiss(); + expect(localStorage.getItem("openscreen_dismissed_update_version")).toBe("1.3.0"); + }); +}); + +describe("forceCheckForUpdate", () => { + beforeEach(() => { + localStorage.clear(); + global.fetch = vi.fn() as unknown as typeof fetch; + toastMock.mockClear(); + toastSuccess.mockClear(); + toastError.mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("bypasses checks-disabled and dismissed-version gates", async () => { + localStorage.setItem("openscreen_update_checks_disabled", "true"); + localStorage.setItem("openscreen_dismissed_update_version", "1.3.0"); + mockReleaseResponse("v1.3.0"); + await forceCheckForUpdate("1.2.0"); + expect(toastMock).toHaveBeenCalledTimes(1); + }); + + it("shows up-to-date success toast when current matches latest", async () => { + mockReleaseResponse("v1.2.0"); + await forceCheckForUpdate("1.2.0"); + expect(toastSuccess).toHaveBeenCalledTimes(1); + expect(toastSuccess.mock.calls[0][0]).toBe("OpenScreen is up to date"); + }); + + it("shows up-to-date success toast when current is newer than latest", async () => { + mockReleaseResponse("v1.2.0"); + await forceCheckForUpdate("1.3.0"); + expect(toastSuccess).toHaveBeenCalledTimes(1); + }); + + it("shows error toast on fetch failure", async () => { + (global.fetch as Mock).mockRejectedValueOnce(new Error("offline")); + await forceCheckForUpdate("1.2.0"); + expect(toastError).toHaveBeenCalledTimes(1); + expect(toastError.mock.calls[0][0]).toBe("Couldn't check for updates"); + }); +}); From 0688e1df0be4d566f3a3b1f039b6e2f8d6273cd0 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:15:42 +0530 Subject: [PATCH 7/8] fix: guard update-check effects with try/catch --- src/App.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7c82ae6f..f7613a40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,8 +40,12 @@ export default function App() { useEffect(() => { if (windowType !== "") return; const id = setTimeout(async () => { - const version = await window.electronAPI.getAppVersion(); - maybeShowUpdateToast(version); + try { + const version = await window.electronAPI.getAppVersion(); + await maybeShowUpdateToast(version); + } catch (error) { + console.error("Error during on-start update check in App useEffect:", error); + } }, UPDATE_CHECK_DELAY_MS); return () => clearTimeout(id); }, [windowType]); @@ -55,8 +59,12 @@ export default function App() { return; } const unsubscribe = window.electronAPI.onMenuCheckForUpdates(async () => { - const version = await window.electronAPI.getAppVersion(); - forceCheckForUpdate(version); + try { + const version = await window.electronAPI.getAppVersion(); + forceCheckForUpdate(version); + } catch (error) { + console.error("Error handling onMenuCheckForUpdates in App:", error); + } }); return unsubscribe; }, [windowType]); From 6d0badc92516b2dbe9c80c668c883b93cdc50917 Mon Sep 17 00:00:00 2001 From: AbhinRustagi Date: Sun, 3 May 2026 18:20:28 +0530 Subject: [PATCH 8/8] refactor: log update check errors and dedupe test setup Surface update-check failures via console.error instead of swallowing them silently, and extract a shared resetUpdateCheckMocks helper used by both maybeShowUpdateToast and forceCheckForUpdate suites. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/checkForUpdate.test.ts | 28 ++++++++++++---------------- src/lib/checkForUpdate.ts | 12 +++++++++--- src/lib/updatePreferences.ts | 14 ++++++++------ 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/lib/checkForUpdate.test.ts b/src/lib/checkForUpdate.test.ts index 81f040ce..5a0484ab 100644 --- a/src/lib/checkForUpdate.test.ts +++ b/src/lib/checkForUpdate.test.ts @@ -48,6 +48,15 @@ function mockReleaseResponse(tagName: string, htmlUrl = "https://example.com/rel }); } +function resetUpdateCheckMocks() { + localStorage.clear(); + global.fetch = vi.fn() as unknown as typeof fetch; + toastMock.mockClear(); + toastSuccess.mockClear(); + toastError.mockClear(); + openExternalUrl.mockClear(); +} + describe("isNewer", () => { it("detects a higher minor version", () => { expect(isNewer("1.3.0", "1.2.0")).toBe(true); @@ -71,14 +80,7 @@ describe("isNewer", () => { }); describe("maybeShowUpdateToast", () => { - beforeEach(() => { - localStorage.clear(); - global.fetch = vi.fn() as unknown as typeof fetch; - toastMock.mockClear(); - toastSuccess.mockClear(); - toastError.mockClear(); - openExternalUrl.mockClear(); - }); + beforeEach(resetUpdateCheckMocks); afterEach(() => { vi.restoreAllMocks(); @@ -121,7 +123,7 @@ describe("maybeShowUpdateToast", () => { mockReleaseResponse("v1.3.0", "https://example.com/release/1.3.0"); await maybeShowUpdateToast("1.2.0"); const [, options] = toastMock.mock.calls[0]; - options.action.onClick(); + await options.action.onClick(); expect(openExternalUrl).toHaveBeenCalledWith("https://example.com/release/1.3.0"); }); @@ -143,13 +145,7 @@ describe("maybeShowUpdateToast", () => { }); describe("forceCheckForUpdate", () => { - beforeEach(() => { - localStorage.clear(); - global.fetch = vi.fn() as unknown as typeof fetch; - toastMock.mockClear(); - toastSuccess.mockClear(); - toastError.mockClear(); - }); + beforeEach(resetUpdateCheckMocks); afterEach(() => { vi.restoreAllMocks(); diff --git a/src/lib/checkForUpdate.ts b/src/lib/checkForUpdate.ts index 5748c35b..ae4f7021 100644 --- a/src/lib/checkForUpdate.ts +++ b/src/lib/checkForUpdate.ts @@ -19,12 +19,16 @@ export async function fetchLatestRelease(): Promise { headers: { Accept: "application/vnd.github+json" }, }); if (!response.ok) return null; - const data = (await response.json()) as { tag_name?: unknown; html_url?: unknown }; + const data = (await response.json()) as { + tag_name?: unknown; + html_url?: unknown; + }; if (typeof data.tag_name !== "string" || typeof data.html_url !== "string") return null; const version = data.tag_name.replace(/^v/, ""); if (version.includes("-")) return null; return { version, htmlUrl: data.html_url }; - } catch { + } catch (error) { + console.error("checkForUpdate failed:", error); return null; } } @@ -79,7 +83,9 @@ export async function forceCheckForUpdate(currentVersion: string): Promise return; } if (!isNewer(release.version, currentVersion)) { - toast.success("OpenScreen is up to date", { description: `You're on ${currentVersion}` }); + toast.success("OpenScreen is up to date", { + description: `You're on ${currentVersion}`, + }); return; } showUpdateAvailableToast(release, currentVersion); diff --git a/src/lib/updatePreferences.ts b/src/lib/updatePreferences.ts index c588a322..d42860e2 100644 --- a/src/lib/updatePreferences.ts +++ b/src/lib/updatePreferences.ts @@ -4,7 +4,8 @@ const CHECKS_DISABLED_KEY = "openscreen_update_checks_disabled"; export function getDismissedUpdateVersion(): string | null { try { return localStorage.getItem(DISMISSED_VERSION_KEY); - } catch { + } catch (error) { + console.error("Failed to read dismissed update version from localStorage:", error); return null; } } @@ -12,15 +13,16 @@ export function getDismissedUpdateVersion(): string | null { export function saveDismissedUpdateVersion(version: string): void { try { localStorage.setItem(DISMISSED_VERSION_KEY, version); - } catch { - // localStorage may be unavailable (e.g. private browsing quota exceeded) + } catch (error) { + console.error("Failed to save dismissed update version to localStorage:", error); } } export function getUpdateChecksDisabled(): boolean { try { return localStorage.getItem(CHECKS_DISABLED_KEY) === "true"; - } catch { + } catch (error) { + console.error("Failed to read update checks disabled state from localStorage:", error); return false; } } @@ -28,7 +30,7 @@ export function getUpdateChecksDisabled(): boolean { export function setUpdateChecksDisabled(disabled: boolean): void { try { localStorage.setItem(CHECKS_DISABLED_KEY, disabled ? "true" : "false"); - } catch { - // localStorage may be unavailable + } catch (error) { + console.error("Failed to save update checks disabled state to localStorage:", error); } }