Files
paseo/packages/app/src/desktop/updates/use-desktop-app-updater.ts
T
Mohamed Boudra eca0a5bf67 Let manual update checks bypass rollout
Automatic desktop update checks still respect rollout admission. Manual checks carry an explicit intent through the app and desktop updater service, and the up-to-date state now shows when the last check completed.
2026-06-09 15:47:30 +07:00

126 lines
3.7 KiB
TypeScript

import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
import {
checkDesktopAppUpdate,
formatVersionWithPrefix,
installDesktopAppUpdate,
shouldShowDesktopUpdateSection,
type DesktopAppUpdateCheckResult,
type DesktopAppUpdateCheckIntent,
type DesktopAppUpdateInstallResult,
} from "@/desktop/updates/desktop-updates";
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
import { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
import {
PENDING_RECHECK_MS,
createDesktopAppUpdater,
formatStatusText,
type DesktopAppUpdateStatus,
} from "@/desktop/updates/desktop-app-updater";
import { formatMessageTimestamp } from "@/utils/time";
export type { DesktopAppUpdateStatus };
export interface UseDesktopAppUpdaterReturn {
isDesktopApp: boolean;
status: DesktopAppUpdateStatus;
statusText: string;
availableUpdate: DesktopAppUpdateCheckResult | null;
errorMessage: string | null;
lastCheckedAt: number | null;
isChecking: boolean;
isInstalling: boolean;
checkForUpdates: (options?: {
intent?: DesktopAppUpdateCheckIntent;
silent?: boolean;
}) => Promise<DesktopAppUpdateCheckResult | null>;
installUpdate: () => Promise<DesktopAppUpdateInstallResult | null>;
}
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
const isDesktopApp = shouldShowDesktopUpdateSection();
const { settings: desktopSettings } = useDesktopSettings();
const releaseChannel = desktopSettings.releaseChannel;
const reportError = useDesktopIpcErrorReporter();
const updater = useMemo(
() =>
createDesktopAppUpdater({
port: {
checkDesktopAppUpdate,
installDesktopAppUpdate,
},
now: () => Date.now(),
reportInstallError: reportError,
}),
[reportError],
);
const snapshot = useSyncExternalStore(
updater.subscribe,
updater.getSnapshot,
updater.getSnapshot,
);
const checkForUpdates = useCallback(
async (options: { intent?: DesktopAppUpdateCheckIntent; silent?: boolean } = {}) => {
if (!isDesktopApp) {
return null;
}
return updater.checkForUpdates({
releaseChannel,
intent: options.intent ?? "manual",
silent: options.silent,
});
},
[isDesktopApp, releaseChannel, updater],
);
const installUpdate = useCallback(async () => {
if (!isDesktopApp) {
return null;
}
return updater.installUpdate({ releaseChannel });
}, [isDesktopApp, releaseChannel, updater]);
useEffect(() => {
if (!isDesktopApp) {
return;
}
void checkForUpdates({ intent: "automatic", silent: true });
}, [checkForUpdates, isDesktopApp]);
useEffect(() => {
if (!isDesktopApp || snapshot.status !== "pending") {
return undefined;
}
const intervalId = setInterval(() => {
void checkForUpdates({ intent: "automatic", silent: true });
}, PENDING_RECHECK_MS);
return () => {
clearInterval(intervalId);
};
}, [checkForUpdates, isDesktopApp, snapshot.status]);
return {
isDesktopApp,
status: snapshot.status,
statusText: formatStatusText({
status: snapshot.status,
availableUpdate: snapshot.availableUpdate,
installMessage: snapshot.installMessage,
lastCheckedAt: snapshot.lastCheckedAt,
formatVersion: formatVersionWithPrefix,
formatLastCheckedAt: (timestamp) => formatMessageTimestamp(new Date(timestamp)),
}),
availableUpdate: snapshot.availableUpdate,
errorMessage: snapshot.errorMessage,
lastCheckedAt: snapshot.lastCheckedAt,
isChecking: snapshot.isChecking,
isInstalling: snapshot.isInstalling,
checkForUpdates,
installUpdate,
};
}