import firebase from '@firebase/app'; import '@firebase/installations'; import { Component } from '@firebase/component'; import { ErrorFactory } from '@firebase/util'; import { openDb, deleteDb } from 'idb'; /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const ERROR_MAP = { ["missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */]: 'Missing App configuration value: "{$valueName}"', ["only-available-in-window" /* AVAILABLE_IN_WINDOW */]: 'This method is available in a Window context.', ["only-available-in-sw" /* AVAILABLE_IN_SW */]: 'This method is available in a service worker context.', ["permission-default" /* PERMISSION_DEFAULT */]: 'The notification permission was not granted and dismissed instead.', ["permission-blocked" /* PERMISSION_BLOCKED */]: 'The notification permission was not granted and blocked instead.', ["unsupported-browser" /* UNSUPPORTED_BROWSER */]: "This browser doesn't support the API's required to use the firebase SDK.", ["failed-service-worker-registration" /* FAILED_DEFAULT_REGISTRATION */]: 'We are unable to register the default service worker. {$browserErrorMessage}', ["token-subscribe-failed" /* TOKEN_SUBSCRIBE_FAILED */]: 'A problem occured while subscribing the user to FCM: {$errorInfo}', ["token-subscribe-no-token" /* TOKEN_SUBSCRIBE_NO_TOKEN */]: 'FCM returned no token when subscribing the user to push.', ["token-unsubscribe-failed" /* TOKEN_UNSUBSCRIBE_FAILED */]: 'A problem occured while unsubscribing the ' + 'user from FCM: {$errorInfo}', ["token-update-failed" /* TOKEN_UPDATE_FAILED */]: 'A problem occured while updating the user from FCM: {$errorInfo}', ["token-update-no-token" /* TOKEN_UPDATE_NO_TOKEN */]: 'FCM returned no token when updating the user to push.', ["use-sw-after-get-token" /* USE_SW_AFTER_GET_TOKEN */]: 'The useServiceWorker() method may only be called once and must be ' + 'called before calling getToken() to ensure your service worker is used.', ["invalid-sw-registration" /* INVALID_SW_REGISTRATION */]: 'The input to useServiceWorker() must be a ServiceWorkerRegistration.', ["invalid-bg-handler" /* INVALID_BG_HANDLER */]: 'The input to setBackgroundMessageHandler() must be a function.', ["invalid-vapid-key" /* INVALID_VAPID_KEY */]: 'The public VAPID key must be a string.', ["use-vapid-key-after-get-token" /* USE_VAPID_KEY_AFTER_GET_TOKEN */]: 'The usePublicVapidKey() method may only be called once and must be ' + 'called before calling getToken() to ensure your VAPID key is used.' }; const ERROR_FACTORY = new ErrorFactory('messaging', 'Messaging', ERROR_MAP); /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function extractAppConfig(app) { if (!app || !app.options) { throw getMissingValueError('App Configuration Object'); } if (!app.name) { throw getMissingValueError('App Name'); } // Required app config keys const configKeys = [ 'projectId', 'apiKey', 'appId', 'messagingSenderId' ]; const { options } = app; for (const keyName of configKeys) { if (!options[keyName]) { throw getMissingValueError(keyName); } } return { appName: app.name, projectId: options.projectId, apiKey: options.apiKey, appId: options.appId, senderId: options.messagingSenderId }; } function getMissingValueError(valueName) { return ERROR_FACTORY.create("missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */, { valueName }); } /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function arrayToBase64(array) { const uint8Array = new Uint8Array(array); const base64String = btoa(String.fromCharCode(...uint8Array)); return base64String .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); } /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const OLD_DB_NAME = 'fcm_token_details_db'; /** * The last DB version of 'fcm_token_details_db' was 4. This is one higher, * so that the upgrade callback is called for all versions of the old DB. */ const OLD_DB_VERSION = 5; const OLD_OBJECT_STORE_NAME = 'fcm_token_object_Store'; async function migrateOldDatabase(senderId) { if ('databases' in indexedDB) { // indexedDb.databases() is an IndexedDB v3 API // and does not exist in all browsers. // TODO: Remove typecast when it lands in TS types. const databases = await indexedDB.databases(); const dbNames = databases.map(db => db.name); if (!dbNames.includes(OLD_DB_NAME)) { // old DB didn't exist, no need to open. return null; } } let tokenDetails = null; const db = await openDb(OLD_DB_NAME, OLD_DB_VERSION, async (db) => { var _a; if (db.oldVersion < 2) { // Database too old, skip migration. return; } if (!db.objectStoreNames.contains(OLD_OBJECT_STORE_NAME)) { // Database did not exist. Nothing to do. return; } const objectStore = db.transaction.objectStore(OLD_OBJECT_STORE_NAME); const value = await objectStore.index('fcmSenderId').get(senderId); await objectStore.clear(); if (!value) { // No entry in the database, nothing to migrate. return; } if (db.oldVersion === 2) { const oldDetails = value; if (!oldDetails.auth || !oldDetails.p256dh || !oldDetails.endpoint) { return; } tokenDetails = { token: oldDetails.fcmToken, createTime: (_a = oldDetails.createTime, (_a !== null && _a !== void 0 ? _a : Date.now())), subscriptionOptions: { auth: oldDetails.auth, p256dh: oldDetails.p256dh, endpoint: oldDetails.endpoint, swScope: oldDetails.swScope, vapidKey: typeof oldDetails.vapidKey === 'string' ? oldDetails.vapidKey : arrayToBase64(oldDetails.vapidKey) } }; } else if (db.oldVersion === 3) { const oldDetails = value; tokenDetails = { token: oldDetails.fcmToken, createTime: oldDetails.createTime, subscriptionOptions: { auth: arrayToBase64(oldDetails.auth), p256dh: arrayToBase64(oldDetails.p256dh), endpoint: oldDetails.endpoint, swScope: oldDetails.swScope, vapidKey: arrayToBase64(oldDetails.vapidKey) } }; } else if (db.oldVersion === 4) { const oldDetails = value; tokenDetails = { token: oldDetails.fcmToken, createTime: oldDetails.createTime, subscriptionOptions: { auth: arrayToBase64(oldDetails.auth), p256dh: arrayToBase64(oldDetails.p256dh), endpoint: oldDetails.endpoint, swScope: oldDetails.swScope, vapidKey: arrayToBase64(oldDetails.vapidKey) } }; } }); db.close(); // Delete all old databases. await deleteDb(OLD_DB_NAME); await deleteDb('fcm_vapid_details_db'); await deleteDb('undefined'); return checkTokenDetails(tokenDetails) ? tokenDetails : null; } function checkTokenDetails(tokenDetails) { if (!tokenDetails || !tokenDetails.subscriptionOptions) { return false; } const { subscriptionOptions } = tokenDetails; return (typeof tokenDetails.createTime === 'number' && tokenDetails.createTime > 0 && typeof tokenDetails.token === 'string' && tokenDetails.token.length > 0 && typeof subscriptionOptions.auth === 'string' && subscriptionOptions.auth.length > 0 && typeof subscriptionOptions.p256dh === 'string' && subscriptionOptions.p256dh.length > 0 && typeof subscriptionOptions.endpoint === 'string' && subscriptionOptions.endpoint.length > 0 && typeof subscriptionOptions.swScope === 'string' && subscriptionOptions.swScope.length > 0 && typeof subscriptionOptions.vapidKey === 'string' && subscriptionOptions.vapidKey.length > 0); } /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Exported for tests. const DATABASE_NAME = 'firebase-messaging-database'; const DATABASE_VERSION = 1; const OBJECT_STORE_NAME = 'firebase-messaging-store'; let dbPromise = null; function getDbPromise() { if (!dbPromise) { dbPromise = openDb(DATABASE_NAME, DATABASE_VERSION, upgradeDb => { // We don't use 'break' in this switch statement, the fall-through // behavior is what we want, because if there are multiple versions between // the old version and the current version, we want ALL the migrations // that correspond to those versions to run, not only the last one. // eslint-disable-next-line default-case switch (upgradeDb.oldVersion) { case 0: upgradeDb.createObjectStore(OBJECT_STORE_NAME); } }); } return dbPromise; } /** Gets record(s) from the objectStore that match the given key. */ async function dbGet(firebaseDependencies) { const key = getKey(firebaseDependencies); const db = await getDbPromise(); const tokenDetails = await db .transaction(OBJECT_STORE_NAME) .objectStore(OBJECT_STORE_NAME) .get(key); if (tokenDetails) { return tokenDetails; } else { // Check if there is a tokenDetails object in the old DB. const oldTokenDetails = await migrateOldDatabase(firebaseDependencies.appConfig.senderId); if (oldTokenDetails) { await dbSet(firebaseDependencies, oldTokenDetails); return oldTokenDetails; } } } /** Assigns or overwrites the record for the given key with the given value. */ async function dbSet(firebaseDependencies, tokenDetails) { const key = getKey(firebaseDependencies); const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).put(tokenDetails, key); await tx.complete; return tokenDetails; } /** Removes record(s) from the objectStore that match the given key. */ async function dbRemove(firebaseDependencies) { const key = getKey(firebaseDependencies); const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).delete(key); await tx.complete; } function getKey({ appConfig }) { return appConfig.appId; } /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const DEFAULT_SW_PATH = '/firebase-messaging-sw.js'; const DEFAULT_SW_SCOPE = '/firebase-cloud-messaging-push-scope'; const DEFAULT_VAPID_KEY = 'BDOU99-h67HcA6JeFXHbSNMu7e2yNNu3RzoMj8TM4W88jITfq7ZmPvIM1Iv-4_l2LxQcYwhqby2xGpWwzjfAnG4'; const ENDPOINT = 'https://fcmregistrations.googleapis.com/v1'; /** Key of FCM Payload in Notification's data field. */ const FCM_MSG = 'FCM_MSG'; const CONSOLE_CAMPAIGN_ID = 'google.c.a.c_id'; const CONSOLE_CAMPAIGN_NAME = 'google.c.a.c_l'; const CONSOLE_CAMPAIGN_TIME = 'google.c.a.ts'; /** Set to '1' if Analytics is enabled for the campaign */ const CONSOLE_CAMPAIGN_ANALYTICS_ENABLED = 'google.c.a.e'; /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ async function requestGetToken(firebaseDependencies, subscriptionOptions) { const headers = await getHeaders(firebaseDependencies); const body = getBody(subscriptionOptions); const subscribeOptions = { method: 'POST', headers, body: JSON.stringify(body) }; let responseData; try { const response = await fetch(getEndpoint(firebaseDependencies.appConfig), subscribeOptions); responseData = await response.json(); } catch (err) { throw ERROR_FACTORY.create("token-subscribe-failed" /* TOKEN_SUBSCRIBE_FAILED */, { errorInfo: err }); } if (responseData.error) { const message = responseData.error.message; throw ERROR_FACTORY.create("token-subscribe-failed" /* TOKEN_SUBSCRIBE_FAILED */, { errorInfo: message }); } if (!responseData.token) { throw ERROR_FACTORY.create("token-subscribe-no-token" /* TOKEN_SUBSCRIBE_NO_TOKEN */); } return responseData.token; } async function requestUpdateToken(firebaseDependencies, tokenDetails) { const headers = await getHeaders(firebaseDependencies); const body = getBody(tokenDetails.subscriptionOptions); const updateOptions = { method: 'PATCH', headers, body: JSON.stringify(body) }; let responseData; try { const response = await fetch(`${getEndpoint(firebaseDependencies.appConfig)}/${tokenDetails.token}`, updateOptions); responseData = await response.json(); } catch (err) { throw ERROR_FACTORY.create("token-update-failed" /* TOKEN_UPDATE_FAILED */, { errorInfo: err }); } if (responseData.error) { const message = responseData.error.message; throw ERROR_FACTORY.create("token-update-failed" /* TOKEN_UPDATE_FAILED */, { errorInfo: message }); } if (!responseData.token) { throw ERROR_FACTORY.create("token-update-no-token" /* TOKEN_UPDATE_NO_TOKEN */); } return responseData.token; } async function requestDeleteToken(firebaseDependencies, token) { const headers = await getHeaders(firebaseDependencies); const unsubscribeOptions = { method: 'DELETE', headers }; try { const response = await fetch(`${getEndpoint(firebaseDependencies.appConfig)}/${token}`, unsubscribeOptions); const responseData = await response.json(); if (responseData.error) { const message = responseData.error.message; throw ERROR_FACTORY.create("token-unsubscribe-failed" /* TOKEN_UNSUBSCRIBE_FAILED */, { errorInfo: message }); } } catch (err) { throw ERROR_FACTORY.create("token-unsubscribe-failed" /* TOKEN_UNSUBSCRIBE_FAILED */, { errorInfo: err }); } } function getEndpoint({ projectId }) { return `${ENDPOINT}/projects/${projectId}/registrations`; } async function getHeaders({ appConfig, installations }) { const authToken = await installations.getToken(); return new Headers({ 'Content-Type': 'application/json', Accept: 'application/json', 'x-goog-api-key': appConfig.apiKey, 'x-goog-firebase-installations-auth': `FIS ${authToken}` }); } function getBody({ p256dh, auth, endpoint, vapidKey }) { const body = { web: { endpoint, auth, p256dh } }; if (vapidKey !== DEFAULT_VAPID_KEY) { body.web.applicationPubKey = vapidKey; } return body; } /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** UpdateRegistration will be called once every week. */ const TOKEN_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days async function getToken(firebaseDependencies, swRegistration, vapidKey) { if (Notification.permission !== 'granted') { throw ERROR_FACTORY.create("permission-blocked" /* PERMISSION_BLOCKED */); } // If a PushSubscription exists it's returned, otherwise a new subscription // is generated and returned. const pushSubscription = await getPushSubscription(swRegistration, vapidKey); const tokenDetails = await dbGet(firebaseDependencies); const subscriptionOptions = { vapidKey, swScope: swRegistration.scope, endpoint: pushSubscription.endpoint, auth: arrayToBase64(pushSubscription.getKey('auth')), p256dh: arrayToBase64(pushSubscription.getKey('p256dh')) }; if (!tokenDetails) { // No token, get a new one. return getNewToken(firebaseDependencies, subscriptionOptions); } else if (!isTokenValid(tokenDetails.subscriptionOptions, subscriptionOptions)) { // Invalid token, get a new one. try { await requestDeleteToken(firebaseDependencies, tokenDetails.token); } catch (e) { // Suppress errors because of #2364 console.warn(e); } return getNewToken(firebaseDependencies, subscriptionOptions); } else if (Date.now() >= tokenDetails.createTime + TOKEN_EXPIRATION_MS) { // Weekly token refresh return updateToken({ token: tokenDetails.token, createTime: Date.now(), subscriptionOptions }, firebaseDependencies, swRegistration); } else { // Valid token, nothing to do. return tokenDetails.token; } } /** * This method deletes the token from the database, unsubscribes the token from * FCM, and unregisters the push subscription if it exists. */ async function deleteToken(firebaseDependencies, swRegistration) { const tokenDetails = await dbGet(firebaseDependencies); if (tokenDetails) { await requestDeleteToken(firebaseDependencies, tokenDetails.token); await dbRemove(firebaseDependencies); } // Unsubscribe from the push subscription. const pushSubscription = await swRegistration.pushManager.getSubscription(); if (pushSubscription) { return pushSubscription.unsubscribe(); } // If there's no SW, consider it a success. return true; } async function updateToken(tokenDetails, firebaseDependencies, swRegistration) { try { const updatedToken = await requestUpdateToken(firebaseDependencies, tokenDetails); const updatedTokenDetails = Object.assign({ token: updatedToken, createTime: Date.now() }, tokenDetails); await dbSet(firebaseDependencies, updatedTokenDetails); return updatedToken; } catch (e) { await deleteToken(firebaseDependencies, swRegistration); throw e; } } async function getNewToken(firebaseDependencies, subscriptionOptions) { const token = await requestGetToken(firebaseDependencies, subscriptionOptions); const tokenDetails = { token, createTime: Date.now(), subscriptionOptions }; await dbSet(firebaseDependencies, tokenDetails); return tokenDetails.token; } /** * Gets a PushSubscription for the current user. */ async function getPushSubscription(swRegistration, vapidKey) { const subscription = await swRegistration.pushManager.getSubscription(); if (subscription) { return subscription; } return swRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKey }); } /** * Checks if the saved tokenDetails object matches the configuration provided. */ function isTokenValid(dbOptions, currentOptions) { const isVapidKeyEqual = currentOptions.vapidKey === dbOptions.vapidKey; const isEndpointEqual = currentOptions.endpoint === dbOptions.endpoint; const isAuthEqual = currentOptions.auth === dbOptions.auth; const isP256dhEqual = currentOptions.p256dh === dbOptions.p256dh; return isVapidKeyEqual && isEndpointEqual && isAuthEqual && isP256dhEqual; } /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var MessageType; (function (MessageType) { MessageType["PUSH_RECEIVED"] = "push-received"; MessageType["NOTIFICATION_CLICKED"] = "notification-clicked"; })(MessageType || (MessageType = {})); /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function isConsoleMessage(data) { // This message has a campaign ID, meaning it was sent using the // Firebase Console. return typeof data === 'object' && !!data && CONSOLE_CAMPAIGN_ID in data; } /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class WindowController { constructor(firebaseDependencies) { this.firebaseDependencies = firebaseDependencies; this.vapidKey = null; this.onMessageCallback = null; navigator.serviceWorker.addEventListener('message', e => this.messageEventListener(e)); } get app() { return this.firebaseDependencies.app; } async getToken() { if (!this.vapidKey) { this.vapidKey = DEFAULT_VAPID_KEY; } const swRegistration = await this.getServiceWorkerRegistration(); // Check notification permission. if (Notification.permission === 'default') { // The user hasn't allowed or denied notifications yet. Ask them. await Notification.requestPermission(); } if (Notification.permission !== 'granted') { throw ERROR_FACTORY.create("permission-blocked" /* PERMISSION_BLOCKED */); } return getToken(this.firebaseDependencies, swRegistration, this.vapidKey); } async deleteToken() { const swRegistration = await this.getServiceWorkerRegistration(); return deleteToken(this.firebaseDependencies, swRegistration); } /** * Request permission if it is not currently granted. * * @return Resolves if the permission was granted, rejects otherwise. * * @deprecated Use Notification.requestPermission() instead. * https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission */ async requestPermission() { if (Notification.permission === 'granted') { return; } const permissionResult = await Notification.requestPermission(); if (permissionResult === 'granted') { return; } else if (permissionResult === 'denied') { throw ERROR_FACTORY.create("permission-blocked" /* PERMISSION_BLOCKED */); } else { throw ERROR_FACTORY.create("permission-default" /* PERMISSION_DEFAULT */); } } // TODO: Deprecate this and make VAPID key a parameter in getToken. usePublicVapidKey(vapidKey) { if (this.vapidKey !== null) { throw ERROR_FACTORY.create("use-vapid-key-after-get-token" /* USE_VAPID_KEY_AFTER_GET_TOKEN */); } if (typeof vapidKey !== 'string' || vapidKey.length === 0) { throw ERROR_FACTORY.create("invalid-vapid-key" /* INVALID_VAPID_KEY */); } this.vapidKey = vapidKey; } useServiceWorker(swRegistration) { if (!(swRegistration instanceof ServiceWorkerRegistration)) { throw ERROR_FACTORY.create("invalid-sw-registration" /* INVALID_SW_REGISTRATION */); } if (this.swRegistration) { throw ERROR_FACTORY.create("use-sw-after-get-token" /* USE_SW_AFTER_GET_TOKEN */); } this.swRegistration = swRegistration; } /** * @param nextOrObserver An observer object or a function triggered on * message. * @return The unsubscribe function for the observer. */ // TODO: Simplify this to only accept a function and not an Observer. onMessage(nextOrObserver) { this.onMessageCallback = typeof nextOrObserver === 'function' ? nextOrObserver : nextOrObserver.next; return () => { this.onMessageCallback = null; }; } setBackgroundMessageHandler() { throw ERROR_FACTORY.create("only-available-in-sw" /* AVAILABLE_IN_SW */); } // Unimplemented onTokenRefresh() { return () => { }; } /** * Creates or updates the default service worker registration. * @return The service worker registration to be used for the push service. */ async getServiceWorkerRegistration() { if (!this.swRegistration) { try { this.swRegistration = await navigator.serviceWorker.register(DEFAULT_SW_PATH, { scope: DEFAULT_SW_SCOPE }); } catch (e) { throw ERROR_FACTORY.create("failed-service-worker-registration" /* FAILED_DEFAULT_REGISTRATION */, { browserErrorMessage: e.message }); } } return this.swRegistration; } async messageEventListener(event) { var _a; if (!((_a = event.data) === null || _a === void 0 ? void 0 : _a.firebaseMessaging)) { // Not a message from FCM return; } const { type, payload } = event.data.firebaseMessaging; if (this.onMessageCallback && type === MessageType.PUSH_RECEIVED) { this.onMessageCallback(payload); } const { data } = payload; if (isConsoleMessage(data) && data[CONSOLE_CAMPAIGN_ANALYTICS_ENABLED] === '1') { // Analytics is enabled on this message, so we should log it. await this.logEvent(type, data); } } async logEvent(messageType, data) { const eventType = getEventType(messageType); const analytics = await this.firebaseDependencies.analyticsProvider.get(); analytics.logEvent(eventType, { /* eslint-disable camelcase */ message_id: data[CONSOLE_CAMPAIGN_ID], message_name: data[CONSOLE_CAMPAIGN_NAME], message_time: data[CONSOLE_CAMPAIGN_TIME], message_device_time: Math.floor(Date.now() / 1000) /* eslint-enable camelcase */ }); } } function getEventType(messageType) { switch (messageType) { case MessageType.NOTIFICATION_CLICKED: return 'notification_open'; case MessageType.PUSH_RECEIVED: return 'notification_foreground'; default: throw new Error(); } } /** * @license * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** Returns a promise that resolves after given time passes. */ function sleep(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class SwController { constructor(firebaseDependencies) { this.firebaseDependencies = firebaseDependencies; this.vapidKey = null; this.bgMessageHandler = null; self.addEventListener('push', e => { e.waitUntil(this.onPush(e)); }); self.addEventListener('pushsubscriptionchange', e => { e.waitUntil(this.onSubChange(e)); }); self.addEventListener('notificationclick', e => { e.waitUntil(this.onNotificationClick(e)); }); } get app() { return this.firebaseDependencies.app; } /** * Calling setBackgroundMessageHandler will opt in to some specific * behaviours. * 1.) If a notification doesn't need to be shown due to a window already * being visible, then push messages will be sent to the page. * 2.) If a notification needs to be shown, and the message contains no * notification data this method will be called * and the promise it returns will be passed to event.waitUntil. * If you do not set this callback then all push messages will let and the * developer can handle them in a their own 'push' event callback * * @param callback The callback to be called when a push message is received * and a notification must be shown. The callback will be given the data from * the push message. */ setBackgroundMessageHandler(callback) { if (!callback || typeof callback !== 'function') { throw ERROR_FACTORY.create("invalid-bg-handler" /* INVALID_BG_HANDLER */); } this.bgMessageHandler = callback; } // TODO: Remove getToken from SW Controller. // Calling this from an old SW can cause all kinds of trouble. async getToken() { var _a, _b, _c; if (!this.vapidKey) { // Call getToken using the current VAPID key if there already is a token. // This is needed because usePublicVapidKey was not available in SW. // It will be removed when vapidKey becomes a parameter of getToken, or // when getToken is removed from SW. const tokenDetails = await dbGet(this.firebaseDependencies); this.vapidKey = (_c = (_b = (_a = tokenDetails) === null || _a === void 0 ? void 0 : _a.subscriptionOptions) === null || _b === void 0 ? void 0 : _b.vapidKey, (_c !== null && _c !== void 0 ? _c : DEFAULT_VAPID_KEY)); } return getToken(this.firebaseDependencies, self.registration, this.vapidKey); } // TODO: Remove deleteToken from SW Controller. // Calling this from an old SW can cause all kinds of trouble. deleteToken() { return deleteToken(this.firebaseDependencies, self.registration); } requestPermission() { throw ERROR_FACTORY.create("only-available-in-window" /* AVAILABLE_IN_WINDOW */); } // TODO: Deprecate this and make VAPID key a parameter in getToken. // TODO: Remove this together with getToken from SW Controller. usePublicVapidKey(vapidKey) { if (this.vapidKey !== null) { throw ERROR_FACTORY.create("use-vapid-key-after-get-token" /* USE_VAPID_KEY_AFTER_GET_TOKEN */); } if (typeof vapidKey !== 'string' || vapidKey.length === 0) { throw ERROR_FACTORY.create("invalid-vapid-key" /* INVALID_VAPID_KEY */); } this.vapidKey = vapidKey; } useServiceWorker() { throw ERROR_FACTORY.create("only-available-in-window" /* AVAILABLE_IN_WINDOW */); } onMessage() { throw ERROR_FACTORY.create("only-available-in-window" /* AVAILABLE_IN_WINDOW */); } onTokenRefresh() { throw ERROR_FACTORY.create("only-available-in-window" /* AVAILABLE_IN_WINDOW */); } /** * A handler for push events that shows notifications based on the content of * the payload. * * The payload must be a JSON-encoded Object with a `notification` key. The * value of the `notification` property will be used as the NotificationOptions * object passed to showNotification. Additionally, the `title` property of the * notification object will be used as the title. * * If there is no notification data in the payload then no notification will be * shown. */ async onPush(event) { const payload = getMessagePayload(event); if (!payload) { return; } const clientList = await getClientList(); if (hasVisibleClients(clientList)) { // App in foreground. Send to page. return sendMessageToWindowClients(clientList, payload); } const notificationDetails = getNotificationData(payload); if (notificationDetails) { await showNotification(notificationDetails); } else if (this.bgMessageHandler) { await this.bgMessageHandler(payload); } } async onSubChange(event) { var _a, _b, _c; const { newSubscription } = event; if (!newSubscription) { // Subscription revoked, delete token await deleteToken(this.firebaseDependencies, self.registration); return; } const tokenDetails = await dbGet(this.firebaseDependencies); await deleteToken(this.firebaseDependencies, self.registration); await getToken(this.firebaseDependencies, self.registration, (_c = (_b = (_a = tokenDetails) === null || _a === void 0 ? void 0 : _a.subscriptionOptions) === null || _b === void 0 ? void 0 : _b.vapidKey, (_c !== null && _c !== void 0 ? _c : DEFAULT_VAPID_KEY))); } async onNotificationClick(event) { var _a, _b; const payload = (_b = (_a = event.notification) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b[FCM_MSG]; if (!payload) { // Not an FCM notification, do nothing. return; } else if (event.action) { // User clicked on an action button. // This will allow devs to act on action button clicks by using a custom // onNotificationClick listener that they define. return; } // Prevent other listeners from receiving the event event.stopImmediatePropagation(); event.notification.close(); const link = getLink(payload); if (!link) { return; } let client = await getWindowClient(link); if (!client) { // Unable to find window client so need to open one. // This also focuses the opened client. client = await self.clients.openWindow(link); // Wait three seconds for the client to initialize and set up the message // handler so that it can receive the message. await sleep(3000); } else { client = await client.focus(); } if (!client) { // Window Client will not be returned if it's for a third party origin. return; } const message = createNewMessage(MessageType.NOTIFICATION_CLICKED, payload); return client.postMessage(message); } } function getMessagePayload({ data }) { if (!data) { return null; } try { return data.json(); } catch (err) { // Not JSON so not an FCM message. return null; } } function getNotificationData(payload) { if (!payload || typeof payload.notification !== 'object') { return; } const notificationInformation = Object.assign({}, payload.notification); // Put the message payload under FCM_MSG name so we can identify the // notification as being an FCM notification vs a notification from // somewhere else (i.e. normal web push or developer generated // notification). notificationInformation.data = Object.assign(Object.assign({}, payload.notification.data), { [FCM_MSG]: payload }); return notificationInformation; } /** * @param url The URL to look for when focusing a client. * @return Returns an existing window client or a newly opened WindowClient. */ async function getWindowClient(url) { // Use URL to normalize the URL when comparing to windowClients. // This at least handles whether to include trailing slashes or not const parsedURL = new URL(url, self.location.href).href; const clientList = await getClientList(); for (const client of clientList) { const parsedClientUrl = new URL(client.url, self.location.href).href; if (parsedClientUrl === parsedURL) { return client; } } return null; } /** * @returns If there is currently a visible WindowClient, this method will * resolve to true, otherwise false. */ function hasVisibleClients(clientList) { return clientList.some(client => client.visibilityState === 'visible' && // Ignore chrome-extension clients as that matches the background pages // of extensions, which are always considered visible for some reason. !client.url.startsWith('chrome-extension://')); } /** * @param payload The data from the push event that should be sent to all * available pages. * @returns Returns a promise that resolves once the message has been sent to * all WindowClients. */ function sendMessageToWindowClients(clientList, payload) { const message = createNewMessage(MessageType.PUSH_RECEIVED, payload); for (const client of clientList) { client.postMessage(message); } } function getClientList() { return self.clients.matchAll({ type: 'window', includeUncontrolled: true // TS doesn't know that "type: 'window'" means it'll return WindowClient[] }); } function createNewMessage(type, payload) { return { firebaseMessaging: { type, payload } }; } function showNotification(details) { var _a; const title = (_a = details.title, (_a !== null && _a !== void 0 ? _a : '')); const { actions } = details; const { maxActions } = Notification; if (actions && maxActions && actions.length > maxActions) { console.warn(`This browser only supports ${maxActions} actions. The remaining actions will not be displayed.`); } return self.registration.showNotification(title, details); } function getLink(payload) { var _a, _b, _c; // eslint-disable-next-line camelcase const link = (_b = (_a = payload.fcmOptions) === null || _a === void 0 ? void 0 : _a.link, (_b !== null && _b !== void 0 ? _b : (_c = payload.notification) === null || _c === void 0 ? void 0 : _c.click_action)); if (link) { return link; } if (isConsoleMessage(payload.data)) { // Notification created in the Firebase Console. Redirect to origin. return self.location.origin; } else { return null; } } /** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const MESSAGING_NAME = 'messaging'; function factoryMethod(container) { // Dependencies. const app = container.getProvider('app').getImmediate(); const appConfig = extractAppConfig(app); const installations = container.getProvider('installations').getImmediate(); const analyticsProvider = container.getProvider('analytics-internal'); const firebaseDependencies = { app, appConfig, installations, analyticsProvider }; if (!isSupported()) { throw ERROR_FACTORY.create("unsupported-browser" /* UNSUPPORTED_BROWSER */); } if (self && 'ServiceWorkerGlobalScope' in self) { // Running in ServiceWorker context return new SwController(firebaseDependencies); } else { // Assume we are in the window context. return new WindowController(firebaseDependencies); } } const NAMESPACE_EXPORTS = { isSupported }; firebase.INTERNAL.registerComponent(new Component(MESSAGING_NAME, factoryMethod, "PUBLIC" /* PUBLIC */).setServiceProps(NAMESPACE_EXPORTS)); function isSupported() { if (self && 'ServiceWorkerGlobalScope' in self) { // Running in ServiceWorker context return isSWControllerSupported(); } else { // Assume we are in the window context. return isWindowControllerSupported(); } } /** * Checks to see if the required APIs exist. */ function isWindowControllerSupported() { return ('indexedDB' in window && indexedDB !== null && navigator.cookieEnabled && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'fetch' in window && ServiceWorkerRegistration.prototype.hasOwnProperty('showNotification') && PushSubscription.prototype.hasOwnProperty('getKey')); } /** * Checks to see if the required APIs exist within SW Context. */ function isSWControllerSupported() { return ('indexedDB' in self && indexedDB !== null && 'PushManager' in self && 'Notification' in self && ServiceWorkerRegistration.prototype.hasOwnProperty('showNotification') && PushSubscription.prototype.hasOwnProperty('getKey')); } //# sourceMappingURL=index.esm2017.js.map