mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-3162] Use system notification for update alert (#15606)
This commit is contained in:
@@ -221,7 +221,7 @@ export class Main {
|
||||
);
|
||||
|
||||
this.messagingMain = new MessagingMain(this, this.desktopSettingsService);
|
||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
|
||||
this.updaterMain = new UpdaterMain(this.i18nService, this.logService, this.windowMain);
|
||||
|
||||
const messageSubject = new Subject<Message<Record<string, unknown>>>();
|
||||
this.messagingService = MessageSender.combine(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { dialog, shell } from "electron";
|
||||
import { dialog, shell, Notification } from "electron";
|
||||
import log from "electron-log";
|
||||
import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { isAppImage, isDev, isMacAppStore, isWindowsPortable, isWindowsStore } from "../utils";
|
||||
|
||||
@@ -11,6 +12,8 @@ import { WindowMain } from "./window.main";
|
||||
const UpdaterCheckInitialDelay = 5 * 1000; // 5 seconds
|
||||
const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours
|
||||
|
||||
const MaxTimeBeforeBlockingUpdateNotification = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
export class UpdaterMain {
|
||||
private doingUpdateCheck = false;
|
||||
private doingUpdateCheckWithFeedback = false;
|
||||
@@ -18,8 +21,19 @@ export class UpdaterMain {
|
||||
private updateDownloaded: UpdateDownloadedEvent = null;
|
||||
private originalRolloutFunction: VerifyUpdateSupport = null;
|
||||
|
||||
// This needs to be tracked to avoid the Notification being garbage collected,
|
||||
// which would break the click handler.
|
||||
private openedNotification: Notification | null = null;
|
||||
|
||||
// This is used to set when the initial update notification was shown.
|
||||
// The system notifications can be easy to miss or be disabled, so we want to
|
||||
// ensure the user is eventually made aware of the update. If the user does not
|
||||
// interact with the notification in a reasonable time, we will prompt them again.
|
||||
private initialUpdateNotificationTime: number | null = null;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {
|
||||
autoUpdater.logger = log;
|
||||
@@ -43,6 +57,8 @@ export class UpdaterMain {
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", async () => {
|
||||
this.initialUpdateNotificationTime ??= Date.now();
|
||||
|
||||
if (this.doingUpdateCheckWithFeedback) {
|
||||
if (this.windowMain.win == null) {
|
||||
this.reset();
|
||||
@@ -87,7 +103,7 @@ export class UpdaterMain {
|
||||
}
|
||||
|
||||
this.updateDownloaded = info;
|
||||
await this.promptRestartUpdate(info);
|
||||
await this.promptRestartUpdate(info, this.doingUpdateCheckWithFeedback);
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
@@ -108,7 +124,7 @@ export class UpdaterMain {
|
||||
}
|
||||
|
||||
if (this.updateDownloaded && withFeedback) {
|
||||
await this.promptRestartUpdate(this.updateDownloaded);
|
||||
await this.promptRestartUpdate(this.updateDownloaded, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +160,50 @@ export class UpdaterMain {
|
||||
this.updateDownloaded = null;
|
||||
}
|
||||
|
||||
private async promptRestartUpdate(info: UpdateDownloadedEvent) {
|
||||
private async promptRestartUpdate(info: UpdateDownloadedEvent, blocking: boolean) {
|
||||
// If we have an initial notification, and it's from a long time ago,
|
||||
// we will block the user with a dialog to ensure they see it.
|
||||
const longTimeSinceInitialNotification =
|
||||
this.initialUpdateNotificationTime != null &&
|
||||
Date.now() - this.initialUpdateNotificationTime > MaxTimeBeforeBlockingUpdateNotification;
|
||||
|
||||
if (!longTimeSinceInitialNotification && !blocking && Notification.isSupported()) {
|
||||
// If the prompt doesn't have to block and we support notifications,
|
||||
// we will show a notification instead of a blocking dialog, which won't steal focus.
|
||||
await this.promptRestartUpdateUsingSystemNotification(info);
|
||||
} else {
|
||||
// If we are blocking, or notifications are not supported, we will show a blocking dialog.
|
||||
// This will steal the user's focus, so we should only do this for user initiated actions
|
||||
// or when there are no other options.
|
||||
await this.promptRestartUpdateUsingDialog(info);
|
||||
}
|
||||
}
|
||||
|
||||
private async promptRestartUpdateUsingSystemNotification(info: UpdateDownloadedEvent) {
|
||||
if (this.openedNotification != null) {
|
||||
this.openedNotification.close();
|
||||
}
|
||||
|
||||
this.openedNotification = new Notification({
|
||||
title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"),
|
||||
body: this.i18nService.t("restartToUpdateDesc", info.version),
|
||||
timeoutType: "never",
|
||||
silent: false,
|
||||
});
|
||||
|
||||
// If the user clicks the notification, prompt again to restart, this time with a blocking dialog.
|
||||
this.openedNotification.on("click", () => {
|
||||
void this.promptRestartUpdate(info, true);
|
||||
});
|
||||
// If the notification fails to show, fall back to the blocking dialog as well.
|
||||
this.openedNotification.on("failed", (error) => {
|
||||
this.logService.error("Update notification failed", error);
|
||||
void this.promptRestartUpdate(info, true);
|
||||
});
|
||||
this.openedNotification.show();
|
||||
}
|
||||
|
||||
private async promptRestartUpdateUsingDialog(info: UpdateDownloadedEvent) {
|
||||
const result = await dialog.showMessageBox(this.windowMain.win, {
|
||||
type: "info",
|
||||
title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"),
|
||||
|
||||
Reference in New Issue
Block a user