diff --git a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts new file mode 100644 index 00000000000..0dfb41d2ed4 --- /dev/null +++ b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts @@ -0,0 +1,82 @@ +import { Observable, Subject } from "rxjs"; + +import { DeviceType } from "@bitwarden/common/enums"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + ButtonLocation, + SystemNotificationClearInfo, + SystemNotificationCreateInfo, + SystemNotificationEvent, + SystemNotificationService, +} from "@bitwarden/common/platform/notifications/system-notification-service"; + +export class BrowserSystemNotificationService implements SystemNotificationService { + private systemNotificationClickedSubject = new Subject(); + notificationClicked$: Observable; + + constructor( + private logService: LogService, + private platformUtilsService: PlatformUtilsService, + ) { + this.notificationClicked$ = this.systemNotificationClickedSubject.asObservable(); + } + + async create(createInfo: SystemNotificationCreateInfo): Promise { + if (!this.isSupported()) { + this.logService.error("While trying to create, found that it is not supported"); + } + + chrome.notifications.create(createInfo.id, { + iconUrl: "https://avatars.githubusercontent.com/u/15990069?s=200", + message: createInfo.title, + type: "basic", + title: createInfo.title, + buttons: createInfo.buttons.map((value) => { + return { title: value.title }; + }), + }); + + // ESLint: Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi. addListener` instead (no-restricted-syntax) + // eslint-disable-next-line no-restricted-syntax + chrome.notifications.onButtonClicked.addListener( + (notificationId: string, buttonIndex: number) => { + this.systemNotificationClickedSubject.next({ + id: notificationId, + type: createInfo.type, + buttonIdentifier: buttonIndex, + }); + }, + ); + + // eslint-disable-next-line no-restricted-syntax + chrome.notifications.onClicked.addListener((notificationId: string) => { + this.systemNotificationClickedSubject.next({ + id: notificationId, + type: createInfo.type, + buttonIdentifier: ButtonLocation.NotificationButton, + }); + }); + return; + } + + clear(clearInfo: SystemNotificationClearInfo): undefined { + if (!this.isSupported()) { + this.logService.error("While trying to clear, found that it is not supported"); + } + + chrome.notifications.clear(clearInfo.id); + } + + isSupported(): boolean { + switch (this.platformUtilsService.getDevice()) { + case DeviceType.EdgeExtension: + case DeviceType.VivaldiExtension: + case DeviceType.OperaExtension: + case DeviceType.ChromeExtension: + return true; + default: + return false; + } + } +} diff --git a/libs/common/src/platform/notifications/system-notification-service.ts b/libs/common/src/platform/notifications/system-notification-service.ts new file mode 100644 index 00000000000..449c38b22c1 --- /dev/null +++ b/libs/common/src/platform/notifications/system-notification-service.ts @@ -0,0 +1,51 @@ +import { Observable } from "rxjs"; + +export const ButtonActions = { + AuthRequestNotification: "authRequestNotification", +}; + +export type ButtonActionsKeys = (typeof ButtonActions)[keyof typeof ButtonActions]; + +// This is currently tailored for chrome extension's api, if safari works +// differently where clicking a notification button produces a different +// identifier we need to reconcile that here. +export const ButtonLocation = { + FirstOptionalButton: 0, // this is the first optional button we can set + SecondOptionalButton: 1, // this is the second optional button we can set + NotificationButton: 2, // this is when you click the notification as a whole +}; + +export type ButtonLocationKeys = (typeof ButtonLocation)[keyof typeof ButtonLocation]; + +export type SystemNotificationsButton = { + title: string; +}; + +export type SystemNotificationCreateInfo = { + id: string; + type: ButtonActionsKeys; + title: string; + body: string; + buttons: SystemNotificationsButton[]; +}; + +export type SystemNotificationClearInfo = { + id: string; +}; + +export type SystemNotificationEvent = { + id: string; + type: string; + buttonIdentifier: number; +}; + +export abstract class SystemNotificationService { + abstract notificationClicked$: Observable; + abstract create(createInfo: SystemNotificationCreateInfo): Promise; + abstract clear(clearInfo: SystemNotificationClearInfo): undefined; + + /** + * Used to know if a given platform supports notifications. + */ + abstract isSupported(): boolean; +} diff --git a/libs/common/src/platform/notifications/unsupported-system-notification.service.ts b/libs/common/src/platform/notifications/unsupported-system-notification.service.ts new file mode 100644 index 00000000000..f03da717138 --- /dev/null +++ b/libs/common/src/platform/notifications/unsupported-system-notification.service.ts @@ -0,0 +1,25 @@ +import { Subject, throwError } from "rxjs"; + +import { + SystemNotificationClearInfo, + SystemNotificationCreateInfo, + SystemNotificationEvent, + SystemNotificationService, +} from "./system-notification-service"; + +export class UnsupportedSystemNotificationService implements SystemNotificationService { + private systemNotificationClickedSubject = new Subject(); + notificationClicked$ = throwError(() => new Error("Notification clicked is not supported.")); + + async create(createInfo: SystemNotificationCreateInfo): Promise { + throw new Error("Create OS Notification unsupported."); + } + + clear(clearInfo: SystemNotificationClearInfo): undefined { + throw new Error("Clear OS Notification unsupported."); + } + + isSupported(): boolean { + return false; + } +}