import { useLocation, useNavigate } from "react-router-dom"; import { useCallback, useEffect, useRef, useState } from "react"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import Card from "@/components/Card"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { Button } from "@components/Button"; import { UpdateState, useDeviceStore, useUpdateStore } from "@/hooks/stores"; import notifications from "@/notifications"; import LoadingSpinner from "@/components/LoadingSpinner"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; export default function SettingsGeneralUpdateRoute() { const navigate = useNavigate(); const location = useLocation(); const { updateSuccess } = location.state || {}; const { setModalView, otaState } = useUpdateStore(); const [send] = useJsonRpc(); const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); setModalView("updating"); }, [send, setModalView]); useEffect(() => { if (otaState.updating) { setModalView("updating"); } else if (otaState.error) { setModalView("error"); } else if (updateSuccess) { setModalView("updateCompleted"); } else { setModalView("loading"); } }, [otaState.updating, otaState.error, setModalView, updateSuccess]); { /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ } return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; } export interface SystemVersionInfo { local: { appVersion: string; systemVersion: string }; remote: { appVersion: string; systemVersion: string }; systemUpdateAvailable: boolean; appUpdateAvailable: boolean; } export function Dialog({ onClose, onConfirmUpdate, }: { onClose: () => void; onConfirmUpdate: () => void; }) { const { navigateTo } = useDeviceUiNavigation(); const [versionInfo, setVersionInfo] = useState(null); const { modalView, setModalView, otaState } = useUpdateStore(); const onFinishedLoading = useCallback( async (versionInfo: SystemVersionInfo) => { const hasUpdate = versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable; setVersionInfo(versionInfo); if (hasUpdate) { setModalView("updateAvailable"); } else { setModalView("upToDate"); } }, [setModalView], ); // Reset modal view when dialog is opened useEffect(() => { setVersionInfo(null); }, [setModalView]); return (
{modalView === "error" && ( setModalView("loading")} /> )} {modalView === "loading" && ( )} {modalView === "updateAvailable" && ( )} {modalView === "updating" && ( navigateTo("/")} /> )} {modalView === "upToDate" && ( setModalView("loading")} onClose={onClose} /> )} {modalView === "updateCompleted" && }
); } function LoadingState({ onFinished, onCancelCheck, }: { onFinished: (versionInfo: SystemVersionInfo) => void; onCancelCheck: () => void; }) { const [progressWidth, setProgressWidth] = useState("0%"); const abortControllerRef = useRef(null); const [send] = useJsonRpc(); const setAppVersion = useDeviceStore(state => state.setAppVersion); const setSystemVersion = useDeviceStore(state => state.setSystemVersion); const getVersionInfo = useCallback(() => { return new Promise((resolve, reject) => { send("getUpdateStatus", {}, async resp => { if ("error" in resp) { notifications.error("Failed to check for updates"); reject(new Error("Failed to check for updates")); } else { const result = resp.result as SystemVersionInfo; setAppVersion(result.local.appVersion); setSystemVersion(result.local.systemVersion); resolve(result); } }); }); }, [send, setAppVersion, setSystemVersion]); const progressBarRef = useRef(null); useEffect(() => { setProgressWidth("0%"); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; const animationTimer = setTimeout(() => { setProgressWidth("100%"); }, 0); getVersionInfo() .then(versionInfo => { // Add a small delay to ensure it's not just flickering return new Promise(resolve => setTimeout(() => resolve(versionInfo), 600)); }) .then(versionInfo => { if (!signal.aborted) { onFinished(versionInfo as SystemVersionInfo); } }) .catch(error => { if (!signal.aborted) { console.error("LoadingState: Error fetching version info", error); } }); return () => { clearTimeout(animationTimer); abortControllerRef.current?.abort(); }; }, [getVersionInfo, onFinished]); return (

Checking for updates...

We{"'"}re ensuring your device has the latest features and improvements.

); } function UpdatingDeviceState({ otaState, onMinimizeUpgradeDialog, }: { otaState: UpdateState["otaState"]; onMinimizeUpgradeDialog: () => void; }) { const formatProgress = (progress: number) => `${Math.round(progress)}%`; const calculateOverallProgress = (type: "system" | "app") => { const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100); const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100); const verificationProgress = Math.round( (otaState[`${type}VerificationProgress`] || 0) * 100, ); if (!downloadProgress && !updateProgress && !verificationProgress) { return 0; } console.log( `For ${type}:\n` + ` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` + ` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` + ` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`, ); if (type === "app") { // App: 65% download, 34% verification, 1% update(There is no "real" update for the app) return Math.min( downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01, 100, ); } else { // System: 10% download, 90% update return Math.min( downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, 100, ); } }; const getUpdateStatus = (type: "system" | "app") => { const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`]; const verfiedAt = otaState[`${type}VerifiedAt`]; const updatedAt = otaState[`${type}UpdatedAt`]; if (!otaState.metadataFetchedAt) { return "Fetching update information..."; } else if (!downloadFinishedAt) { return `Downloading ${type} update...`; } else if (!verfiedAt) { return `Verifying ${type} update...`; } else if (!updatedAt) { return `Installing ${type} update...`; } else { return `Awaiting reboot`; } }; const isUpdateComplete = (type: "system" | "app") => { return !!otaState[`${type}UpdatedAt`]; }; const areAllUpdatesComplete = () => { if (otaState.systemUpdatePending && otaState.appUpdatePending) { return isUpdateComplete("system") && isUpdateComplete("app"); } return ( (otaState.systemUpdatePending && isUpdateComplete("system")) || (otaState.appUpdatePending && isUpdateComplete("app")) ); }; return (

Updating your device

Please don{"'"}t turn off your device. This process may take a few minutes.

{areAllUpdatesComplete() ? (
Rebooting to complete the update...
) : ( <> {!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
)} {otaState.systemUpdatePending && (

Linux System Update

{calculateOverallProgress("system") < 100 ? ( ) : ( )}
{getUpdateStatus("system")} {calculateOverallProgress("system") < 100 ? ( {formatProgress(calculateOverallProgress("system"))} ) : null}
)} {otaState.appUpdatePending && ( <> {otaState.systemUpdatePending && (
)}

App Update

{calculateOverallProgress("app") < 100 ? ( ) : ( )}
{getUpdateStatus("app")} {calculateOverallProgress("system") < 100 ? ( {formatProgress(calculateOverallProgress("app"))} ) : null}
)} )}
); } function SystemUpToDateState({ checkUpdate, onClose, }: { checkUpdate: () => void; onClose: () => void; }) { return (

System is up to date

Your system is running the latest version. No updates are currently available.

); } function UpdateAvailableState({ versionInfo, onConfirmUpdate, onClose, }: { versionInfo: SystemVersionInfo; onConfirmUpdate: () => void; onClose: () => void; }) { return (

Update available

A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.

{versionInfo?.systemUpdateAvailable ? ( <> System:{" "} {versionInfo?.remote.systemVersion}
) : null} {versionInfo?.appUpdateAvailable ? ( <> App: {versionInfo?.remote.appVersion} ) : null}

); } function UpdateCompletedState({ onClose }: { onClose: () => void }) { return (

Update Completed Successfully

Your device has been successfully updated to the latest version. Enjoy the new features and improvements!

); } function UpdateErrorState({ errorMessage, onClose, onRetryUpdate, }: { errorMessage: string | null; onClose: () => void; onRetryUpdate: () => void; }) { return (

Update Error

An error occurred while updating your device. Please try again later.

{errorMessage && (

Error details: {errorMessage}

)}
); }