| /** |
| * @license |
| * Copyright 2022 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| import {FlagsService, KnownExperimentId} from './flags/flags'; |
| import { |
| areNotificationsEnabled, |
| registerServiceWorker, |
| } from '../utils/worker-util'; |
| import {UserModel} from '../models/user/user-model'; |
| import {AccountDetailInfo} from '../api/rest-api'; |
| import {until} from '../utils/async-util'; |
| import {LifeCycle} from '../constants/reporting'; |
| import {ReportingService} from './gr-reporting/gr-reporting'; |
| import {define} from '../models/dependency'; |
| import {Model} from '../models/model'; |
| import {Observable} from 'rxjs'; |
| import {select} from '../utils/observable-util'; |
| |
| /** Type of incoming messages for ServiceWorker. */ |
| export enum ServiceWorkerMessageType { |
| TRIGGER_NOTIFICATIONS = 'TRIGGER_NOTIFICATIONS', |
| USER_PREFERENCE_CHANGE = 'USER_PREFERENCE_CHANGE', |
| REPORTING = 'REPORTING', |
| } |
| |
| export const TRIGGER_NOTIFICATION_UPDATES_MS = 5 * 60 * 1000; |
| |
| export const serviceWorkerInstallerToken = define<ServiceWorkerInstaller>( |
| 'service-worker-installer' |
| ); |
| |
| /** |
| * Service worker state: |
| * initialized - True when service worker registered and event listeners added. |
| * - False otherwise |
| * shouldShowPrompt - True when user didn't make decision about notifications |
| * - False otherwise |
| */ |
| export interface ServiceWorkerInstallerState { |
| initialized: boolean; |
| shouldShowPrompt: boolean; |
| } |
| |
| export class ServiceWorkerInstaller extends Model<ServiceWorkerInstallerState> { |
| readonly initialized$: Observable<Boolean | undefined> = select( |
| this.state$, |
| state => state.initialized |
| ); |
| |
| readonly shouldShowPrompt$: Observable<Boolean | undefined> = select( |
| this.initialized$, |
| _ => this.shouldShowPrompt() |
| ); |
| |
| // Internal state, it's exposed in initialized$ |
| private initialized = false; |
| |
| account?: AccountDetailInfo; |
| |
| allowBrowserNotificationsPreference?: boolean; |
| |
| constructor( |
| private readonly flagsService: FlagsService, |
| private readonly reportingService: ReportingService, |
| private readonly userModel: UserModel |
| ) { |
| super({initialized: false, shouldShowPrompt: false}); |
| if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) { |
| return; |
| } |
| this.userModel.account$.subscribe(acc => (this.account = acc)); |
| this.userModel.preferences$.subscribe(prefs => { |
| if ( |
| this.allowBrowserNotificationsPreference !== |
| prefs.allow_browser_notifications |
| ) { |
| this.allowBrowserNotificationsPreference = |
| prefs.allow_browser_notifications; |
| // flag can disable notifications similar to user setting |
| navigator.serviceWorker.controller?.postMessage({ |
| type: ServiceWorkerMessageType.USER_PREFERENCE_CHANGE, |
| allowBrowserNotificationsPreference: |
| this.allowBrowserNotificationsPreference && |
| this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS), |
| }); |
| } |
| }); |
| Promise.all([ |
| until(this.userModel.account$, account => !!account), |
| until( |
| this.userModel.preferences$, |
| prefs => !!prefs.allow_browser_notifications |
| ), |
| ]).then(() => { |
| this.init(); |
| }); |
| } |
| |
| private async init() { |
| if (this.initialized) return; |
| if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) { |
| return; |
| } |
| if (!this.areNotificationsEnabled()) return; |
| |
| if (!('serviceWorker' in navigator)) { |
| console.error('Service worker API not available'); |
| return; |
| } |
| await registerServiceWorker('/service-worker.js'); |
| const permission = Notification.permission; |
| this.reportingService.reportLifeCycle(LifeCycle.NOTIFICATION_PERMISSION, { |
| permission, |
| }); |
| if (this.isPermitted(permission)) this.startTriggerTimer(); |
| this.initialized = true; |
| this.updateState({initialized: true}); |
| // Assumption: service worker will send event only to 1 client. |
| navigator.serviceWorker.onmessage = event => { |
| if (event.data?.type === ServiceWorkerMessageType.REPORTING) { |
| this.reportingService.reportLifeCycle(LifeCycle.SERVICE_WORKER_UPDATE, { |
| eventName: event.data.eventName as string | undefined, |
| }); |
| } |
| }; |
| } |
| |
| // private, used in test |
| shouldShowPrompt(): boolean { |
| if (!this.initialized) return false; |
| if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) { |
| return false; |
| } |
| if (!this.areNotificationsEnabled()) return false; |
| return Notification.permission === 'default'; |
| } |
| |
| public async requestPermission() { |
| const permission = await Notification.requestPermission(); |
| this.reportingService.reportLifeCycle(LifeCycle.NOTIFICATION_PERMISSION, { |
| requested: true, |
| permission, |
| }); |
| if (this.isPermitted(permission)) this.startTriggerTimer(); |
| } |
| |
| areNotificationsEnabled() { |
| // Push Notification developer can have notification enabled even if they |
| // are disabled for this.account. |
| if ( |
| !this.flagsService.isEnabled( |
| KnownExperimentId.PUSH_NOTIFICATIONS_DEVELOPER |
| ) && |
| !areNotificationsEnabled(this.account) |
| ) { |
| return false; |
| } |
| |
| return this.allowBrowserNotificationsPreference; |
| } |
| |
| /** |
| * Every 5 minutes, we trigger service-worker to get |
| * latest updates in attention set and service-worker will create |
| * notifications. |
| */ |
| startTriggerTimer() { |
| setTimeout(() => { |
| this.startTriggerTimer(); |
| navigator.serviceWorker.controller?.postMessage({ |
| type: ServiceWorkerMessageType.TRIGGER_NOTIFICATIONS, |
| account: this.account, |
| }); |
| }, TRIGGER_NOTIFICATION_UPDATES_MS); |
| } |
| |
| isPermitted(permission: NotificationPermission) { |
| return permission === 'granted'; |
| } |
| } |