import { getBrowserDisplayName } from "@/lib/browser-utils"; import { showAutoUpdateToast, showToast } from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useRef, useState } from "react"; interface UpdateNotification { id: string; browser: string; current_version: string; new_version: string; affected_profiles: string[]; is_stable_update: boolean; timestamp: number; is_rolling_release: boolean; } export function useUpdateNotifications( onProfilesUpdated?: () => Promise, ) { const [notifications, setNotifications] = useState([]); const [updatingBrowsers, setUpdatingBrowsers] = useState>( new Set(), ); const [processedNotifications, setProcessedNotifications] = useState< Set >(new Set()); // Add refs to track ongoing operations to prevent duplicates const isCheckingForUpdates = useRef(false); const activeDownloads = useRef>(new Set()); // Track "browser-version" keys const checkForUpdates = useCallback(async () => { // Prevent multiple simultaneous calls if (isCheckingForUpdates.current) { console.log("Already checking for updates, skipping duplicate call"); return; } isCheckingForUpdates.current = true; try { const updates = await invoke( "check_for_browser_updates", ); // Filter out already processed notifications const newUpdates = updates.filter((notification) => { return !processedNotifications.has(notification.id); }); setNotifications(newUpdates); // Automatically start downloads for new update notifications for (const notification of newUpdates) { if (!processedNotifications.has(notification.id)) { setProcessedNotifications((prev) => new Set(prev).add(notification.id), ); // Start automatic update without user interaction void handleAutoUpdate( notification.browser, notification.new_version, notification.id, ); } } } catch (error) { console.error("Failed to check for updates:", error); } finally { isCheckingForUpdates.current = false; } }, [processedNotifications]); const handleAutoUpdate = useCallback( async (browser: string, newVersion: string, notificationId: string) => { const downloadKey = `${browser}-${newVersion}`; // Check if this download is already in progress if (activeDownloads.current.has(downloadKey)) { console.log( `Download already in progress for ${downloadKey}, skipping duplicate`, ); return; } // Mark download as active activeDownloads.current.add(downloadKey); try { setUpdatingBrowsers((prev) => new Set(prev).add(browser)); const browserDisplayName = getBrowserDisplayName(browser); // Dismiss the notification in the backend await invoke("dismiss_update_notification", { notificationId, }); // Show auto-update started toast showAutoUpdateToast(browserDisplayName, newVersion); try { // Check if browser already exists before downloading const isDownloaded = await invoke("check_browser_exists", { browserStr: browser, version: newVersion, }); if (isDownloaded) { // Browser already exists, skip download and go straight to profile update console.log( `${browserDisplayName} ${newVersion} already exists, skipping download`, ); } else { // Mark download as auto-update in the backend for toast suppression await invoke("mark_auto_update_download", { browser, version: newVersion, }); // Download the browser (progress will be handled by use-browser-download hook) await invoke("download_browser", { browserStr: browser, version: newVersion, }); } // Complete the update with auto-update of profile versions const updatedProfiles = await invoke( "complete_browser_update_with_auto_update", { browser, newVersion, }, ); // Show success message based on whether profiles were updated if (updatedProfiles.length > 0) { const profileText = updatedProfiles.length === 1 ? `Profile "${updatedProfiles[0]}" has been updated` : `${updatedProfiles.length} profiles have been updated`; showToast({ id: `auto-update-success-${browser}-${newVersion}`, type: "success", title: `${browserDisplayName} update completed`, description: `${profileText} to version ${newVersion}. To update running profiles, restart them.`, duration: 5000, }); } else { showToast({ id: `auto-update-success-${browser}-${newVersion}`, type: "success", title: `${browserDisplayName} update ready`, description: "All affected profiles are currently running. To update them, restart them.", duration: 5000, }); } // Trigger profile refresh to update UI with new versions if (onProfilesUpdated) { void onProfilesUpdated(); } } catch (downloadError) { console.error("Failed to download browser:", downloadError); // Clean up auto-update tracking on error try { await invoke("remove_auto_update_download", { browser, version: newVersion, }); } catch (e) { console.error("Failed to clean up auto-update tracking:", e); } showToast({ id: `auto-update-error-${browser}-${newVersion}`, type: "error", title: `Failed to download ${browserDisplayName} ${newVersion}`, description: String(downloadError), duration: 6000, }); throw downloadError; } // Don't call checkForUpdates() again here as it can cause recursion and duplicates // The periodic checks will handle finding any remaining updates } catch (error) { console.error("Failed to start auto-update:", error); const browserDisplayName = getBrowserDisplayName(browser); showToast({ id: `auto-update-error-${browser}-${newVersion}`, type: "error", title: `Failed to update ${browserDisplayName}`, description: String(error), duration: 6000, }); } finally { // Remove from active downloads and updating browsers activeDownloads.current.delete(downloadKey); setUpdatingBrowsers((prev) => { const next = new Set(prev); next.delete(browser); return next; }); } }, [onProfilesUpdated], ); // Clean up notifications when they're no longer needed useEffect(() => { // Remove notifications that have been processed setNotifications((prev) => prev.filter( (notification) => !processedNotifications.has(notification.id), ), ); }, [processedNotifications]); return { notifications, checkForUpdates, isUpdating: (browser: string) => updatingBrowsers.has(browser), }; }