import { useCallback } from 'react'; import { atom, useAtom, useSetAtom } from 'jotai'; import { useTauriEvent } from './useTauri'; import { UpdatePhase, parseUpdateError } from '../utils/updateErrors'; type TauriInternals = { invoke: (cmd: string, args?: Record) => Promise }; const tauriInvoke = (): TauriInternals['invoke'] | undefined => (window as unknown as { __TAURI_INTERNALS__?: TauriInternals }).__TAURI_INTERNALS__?.invoke; /** Detail of the native `lotus-update-progress` event (see lib.rs `install_update`). */ export type UpdateProgress = | { phase: 'downloading'; attempt: number; maxAttempts: number; downloaded: number; total: number | null; } | { phase: 'retrying'; attempt: number; maxAttempts: number; waitSecs: number; error: string } | { phase: 'installing' }; export type UpdateStatus = | { state: 'idle' } | { state: 'checking' } | { state: 'up-to-date' } | { state: 'available'; version: string } | { state: 'installing'; progress?: UpdateProgress } | { state: 'error'; phase: UpdatePhase; message: string }; export type UpdateFailure = { phase: UpdatePhase; message: string }; // Module-level so the result of a manual "Check for updates" survives closing // and reopening Settings (it used to be component state, so the "update // available" answer was thrown away with the tab and had to be re-checked). // A transient 'checking'/'installing' state is reset if the tab unmounts // mid-request by the resolve/reject below, so nothing gets stuck. const updateStatusAtom = atom({ state: 'idle' }); /** * Mirror native download progress into the status. Mounted once (in * TauriUpdateFeature) so the listener isn't duplicated per consumer. */ export function useTauriUpdateProgress(): void { const setStatus = useSetAtom(updateStatusAtom); useTauriEvent('lotus-update-progress', (progress) => setStatus((prev) => (prev.state === 'installing' ? { state: 'installing', progress } : prev)), ); } export function useTauriUpdater() { const isTauri = !!tauriInvoke(); const [status, setStatus] = useAtom(updateStatusAtom); const check = useCallback(async () => { const invoke = tauriInvoke(); if (!invoke) return; setStatus({ state: 'checking' }); try { const result = (await invoke('check_for_update')) as { available: boolean; version?: string }; setStatus( result.available && result.version ? { state: 'available', version: result.version } : { state: 'up-to-date' }, ); } catch (e) { setStatus({ state: 'error', ...parseUpdateError(String(e), 'check') }); } }, [setStatus]); /** * Resolves `undefined` when nothing needed installing (a successful install * restarts the app instead), or the failure. */ const install = useCallback(async (): Promise => { const invoke = tauriInvoke(); if (!invoke) return undefined; setStatus({ state: 'installing' }); try { await invoke('install_update'); // On a successful install the native side restarts the app, so this // resolve is only reached when nothing was installed (no update found) — // don't leave the UI stuck on "installing". setStatus({ state: 'up-to-date' }); return undefined; } catch (e) { const failure = parseUpdateError(String(e), 'download'); setStatus({ state: 'error', ...failure }); return failure; } }, [setStatus]); return { isTauri, status, check, install }; }