diff --git a/ui/src/components/UpdateDialog.tsx b/ui/src/components/UpdateDialog.tsx deleted file mode 100644 index 8fb879e..0000000 --- a/ui/src/components/UpdateDialog.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import Card, { GridCard } from "@/components/Card"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { Button } from "@components/Button"; -import LogoBlueIcon from "@/assets/logo-blue.svg"; -import LogoWhiteIcon from "@/assets/logo-white.svg"; -import Modal from "@components/Modal"; -import { UpdateState, useUpdateStore } from "@/hooks/stores"; -import notifications from "@/notifications"; -import { CheckCircleIcon } from "@heroicons/react/20/solid"; -import LoadingSpinner from "./LoadingSpinner"; - -export interface SystemVersionInfo { - local: { appVersion: string; systemVersion: string }; - remote: { appVersion: string; systemVersion: string }; - systemUpdateAvailable: boolean; - appUpdateAvailable: boolean; -} - -export default function UpdateDialog({ - open, - setOpen, -}: { - open: boolean; - setOpen: (open: boolean) => void; -}) { - // We need to keep track of the update state in the dialog even if the dialog is minimized - const { setModalView } = useUpdateStore(); - - const [send] = useJsonRpc(); - - const onConfirmUpdate = useCallback(() => { - send("tryUpdate", {}); - setModalView("updating"); - }, [send, setModalView]); - - return ( - setOpen(false)}> - - - ); -} - -export function Dialog({ - setOpen, - onConfirmUpdate, -}: { - setOpen: (open: boolean) => void; - onConfirmUpdate: () => void; -}) { - 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" && ( - setOpen(false)} - onRetryUpdate={() => setModalView("loading")} - /> - )} - - {modalView === "loading" && ( - setOpen(false)} - /> - )} - - {modalView === "updateAvailable" && ( - setOpen(false)} - versionInfo={versionInfo!} - /> - )} - - {modalView === "updating" && ( - { - setOpen(false); - }} - /> - )} - - {modalView === "upToDate" && ( - setModalView("loading")} - onClose={() => setOpen(false)} - /> - )} - - {modalView === "updateCompleted" && ( - setOpen(false)} /> - )} -
-
- ); -} - -function LoadingState({ - onFinished, - onCancelCheck, -}: { - onFinished: (versionInfo: SystemVersionInfo) => void; - onCancelCheck: () => void; -}) { - const [progressWidth, setProgressWidth] = useState("0%"); - const abortControllerRef = useRef(null); - const [send] = useJsonRpc(); - - 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; - resolve(result); - } - }); - }); - }, [send]); - - const progressBarRef = useRef(null); - useEffect(() => { - setProgressWidth("0%"); - - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - const animationTimer = setTimeout(() => { - setProgressWidth("100%"); - }, 500); - - getVersionInfo() - .then(versionInfo => { - if (progressBarRef.current) { - progressBarRef.current?.classList.add("!duration-1000"); - } - return new Promise(resolve => setTimeout(() => resolve(versionInfo), 1000)); - }) - .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} -

- )} -
-
-
-
- ); -} diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index b7bd949..6c5f0d2 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -18,16 +18,15 @@ import { isOnDevice } from "@/main"; import PointingFinger from "@/assets/pointing-finger.svg"; import MouseIcon from "@/assets/mouse-icon.svg"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { SelectMenuBasic } from "../SelectMenuBasic"; -import { SystemVersionInfo } from "@components/UpdateDialog"; +import { SelectMenuBasic } from "@/components/SelectMenuBasic"; +import { SystemVersionInfo } from "@/routes/devices.$id.update"; import notifications from "@/notifications"; import api from "../../api"; -import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog"; import { LocalDevice } from "@routes/devices.$id"; import { useRevalidator, useNavigate } from "react-router-dom"; import { ShieldCheckIcon } from "@heroicons/react/20/solid"; import { CLOUD_APP, DEVICE_API } from "@/ui.config"; -import { InputFieldWithLabel } from "../InputField"; +import { InputFieldWithLabel } from "@/components/InputField"; export function SettingsItem({ title, @@ -241,7 +240,7 @@ export default function SettingsSidebar() { setBacklightSettings(settings); handleBacklightSettingsSave(); - } + }; const handleBacklightSettingsSave = () => { send("setBacklightSettings", { params: settings.backlightSettings }, resp => { @@ -385,7 +384,7 @@ export default function SettingsSidebar() { } const result = resp.result as BacklightSettings; setBacklightSettings(result); - }) + }); send("getDevModeState", {}, resp => { if ("error" in resp) return; @@ -431,8 +430,9 @@ export default function SettingsSidebar() { } }, []); - const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore(); - const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false); + const { setModalView: setLocalAuthModalView, modalView: localAuthModalView } = + useLocalAuthModalStore(); + const [isLocalAuthDialogOpen] = useState(false); useEffect(() => { if (isOnDevice) getDevice(); @@ -440,13 +440,13 @@ export default function SettingsSidebar() { useEffect(() => { if (!isOnDevice) return; - // Refresh device status when the local auth dialog is closed - if (!isLocalAuthDialogOpen) { + // Refresh device status when the local auth dialog succeeds + if ( + ["creationSuccess", "deleteSuccess", "updateSuccess"].includes(localAuthModalView) + ) { getDevice(); } - }, [getDevice, isLocalAuthDialogOpen]); - - const revalidator = useRevalidator(); + }, [getDevice, isLocalAuthDialogOpen, localAuthModalView]); const [currentTheme, setCurrentTheme] = useState(() => { return localStorage.theme || "system"; @@ -484,18 +484,18 @@ export default function SettingsSidebar() { return (
e.stopPropagation()} onKeyUp={e => e.stopPropagation()} >
-
+
+ {error &&

{error}

} +
+
+ ); +} + +function DeletePasswordModal({ + onDeletePassword, + onCancel, + error, +}: { + onDeletePassword: (password: string) => void; + onCancel: () => void; + error: string | null; +}) { + const [password, setPassword] = useState(""); + + return ( +
+
+ + +
+
+
+

+ Disable Local Device Protection +

+

+ Enter your current password to disable local device protection. +

+
+ setPassword(e.target.value)} + /> +
+
+ {error &&

{error}

} +
+
+ ); +} + +function UpdatePasswordModal({ + onUpdatePassword, + onCancel, + error, +}: { + onUpdatePassword: ( + oldPassword: string, + newPassword: string, + confirmNewPassword: string, + ) => void; + onCancel: () => void; + error: string | null; +}) { + const [oldPassword, setOldPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmNewPassword, setConfirmNewPassword] = useState(""); + + return ( +
+
+ + +
+
+
+

+ Change Local Device Password +

+

+ Enter your current password and a new password to update your local device + protection. +

+
+ setOldPassword(e.target.value)} + /> + setNewPassword(e.target.value)} + /> + setConfirmNewPassword(e.target.value)} + /> +
+
+ {error &&

{error}

} +
+
+ ); +} + +function SuccessModal({ + headline, + description, + onClose, +}: { + headline: string; + description: string; + onClose: () => void; +}) { + return ( +
+
+ + +
+
+
+

{headline}

+

{description}

+
+
+
+ ); +} diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 142f29c..f2b7aac 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -31,7 +31,6 @@ import { useInterval } from "usehooks-ts"; import SettingsSidebar from "@/components/sidebar/settings"; import ConnectionStatsSidebar from "@/components/sidebar/connectionStats"; import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc"; -import UpdateDialog from "@components/UpdateDialog"; import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard"; import api from "../api"; import { DeviceStatus } from "./welcome-local"; @@ -478,16 +477,31 @@ export default function KvmIdRoute() {
- location.pathname !== "/other-session" && navigate("..")} +
{ + e.stopPropagation(); + }} + onKeyDown={e => { + e.stopPropagation(); + if (e.key === "Escape") { + if (location.pathname !== "/other-session") { + navigate(".."); + } + } + }} > - - + location.pathname !== "/other-session" && navigate("..")} + > + + +
{kvmTerminal && ( )} + {serialConsole && ( )} diff --git a/ui/src/routes/devices.$id.update.tsx b/ui/src/routes/devices.$id.update.tsx index c8a6641..269b887 100644 --- a/ui/src/routes/devices.$id.update.tsx +++ b/ui/src/routes/devices.$id.update.tsx @@ -1,9 +1,14 @@ import { useNavigate } from "react-router-dom"; -import { GridCard } from "@/components/Card"; -import { useUpdateStore } from "@/hooks/stores"; -import { Dialog } from "@/components/UpdateDialog"; +import Card, { GridCard } from "@/components/Card"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { useCallback } from "react"; +import { Button } from "@components/Button"; +import LogoBlueIcon from "@/assets/logo-blue.svg"; +import LogoWhiteIcon from "@/assets/logo-white.svg"; +import { UpdateState, useUpdateStore } from "@/hooks/stores"; +import notifications from "@/notifications"; +import { CheckCircleIcon } from "@heroicons/react/20/solid"; +import LoadingSpinner from "@/components/LoadingSpinner"; export default function UpdateRoute() { const navigate = useNavigate(); @@ -29,3 +34,519 @@ export default function UpdateRoute() { ); } + +export interface SystemVersionInfo { + local: { appVersion: string; systemVersion: string }; + remote: { appVersion: string; systemVersion: string }; + systemUpdateAvailable: boolean; + appUpdateAvailable: boolean; +} + +export function Dialog({ + setOpen, + onConfirmUpdate, +}: { + setOpen: (open: boolean) => void; + onConfirmUpdate: () => void; +}) { + 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" && ( + setOpen(false)} + onRetryUpdate={() => setModalView("loading")} + /> + )} + + {modalView === "loading" && ( + setOpen(false)} + /> + )} + + {modalView === "updateAvailable" && ( + setOpen(false)} + versionInfo={versionInfo!} + /> + )} + + {modalView === "updating" && ( + { + setOpen(false); + }} + /> + )} + + {modalView === "upToDate" && ( + setModalView("loading")} + onClose={() => setOpen(false)} + /> + )} + + {modalView === "updateCompleted" && ( + setOpen(false)} /> + )} +
+
+ ); +} + +function LoadingState({ + onFinished, + onCancelCheck, +}: { + onFinished: (versionInfo: SystemVersionInfo) => void; + onCancelCheck: () => void; +}) { + const [progressWidth, setProgressWidth] = useState("0%"); + const abortControllerRef = useRef(null); + const [send] = useJsonRpc(); + + 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; + resolve(result); + } + }); + }); + }, [send]); + + const progressBarRef = useRef(null); + useEffect(() => { + setProgressWidth("0%"); + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + const animationTimer = setTimeout(() => { + setProgressWidth("100%"); + }, 500); + + getVersionInfo() + .then(versionInfo => { + if (progressBarRef.current) { + progressBarRef.current?.classList.add("!duration-1000"); + } + return new Promise(resolve => setTimeout(() => resolve(versionInfo), 1000)); + }) + .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} +

+ )} +
+
+
+
+ ); +}