Check for updates button in moar menus & toasts (#7369)

* Check for update button in more menus
Fixes #7368

* Add menubar item

* Another one

* Add Checking for updates... and No new update toasts

* Lint

* Trigger CI

* Update src/main.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update electron-builder.yml

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update electron-builder.yml

* Moar clean up

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
Pierre Jacquier
2025-06-05 17:12:14 -04:00
committed by GitHub
parent 3c23cada8e
commit 11d8179368
9 changed files with 94 additions and 21 deletions

2
interface.d.ts vendored
View File

@ -92,6 +92,8 @@ export interface IElectronAPI {
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: (machineApiIp: string) => Promise<MachinesListing> listMachines: (machineApiIp: string) => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
onUpdateChecking: (callback: () => void) => Electron.IpcRenderer
onUpdateNotAvailable: (callback: () => void) => Electron.IpcRenderer
onUpdateDownloadStart: ( onUpdateDownloadStart: (
callback: (value: { version: string }) => void callback: (value: { version: string }) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer

View File

@ -14,6 +14,8 @@ import {
catchOnboardingWarnError, catchOnboardingWarnError,
} from '@src/routes/Onboarding/utils' } from '@src/routes/Onboarding/utils'
import { onboardingStartPath } from '@src/lib/onboardingPaths' import { onboardingStartPath } from '@src/lib/onboardingPaths'
import { reportRejection } from '@src/lib/trap'
import { isDesktop } from '@src/lib/isDesktop'
const HelpMenuDivider = () => ( const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" /> <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -117,6 +119,17 @@ export function HelpMenu({
> >
Release notes Release notes
</HelpMenuItem> </HelpMenuItem>
{isDesktop() && (
<HelpMenuItem
as="button"
onClick={() => {
close()
window.electron.appCheckForUpdates().catch(reportRejection)
}}
>
Check for updates
</HelpMenuItem>
)}
<HelpMenuItem <HelpMenuItem
as="button" as="button"
onClick={() => { onClick={() => {

View File

@ -12,6 +12,7 @@ import usePlatform from '@src/hooks/usePlatform'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths' import { PATHS } from '@src/lib/paths'
import { authActor } from '@src/lib/singletons' import { authActor } from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap'
type User = Models['User_type'] type User = Models['User_type']
@ -129,6 +130,15 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</> </>
), ),
}, },
{
id: 'check-for-updates',
Element: 'button',
hide: !isDesktop(),
onClick: () => {
window.electron.appCheckForUpdates().catch(reportRejection)
},
children: <span className="flex-1">Check for updates</span>,
},
'break', 'break',
{ {
id: 'sign-out', id: 'sign-out',

View File

@ -81,21 +81,31 @@ root.render(
reportWebVitals() reportWebVitals()
if (isDesktop()) { if (isDesktop()) {
// Listen for update download progress to begin window.electron.onUpdateChecking(() => {
// to show a loading toast. const message = `Checking for updates...`
console.log(message)
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
})
window.electron.onUpdateNotAvailable(() => {
const message = `You're already using the latest version of the app.`
console.log(message)
toast.success(message, { id: AUTO_UPDATER_TOAST_ID })
})
window.electron.onUpdateDownloadStart(() => { window.electron.onUpdateDownloadStart(() => {
const message = `Downloading app update...` const message = `Downloading app update...`
console.log(message) console.log(message)
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID }) toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
}) })
// Listen for update download errors to show
// an error toast and clear the loading toast.
window.electron.onUpdateError(({ error }) => { window.electron.onUpdateError(({ error }) => {
console.error(error) console.error(error)
toast.error('An error occurred while downloading the update.', { toast.error('An error occurred while downloading the update.', {
id: AUTO_UPDATER_TOAST_ID, id: AUTO_UPDATER_TOAST_ID,
}) })
}) })
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => { window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.` const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message) console.log(message)

View File

@ -19,9 +19,9 @@ import {
shell, shell,
systemPreferences, systemPreferences,
} from 'electron' } from 'electron'
import electronUpdater, { type AppUpdater } from 'electron-updater'
import { Issuer } from 'openid-client' import { Issuer } from 'openid-client'
import { getAutoUpdater } from '@src/updater'
import { import {
argvFromYargs, argvFromYargs,
getPathOrUrlFromArgs, getPathOrUrlFromArgs,
@ -442,16 +442,6 @@ ipcMain.handle('disable-menu', (event, data) => {
disableMenu(menuId) disableMenu(menuId)
}) })
export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
// Allows us to rollback to a previous version if needed.
// See https://github.com/electron-userland/electron-builder/blob/7dbc6c77c340c869d1e7effa22135fc740003a0f/packages/electron-updater/src/AppUpdater.ts#L450-L451
autoUpdater.allowDowngrade = true
return autoUpdater
}
app.on('ready', () => { app.on('ready', () => {
// Disable auto updater on non-versioned builds // Disable auto updater on non-versioned builds
if (packageJSON.version === '0.0.0' && viteEnv.MODE !== 'production') { if (packageJSON.version === '0.0.0' && viteEnv.MODE !== 'production') {
@ -462,13 +452,36 @@ app.on('ready', () => {
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup, // TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
// so at the moment this isn't worth enabling // so at the moment this isn't worth enabling
autoUpdater.disableDifferentialDownload = true autoUpdater.disableDifferentialDownload = true
setTimeout(() => {
autoUpdater.checkForUpdates().catch(reportRejection) // Check for updates in the background at startup and then every 15 minutes
}, 1000) let backgroundCheckingForUpdates = false
const checkForUpdatesBackground = () => {
backgroundCheckingForUpdates = true
autoUpdater
.checkForUpdates()
.catch(reportRejection)
.finally(() => {
backgroundCheckingForUpdates = false
})
}
const oneSecond = 1000
const fifteenMinutes = 15 * 60 * 1000 const fifteenMinutes = 15 * 60 * 1000
setInterval(() => { setTimeout(checkForUpdatesBackground, oneSecond)
autoUpdater.checkForUpdates().catch(reportRejection) setInterval(checkForUpdatesBackground, fifteenMinutes)
}, fifteenMinutes)
autoUpdater.on('checking-for-update', () => {
console.log('checking-for-update')
if (!backgroundCheckingForUpdates) {
mainWindow?.webContents.send('update-checking')
}
})
autoUpdater.on('update-not-available', (info) => {
console.log('update-not-available', info)
if (!backgroundCheckingForUpdates) {
mainWindow?.webContents.send('update-not-available')
}
})
autoUpdater.on('error', (error) => { autoUpdater.on('error', (error) => {
console.error('update-error', error) console.error('update-error', error)

View File

@ -4,6 +4,7 @@ import { shell } from 'electron'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { typeSafeWebContentsSend } from '@src/menu/channels' import { typeSafeWebContentsSend } from '@src/menu/channels'
import type { ZooMenuItemConstructorOptions } from '@src/menu/roles' import type { ZooMenuItemConstructorOptions } from '@src/menu/roles'
import { getAutoUpdater } from '@src/updater'
export const helpRole = ( export const helpRole = (
mainWindow: BrowserWindow mainWindow: BrowserWindow
@ -105,6 +106,12 @@ export const helpRole = (
.catch(reportRejection) .catch(reportRejection)
}, },
}, },
{
label: 'Check for updates',
click: () => {
getAutoUpdater().checkForUpdates().catch(reportRejection)
},
},
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Manage account', label: 'Manage account',

View File

@ -47,6 +47,7 @@ type HelpRoleLabel =
| 'KCL docs' | 'KCL docs'
| 'Replay onboarding tutorial' | 'Replay onboarding tutorial'
| 'Show release notes' | 'Show release notes'
| 'Check for updates'
| 'Manage account' | 'Manage account'
| 'Get started with Text-to-CAD' | 'Get started with Text-to-CAD'
| 'Show all commands' | 'Show all commands'

View File

@ -45,6 +45,10 @@ const onUpdateDownloadStart = (
ipcRenderer.on('update-download-start', (_event: any, value) => ipcRenderer.on('update-download-start', (_event: any, value) =>
callback(value) callback(value)
) )
const onUpdateChecking = (callback: () => void) =>
ipcRenderer.on('update-checking', (_event: any) => callback())
const onUpdateNotAvailable = (callback: () => void) =>
ipcRenderer.on('update-not-available', (_event: any) => callback())
const onUpdateError = (callback: (value: Error) => void) => const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event: any, value) => callback(value)) ipcRenderer.on('update-error', (_event: any, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart') const appRestart = () => ipcRenderer.invoke('app.restart')
@ -305,6 +309,8 @@ contextBridge.exposeInMainWorld('electron', {
kittycad, kittycad,
listMachines, listMachines,
getMachineApiIp, getMachineApiIp,
onUpdateChecking,
onUpdateNotAvailable,
onUpdateDownloadStart, onUpdateDownloadStart,
onUpdateDownloaded, onUpdateDownloaded,
onUpdateError, onUpdateError,

11
src/updater.ts Normal file
View File

@ -0,0 +1,11 @@
import electronUpdater, { type AppUpdater } from 'electron-updater'
export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
// Allows us to rollback to a previous version if needed.
// See https://github.com/electron-userland/electron-builder/blob/7dbc6c77c340c869d1e7effa22135fc740003a0f/packages/electron-updater/src/AppUpdater.ts#L450-L451
autoUpdater.allowDowngrade = true
return autoUpdater
}