diff --git a/interface.d.ts b/interface.d.ts index 881ee28f9..265b6bf2f 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -63,6 +63,10 @@ export interface IElectronAPI { kittycad: (access: string, args: any) => any listMachines: () => Promise getMachineApiIp: () => Promise + onUpdateDownloaded: ( + callback: (value: string) => void + ) => Electron.IpcRenderer + appRestart: () => void } declare global { diff --git a/src/components/ActionButton.test.tsx b/src/components/ActionButton.test.tsx new file mode 100644 index 000000000..fd649fa67 --- /dev/null +++ b/src/components/ActionButton.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { ActionButton } from './ActionButton' + +describe('ActionButton tests', () => { + it('ActionButton with no iconStart or iconEnd should have even left and right padding', () => { + render(No icons) + expect(screen.getByRole('button')).toHaveClass('px-2') + }) + + it('ActionButton with iconStart should have no padding on the left', () => { + render( + + Start icon only + + ) + expect(screen.getByRole('button')).toHaveClass('pr-2') + }) + + it('ActionButton with iconEnd should have no padding on the right', () => { + render( + + End icon only + + ) + expect(screen.getByRole('button')).toHaveClass('pl-2') + }) + + it('ActionButton with both icons should have no padding on either side', () => { + render( + + Both icons + + ) + expect(screen.getByRole('button')).not.toHaveClass('px-2') + expect(screen.getByRole('button')).toHaveClass('px-0') + }) +}) diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index 287192f75..2905f8ae0 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -44,11 +44,11 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => { const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${ props.iconStart ? props.iconEnd - ? 'px-0' - : 'pr-2' + ? 'px-0' // No padding if both icons are present + : 'pr-2' // Padding on the right if only the start icon is present : props.iconEnd - ? 'px-2' - : 'pl-2' + ? 'pl-2' // Padding on the left if only the end icon is present + : 'px-2' // Padding on both sides if no icons are present } ${props.className ? props.className : ''}` switch (props.Element) { diff --git a/src/components/ToastUpdate.tsx b/src/components/ToastUpdate.tsx new file mode 100644 index 000000000..500edbfe0 --- /dev/null +++ b/src/components/ToastUpdate.tsx @@ -0,0 +1,64 @@ +import toast from 'react-hot-toast' +import { ActionButton } from './ActionButton' +import { openExternalBrowserIfDesktop } from 'lib/openWindow' + +export function ToastUpdate({ + version, + onRestart, +}: { + version: string + onRestart: () => void +}) { + return ( +
+
+
+ + v{version} + + + A new update has downloaded and will be available next time you + start the app. You can view the release notes{' '} + + here on GitHub. + + +
+
+ + Restart app now + + { + toast.dismiss() + }} + > + Got it + +
+
+
+ ) +} diff --git a/src/index.tsx b/src/index.tsx index 2c1b80209..6e9b4f4a7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,12 +1,13 @@ import ReactDOM from 'react-dom/client' import './index.css' import reportWebVitals from './reportWebVitals' -import { Toaster } from 'react-hot-toast' +import toast, { Toaster } from 'react-hot-toast' import { Router } from './Router' import { HotkeysProvider } from 'react-hotkeys-hook' import ModalContainer from 'react-modal-promise' import { isDesktop } from 'lib/isDesktop' import { AppStreamProvider } from 'AppState' +import { ToastUpdate } from 'components/ToastUpdate' // uncomment for xstate inspector // import { DEV } from 'env' @@ -52,4 +53,17 @@ root.render( // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals() -isDesktop() +isDesktop() && + window.electron.onUpdateDownloaded((version: string) => { + const message = `A new update (${version}) was downloaded and will be available next time you open the app.` + console.log(message) + toast.custom( + ToastUpdate({ + version, + onRestart: () => { + window.electron.appRestart() + }, + }), + { duration: 30000 } + ) + }) diff --git a/src/main.ts b/src/main.ts index 9213458b9..c561910b0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -251,18 +251,14 @@ export function getAutoUpdater(): AppUpdater { return autoUpdater } -export async function checkForUpdates(autoUpdater: AppUpdater) { - // TODO: figure out how to get the update modal back - const result = await autoUpdater.checkForUpdatesAndNotify() - console.log(result) -} - app.on('ready', () => { const autoUpdater = getAutoUpdater() - checkForUpdates(autoUpdater).catch(reportRejection) + setTimeout(() => { + autoUpdater.checkForUpdates().catch(reportRejection) + }, 1000) const fifteenMinutes = 15 * 60 * 1000 setInterval(() => { - checkForUpdates(autoUpdater).catch(reportRejection) + autoUpdater.checkForUpdates().catch(reportRejection) }, fifteenMinutes) autoUpdater.on('update-available', (info) => { @@ -271,6 +267,11 @@ app.on('ready', () => { autoUpdater.on('update-downloaded', (info) => { console.log('update-downloaded', info) + mainWindow?.webContents.send('update-downloaded', info.version) + }) + + ipcMain.handle('app.restart', () => { + autoUpdater.quitAndInstall() }) }) diff --git a/src/preload.ts b/src/preload.ts index 6968df94d..2d5e3eec4 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -15,6 +15,9 @@ const startDeviceFlow = (host: string): Promise => ipcRenderer.invoke('startDeviceFlow', host) const loginWithDeviceFlow = (): Promise => ipcRenderer.invoke('loginWithDeviceFlow') +const onUpdateDownloaded = (callback: (value: string) => void) => + ipcRenderer.on('update-downloaded', (_event, value) => callback(value)) +const appRestart = () => ipcRenderer.invoke('app.restart') const isMac = os.platform() === 'darwin' const isWindows = os.platform() === 'win32' @@ -123,4 +126,6 @@ contextBridge.exposeInMainWorld('electron', { kittycad, listMachines, getMachineApiIp, + onUpdateDownloaded, + appRestart, })