diff --git a/packages/app/src-tauri/Cargo.toml b/packages/app/src-tauri/Cargo.toml
index 8559873e6..dc57254fd 100644
--- a/packages/app/src-tauri/Cargo.toml
+++ b/packages/app/src-tauri/Cargo.toml
@@ -17,7 +17,7 @@ tauri-build = { version = "1.2.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
-tauri = { version = "1.2.4", features = [ "shell-execute", "http-all", "path-all", "updater", "shell-open", "dialog-all", "fs-all", "global-shortcut-all", "shell-sidecar", "window-all", "devtools"] }
+tauri = { version = "1.2.4", features = [ "process-relaunch", "shell-execute", "http-all", "path-all", "updater", "shell-open", "dialog-all", "fs-all", "global-shortcut-all", "shell-sidecar", "window-all", "devtools"] }
tauri-plugin-persisted-scope = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tar = "0.4.40"
diff --git a/packages/app/src-tauri/tauri.conf.json b/packages/app/src-tauri/tauri.conf.json
index eb60286fc..aa164b1cb 100644
--- a/packages/app/src-tauri/tauri.conf.json
+++ b/packages/app/src-tauri/tauri.conf.json
@@ -22,6 +22,9 @@
"dialog": {
"all": true
},
+ "process": {
+ "relaunch": true
+ },
"shell": {
"sidecar": true,
"open": true,
@@ -89,7 +92,7 @@
"updater": {
"active": true,
"endpoints": ["https://github.com/Ironclad/rivet/releases/latest/download/latest.json"],
- "dialog": true,
+ "dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQwRTgxNDEzODJDREJDNzkKUldSNXZNMkNFeFRvUU5VK0NXdEIwNlc3NTREUWpNSmpaeGFwWjFwc21ZT0U5cFlzcy81QndDOXYK"
},
"windows": [
diff --git a/packages/app/src/components/RivetApp.tsx b/packages/app/src/components/RivetApp.tsx
index 48feb2756..fd44cfd37 100644
--- a/packages/app/src/components/RivetApp.tsx
+++ b/packages/app/src/components/RivetApp.tsx
@@ -21,6 +21,10 @@ import { useLoadStaticData } from '../hooks/useLoadStaticData';
import { DataStudioRenderer } from './dataStudio/DataStudio';
import { StatusBar } from './StatusBar';
import { PluginsOverlayRenderer } from './PluginsOverlay';
+import { useCheckForUpdate } from '../hooks/useCheckForUpdate';
+import useAsyncEffect from 'use-async-effect';
+import { UpdateModalRenderer } from './UpdateModal';
+import { useMonitorUpdateStatus } from '../hooks/useMonitorUpdateStatus';
const styles = css`
overflow: hidden;
@@ -40,6 +44,14 @@ export const RivetApp: FC = () => {
onRunGraph: tryRunGraph,
});
+ const checkForUpdate = useCheckForUpdate();
+
+ useAsyncEffect(async () => {
+ await checkForUpdate();
+ }, []);
+
+ useMonitorUpdateStatus();
+
return (
@@ -60,6 +72,7 @@ export const RivetApp: FC = () => {
+
);
diff --git a/packages/app/src/components/UpdateModal.tsx b/packages/app/src/components/UpdateModal.tsx
new file mode 100644
index 000000000..c96a3d3f4
--- /dev/null
+++ b/packages/app/src/components/UpdateModal.tsx
@@ -0,0 +1,105 @@
+import { useState, type FC, useEffect } from 'react';
+
+import Modal, { ModalTransition, ModalBody, ModalFooter, ModalHeader, ModalTitle } from '@atlaskit/modal-dialog';
+import { useRecoilState, useSetRecoilState } from 'recoil';
+import { updateModalOpenState, updateStatusState } from '../state/settings';
+import Button from '@atlaskit/button';
+import useAsyncEffect from 'use-async-effect';
+import { checkUpdate, installUpdate, onUpdaterEvent } from '@tauri-apps/api/updater';
+import { getVersion } from '@tauri-apps/api/app';
+import { css } from '@emotion/react';
+import { relaunch } from '@tauri-apps/api/process';
+
+const bodyStyle = css`
+ pre {
+ font-family: var(--font-family);
+ }
+`;
+
+export const UpdateModalRenderer: FC = () => {
+ const [modalOpen] = useRecoilState(updateModalOpenState);
+
+ return {modalOpen && };
+};
+
+export const UpdateModal: FC = () => {
+ const setModalOpen = useSetRecoilState(updateModalOpenState);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [updateStatus, setUpdateStatus] = useRecoilState(updateStatusState);
+
+ const [currentVersion, setCurrentVersion] = useState('');
+ const [latestVersion, setLatestVersion] = useState('');
+ const [updateBody, setUpdateBody] = useState('');
+
+ useAsyncEffect(async () => {
+ setCurrentVersion(await getVersion());
+ const { manifest } = await checkUpdate();
+ if (manifest) {
+ setLatestVersion(manifest.version);
+ setUpdateBody(manifest.body);
+ }
+ }, []);
+
+ const doUpdate = async () => {
+ try {
+ setUpdateStatus('Starting update...');
+ setIsUpdating(true);
+
+ await installUpdate();
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ const handleModalClose = () => {
+ if (isUpdating) {
+ return;
+ }
+ setModalOpen(false);
+ };
+
+ useAsyncEffect(async () => {
+ if (updateStatus === 'Installed.') {
+ await relaunch();
+ }
+ }, [updateStatus]);
+
+ const skipUpdate = () => {};
+
+ const canRender = currentVersion && latestVersion && updateBody;
+
+ return (
+ canRender && (
+
+
+ 🎉 Update Available
+
+
+
+
+ A new version {latestVersion} of Rivet is available. You are on currently on version{' '}
+ {currentVersion}. Would you like to install it now?
+
+
Update Notes:
+
{updateBody}
+
+
+
+ {isUpdating ? (
+ {updateStatus}
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+
+ )
+ );
+};
diff --git a/packages/app/src/hooks/useCheckForUpdate.tsx b/packages/app/src/hooks/useCheckForUpdate.tsx
new file mode 100644
index 000000000..8fe04de3e
--- /dev/null
+++ b/packages/app/src/hooks/useCheckForUpdate.tsx
@@ -0,0 +1,68 @@
+import { useEffect } from 'react';
+import { checkUpdate, installUpdate, onUpdaterEvent } from '@tauri-apps/api/updater';
+import useAsyncEffect from 'use-async-effect';
+import { toast } from 'react-toastify';
+import { css } from '@emotion/react';
+import { isInTauri } from '../utils/tauri';
+import { useSetRecoilState } from 'recoil';
+import { updateModalOpenState } from '../state/settings';
+
+const toastStyle = css`
+ display: flex;
+ flex-direction: column;
+
+ .actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
+
+ button {
+ background-color: var(--grey);
+ color: var(--grey-lightest);
+ font-family: apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
+ 'Droid Sans', 'Helvetica Neue', sans-serif;
+ border: 1px solid var(--grey-lightest);
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ white-space: nowrap;
+ font-size: 14px;
+ cursor: pointer;
+
+ &.primary {
+ background-color: var(--primary);
+ color: var(--foreground-on-primary);
+ }
+ }
+`;
+
+export function useCheckForUpdate() {
+ const setUpdateModalOpen = useSetRecoilState(updateModalOpenState);
+
+ return async () => {
+ const { shouldUpdate, manifest } = await checkUpdate();
+
+ if (shouldUpdate) {
+ toast.success(
+ ({ closeToast }) => (
+
+
Rivet version {manifest?.version} is now available!
+
+
+
+
+
+
+ ),
+ {
+ autoClose: false,
+ closeButton: false,
+ },
+ );
+ }
+ };
+}
diff --git a/packages/app/src/hooks/useMonitorUpdateStatus.ts b/packages/app/src/hooks/useMonitorUpdateStatus.ts
new file mode 100644
index 000000000..933585c7f
--- /dev/null
+++ b/packages/app/src/hooks/useMonitorUpdateStatus.ts
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+import { isInTauri } from '../utils/tauri';
+import { onUpdaterEvent } from '@tauri-apps/api/updater';
+import { useSetRecoilState } from 'recoil';
+import { updateStatusState } from '../state/settings';
+import { match } from 'ts-pattern';
+import useAsyncEffect from 'use-async-effect';
+import { relaunch } from '@tauri-apps/api/process';
+
+export function useMonitorUpdateStatus() {
+ const setUpdateStatus = useSetRecoilState(updateStatusState);
+
+ useAsyncEffect(async () => {
+ let unlisten: any | undefined = undefined;
+
+ if (isInTauri()) {
+ unlisten = await onUpdaterEvent(({ error, status }) => {
+ match(status as typeof status | 'DOWNLOADED') // -.-
+ .with('PENDING', async () => setUpdateStatus('Downloading...'))
+ .with('DONE', async () => setUpdateStatus('Installed.'))
+ .with('ERROR', async () => setUpdateStatus(`Error - ${error}`))
+ .with('UPTODATE', async () => setUpdateStatus('Up to date.'))
+ .with('DOWNLOADED', async () => setUpdateStatus('Installing...'))
+ .exhaustive();
+ });
+ }
+
+ return () => {
+ if (unlisten) {
+ unlisten();
+ }
+ };
+ }, []);
+}
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index 5f84342c3..8aaabaf84 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -1,7 +1,9 @@
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
+
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
'Droid Sans', 'Helvetica Neue', sans-serif;
+ font-family: var(--font-family);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
diff --git a/packages/app/src/state/settings.ts b/packages/app/src/state/settings.ts
index 6ab67bc79..368bd5904 100644
--- a/packages/app/src/state/settings.ts
+++ b/packages/app/src/state/settings.ts
@@ -65,3 +65,19 @@ export const previousDataPerNodeToKeepState = atom({
default: -1,
effects_UNSTABLE: [persistAtom],
});
+
+export const skippedMaxVersion = atom({
+ key: 'skippedMaxVersion',
+ default: undefined,
+ effects_UNSTABLE: [persistAtom],
+});
+
+export const updateModalOpenState = atom({
+ key: 'updateModalOpen',
+ default: false,
+});
+
+export const updateStatusState = atom({
+ key: 'updateStatus',
+ default: undefined,
+});