From 53d2dde87aea3c325cc5fc767d9c17fff7fb0373 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 15 Jul 2026 14:02:12 +0300 Subject: [PATCH] fix: update check for desktop app --- packages/ui/src/stores/useUpdateStore.ts | 33 +++++++++++++++++-- .../server/lib/opencode/openchamber-routes.js | 1 + packages/web/server/lib/package-manager.js | 7 ++-- .../web/server/lib/package-manager.test.js | 8 +++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/stores/useUpdateStore.ts b/packages/ui/src/stores/useUpdateStore.ts index 22c2fed8..18a24875 100644 --- a/packages/ui/src/stores/useUpdateStore.ts +++ b/packages/ui/src/stores/useUpdateStore.ts @@ -39,6 +39,23 @@ interface UpdateStore extends UpdateState { type ClientRuntime = 'desktop' | 'web' | 'vscode' | 'mobile'; +const CLIENT_INSTALL_ID_KEY = 'openchamber.update-install-id'; + +function getClientInstallId(): string | undefined { + if (typeof window === 'undefined' || typeof crypto.randomUUID !== 'function') return undefined; + + try { + const existing = window.localStorage.getItem(CLIENT_INSTALL_ID_KEY)?.trim(); + if (existing) return existing; + + const installId = crypto.randomUUID(); + window.localStorage.setItem(CLIENT_INSTALL_ID_KEY, installId); + return installId; + } catch { + return undefined; + } +} + function detectDeviceClass(): 'mobile' | 'tablet' | 'desktop' | 'unknown' { if (typeof window === 'undefined') return 'unknown'; try { @@ -86,6 +103,10 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams { params.set('deviceClass', detectDeviceClass()); params.set('arch', detectArch()); params.set('platform', detectPlatform()); + if (shouldReportUsage && (runtime === 'desktop' || runtime === 'mobile')) { + const installId = getClientInstallId(); + if (installId) params.set('installId', installId); + } if (runtime === 'desktop') { params.set('appType', 'desktop-electron'); params.set('instanceMode', isDesktopLocalOriginActive() ? 'local' : 'remote'); @@ -186,13 +207,21 @@ export const useUpdateStore = create()((set, get) => ({ let suggestedSec: number | null = null; if (runtime === 'desktop') { - const desktopInfo = await checkForDesktopUpdates(); + const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : undefined; + const [desktopResult, apiResult] = await Promise.allSettled([ + checkForDesktopUpdates(), + checkForWebUpdates('desktop', appVersion), + ]); + const desktopInfo = desktopResult.status === 'fulfilled' ? desktopResult.value : null; + suggestedSec = apiResult.status === 'fulfilled' + ? (apiResult.value?.nextSuggestedCheckInSec ?? null) + : null; set({ checking: false, available: desktopInfo?.available ?? false, info: desktopInfo, lastChecked: Date.now(), - nextCheckInSec: null, + nextCheckInSec: suggestedSec, }); return suggestedSec; diff --git a/packages/web/server/lib/opencode/openchamber-routes.js b/packages/web/server/lib/opencode/openchamber-routes.js index 21694abb..4c3d88f5 100644 --- a/packages/web/server/lib/opencode/openchamber-routes.js +++ b/packages/web/server/lib/opencode/openchamber-routes.js @@ -39,6 +39,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => { arch: parseString(req.query.arch), instanceMode: parseString(req.query.instanceMode), currentVersion: parseString(req.query.currentVersion), + installId: parseString(req.query.installId), reportUsage: parseReportUsage(parseString(req.query.reportUsage)), }); res.json(updateInfo); diff --git a/packages/web/server/lib/package-manager.js b/packages/web/server/lib/package-manager.js index 153a84cc..692a343f 100644 --- a/packages/web/server/lib/package-manager.js +++ b/packages/web/server/lib/package-manager.js @@ -125,9 +125,10 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) { const appType = normalizeAppType(options.appType); const hostPlatform = mapPlatform(process.platform); const hostArch = mapArch(process.arch); - const shouldTrustClientPlatform = appType === 'vscode' || appType === 'mobile-capacitor'; + const shouldTrustClientPlatform = appType === 'desktop-electron' || appType === 'vscode' || appType === 'mobile-capacitor'; const platform = shouldTrustClientPlatform ? normalizePlatform(options.platform) : hostPlatform; const arch = shouldTrustClientPlatform ? normalizeArch(options.arch) : hostArch; + const reportUsage = options.reportUsage !== false; const payload = { appType, deviceClass: normalizeDeviceClass(options.deviceClass), @@ -135,9 +136,9 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) { arch, channel: 'stable', currentVersion, - installId: getOrCreateInstallId(appType), + installId: reportUsage ? (options.installId || getOrCreateInstallId(appType)) : undefined, instanceMode: options.instanceMode || 'unknown', - reportUsage: options.reportUsage !== false, + reportUsage, }; const response = await fetch(UPDATE_CHECK_URL, { diff --git a/packages/web/server/lib/package-manager.test.js b/packages/web/server/lib/package-manager.test.js index 24867d28..10af71ed 100644 --- a/packages/web/server/lib/package-manager.test.js +++ b/packages/web/server/lib/package-manager.test.js @@ -134,11 +134,19 @@ describe('checkForUpdates', () => { const result = await checkForUpdates({ appType: 'desktop-electron', currentVersion: '1.9.10', + installId: '4f4dfead-9688-4c4f-97d7-4607fbbfc3ab', + platform: 'windows', + arch: 'arm64', }); expect(result.available).toBe(true); expect(result.version).toBe('1.10.0'); expect(fetchMock).toHaveBeenCalledTimes(1); + expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({ + installId: '4f4dfead-9688-4c4f-97d7-4607fbbfc3ab', + platform: 'windows', + arch: 'arm64', + }); }); it('resolves an Android APK asset when the update API returns an AAB', async () => {