Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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();
Expand Down
6 changes: 0 additions & 6 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -95,11 +94,6 @@ export function HomeRouteScreen() {
[selectThreadRoute],
);

useEffect(() => {
void checkForAppUpdateOnLaunch();
startAppUpdateForegroundRecheck();
}, []);

const {
archiveThread,
confirmDeleteThread,
Expand Down
22 changes: 11 additions & 11 deletions apps/mobile/src/features/updates/app-updates.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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();
});
});
61 changes: 23 additions & 38 deletions apps/mobile/src/features/updates/app-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> | 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(
Expand All @@ -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();
Loading