77 lines · 3.1 KB · TypeScript
RawDownload
// Renderer view of the desktop auto-update status the Electron main process// exposes on `window.executor` (see apps/desktop/src/shared/update.ts for the// wire contract and apps/desktop/src/preload for the bridge). On web and// CLI-served installs there is no bridge, so `useDesktopUpdate` returns null and// the shell falls back to its npm UpdateCard. In the desktop app it drives a// native "Restart to update" card instead, because desktop updates ship as a// new bundle via electron-updater, not `npm i -g`.import { useEffect, useMemo, useState } from "react"; export type DesktopUpdateStatus =  | { readonly state: "idle" }  | { readonly state: "available"; readonly version: string }  | { readonly state: "downloading"; readonly version: string; readonly percent: number }  | { readonly state: "downloaded"; readonly version: string }  | { readonly state: "error"; readonly version: string; readonly message: string }  | { readonly state: "installing"; readonly version: string }; type DesktopUpdateBridge = {  getUpdateStatus: () => Promise<DesktopUpdateStatus>;  onUpdateStatus: (cb: (status: DesktopUpdateStatus) => void) => () => void;  installUpdate: () => Promise<void>;}; const getDesktopUpdateBridge = (): DesktopUpdateBridge | null => {  if (typeof window === "undefined") return null;  const candidate = (window as { readonly executor?: Partial<DesktopUpdateBridge> }).executor;  if (    candidate &&    typeof candidate.getUpdateStatus === "function" &&    typeof candidate.onUpdateStatus === "function" &&    typeof candidate.installUpdate === "function"  ) {    // oxlint-disable-next-line executor/no-double-cast -- boundary: narrowed by the typeof guards above    return candidate as DesktopUpdateBridge;  }  return null;}; export interface DesktopUpdate {  readonly status: DesktopUpdateStatus;  /** Apply a downloaded update and restart the app. */  readonly install: () => void;} /** * Subscribe to the desktop app's auto-update status. Returns null when not * running inside the desktop bridge (web / CLI-served), so the caller can fall * back to its npm upgrade card. Hook order is stable: the bridge is resolved * once and the subscription effect always runs. */export function useDesktopUpdate(): DesktopUpdate | null {  const bridge = useMemo(() => getDesktopUpdateBridge(), []);  const [status, setStatus] = useState<DesktopUpdateStatus>({ state: "idle" });   useEffect(() => {    if (!bridge) return;    let active = true;    // The initial snapshot is an async IPC round-trip; a push (e.g. download    // finished) can land first. Never let the stale snapshot overwrite a push    // that already arrived.    let receivedPush = false;    void bridge.getUpdateStatus().then((current) => {      if (active && !receivedPush) setStatus(current);    });    const unsubscribe = bridge.onUpdateStatus((next) => {      receivedPush = true;      setStatus(next);    });    return () => {      active = false;      unsubscribe();    };  }, [bridge]);   if (!bridge) return null;  return { status, install: () => void bridge.installUpdate() };}