Skip to content

Commit 790ba13

Browse files
refactor(mobile): app update checks start with the app (#25856)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent edf5a24 commit 790ba13

4 files changed

Lines changed: 36 additions & 55 deletions

File tree

‎apps/mobile/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import {
8585
} from "./lib/rootNavigation";
8686
import { pruneNewTaskFlowStates } from "./features/threads/new-task-flow-state";
8787
import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen";
88+
import { installAppUpdateChecks } from "./features/updates/app-updates";
8889

8990
import "../global.css";
9091

@@ -176,6 +177,7 @@ const appServiceDisposers = [
176177
startIncomingShareInbox(),
177178
installAgentNotificationNavigation(linkToAppPath),
178179
installAppShortcuts(linkToAppPath),
180+
installAppUpdateChecks(),
179181
];
180182
disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => {
181183
for (const dispose of appServiceDisposers) dispose();

‎apps/mobile/src/features/home/HomeRouteScreen.tsx‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
2222
import { useDeviceType, useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
2323
import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";
2424
import { AndroidScreenHeader } from "../../components/ui/AndroidScreenHeader";
25-
import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";
2625
import { AndroidHomeFabLayout } from "./AndroidHomeFab";
2726
import { HomeScreen } from "./HomeScreen";
2827
import { HomeHeader } from "./HomeHeader";
@@ -95,11 +94,6 @@ export function HomeRouteScreen() {
9594
[selectThreadRoute],
9695
);
9796

98-
useEffect(() => {
99-
void checkForAppUpdateOnLaunch();
100-
startAppUpdateForegroundRecheck();
101-
}, []);
102-
10397
const {
10498
archiveThread,
10599
confirmDeleteThread,

‎apps/mobile/src/features/updates/app-updates.test.ts‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { AppState } from "react-native";
12
import { describe, expect, it, vi } from "vite-plus/test";
23

34
import {
45
createAppUpdateDeferral,
5-
createAppUpdateLaunchCheck,
6+
installAppUpdateChecks,
67
runAppUpdateCheck,
78
type AppUpdateCheckState,
89
type AppUpdateClient,
@@ -234,10 +235,9 @@ describe("runAppUpdateCheck", () => {
234235
const client = makeUpdateClient({
235236
checkForUpdateAsync: vi.fn(() => checkResult),
236237
});
237-
const checkOnLaunch = createAppUpdateLaunchCheck(client);
238238
const manualStates: AppUpdateCheckState[] = [];
239239

240-
const launchCheck = checkOnLaunch();
240+
const launchCheck = runAppUpdateCheck({ client });
241241
const manualCheck = runAppUpdateCheck({
242242
client,
243243
onStateChange: (state) => manualStates.push(state),
@@ -303,16 +303,16 @@ describe("runAppUpdateCheck", () => {
303303
});
304304
});
305305

306-
describe("createAppUpdateLaunchCheck", () => {
307-
it("checks at most once for each JavaScript launch", async () => {
306+
describe("installAppUpdateChecks", () => {
307+
it("checks once at launch and stops listening for foregrounds when disposed", () => {
308+
const remove = vi.fn();
309+
vi.mocked(AppState.addEventListener).mockReturnValueOnce({ remove });
308310
const client = makeUpdateClient();
309-
const checkOnLaunch = createAppUpdateLaunchCheck(client);
310311

311-
const first = checkOnLaunch();
312-
const second = checkOnLaunch();
313-
await first;
314-
315-
expect(second).toBeUndefined();
312+
const dispose = installAppUpdateChecks(client);
316313
expect(client.checkForUpdateAsync).toHaveBeenCalledOnce();
314+
dispose();
315+
316+
expect(remove).toHaveBeenCalledOnce();
317317
});
318318
});

‎apps/mobile/src/features/updates/app-updates.ts‎

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -466,20 +466,6 @@ function isAppUpdateUnavailableError(error: unknown): boolean {
466466
return typeof code === "string" && UPDATE_CHECK_UNAVAILABLE_ERROR_CODES.has(code);
467467
}
468468

469-
export function createAppUpdateLaunchCheck(
470-
client: AppUpdateClient = Updates,
471-
): () => Promise<void> | undefined {
472-
let started = false;
473-
474-
return () => {
475-
if (started || !isAppUpdateCheckAvailable(client)) return undefined;
476-
started = true;
477-
return runAppUpdateCheck({ client });
478-
};
479-
}
480-
481-
export const checkForAppUpdateOnLaunch = createAppUpdateLaunchCheck();
482-
483469
export const FOREGROUND_APP_UPDATE_RECHECK_AFTER_MS = 15 * 60 * 1000;
484470

485471
export function shouldRecheckAppUpdateOnForeground(
@@ -494,31 +480,30 @@ export function shouldRecheckAppUpdateOnForeground(
494480
);
495481
}
496482

497-
export function createAppUpdateForegroundRecheck(
483+
/**
484+
* App-lifetime update checks: one at launch, then again when the app returns
485+
* from a long stay in the background.
486+
*/
487+
export function installAppUpdateChecks(
498488
client: AppUpdateClient = Updates,
499489
deferral: AppUpdateDeferral = appUpdateDeferral,
500490
): () => void {
501-
let started = false;
502-
503-
return () => {
504-
if (started || !isAppUpdateCheckAvailable(client)) return;
505-
started = true;
506-
let backgroundedAtMs: number | null = null;
507-
AppState.addEventListener("change", (state) => {
508-
if (state === "background") {
509-
backgroundedAtMs = Date.now();
510-
return;
511-
}
512-
if (state !== "active") return;
513-
const shouldCheck = shouldRecheckAppUpdateOnForeground(
514-
backgroundedAtMs,
515-
Date.now(),
516-
deferral.pendingInstall,
517-
);
518-
backgroundedAtMs = null;
519-
if (shouldCheck) void runAppUpdateCheck({ client, deferral });
520-
});
521-
};
491+
if (!isAppUpdateCheckAvailable(client)) return () => undefined;
492+
void runAppUpdateCheck({ client, deferral });
493+
let backgroundedAtMs: number | null = null;
494+
const subscription = AppState.addEventListener("change", (state) => {
495+
if (state === "background") {
496+
backgroundedAtMs = Date.now();
497+
return;
498+
}
499+
if (state !== "active") return;
500+
const shouldCheck = shouldRecheckAppUpdateOnForeground(
501+
backgroundedAtMs,
502+
Date.now(),
503+
deferral.pendingInstall,
504+
);
505+
backgroundedAtMs = null;
506+
if (shouldCheck) void runAppUpdateCheck({ client, deferral });
507+
});
508+
return () => subscription.remove();
522509
}
523-
524-
export const startAppUpdateForegroundRecheck = createAppUpdateForegroundRecheck();

0 commit comments

Comments
 (0)