diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index a735d7bca2..ee14578616 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -770,7 +770,10 @@ } } }, - "updateAvailableDesc": { + "restartToUpdate": { + "message": "Restart To Update" + }, + "restartToUpdateDesc": { "message": "Version $VERSION_NUM$ is ready to install. You must restart Bitwarden to complete the installation. Do you want to restart and update now?", "placeholders": { "version_num": { @@ -782,10 +785,19 @@ "updateAvailable": { "message": "Update Available" }, + "updateAvailableDesc": { + "message": "An update was found. Do you want to download it now?" + }, "update": { "message": "Update" }, "later": { "message": "Later" + }, + "noUpdatesAvailable": { + "message": "No updates are currently available. You are using the latest version." + }, + "updateError": { + "message": "Update Error" } } diff --git a/src/main/menu.main.ts b/src/main/menu.main.ts index 9b75c0b343..4828166366 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu.main.ts @@ -5,6 +5,7 @@ import { dialog, ipcMain, Menu, + MenuItem, MenuItemConstructorOptions, shell, } from 'electron'; @@ -20,6 +21,11 @@ export class MenuMain { private i18nService: I18nService, private messagingService: MessagingService) { } init() { + this.updaterMain.updateMenuItem = { + label: this.i18nService.t('checkForUpdates'), + click: () => this.updaterMain.checkForUpdate(true), + }; + const template: MenuItemConstructorOptions[] = [ { label: this.i18nService.t('file'), @@ -308,10 +314,7 @@ export class MenuMain { if (process.platform === 'darwin') { const firstMenuPart: MenuItemConstructorOptions[] = [ { role: 'about' }, - { - label: this.i18nService.t('checkForUpdates'), - click: () => this.updaterMain.checkForUpdate(), - }, + this.updaterMain.updateMenuItem, ]; template.unshift({ @@ -345,10 +348,7 @@ export class MenuMain { template[template.length - 1].submenu = (template[template.length - 1].submenu as MenuItemConstructorOptions[]).concat([ { type: 'separator' }, - { - label: this.i18nService.t('checkForUpdates'), - click: () => this.updaterMain.checkForUpdate(), - }, + this.updaterMain.updateMenuItem, { label: this.i18nService.t('about'), click: () => { diff --git a/src/main/updater.main.ts b/src/main/updater.main.ts index e5aed96983..23d4d52a5e 100644 --- a/src/main/updater.main.ts +++ b/src/main/updater.main.ts @@ -1,4 +1,7 @@ -import { dialog } from 'electron'; +import { + dialog, + MenuItemConstructorOptions, +} from 'electron'; import { autoUpdater } from 'electron-updater'; import { WindowMain } from './window.main'; @@ -9,30 +12,95 @@ const UpdaterCheckInitalDelay = 5 * 1000; // 5 seconds const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours export class UpdaterMain { + updateMenuItem: MenuItemConstructorOptions; + + private doingUpdateCheck = false; + private doingUpdateCheckWithFeedback = false; + constructor(private windowMain: WindowMain, private i18nService: I18nService) { } async init() { global.setTimeout(async () => await this.checkForUpdate(), UpdaterCheckInitalDelay); global.setInterval(async () => await this.checkForUpdate(), UpdaterCheckInterval); + autoUpdater.on('error', (error) => { + if (this.doingUpdateCheckWithFeedback) { + dialog.showErrorBox(this.i18nService.t('updateError'), + error == null ? "unknown" : (error.stack || error).toString()); + } + + this.reset(); + }); + autoUpdater.on('update-downloaded', (info) => { + this.updateMenuItem.label = this.i18nService.t('restartToUpdate'); + const result = dialog.showMessageBox(this.windowMain.win, { type: 'info', - title: this.i18nService.t('updateAvailable'), - message: this.i18nService.t('updateAvailable'), - detail: this.i18nService.t('updateAvailableDesc', info.version), - buttons: [this.i18nService.t('update'), this.i18nService.t('later')], + title: this.i18nService.t('restartToUpdate'), + message: this.i18nService.t('restartToUpdate'), + detail: this.i18nService.t('restartToUpdateDesc', info.version), + buttons: [this.i18nService.t('restart'), this.i18nService.t('later')], cancelId: 1, defaultId: 0, noLink: true, }); + if (result === 0) { autoUpdater.quitAndInstall(); } }); + + autoUpdater.on('update-available', () => { + if (this.doingUpdateCheckWithFeedback) { + const result = dialog.showMessageBox(this.windowMain.win, { + type: 'info', + title: this.i18nService.t('updateAvailable'), + message: this.i18nService.t('updateAvailable'), + detail: this.i18nService.t('updateAvailableDesc'), + buttons: [this.i18nService.t('yes'), this.i18nService.t('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + + if (result === 0) { + autoUpdater.downloadUpdate(); + } else { + this.reset(); + } + } + }); + + autoUpdater.on('update-not-available', () => { + if (this.doingUpdateCheckWithFeedback) { + dialog.showMessageBox(this.windowMain.win, { + message: this.i18nService.t('noUpdatesAvailable'), + }); + } + + this.reset(); + }); } - async checkForUpdate() { - return await autoUpdater.checkForUpdatesAndNotify(); + async checkForUpdate(withFeedback: boolean = false) { + if (this.doingUpdateCheck) { + return; + } + + this.updateMenuItem.enabled = false; + this.doingUpdateCheck = true; + this.doingUpdateCheckWithFeedback = withFeedback; + + if (withFeedback) { + await autoUpdater.checkForUpdates(); + } else { + await autoUpdater.checkForUpdatesAndNotify(); + } + } + + private reset() { + this.updateMenuItem.enabled = true; + this.doingUpdateCheck = false; } }