import React, { useState, useEffect, useCallback } from 'react'; import { Box, Text } from 'folds'; import * as css from './UpdateNotification.css'; interface UpdateInfo { version: string; releaseNotes?: string; releaseDate?: string; mock?: boolean; } interface DownloadProgress { percent: number; transferred: number; total: number; mock?: boolean; } type UpdaterPhase = 'idle' | 'checking' | 'available' | 'downloading' | 'ready'; export function UpdateNotification() { const [phase, setPhase] = useState('idle'); const [updateInfo, setUpdateInfo] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); const [isMock, setIsMock] = useState(false); const [hovered, setHovered] = useState(false); useEffect(() => { const electron = (window as any).electron; if (!electron?.updater) return undefined; const { updater } = electron; updater.isMock?.().then((result: { success?: boolean; data?: { mock?: boolean } }) => { if (result?.data?.mock) setIsMock(true); }).catch(() => {}); updater.onUpdateAvailable((info: UpdateInfo) => { setUpdateInfo(info); if (info.mock) setIsMock(true); setPhase('available'); setDownloadProgress(0); }); updater.onUpdateDownloadProgress((progress: DownloadProgress) => { setPhase('downloading'); setDownloadProgress(Math.min(100, Math.round(progress.percent))); }); updater.onUpdateDownloaded((info: UpdateInfo) => { setUpdateInfo(info); setPhase('ready'); setDownloadProgress(100); }); const onNotAvailable = () => { setPhase('idle'); setUpdateInfo(null); setDownloadProgress(0); }; // Optional channel — ignore if preload doesn't expose a dedicated listener if (electron.updater.onUpdateNotAvailable) { electron.updater.onUpdateNotAvailable(onNotAvailable); } return undefined; }, []); const handleCheck = useCallback(async () => { setPhase('checking'); try { const result = await (window as any).electron.updater.checkForUpdates(); if (!result?.success) { setPhase('idle'); } // available event will advance phase; if nothing comes, fall back window.setTimeout(() => { setPhase((current) => (current === 'checking' ? 'idle' : current)); }, 4000); } catch { setPhase('idle'); } }, []); const handleDownload = useCallback(async () => { setPhase('downloading'); setDownloadProgress(0); try { await (window as any).electron.updater.downloadUpdate(); } catch { setPhase('available'); } }, []); const handleInstall = useCallback(async () => { try { await (window as any).electron.updater.installUpdate(); if (isMock) { setPhase('idle'); setUpdateInfo(null); setDownloadProgress(0); } } catch { // keep ready state } }, [isMock]); const handleDismiss = useCallback(() => { setPhase('idle'); setUpdateInfo(null); setDownloadProgress(0); }, []); if (typeof window === 'undefined' || !(window as any).electron?.updater) { return null; } if (phase === 'idle') { return (
setHovered(true)} onMouseLeave={() => setHovered(false)} >
); } if (phase === 'checking') { return (
Checking…
); } if (phase === 'downloading') { return (
{downloadProgress}%
); } if (phase === 'ready') { return ( {isMock ? 'Mock ready' : 'Update ready'} {updateInfo ? ` · ${updateInfo.version}` : ''} ); } // available return ( {isMock ? 'Mock update' : 'Update'} {updateInfo ? ` ${updateInfo.version}` : ''} ); }