From 7e6a2100e55cef72bb16b2f0394a691299a3bc9a Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:50:37 +0200 Subject: [PATCH] fix(updater): hold Windows update notice during WinGet moderation window (#1200) * fix(updater): hold Windows update notice during WinGet moderation window * docs(changelog): Windows update notice WinGet moderation delay --- CHANGELOG.md | 6 ++++ src/hooks/useAppUpdater.ts | 8 +++-- .../appUpdaterHelpers.test.ts | 36 +++++++++++++++++++ .../componentHelpers/appUpdaterHelpers.ts | 22 ++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/utils/componentHelpers/appUpdaterHelpers.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a87583c..1b0984bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -351,6 +351,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * If a screen hit an unexpected rendering error, the whole window could go blank with no way back. The app now shows a small recoverable error card (Try again / Reload app) instead, and playback keeps going. +### Windows update notice waits out WinGet moderation + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1200](https://github.com/Psychotoxical/psysonic/pull/1200)** + +* On Windows, the "update available" notice now waits until a release is a couple of days old, so it no longer points to a version that WinGet has not finished publishing yet. macOS and Linux are unaffected. + ## [1.48.1] - 2026-06-15 ## Fixed diff --git a/src/hooks/useAppUpdater.ts b/src/hooks/useAppUpdater.ts index 2ca85ac0..1d945498 100644 --- a/src/hooks/useAppUpdater.ts +++ b/src/hooks/useAppUpdater.ts @@ -4,8 +4,8 @@ import { dirname } from '@tauri-apps/api/path'; import { invoke } from '@tauri-apps/api/core'; import { useTranslation } from 'react-i18next'; import { version as currentVersion } from '../../package.json'; -import { IS_LINUX, IS_MACOS } from '../utils/platform'; -import { SKIP_KEY, isNewer, pickAsset, type ReleaseData, type DlState } from '../utils/componentHelpers/appUpdaterHelpers'; +import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform'; +import { SKIP_KEY, isNewer, isWithinModerationWindow, pickAsset, type ReleaseData, type DlState } from '../utils/componentHelpers/appUpdaterHelpers'; /** All update-modal state, the GitHub release probe and the download/relaunch * handlers. The component owns only the early-return guard and the JSX. */ @@ -36,6 +36,10 @@ export function useAppUpdater() { if (!isNewer(version, currentVersion)) return; const skipped = localStorage.getItem(SKIP_KEY); if (skipped === version) return; + // Windows updates go through WinGet moderation; hold the notice until + // the release clears the moderation window so users aren't pointed at a + // version WinGet hasn't published yet. macOS/Linux use other channels. + if (IS_WINDOWS && isWithinModerationWindow(data.published_at, Date.now())) return; } setDismissed(false); setDlState('idle'); diff --git a/src/utils/componentHelpers/appUpdaterHelpers.test.ts b/src/utils/componentHelpers/appUpdaterHelpers.test.ts new file mode 100644 index 00000000..94c87a5e --- /dev/null +++ b/src/utils/componentHelpers/appUpdaterHelpers.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { isWithinModerationWindow, WINGET_MODERATION_DELAY_MS } from './appUpdaterHelpers'; + +describe('isWithinModerationWindow', () => { + const published = '2026-06-27T00:00:00Z'; + const publishedMs = Date.parse(published); + + it('returns true while the release is younger than the window', () => { + const now = publishedMs + 12 * 60 * 60 * 1000; // 12h after release + expect(isWithinModerationWindow(published, now)).toBe(true); + }); + + it('returns false once the release is older than the window', () => { + const now = publishedMs + WINGET_MODERATION_DELAY_MS + 1; + expect(isWithinModerationWindow(published, now)).toBe(false); + }); + + it('returns false exactly at the window boundary', () => { + const now = publishedMs + WINGET_MODERATION_DELAY_MS; + expect(isWithinModerationWindow(published, now)).toBe(false); + }); + + it('respects a custom window override', () => { + const now = publishedMs + 2 * 60 * 60 * 1000; // 2h after release + expect(isWithinModerationWindow(published, now, 1 * 60 * 60 * 1000)).toBe(false); + expect(isWithinModerationWindow(published, now, 3 * 60 * 60 * 1000)).toBe(true); + }); + + it('fails open on a missing date', () => { + expect(isWithinModerationWindow(undefined, publishedMs)).toBe(false); + }); + + it('fails open on an unparseable date', () => { + expect(isWithinModerationWindow('not-a-date', publishedMs)).toBe(false); + }); +}); diff --git a/src/utils/componentHelpers/appUpdaterHelpers.ts b/src/utils/componentHelpers/appUpdaterHelpers.ts index 3872b08e..490d428c 100644 --- a/src/utils/componentHelpers/appUpdaterHelpers.ts +++ b/src/utils/componentHelpers/appUpdaterHelpers.ts @@ -13,6 +13,28 @@ export function isNewer(a: string, b: string): boolean { return false; } +// Windows updates ship through WinGet, which moderates each new release for a +// while after the GitHub release goes live (installer scan + manual review, +// longer for freshly signed binaries building SmartScreen reputation). Holding +// the update notice back until the release clears this window avoids pointing +// Windows users at a version WinGet has not published yet. Conservative start +// value — tune down once real winget-pkgs merge times are known. +export const WINGET_MODERATION_DELAY_MS = 48 * 60 * 60 * 1000; + +// True while `publishedAt` is younger than `windowMs` relative to `now`. +// Missing or unparseable date → false (fail open: show the notice rather than +// hide it indefinitely). Platform-agnostic so the time logic stays testable. +export function isWithinModerationWindow( + publishedAt: string | undefined, + now: number, + windowMs: number = WINGET_MODERATION_DELAY_MS, +): boolean { + if (!publishedAt) return false; + const published = Date.parse(publishedAt); + if (Number.isNaN(published)) return false; + return now - published < windowMs; +} + export interface GithubAsset { name: string; browser_download_url: string;