import { v4 } from 'uuid' ; import { Data } from '../../../../modules/Data/Data' ; import { Api } from '../../../../modules/Api/Api' ; import { Logger } from '../../../../logger' ; import * as CONSTANTS from '../../../../constants' ; import { IPushService , IPushServiceSubscriptionKeys , } from '../../PushService.types' ; import { IPushServiceDefaultConfig , IPushServiceFcmRequest , IPushServiceFcmResponse , } from './PushServiceDefault.types' ; export class PushServiceDefault implements IPushService { private readonly api : Api ; private readonly data : Data ; private readonly config : IPushServiceDefaultConfig ; private registration ? : ServiceWorkerRegistration ; constructor ( api , data , config ) { this . api = api ; this . data = data ; this . config = config ; } public getPermission ( ) { return Notification . permission ; } public checkIsPermissionGranted ( ) { return this . getPermission ( ) === CONSTANTS . PERMISSION_GRANTED ; } public checkIsPermissionDefault ( ) { return this . getPermission ( ) === CONSTANTS . PERMISSION_PROMPT ; } public async checkIsManualUnsubscribed ( ) { return this . data . getStatusManualUnsubscribed ( ) ; } public async askPermission ( ) { await Notification . requestPermission ( ) ; } public async getTokens ( ) { return this . data . getTokens ( ) ; } public async subscribe ( subscription ? ) { let currentSubscription = subscription ; const isPermissionGranted = this . checkIsPermissionGranted ( ) ; if ( ! isPermissionGranted ) { Logger . error ( 'You must have permission granted before subscribe!' ) ; return ; } if ( ! currentSubscription ) { currentSubscription = await this . trySubscribe ( ) ; const senderId = await this . getSenderIdFromManifest ( ) ; const isChangeSenderId = await this . checkIsChangeSenderId ( senderId ) ; if ( isChangeSenderId ) { await this . unsubscribe ( ) ; await this . data . setSenderId ( senderId ) ; } } const applicationServerKey = await this . getApplicationServerKey ( ) ; const senderId = await this . data . getSenderId ( ) ; const pushToken = await this . getPushToken ( currentSubscription ) ; const _p256dn = currentSubscription . getKey ( 'p256dh' ) ; const _auth = currentSubscription . getKey ( 'auth' ) ; if ( ! _p256dn || ! _auth ) { throw new Error ( 'Can\'t get subscription keys!' ) ; } const p256dh = btoa ( String . fromCharCode . apply ( String , new Uint8Array ( _p256dn ) ) ) ; const auth = btoa ( String . fromCharCode . apply ( String , new Uint8Array ( _auth ) ) ) ; const { token , pushSet } = await this . getFcmKeys ( { endpoint : currentSubscription . endpoint , application_pub_key : applicationServerKey , encryption_key : p256dh , encryption_auth : auth , authorized_entity : senderId , } ) ; await this . data . setTokens ( { publicKey : p256dh , pushToken : pushToken , authToken : auth , fcmPushSet : pushSet , fcmToken : token , endpoint : currentSubscription . endpoint , } ) ; await this . api . registerDevice ( ) ; } public async unsubscribe ( ) { const registration = await this . getServiceWorkerRegistration ( ) ; const subscription = await registration . pushManager . getSubscription ( ) ; await this . data . setTokens ( { } ) ; await this . data . setStatusManualUnsubscribed ( true ) ; await this . api . unregisterDevice ( ) ; if ( ! subscription ) { return ; } await subscription . unsubscribe ( ) ; } public async checkIsRegister ( ) { return this . api . checkDeviceSubscribeForPushNotifications ( ) ; } public async checkIsNeedResubscribe ( ) { const savedSenderId = await this . data . getSenderId ( ) ; const isExistSavedSenderId = typeof savedSenderId !== 'undefined' ; const manifestSenderId = await this . getSenderIdFromManifest ( ) ; const isChangeSenderID = isExistSavedSenderId && manifestSenderId !== savedSenderId ; await this . data . setSenderId ( manifestSenderId ) ; const lastPermission = await this . data . getLastPermissionStatus ( ) ; const permission = this . getPermission ( ) ; if ( isExistSavedSenderId && lastPermission !== permission ) { await this . data . setLastPermissionStatus ( permission ) ; return true ; } const credentials = await this . getCredentials ( ) ; const pushTokenFromSubscription = await this . getPushToken ( credentials ) ; const pushTokenFromStore = await this . data . getTokens ( ) ; const isEqualPushTokens = pushTokenFromSubscription === ( pushTokenFromStore && pushTokenFromStore . pushToken ) ; return isChangeSenderID || ! isEqualPushTokens ; } private async getServiceWorkerRegistration ( ) { if ( ! this . registration ) { await this . registerServiceWorker ( ) ; const url = await this . data . getServiceWorkerUrl ( ) ; this . registration = await navigator . serviceWorker . getRegistration ( url ) ; await ( this . registration as ServiceWorkerRegistration ) . update ( ) ; } if ( ! this . registration ) { throw new Error ( `Internal Error: Can't register service worker!` ) ; } return this . registration ; } private async registerServiceWorker ( ) { const url = await this . data . getServiceWorkerUrl ( ) ; const scope = await this . data . getServiceWorkerScope ( ) ; const sdkVersion = await this . data . getSdkVersion ( ) ; const serviceWorkerVersion = await this . data . getServiceWorkerVersion ( ) ; let cleanCache = '' ; if ( sdkVersion !== serviceWorkerVersion ) { cleanCache = `?cache_clean=${ v4 ( ) }` ; } await navigator . serviceWorker . register ( `${ url }${ cleanCache }` , { scope } ) ; } private async trySubscribe ( ) { try { return await this . subscribePushManager ( ) ; } catch ( error ) { console . error ( error ) ; await this . unsubscribe ( ) ; return this . subscribePushManager ( ) ; } } private async subscribePushManager ( ) { const registration = await this . getServiceWorkerRegistration ( ) ; const applicationServerKey = await this . getApplicationServerKey ( ) ; return registration . pushManager . subscribe ( { userVisibleOnly : true , applicationServerKey : applicationServerKey ? this . urlBase64ToUint8Array ( applicationServerKey ) : null , } ) ; } private async getCredentials ( ) { const registration = await this . getServiceWorkerRegistration ( ) ; return await registration . pushManager . getSubscription ( ) ; } private async getFcmKeys ( config ) { const response = await fetch ( this . config . entrypoint || 'https://fcm.googleapis.com/fcm/connect/subscribe' , { method : 'POST' , headers : { 'Content-Type' : 'text/plain;charset=UTF-8' , } , body : JSON . stringify ( config ) } ) ; if ( response . status === 200 ) { return await response . json ( ) ; } throw new Error ( 'Internal error: Can\'t register device in fcm. Status: ' + response . status + '. Message: ' + response . statusText ) ; } private async getPushToken ( subscription ) { const deviceType = await this . data . getDeviceType ( ) ; if ( ! subscription ) { return '' ; } if ( deviceType === 12 ) { return subscription . endpoint ; } return subscription . endpoint . split ( '/' ) . pop ( ) || '' ; } private async getApplicationServerKey ( ) { const deviceType = await this . data . getDeviceType ( ) ; if ( deviceType !== 11 ) { return ; } return await this . data . getApplicationServerKey ( ) ; } private async getSenderIdFromManifest ( ) { const manifest = document . querySelector ( 'link[rel="manifest"]' ) ; if ( ! manifest ) { throw new Error ( 'Error: manifest.json not found!' ) ; } const url = manifest . getAttribute ( 'href' ) ; if ( ! url ) { throw new Error ( 'Error: manifest.json url not found!' ) ; } const response = await fetch ( url , { method : 'GET' , headers : { 'Content-Type' : 'application/json;charset=UTF-8' , } , } ) ; if ( response . status !== 200 ) { throw new Error ( 'Error: Can\'t load manifest.json! ' + response . statusText ) ; } const data = await response . text ( ) ; const match = data . match ( /("|')?gcm_sender_id("|')?:\s*("|')?(\d+)("|')?/ ) ; if ( ! match || typeof match [ 4 ] !== 'string' ) { throw new Error ( 'Error: Can\'t find gcm_sender_id in manifest.json!' ) ; } return match [ 4 ] ; } private async checkIsChangeSenderId ( currentSenderId ) { const senderIdFromIndexedDB = await this . data . getSenderId ( ) ; return currentSenderId !== senderIdFromIndexedDB ; } private urlBase64ToUint8Array ( base64String ) { const padding = '=' . repeat ( ( 4 - base64String . length % 4 ) % 4 ) ; const base64 = ( base64String + padding ) . replace ( /\-/g , '+' ) . replace ( /_/g , '/' ) ; const rawData = window . atob ( base64 ) ; const outputArray = new Uint8Array ( rawData . length ) ; for ( let i = 0 ; i < rawData . length ; ++ i ) { outputArray [ i ] = rawData . charCodeAt ( i ) ; } return outputArray ; } }