208 lines
5.3 KiB
TypeScript
208 lines
5.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
checkDesktopAppUpdate,
|
|
formatVersionWithPrefix,
|
|
installDesktopAppUpdate,
|
|
shouldShowDesktopUpdateSection,
|
|
type DesktopAppUpdateCheckResult,
|
|
type DesktopAppUpdateInstallResult,
|
|
} from "@/desktop/updates/desktop-updates";
|
|
|
|
export type DesktopAppUpdateStatus =
|
|
| "idle"
|
|
| "checking"
|
|
| "pending"
|
|
| "up-to-date"
|
|
| "available"
|
|
| "installing"
|
|
| "installed"
|
|
| "error";
|
|
|
|
const PENDING_RECHECK_MS = 10_000;
|
|
|
|
export interface UseDesktopAppUpdaterReturn {
|
|
isDesktopApp: boolean;
|
|
status: DesktopAppUpdateStatus;
|
|
statusText: string;
|
|
availableUpdate: DesktopAppUpdateCheckResult | null;
|
|
errorMessage: string | null;
|
|
lastCheckedAt: number | null;
|
|
isChecking: boolean;
|
|
isInstalling: boolean;
|
|
checkForUpdates: (options?: { silent?: boolean }) => Promise<DesktopAppUpdateCheckResult | null>;
|
|
installUpdate: () => Promise<DesktopAppUpdateInstallResult | null>;
|
|
}
|
|
|
|
function getErrorMessage(error: unknown): string {
|
|
if (error instanceof Error && typeof error.message === "string") {
|
|
return error.message;
|
|
}
|
|
return String(error);
|
|
}
|
|
|
|
function formatStatusText(input: {
|
|
status: DesktopAppUpdateStatus;
|
|
availableUpdate: DesktopAppUpdateCheckResult | null;
|
|
installMessage: string | null;
|
|
}): string {
|
|
const { status, availableUpdate, installMessage } = input;
|
|
|
|
if (status === "checking") {
|
|
return "Checking for app updates...";
|
|
}
|
|
|
|
if (status === "installing") {
|
|
return "Installing app update...";
|
|
}
|
|
|
|
if (status === "up-to-date") {
|
|
return "App is up to date.";
|
|
}
|
|
|
|
if (status === "pending") {
|
|
return "We'll let you know when the update is ready.";
|
|
}
|
|
|
|
if (status === "available") {
|
|
if (availableUpdate?.latestVersion) {
|
|
return `Update ready: ${formatVersionWithPrefix(availableUpdate.latestVersion)}`;
|
|
}
|
|
return "An app update is ready to install.";
|
|
}
|
|
|
|
if (status === "installed") {
|
|
return installMessage ?? "App update installed. Restart required.";
|
|
}
|
|
|
|
if (status === "error") {
|
|
return "Failed to update app.";
|
|
}
|
|
|
|
return "Update status has not been checked yet.";
|
|
}
|
|
|
|
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|
const isDesktopApp = shouldShowDesktopUpdateSection();
|
|
const requestVersionRef = useRef(0);
|
|
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
|
|
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
const [installMessage, setInstallMessage] = useState<string | null>(null);
|
|
const [lastCheckedAt, setLastCheckedAt] = useState<number | null>(null);
|
|
|
|
const checkForUpdates = useCallback(
|
|
async (options: { silent?: boolean } = {}) => {
|
|
if (!isDesktopApp) {
|
|
return null;
|
|
}
|
|
|
|
const requestVersion = requestVersionRef.current + 1;
|
|
requestVersionRef.current = requestVersion;
|
|
|
|
if (!options.silent) {
|
|
setStatus("checking");
|
|
}
|
|
setErrorMessage(null);
|
|
|
|
try {
|
|
const result = await checkDesktopAppUpdate();
|
|
if (requestVersion !== requestVersionRef.current) {
|
|
return result;
|
|
}
|
|
|
|
setInstallMessage(null);
|
|
setLastCheckedAt(Date.now());
|
|
|
|
if (result.readyToInstall) {
|
|
setAvailableUpdate(result);
|
|
setStatus("available");
|
|
} else if (result.hasUpdate) {
|
|
setAvailableUpdate(null);
|
|
setStatus("pending");
|
|
} else {
|
|
setAvailableUpdate(null);
|
|
setStatus("up-to-date");
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
if (requestVersion !== requestVersionRef.current) {
|
|
return null;
|
|
}
|
|
|
|
const message = getErrorMessage(error);
|
|
if (options.silent) {
|
|
console.warn("[DesktopUpdater] Silent update check failed", message);
|
|
} else {
|
|
setStatus("error");
|
|
setErrorMessage(message);
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
[isDesktopApp],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!isDesktopApp || status !== "pending") {
|
|
return undefined;
|
|
}
|
|
|
|
const intervalId = setInterval(() => {
|
|
void checkForUpdates({ silent: true });
|
|
}, PENDING_RECHECK_MS);
|
|
|
|
return () => {
|
|
clearInterval(intervalId);
|
|
};
|
|
}, [checkForUpdates, isDesktopApp, status]);
|
|
|
|
const installUpdate = useCallback(async () => {
|
|
if (!isDesktopApp) {
|
|
return null;
|
|
}
|
|
|
|
setStatus("installing");
|
|
setErrorMessage(null);
|
|
|
|
try {
|
|
const result = await installDesktopAppUpdate();
|
|
setLastCheckedAt(Date.now());
|
|
|
|
if (result.installed) {
|
|
setAvailableUpdate(null);
|
|
setInstallMessage(result.message);
|
|
setStatus("installed");
|
|
} else {
|
|
setAvailableUpdate(null);
|
|
setInstallMessage(result.message);
|
|
setStatus("up-to-date");
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
const message = getErrorMessage(error);
|
|
setStatus("error");
|
|
setErrorMessage(message);
|
|
return null;
|
|
}
|
|
}, [isDesktopApp]);
|
|
|
|
return {
|
|
isDesktopApp,
|
|
status,
|
|
statusText: formatStatusText({
|
|
status,
|
|
availableUpdate,
|
|
installMessage,
|
|
}),
|
|
availableUpdate,
|
|
errorMessage,
|
|
lastCheckedAt,
|
|
isChecking: status === "checking",
|
|
isInstalling: status === "installing",
|
|
checkForUpdates,
|
|
installUpdate,
|
|
};
|
|
}
|