diff --git a/src-gui/src/renderer/components/App.tsx b/src-gui/src/renderer/components/App.tsx index 1d612f29..a911241c 100644 --- a/src-gui/src/renderer/components/App.tsx +++ b/src-gui/src/renderer/components/App.tsx @@ -9,6 +9,7 @@ import HistoryPage from "./pages/history/HistoryPage"; import SwapPage from "./pages/swap/SwapPage"; import WalletPage from "./pages/wallet/WalletPage"; import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider"; +import UpdaterDialog from "./modal/updater/UpdaterDialog"; const useStyles = makeStyles((theme) => ({ innerContent: { @@ -58,6 +59,7 @@ export default function App() { + diff --git a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx new file mode 100644 index 00000000..58d6ffe1 --- /dev/null +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + LinearProgress, + Typography, + makeStyles, + LinearProgressProps, + Box, +} from '@material-ui/core'; +import SystemUpdateIcon from '@material-ui/icons/SystemUpdate'; +import { check, Update, DownloadEvent } from '@tauri-apps/plugin-updater'; +import { useSnackbar } from 'notistack'; +import { relaunch } from '@tauri-apps/plugin-process'; + +const useStyles = makeStyles((theme) => ({ + progress: { + marginTop: theme.spacing(2) + }, + releaseNotes: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1) + }, + noteContent: { + whiteSpace: 'pre-line' + } +})); + +interface DownloadProgress { + contentLength: number | null; + downloadedBytes: number; +} + +function LinearProgressWithLabel(props: LinearProgressProps & { label?: string }) { + return ( + + + + + + + {props.label || `${Math.round(props.value)}%`} + + + + ); +} + +export default function UpdaterDialog() { + const classes = useStyles(); + const [availableUpdate, setAvailableUpdate] = useState(null); + const [downloadProgress, setDownloadProgress] = useState(null); + const {enqueueSnackbar} = useSnackbar(); + + useEffect(() => { + // Check for updates when component mounts + check().then((updateResponse) => { + setAvailableUpdate(updateResponse); + }).catch((err) => { + enqueueSnackbar(`Failed to check for updates: ${err}`, { + variant: 'error', + }); + }); + }, []); + + // If no update is available, don't render the dialog + if (!availableUpdate?.available) return null; + + function hideNotification() { + setAvailableUpdate(null); + }; + + async function handleInstall() { + try { + await availableUpdate.downloadAndInstall((event: DownloadEvent) => { + if (event.event === 'Started') { + setDownloadProgress({ + contentLength: event.data.contentLength || null, + downloadedBytes: 0, + }); + } else if (event.event === 'Progress') { + setDownloadProgress(prev => ({ + ...prev, + downloadedBytes: prev.downloadedBytes + event.data.chunkLength, + })); + } else if (event.event === 'Finished') { + // Relaunch the application for the new version to be used + relaunch(); + } + }); + } catch (err) { + enqueueSnackbar(`Failed to install update: ${err}`, { + variant: "error" + }); + } + }; + + const isDownloading = downloadProgress !== null; + + const progress = isDownloading + ? Math.round((downloadProgress.downloadedBytes / downloadProgress.contentLength) * 100) + : 0; + + return ( + + Update Available + + + A new version (v{availableUpdate.version}) is available. Your current version is {availableUpdate.currentVersion}. + The update will be verified using PGP signature verification to ensure authenticity. + {availableUpdate.body && ( + <> + + Release Notes: + + + {availableUpdate.body} + + + )} + + + {isDownloading && ( + + + + )} + + + + + + + ); +} diff --git a/src-gui/src/renderer/index.tsx b/src-gui/src/renderer/index.tsx index bb2719e0..d0e9ff1b 100644 --- a/src-gui/src/renderer/index.tsx +++ b/src-gui/src/renderer/index.tsx @@ -18,8 +18,6 @@ import { import App from "./components/App"; import { initEventListeners } from "./rpc"; import { persistor, store } from "./store/storeRenderer"; -import { Box } from "@material-ui/core"; -import { checkForAppUpdates } from "./updater"; const container = document.getElementById("root"); const root = createRoot(container!); @@ -76,5 +74,4 @@ async function fetchInitialData() { } fetchInitialData(); -initEventListeners(); -checkForAppUpdates(); +initEventListeners(); \ No newline at end of file diff --git a/src-gui/src/renderer/updater.ts b/src-gui/src/renderer/updater.ts deleted file mode 100644 index 86a4a626..00000000 --- a/src-gui/src/renderer/updater.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { check } from "@tauri-apps/plugin-updater"; -import { ask, message } from "@tauri-apps/plugin-dialog"; -import { relaunch } from "@tauri-apps/plugin-process"; - -export async function checkForAppUpdates() { - const update = await check(); - - if (update?.available) { - const yes = await ask( - ` -Update to ${update.version} is available! -Release notes: ${update.body} - `, - { - title: "Update Now!", - kind: "info", - okLabel: "Update", - cancelLabel: "Cancel", - } - ); - - if (yes) { - await update.downloadAndInstall(); - await relaunch(); - } - } -} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 24c7326d..b85d0b2e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -41,7 +41,7 @@ "endpoints": [ "https://cdn.crabnebula.app/update/unstoppableswap/unstoppableswap-gui-rs/{{target}}-{{arch}}/{{current_version}}" ], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyNTQ3NTQxQTQ2MkI4N0IKUldSN3VHS2tRWFZVMGpWYytkRFg4dFBzNEh5ZnlxaHBubGpRalVMMG5nVytiR3JPOUE3QjRxc00K" + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEE2MDgxRDEwMDZENkYxNUMKUldSYzhkWUdFQjBJcGwzN24yZlduTzNndFZnVW9Qa1k2WFVTMEMxcHBSc2dSVVlzbVNHdGNFQ0EK" }, "cli": { "description": "Start the GUI application",