From 5aba30eb4f4f27cf2f5e24dffb1e82eb6bb3de92 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Wed, 15 Apr 2026 15:57:51 +0800 Subject: [PATCH] fix(desktop): derive MAS review state from OTA versions --- .../renderer/src/atoms/server-configs.ts | 31 +++++--- .../src/lib/__tests__/mas-review.test.ts | 73 +++++++++++++++++++ .../layer/renderer/src/lib/mas-review.ts | 45 ++++++++++++ .../src/providers/server-configs-provider.tsx | 9 ++- .../renderer/src/queries/ota-versions.ts | 25 +++++++ 5 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/lib/__tests__/mas-review.test.ts create mode 100644 apps/desktop/layer/renderer/src/lib/mas-review.ts create mode 100644 apps/desktop/layer/renderer/src/queries/ota-versions.ts diff --git a/apps/desktop/layer/renderer/src/atoms/server-configs.ts b/apps/desktop/layer/renderer/src/atoms/server-configs.ts index 96b342b52..861d10225 100644 --- a/apps/desktop/layer/renderer/src/atoms/server-configs.ts +++ b/apps/desktop/layer/renderer/src/atoms/server-configs.ts @@ -4,6 +4,7 @@ import PKG from "@pkg" import { atomWithStorage } from "jotai/utils" import { createAtomHooks } from "~/lib/jotai" +import { isLocalMASVersionInReview } from "~/lib/mas-review" export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = createAtomHooks( atomWithStorage>>( @@ -16,26 +17,32 @@ export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = crea ), ) +export const [, , useMASStoreVersion, , getMASStoreVersion, setMASStoreVersion] = createAtomHooks( + atomWithStorage(getStorageNS("mas-store-version"), null, undefined, { + getOnInit: true, + }), +) + export type ServerConfigs = ExtractResponseData export type PaymentPlan = ServerConfigs["PAYMENT_PLAN_LIST"][number] export type PaymentFeature = PaymentPlan["limit"] export const useIsInMASReview = () => { - const serverConfigs = useServerConfigs() - return ( - typeof process !== "undefined" && - process.mas && - serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version - ) + const masStoreVersion = useMASStoreVersion() + return isLocalMASVersionInReview({ + isMASBuild: typeof process !== "undefined" && !!process.mas, + localVersion: PKG.version, + storeVersion: masStoreVersion, + }) } export const getIsInMASReview = () => { - const serverConfigs = getServerConfigs() - return ( - typeof process !== "undefined" && - process.mas && - serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version - ) + const masStoreVersion = getMASStoreVersion() + return isLocalMASVersionInReview({ + isMASBuild: typeof process !== "undefined" && !!process.mas, + localVersion: PKG.version, + storeVersion: masStoreVersion, + }) } export const useIsPaymentEnabled = () => { diff --git a/apps/desktop/layer/renderer/src/lib/__tests__/mas-review.test.ts b/apps/desktop/layer/renderer/src/lib/__tests__/mas-review.test.ts new file mode 100644 index 000000000..3f85b17ad --- /dev/null +++ b/apps/desktop/layer/renderer/src/lib/__tests__/mas-review.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest" + +import { getMASStoreVersionFromOTAVersions, isLocalMASVersionInReview } from "../mas-review" + +describe("getMASStoreVersionFromOTAVersions", () => { + it("reads the MAS version from the OTA payload", () => { + expect( + getMASStoreVersionFromOTAVersions({ + store: { + desktop: { + mas: { + version: "1.6.0", + }, + }, + }, + }), + ).toBe("1.6.0") + }) + + it("returns null when the OTA payload has no MAS version", () => { + expect(getMASStoreVersionFromOTAVersions({})).toBeNull() + }) +}) + +describe("isLocalMASVersionInReview", () => { + it("returns true when the local MAS build is newer than the store version", () => { + expect( + isLocalMASVersionInReview({ + isMASBuild: true, + localVersion: "1.6.1", + storeVersion: "1.6.0", + }), + ).toBe(true) + }) + + it("returns false when the local version matches the store version", () => { + expect( + isLocalMASVersionInReview({ + isMASBuild: true, + localVersion: "1.6.1", + storeVersion: "1.6.1", + }), + ).toBe(false) + }) + + it("returns false when the build is not a MAS build", () => { + expect( + isLocalMASVersionInReview({ + isMASBuild: false, + localVersion: "1.6.1", + storeVersion: "1.6.0", + }), + ).toBe(false) + }) + + it("returns false when the store version is missing or invalid", () => { + expect( + isLocalMASVersionInReview({ + isMASBuild: true, + localVersion: "1.6.1", + storeVersion: null, + }), + ).toBe(false) + + expect( + isLocalMASVersionInReview({ + isMASBuild: true, + localVersion: "1.6.1", + storeVersion: "latest", + }), + ).toBe(false) + }) +}) diff --git a/apps/desktop/layer/renderer/src/lib/mas-review.ts b/apps/desktop/layer/renderer/src/lib/mas-review.ts new file mode 100644 index 000000000..f1ac14c39 --- /dev/null +++ b/apps/desktop/layer/renderer/src/lib/mas-review.ts @@ -0,0 +1,45 @@ +import { gt, valid } from "semver" + +export interface OTAVersionsResponse { + store?: { + desktop?: { + mas?: { + version?: null | string + } + } + } +} + +const normalizeVersion = (version?: null | string) => { + if (!version) { + return null + } + + return valid(version.trim()) +} + +export const getMASStoreVersionFromOTAVersions = (payload: OTAVersionsResponse) => + payload.store?.desktop?.mas?.version ?? null + +export const isLocalMASVersionInReview = ({ + isMASBuild, + localVersion, + storeVersion, +}: { + isMASBuild: boolean + localVersion: string + storeVersion?: null | string +}) => { + if (!isMASBuild) { + return false + } + + const normalizedLocalVersion = normalizeVersion(localVersion) + const normalizedStoreVersion = normalizeVersion(storeVersion) + + if (!normalizedLocalVersion || !normalizedStoreVersion) { + return false + } + + return gt(normalizedLocalVersion, normalizedStoreVersion) +} diff --git a/apps/desktop/layer/renderer/src/providers/server-configs-provider.tsx b/apps/desktop/layer/renderer/src/providers/server-configs-provider.tsx index 9e8a70443..e89a7aa03 100644 --- a/apps/desktop/layer/renderer/src/providers/server-configs-provider.tsx +++ b/apps/desktop/layer/renderer/src/providers/server-configs-provider.tsx @@ -1,11 +1,13 @@ import { useEffect } from "react" -import { setServerConfigs } from "~/atoms/server-configs" +import { setMASStoreVersion, setServerConfigs } from "~/atoms/server-configs" import { syncServerShortcuts } from "~/atoms/settings/ai" +import { useMASStoreVersionQuery } from "~/queries/ota-versions" import { useServerConfigsQuery } from "~/queries/server-configs" export const ServerConfigsProvider = () => { const serverConfigs = useServerConfigsQuery() + const masStoreVersion = useMASStoreVersionQuery() useEffect(() => { if (!serverConfigs) return @@ -13,5 +15,10 @@ export const ServerConfigsProvider = () => { syncServerShortcuts(serverConfigs.AI_SHORTCUTS) }, [serverConfigs]) + useEffect(() => { + if (masStoreVersion === undefined) return + setMASStoreVersion(masStoreVersion) + }, [masStoreVersion]) + return null } diff --git a/apps/desktop/layer/renderer/src/queries/ota-versions.ts b/apps/desktop/layer/renderer/src/queries/ota-versions.ts new file mode 100644 index 000000000..39ced69b7 --- /dev/null +++ b/apps/desktop/layer/renderer/src/queries/ota-versions.ts @@ -0,0 +1,25 @@ +import { useQuery } from "@tanstack/react-query" +import { ofetch } from "ofetch" + +import type { OTAVersionsResponse } from "~/lib/mas-review" +import { getMASStoreVersionFromOTAVersions } from "~/lib/mas-review" + +const OTA_VERSIONS_URL = "https://ota.folo.is/versions" + +const isMASBuild = () => typeof process !== "undefined" && !!process.mas + +export const useMASStoreVersionQuery = () => { + const { data } = useQuery({ + queryKey: ["ota-versions", "store", "desktop", "mas"], + queryFn: async () => { + const response = await ofetch(OTA_VERSIONS_URL, { + cache: "no-store", + }) + + return getMASStoreVersionFromOTAVersions(response) + }, + enabled: isMASBuild(), + }) + + return data +}