diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 1443544d94697..844835236605b 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -76,6 +76,7 @@ import { rootNavigationRef, } from "./lib/rootNavigation"; import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen"; +import { installAppUpdateChecks } from "./features/updates/app-updates"; import "../global.css"; @@ -160,6 +161,7 @@ const appServiceDisposers = [ startIncomingShareInbox(), installAgentNotificationNavigation(linkToAppPath), installAppShortcuts(linkToAppPath), + installAppUpdateChecks(), ]; disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => { for (const dispose of appServiceDisposers) dispose(); diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 5756a140e779b..b4db8f499ecc9 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -22,7 +22,6 @@ import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { useDeviceType, useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail"; import { AndroidScreenHeader } from "../../components/ui/AndroidScreenHeader"; -import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates"; import { AndroidHomeFabLayout } from "./AndroidHomeFab"; import { HomeScreen } from "./HomeScreen"; import { HomeHeader } from "./HomeHeader"; @@ -95,11 +94,6 @@ export function HomeRouteScreen() { [selectThreadRoute], ); - useEffect(() => { - void checkForAppUpdateOnLaunch(); - startAppUpdateForegroundRecheck(); - }, []); - const { archiveThread, confirmDeleteThread, diff --git a/apps/mobile/src/features/updates/app-updates.test.ts b/apps/mobile/src/features/updates/app-updates.test.ts index 73c95d01d8eb1..882659bbea324 100644 --- a/apps/mobile/src/features/updates/app-updates.test.ts +++ b/apps/mobile/src/features/updates/app-updates.test.ts @@ -1,8 +1,9 @@ +import { AppState } from "react-native"; import { describe, expect, it, vi } from "vite-plus/test"; import { createAppUpdateDeferral, - createAppUpdateLaunchCheck, + installAppUpdateChecks, runAppUpdateCheck, type AppUpdateCheckState, type AppUpdateClient, @@ -234,10 +235,9 @@ describe("runAppUpdateCheck", () => { const client = makeUpdateClient({ checkForUpdateAsync: vi.fn(() => checkResult), }); - const checkOnLaunch = createAppUpdateLaunchCheck(client); const manualStates: AppUpdateCheckState[] = []; - const launchCheck = checkOnLaunch(); + const launchCheck = runAppUpdateCheck({ client }); const manualCheck = runAppUpdateCheck({ client, onStateChange: (state) => manualStates.push(state), @@ -303,16 +303,16 @@ describe("runAppUpdateCheck", () => { }); }); -describe("createAppUpdateLaunchCheck", () => { - it("checks at most once for each JavaScript launch", async () => { +describe("installAppUpdateChecks", () => { + it("checks once at launch and stops listening for foregrounds when disposed", () => { + const remove = vi.fn(); + vi.mocked(AppState.addEventListener).mockReturnValueOnce({ remove }); const client = makeUpdateClient(); - const checkOnLaunch = createAppUpdateLaunchCheck(client); - const first = checkOnLaunch(); - const second = checkOnLaunch(); - await first; - - expect(second).toBeUndefined(); + const dispose = installAppUpdateChecks(client); expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + dispose(); + + expect(remove).toHaveBeenCalledOnce(); }); }); diff --git a/apps/mobile/src/features/updates/app-updates.ts b/apps/mobile/src/features/updates/app-updates.ts index 87111109ebb8a..ead6f68ccc783 100644 --- a/apps/mobile/src/features/updates/app-updates.ts +++ b/apps/mobile/src/features/updates/app-updates.ts @@ -466,20 +466,6 @@ function isAppUpdateUnavailableError(error: unknown): boolean { return typeof code === "string" && UPDATE_CHECK_UNAVAILABLE_ERROR_CODES.has(code); } -export function createAppUpdateLaunchCheck( - client: AppUpdateClient = Updates, -): () => Promise | undefined { - let started = false; - - return () => { - if (started || !isAppUpdateCheckAvailable(client)) return undefined; - started = true; - return runAppUpdateCheck({ client }); - }; -} - -export const checkForAppUpdateOnLaunch = createAppUpdateLaunchCheck(); - export const FOREGROUND_APP_UPDATE_RECHECK_AFTER_MS = 15 * 60 * 1000; export function shouldRecheckAppUpdateOnForeground( @@ -494,31 +480,30 @@ export function shouldRecheckAppUpdateOnForeground( ); } -export function createAppUpdateForegroundRecheck( +/** + * App-lifetime update checks: one at launch, then again when the app returns + * from a long stay in the background. + */ +export function installAppUpdateChecks( client: AppUpdateClient = Updates, deferral: AppUpdateDeferral = appUpdateDeferral, ): () => void { - let started = false; - - return () => { - if (started || !isAppUpdateCheckAvailable(client)) return; - started = true; - let backgroundedAtMs: number | null = null; - AppState.addEventListener("change", (state) => { - if (state === "background") { - backgroundedAtMs = Date.now(); - return; - } - if (state !== "active") return; - const shouldCheck = shouldRecheckAppUpdateOnForeground( - backgroundedAtMs, - Date.now(), - deferral.pendingInstall, - ); - backgroundedAtMs = null; - if (shouldCheck) void runAppUpdateCheck({ client, deferral }); - }); - }; + if (!isAppUpdateCheckAvailable(client)) return () => undefined; + void runAppUpdateCheck({ client, deferral }); + let backgroundedAtMs: number | null = null; + const subscription = AppState.addEventListener("change", (state) => { + if (state === "background") { + backgroundedAtMs = Date.now(); + return; + } + if (state !== "active") return; + const shouldCheck = shouldRecheckAppUpdateOnForeground( + backgroundedAtMs, + Date.now(), + deferral.pendingInstall, + ); + backgroundedAtMs = null; + if (shouldCheck) void runAppUpdateCheck({ client, deferral }); + }); + return () => subscription.remove(); } - -export const startAppUpdateForegroundRecheck = createAppUpdateForegroundRecheck();