From 22a0ce5b1ac1420dd91d05705464cd9d8c0e0f68 Mon Sep 17 00:00:00 2001 From: kai Date: Thu, 18 Jun 2020 09:29:09 -0700 Subject: [PATCH 01/12] FCM Pre Modualization --- integration/messaging/test/static/helpers.js | 51 ++++ integration/messaging/test/static/sw-base.js | 38 +-- .../valid-vapid-key-modern-sw/index.html | 28 +++ .../static/valid-vapid-key-modern-sw/sw.js | 37 +++ integration/messaging/test/test-send.js | 134 +++++------ packages/firebase/index.d.ts | 99 +++++++- packages/messaging-types/index.d.ts | 27 ++- .../src/controllers/sw-controller.test.ts | 52 +++-- .../src/controllers/sw-controller.ts | 76 ++++-- .../src/controllers/window-controller.test.ts | 168 ++++++++++++-- .../src/controllers/window-controller.ts | 218 ++++++++++++------ .../src/core/token-management.test.ts | 21 +- .../src/interfaces/message-payload.ts | 15 +- .../src/testing/fakes/service-worker.ts | 1 - packages/messaging/src/util/constants.ts | 1 + 15 files changed, 699 insertions(+), 267 deletions(-) create mode 100644 integration/messaging/test/static/helpers.js create mode 100644 integration/messaging/test/static/valid-vapid-key-modern-sw/index.html create mode 100644 integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js diff --git a/integration/messaging/test/static/helpers.js b/integration/messaging/test/static/helpers.js new file mode 100644 index 00000000000..4589f75389f --- /dev/null +++ b/integration/messaging/test/static/helpers.js @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 addPayloadToDb(payload) { + const dbOpenReq = indexedDB.open(TEST_DB); + + dbOpenReq.onupgradeneeded = () => { + const db = dbOpenReq.result; + + // store creation is a synchronized call + console.log('creating object store...'); + db.createObjectStore(BACKGROUND_MESSAGES_OBJECT_STORE, { + keyPath: BACKGROUND_MESSAGES_OBJECT_STORE_PRIMARY_KEY + }); + }; + + dbOpenReq.onsuccess = () => { + const db = dbOpenReq.result; + + addPayloadToDbInternal(db, { + ...payload, + // ndx is required as the primary key of the store. It doesn't have any other testing purpose + ndx: BACKGROUND_MESSAGES_OBJECT_STORE_DEFAULT_NDX + }); + }; +} + +async function addPayloadToDbInternal(db, payload) { + // onsuccess might race with onupgradeneeded. Consequently causing "object stores was not found" error. Therefore, wait briefly for db.createObjectStore to complete + const delay = ms => new Promise(res => setTimeout(res, ms)); + await delay(/* milliseconds= */ 30000); + + tx = db.transaction(BACKGROUND_MESSAGES_OBJECT_STORE, 'readwrite'); + + console.log('adding message payload to db: ' + JSON.stringify(payload)); + addReq = tx.objectStore(BACKGROUND_MESSAGES_OBJECT_STORE).add(payload); +} diff --git a/integration/messaging/test/static/sw-base.js b/integration/messaging/test/static/sw-base.js index 01d2baf137d..d7617bee105 100644 --- a/integration/messaging/test/static/sw-base.js +++ b/integration/messaging/test/static/sw-base.js @@ -16,6 +16,7 @@ */ importScripts('../constants.js'); +importScripts('../helpers.js'); // HEAD targets served through express importScripts('/firebase-app.js'); @@ -27,45 +28,10 @@ const messaging = firebase.messaging(); messaging.setBackgroundMessageHandler(payload => { console.log( TAG + - 'a background message is received: ' + + 'a background message is received in the background handler hook: ' + JSON.stringify(payload) + '. Storing it into idb for tests to read...' ); addPayloadToDb(payload); }); - -async function addPayloadToDb(payload) { - const dbOpenReq = indexedDB.open(TEST_DB); - - dbOpenReq.onupgradeneeded = () => { - const db = dbOpenReq.result; - - // store creation is a synchronized call - console.log('creating object store...'); - db.createObjectStore(BACKGROUND_MESSAGES_OBJECT_STORE, { - keyPath: BACKGROUND_MESSAGES_OBJECT_STORE_PRIMARY_KEY - }); - }; - - dbOpenReq.onsuccess = () => { - const db = dbOpenReq.result; - - addPayloadToDbInternal(db, { - ...payload, - // ndx is required as the primary key of the store. It doesn't have any other testing purpose - ndx: BACKGROUND_MESSAGES_OBJECT_STORE_DEFAULT_NDX - }); - }; -} - -async function addPayloadToDbInternal(db, payload) { - // onsuccess might race with onupgradeneeded. Consequently causing " object stores was not found" error. Therefore, wait briefly for db.createObjectStore to complete - const delay = ms => new Promise(res => setTimeout(res, ms)); - await delay(/* milliseconds= */ 30000); - - tx = db.transaction(BACKGROUND_MESSAGES_OBJECT_STORE, 'readwrite'); - - console.log('adding message payload to db: ' + JSON.stringify(payload)); - addReq = tx.objectStore(BACKGROUND_MESSAGES_OBJECT_STORE).add(payload); -} diff --git a/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html b/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html new file mode 100644 index 00000000000..269a41f0d91 --- /dev/null +++ b/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html @@ -0,0 +1,28 @@ + + + FCM Demo + + + +

Valid WITH VAPID Key - Modern SW

+ + + + + + + + + diff --git a/integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js b/integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js new file mode 100644 index 00000000000..459e83e948a --- /dev/null +++ b/integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + +importScripts('../constants.js'); +importScripts('../helpers.js'); + +// HEAD targets served through express +importScripts('/firebase-app.js'); +importScripts('/firebase-messaging.js'); + +firebase.initializeApp(FIREBASE_CONFIG); +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage(payload => { + console.log( + TAG + + 'a background message is received in the onBackgroundMessage hook: ' + + JSON.stringify(payload) + + '. Storing it into idb for tests to read...' + ); + + addPayloadToDb(payload); +}); diff --git a/integration/messaging/test/test-send.js b/integration/messaging/test/test-send.js index 06114616b70..d9d23d2e899 100644 --- a/integration/messaging/test/test-send.js +++ b/integration/messaging/test/test-send.js @@ -24,7 +24,7 @@ const getReceivedBackgroundMessages = require('./utils/getReceivedBackgroundMess const openNewTab = require('./utils/openNewTab'); const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); -const TEST_DOMAIN = 'valid-vapid-key'; +const TEST_DOMAINS = ['valid-vapid-key', 'valid-vapid-key-modern-sw']; const TEST_PROJECT_SENDER_ID = '750970317741'; const DEFAULT_COLLAPSE_KEY_VALUE = 'do_not_collapse'; const FIELD_FROM = 'from'; @@ -58,69 +58,71 @@ describe('Starting Integration Test > Sending and Receiving ', function() { return; } - describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${TEST_DOMAIN}`, function() { - before(async function() { - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - }); - - it('Background app can receive a {} empty message from sw', async function() { - this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - // Clearing the cache and db data by killing the previously instantiated driver. Note that ideally this call is placed inside the after/before hooks. However, Mocha forbids operations longer than 2s in hooks. Hence, this clearing call needs to be inside the test unit. - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - prepareBackgroundApp(globalWebDriver); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver) - }) - ); - - await wait( - WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS - ); - - checkMessageReceived( - await getReceivedBackgroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ null - ); - }); - - it('Background app can receive a {"data"} message frow sw', async function() { - this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - prepareBackgroundApp(globalWebDriver); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver), - data: getTestDataPayload() - }) - ); - - await wait( - WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS - ); - - checkMessageReceived( - await getReceivedBackgroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ getTestDataPayload() - ); + TEST_DOMAINS.forEach(domain => { + describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${domain}`, function() { + before(async function() { + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + }); + + it('Background app can receive a {} empty message from sw', async function() { + this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + // Clearing the cache and db data by killing the previously instantiated driver. Note that ideally this call is placed inside the after/before hooks. However, Mocha forbids operations longer than 2s in hooks. Hence, this clearing call needs to be inside the test unit. + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + prepareBackgroundApp(globalWebDriver, domain); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver) + }) + ); + + await wait( + WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS + ); + + checkMessageReceived( + await getReceivedBackgroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ null + ); + }); + + it('Background app can receive a {"data"} message frow sw', async function() { + this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + prepareBackgroundApp(globalWebDriver, domain); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver), + data: getTestDataPayload() + }) + ); + + await wait( + WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS + ); + + checkMessageReceived( + await getReceivedBackgroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ getTestDataPayload() + ); + }); }); }); }); @@ -168,8 +170,8 @@ function getTestDataPayload() { return { hello: 'world' }; } -async function prepareBackgroundApp(globalWebDriver) { - await globalWebDriver.get(`${testServer.serverAddress}/${TEST_DOMAIN}/`); +async function prepareBackgroundApp(globalWebDriver, domain) { + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); // TODO: remove the try/catch block once the underlying bug has been resolved. // Shift window focus away from app window so that background messages can be received/processed diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 93df8ad400f..4c6e8dc041c 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -6949,6 +6949,14 @@ declare namespace firebase.messaging { * */ interface Messaging { + /** + * Deletes the only registration token associated with this messaging instance and unsubscribes + * this messaging instance from the push subscription. + * + * @return The promise resolves when the token has been successfully deleted. + */ + deleteToken(): Promise; + /** * To forcibly stop a registration token from being used, delete it * by calling this method. @@ -6956,8 +6964,34 @@ declare namespace firebase.messaging { * @param token The token to delete. * @return The promise resolves when the token has been * successfully deleted. + * + * @deprecated Use deleteToken() instead. */ deleteToken(token: string): Promise; + + /** + * Subscribes the user to push notifications. Returns an FCM registration + * token that can be used to send push messages to the user. + * + * If notification permission isn't already granted, this method asks the + * user for permission. The returned promise rejects if the user does not + * allow the app to show notifications. + * + * @param options.vapidKey the public server key provided to push services. It is used to authenticate + * the push subscribers to receive push messages only from sending servers that holds the corresponding private key. If it is not provided, a default VAPID key will be be used. Note that some push services (Chrome Push Service) require a non-default VAPID key. Therefore, it is recommended to to generate and import a VAPID key for your project with {@link https://firebase.google.com/docs/cloud-messaging/js/client#configure_web_credentials_with_fcm Configure Web Credentials with FCM}. Also See {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} for details on web push services.} + * + * @param options.serviceWorkerRegistration the service worker registration for receiving push messaging. + * If it is not provided explicitly, you need to have a `firebase-messaging-sw.js` at your root location. + * See {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} for more details. + * + * @return The promise resolves with the FCM token string. + * + */ + getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise; + /** * Subscribes the user to push notifications and returns an FCM registration * token that can be used to send push messages to the user. @@ -6967,8 +7001,14 @@ declare namespace firebase.messaging { * allow the app to show notifications. * * @return The promise resolves with the FCM token string. + * + * @deprecated Use getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise;. */ getToken(): Promise; + /** * When a push message is received and the user is currently on a page * for your origin, the message is passed to the page and an `onMessage()` @@ -6988,6 +7028,24 @@ declare namespace firebase.messaging { error?: firebase.ErrorFn, completed?: firebase.CompleteFn ): firebase.Unsubscribe; + + /** + * Called when a message is received while the app is in the background. An app is considered + * as a background app if no active window is displayed. + * + * @param + * nextOrObserver This function, or observer object with `next` defined, + * is called when a message is received and the app is currently in the background. + * + * @return To stop listening for messages + * execute this returned function + */ + onBackgroundMessage( + nextOrObserver: firebase.NextFn | firebase.Observer, + error?: firebase.ErrorFn, + completed?: firebase.CompleteFn + ): firebase.Unsubscribe; + /** * You should listen for token refreshes so your web app knows when FCM * has invalidated your existing token and you need to call `getToken()` @@ -6998,12 +7056,15 @@ declare namespace firebase.messaging { * is called when a token refresh has occurred. * @return To stop listening for token * refresh events execute this returned function. + * + * @deprecated There is no need to handle token rotation. */ onTokenRefresh( nextOrObserver: firebase.NextFn | firebase.Observer, error?: firebase.ErrorFn, completed?: firebase.CompleteFn ): firebase.Unsubscribe; + /** * Notification permissions are required to send a user push messages. * Calling this method displays the permission dialog to the user and @@ -7013,10 +7074,10 @@ declare namespace firebase.messaging { * @return The promise resolves if permission is * granted. Otherwise, the promise is rejected with an error. * - * @deprecated Use Notification.requestPermission() instead. - * https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission + * @deprecated Use {@link https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission Notification.requestPermission()} instead. */ requestPermission(): Promise; + /** * FCM directs push messages to your web page's `onMessage()` callback * if the user currently has it open. Otherwise, it calls @@ -7030,17 +7091,51 @@ declare namespace firebase.messaging { setBackgroundMessageHandler( callback: (payload: any) => Promise | void ): void; + /** * To use your own service worker for receiving push messages, you * can pass in your service worker registration in this method. * * @param registration The service worker * registration you wish to use for push messaging. + * + * @deprecated Use getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise;. */ + useServiceWorker(registration: ServiceWorkerRegistration): void; + + /** + * @deprecated Use getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise;. + */ usePublicVapidKey(b64PublicKey: string): void; } + /** + * Message payload that contains the notification payload that is represented with {@link firebase.Messaging.NotificationPayload} and the data payload that contains an arbitrary number of key-value pairs sent by developers through the {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} + */ + export interface MessagePayload { + notification?: NotificationPayload; + data?: { [key: string]: string }; + } + + /** + * Notification parameters that define the display and behavior properties of a push notification. + */ + export interface NotificationPayload { + title?: string; + body?: string; + image?: string; + clickAction?: string; + link?: string; + analyticsLabel?: string; + } + function isSupported(): boolean; } diff --git a/packages/messaging-types/index.d.ts b/packages/messaging-types/index.d.ts index 4c056594553..06b87e6e3fc 100644 --- a/packages/messaging-types/index.d.ts +++ b/packages/messaging-types/index.d.ts @@ -23,27 +23,36 @@ import { ErrorFn, CompleteFn } from '@firebase/util'; +import { MessagePayload } from '../messaging/src/interfaces/message-payload'; export interface FirebaseMessaging { - // TODO: remove the token parameter and just delete the token that matches - // this app if it exists. - deleteToken(token: string): Promise; - getToken(): Promise; + /** window controller */ + deleteToken(): Promise; + getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise; onMessage( nextOrObserver: NextFn | Observer, error?: ErrorFn, completed?: CompleteFn ): Unsubscribe; + + /** service worker controller */ + onBackgroundMessage( + nextOrObserver: NextFn | Observer, + error?: ErrorFn, + completed?: CompleteFn + ): Unsubscribe; + + /** @deprecated */ + deleteToken(token: string): Promise; + getToken(): Promise; onTokenRefresh( nextOrObserver: NextFn | Observer, error?: ErrorFn, completed?: CompleteFn ): Unsubscribe; - /** - * @deprecated Use Notification.requestPermission() instead. - * https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission - */ - requestPermission(): Promise; setBackgroundMessageHandler( callback: (payload: any) => Promise | void ): void; diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts index 45a90c22aab..3e6a60d3606 100644 --- a/packages/messaging/src/controllers/sw-controller.test.ts +++ b/packages/messaging/src/controllers/sw-controller.test.ts @@ -1,3 +1,30 @@ +import '../testing/setup'; + +import * as tokenManagementModule from '../core/token-management'; + +import { BgMessageHandler, SwController } from './sw-controller'; +import { + CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, + CONSOLE_CAMPAIGN_ID, + CONSOLE_CAMPAIGN_NAME, + CONSOLE_CAMPAIGN_TIME, + DEFAULT_VAPID_KEY, + FCM_MSG +} from '../util/constants'; +import { DeepPartial, ValueOf, Writable } from 'ts-essentials'; +import { + FakeEvent, + FakePushSubscription, + mockServiceWorker, + restoreServiceWorker +} from '../testing/fakes/service-worker'; +import { InternalMessage, MessageType } from '../interfaces/internal-message'; +import { spy, stub } from 'sinon'; + +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { MessagePayload } from '../interfaces/message-payload'; +import { Stub } from '../testing/sinon-types'; +import { dbSet } from '../helpers/idb-manager'; /** * @license * Copyright 2017 Google LLC @@ -15,33 +42,8 @@ * limitations under the License. */ import { expect } from 'chai'; -import { stub, spy } from 'sinon'; - import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; -import '../testing/setup'; -import { SwController, BgMessageHandler } from './sw-controller'; -import * as tokenManagementModule from '../core/token-management'; -import { Stub } from '../testing/sinon-types'; -import { Writable, ValueOf, DeepPartial } from 'ts-essentials'; -import { MessagePayload } from '../interfaces/message-payload'; -import { MessageType, InternalMessage } from '../interfaces/internal-message'; -import { - mockServiceWorker, - restoreServiceWorker, - FakeEvent, - FakePushSubscription -} from '../testing/fakes/service-worker'; -import { - FCM_MSG, - DEFAULT_VAPID_KEY, - CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_TIME, - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED -} from '../util/constants'; -import { dbSet } from '../helpers/idb-manager'; import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; // Add fake SW types. declare const self: Window & Writable; diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index ff719e199c4..41121126906 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -15,22 +15,23 @@ * limitations under the License. */ -import { deleteToken, getToken } from '../core/token-management'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { FirebaseMessaging } from '@firebase/messaging-types'; +import { DEFAULT_VAPID_KEY, FCM_MSG } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { InternalMessage, MessageType } from '../interfaces/internal-message'; import { MessagePayload, - NotificationDetails + NotificationPayload } from '../interfaces/message-payload'; -import { FCM_MSG, DEFAULT_VAPID_KEY } from '../util/constants'; -import { MessageType, InternalMessage } from '../interfaces/internal-message'; -import { dbGet } from '../helpers/idb-manager'; -import { Unsubscribe } from '@firebase/util'; -import { sleep } from '../helpers/sleep'; +import { NextFn, Observer, Unsubscribe } from '@firebase/util'; +import { deleteToken, getToken } from '../core/token-management'; + import { FirebaseApp } from '@firebase/app-types'; -import { isConsoleMessage } from '../helpers/is-console-message'; +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; +import { dbGet } from '../helpers/idb-manager'; +import { isConsoleMessage } from '../helpers/is-console-message'; +import { sleep } from '../helpers/sleep'; // Let TS know that this is a service worker declare const self: ServiceWorkerGlobalScope; @@ -40,6 +41,10 @@ export type BgMessageHandler = (payload: MessagePayload) => unknown; export class SwController implements FirebaseMessaging, FirebaseService { private vapidKey: string | null = null; private bgMessageHandler: BgMessageHandler | null = null; + private onBackgroundMessageCallback: + | NextFn + | Observer + | null = null; constructor( private readonly firebaseDependencies: FirebaseInternalDependencies @@ -61,7 +66,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { /** * Calling setBackgroundMessageHandler will opt in to some specific - * behaviours. + * behaviors. * 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 @@ -82,6 +87,24 @@ export class SwController implements FirebaseMessaging, FirebaseService { this.bgMessageHandler = callback; } + onBackgroundMessage( + nextOrObserver: NextFn | Observer + ): Unsubscribe { + if (typeof nextOrObserver === 'function') { + this.onBackgroundMessageCallback = nextOrObserver; + } else if (typeof nextOrObserver.next === 'function') { + this.onBackgroundMessageCallback = nextOrObserver.next; + } else { + this.onBackgroundMessageCallback = nextOrObserver as Observer< + MessagePayload + >; + } + + return () => { + this.onBackgroundMessageCallback = null; + }; + } + // TODO: Remove getToken from SW Controller. // Calling this from an old SW can cause all kinds of trouble. async getToken(): Promise { @@ -112,7 +135,6 @@ export class SwController implements FirebaseMessaging, FirebaseService { throw ERROR_FACTORY.create(ErrorCode.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: string): void { if (this.vapidKey !== null) { @@ -156,18 +178,32 @@ export class SwController implements FirebaseMessaging, FirebaseService { return; } + // foreground handling 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); + // background handling + const notificationPayload = getNotificationPayload(payload); + if (notificationPayload) { + await showNotification(notificationPayload); } else if (this.bgMessageHandler) { await this.bgMessageHandler(payload); } + + if (this.onBackgroundMessageCallback) { + if (this.onBackgroundMessageCallback as NextFn) { + await (this.onBackgroundMessageCallback as NextFn)( + payload + ); + return; + } + + await (this.onBackgroundMessageCallback as Observer).next( + payload + ); + } } async onSubChange(event: PushSubscriptionChangeEvent): Promise { @@ -243,14 +279,14 @@ function getMessagePayload({ data }: PushEvent): MessagePayload | null { } } -function getNotificationData( +function getNotificationPayload( payload: MessagePayload -): NotificationDetails | undefined { +): NotificationPayload | undefined { if (!payload || typeof payload.notification !== 'object') { return; } - const notificationInformation: NotificationDetails = { + const notificationInformation: NotificationPayload = { ...payload.notification }; @@ -335,7 +371,7 @@ function createNewMessage( }; } -function showNotification(details: NotificationDetails): Promise { +function showNotification(details: NotificationPayload): Promise { const title = details.title ?? ''; const { actions } = details; diff --git a/packages/messaging/src/controllers/window-controller.test.ts b/packages/messaging/src/controllers/window-controller.test.ts index 53c9d9a01a3..45c8a888de8 100644 --- a/packages/messaging/src/controllers/window-controller.test.ts +++ b/packages/messaging/src/controllers/window-controller.test.ts @@ -1,3 +1,19 @@ +import '../testing/setup'; + +import * as tokenManagementModule from '../core/token-management'; + +import { + CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, + CONSOLE_CAMPAIGN_ID, + CONSOLE_CAMPAIGN_NAME, + CONSOLE_CAMPAIGN_TIME, + DEFAULT_SW_PATH, + DEFAULT_SW_SCOPE, + DEFAULT_VAPID_KEY +} from '../util/constants'; +import { InternalMessage, MessageType } from '../interfaces/internal-message'; +import { SinonFakeTimers, SinonSpy, spy, stub, useFakeTimers } from 'sinon'; +import { Spy, Stub } from '../testing/sinon-types'; /** * @license * Copyright 2017 Google LLC @@ -14,32 +30,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect } from 'chai'; -import { stub, spy, SinonSpy, useFakeTimers, SinonFakeTimers } from 'sinon'; +import { assert, expect } from 'chai'; +import { ErrorCode } from '../util/errors'; +import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; import { FirebaseAnalyticsInternal } from '@firebase/analytics-interop-types'; +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { WindowController } from './window-controller'; import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; -import { ErrorCode } from '../util/errors'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import * as tokenManagementModule from '../core/token-management'; -import { - DEFAULT_VAPID_KEY, - DEFAULT_SW_SCOPE, - DEFAULT_SW_PATH, - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, - CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_TIME -} from '../util/constants'; -import { Stub, Spy } from '../testing/sinon-types'; -import '../testing/setup'; -import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; -import { MessageType, InternalMessage } from '../interfaces/internal-message'; type MessageEventListener = (event: Event) => Promise; -const ORIGINAL_SW_REGISTRATION = ServiceWorkerRegistration; +const ORIGINAL_SW_REGISTRATION = FakeServiceWorkerRegistration; describe('WindowController', () => { let firebaseDependencies: FirebaseInternalDependencies; @@ -102,6 +104,134 @@ describe('WindowController', () => { }); describe('getToken', () => { + it('uses default sw if none was registered nor provided', async () => { + assert.isUndefined(windowController.getSwReg()); + + await windowController.getToken({}); + + expect(registerStub).to.have.been.calledOnceWith(DEFAULT_SW_PATH, { + scope: DEFAULT_SW_SCOPE + }); + }); + + it('uses option-provided swReg if non was registered', async () => { + assert.isUndefined(windowController.getSwReg()); + + await windowController.getToken({ + serviceWorkerRegistration: swRegistration + }); + + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + DEFAULT_VAPID_KEY + ); + }); + + it('uses previsouly stored sw if non is provided in the option parameter', async () => { + windowController.useServiceWorker(swRegistration); + assert.strictEqual( + JSON.stringify(windowController.getSwReg()), + JSON.stringify(swRegistration) + ); + + await windowController.getToken({}); + + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + DEFAULT_VAPID_KEY + ); + }); + + it('new swReg overrides existing swReg ', async () => { + windowController.useServiceWorker(swRegistration); + assert.strictEqual( + JSON.stringify(windowController.getSwReg()), + JSON.stringify(swRegistration) + ); + + const otherSwReg = new FakeServiceWorkerRegistration(); + + await windowController.getToken({ + serviceWorkerRegistration: otherSwReg + }); + + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + otherSwReg, + DEFAULT_VAPID_KEY + ); + }); + + it('uses default VAPID if: a) no VAPID was stored and b) non iss provided in option', async () => { + assert.strictEqual(windowController.getVapidKey(), null); + + await windowController.getToken({}); + + assert.strictEqual(windowController.getVapidKey(), DEFAULT_VAPID_KEY); + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + DEFAULT_VAPID_KEY + ); + }); + + it('uses option-provided VAPID if no VAPID has been registered', async () => { + assert.strictEqual(windowController.getVapidKey(), null); + + await windowController.getToken({ vapidKey: 'test_vapid_key' }); + + assert.strictEqual(windowController.getVapidKey(), 'test_vapid_key'); + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + 'test_vapid_key' + ); + }); + + it('uses option-provided VAPID if it is different from currently registered VAPID', async () => { + windowController.usePublicVapidKey('old_key'); + assert.strictEqual(windowController.getVapidKey(), 'old_key'); + + await windowController.getToken({ vapidKey: 'new_key' }); + + assert.strictEqual(windowController.getVapidKey(), 'new_key'); + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + 'new_key' + ); + }); + + it('uses existing VAPID if newly provided has the same value', async () => { + windowController.usePublicVapidKey('key'); + assert.strictEqual(windowController.getVapidKey(), 'key'); + + await windowController.getToken({ vapidKey: 'key' }); + + assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + 'key' + ); + }); + + it('uses existing VAPID if non is provided in the option parameter', async () => { + windowController.usePublicVapidKey('key'); + assert.strictEqual(windowController.getVapidKey(), 'key'); + + await windowController.getToken({}); + + assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(getTokenStub).to.have.been.calledOnceWith( + firebaseDependencies, + swRegistration, + 'key' + ); + }); + it('throws if permission is denied', async () => { stub(Notification, 'permission').value('denied'); diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index 663d8fc0067..0de689f848f 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /** * @license * Copyright 2017 Google LLC @@ -15,25 +16,36 @@ * limitations under the License. */ -import { getToken, deleteToken } from '../core/token-management'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { FirebaseMessaging } from '@firebase/messaging-types'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { NextFn, Observer, Unsubscribe } from '@firebase/util'; -import { InternalMessage, MessageType } from '../interfaces/internal-message'; import { - CONSOLE_CAMPAIGN_ID, CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, + CONSOLE_CAMPAIGN_ID, CONSOLE_CAMPAIGN_NAME, CONSOLE_CAMPAIGN_TIME, DEFAULT_SW_PATH, DEFAULT_SW_SCOPE, - DEFAULT_VAPID_KEY + DEFAULT_VAPID_KEY, + TAG } from '../util/constants'; +import { + CompleteFn, + ErrorFn, + NextFn, + Observer, + Unsubscribe +} from '@firebase/util'; +import { + ConsoleMessageData, + MessagePayload +} from '../interfaces/message-payload'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { InternalMessage, MessageType } from '../interfaces/internal-message'; +import { deleteToken, getToken } from '../core/token-management'; + import { FirebaseApp } from '@firebase/app-types'; -import { ConsoleMessageData } from '../interfaces/message-payload'; -import { isConsoleMessage } from '../helpers/is-console-message'; +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; +import { isConsoleMessage } from '../helpers/is-console-message'; export class WindowController implements FirebaseMessaging, FirebaseService { private vapidKey: string | null = null; @@ -52,16 +64,41 @@ export class WindowController implements FirebaseMessaging, FirebaseService { return this.firebaseDependencies.app; } - async getToken(): Promise { - if (!this.vapidKey) { - this.vapidKey = DEFAULT_VAPID_KEY; + private async messageEventListener(event: MessageEvent): Promise { + if (!event.data?.firebaseMessaging) { + // Not a message from FCM + return; } - const swRegistration = await this.getServiceWorkerRegistration(); + const { type, payload } = (event.data as InternalMessage).firebaseMessaging; + + if (this.onMessageCallback && type === MessageType.PUSH_RECEIVED) { + this.onMessageCallback(payload); + } - // Check notification permission. + 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); + } + } + + getVapidKey(): string | null { + return this.vapidKey; + } + + getSwReg(): ServiceWorkerRegistration | undefined { + return this.swRegistration; + } + + async getToken(options?: { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise { if (Notification.permission === 'default') { - // The user hasn't allowed or denied notifications yet. Ask them. await Notification.requestPermission(); } @@ -69,13 +106,89 @@ export class WindowController implements FirebaseMessaging, FirebaseService { throw ERROR_FACTORY.create(ErrorCode.PERMISSION_BLOCKED); } - return getToken(this.firebaseDependencies, swRegistration, this.vapidKey); + await this.updateVapidKey(options?.vapidKey); + await this.updateSwReg(options?.serviceWorkerRegistration); + + if (!this.swRegistration) { + console.debug( + TAG + + 'no sw has been provided explicitly. Attempting to find firebase-messaging-sw.js in default directory.' + ); + await this.registerDefaultSw(); + } + + return getToken( + this.firebaseDependencies, + this.swRegistration!, + this.vapidKey! + ); + } + + async updateVapidKey(vapidKey: string | undefined): Promise { + if (!!this.vapidKey && !!vapidKey && this.vapidKey !== vapidKey) { + console.debug( + TAG + + 'newly provided VapidKey is different from previously stored VapidKey. New VapidKey is overriding.' + ); + this.vapidKey = vapidKey; + } + + if (!this.vapidKey && !!vapidKey) { + this.vapidKey = vapidKey; + } + + if (!this.vapidKey && !vapidKey) { + console.debug( + TAG + + 'no VapidKey is provided. Using the default VapidKey. Note that Push will NOT work in Chrome without a non-default VapidKey.' + ); + this.vapidKey = DEFAULT_VAPID_KEY; + } + } + + async updateSwReg( + swRegistration: ServiceWorkerRegistration | undefined + ): Promise { + if (!swRegistration) { + return; + } + + if (!(swRegistration instanceof ServiceWorkerRegistration)) { + throw ERROR_FACTORY.create(ErrorCode.INVALID_SW_REGISTRATION); + } + + this.swRegistration = swRegistration; + } + + private async registerDefaultSw(): Promise { + try { + this.swRegistration = await navigator.serviceWorker.register( + DEFAULT_SW_PATH, + { + scope: DEFAULT_SW_SCOPE + } + ); + + // The timing when browser updates sw when sw has an update is unreliable by my experiment. + // It leads to version conflict when the SDK upgrades to a newer version in the main page, but + // sw is stuck with the old version. For example, https://github.com/firebase/firebase-js-sdk/issues/2590 + // The following line reliably updates sw if there was an update. + this.swRegistration.update().catch(() => { + /* it is non blocking and we don't care if it failed */ + }); + } catch (e) { + throw ERROR_FACTORY.create(ErrorCode.FAILED_DEFAULT_REGISTRATION, { + browserErrorMessage: e.message + }); + } } async deleteToken(): Promise { - const swRegistration = await this.getServiceWorkerRegistration(); + if (!this.swRegistration) { + await this.registerDefaultSw(); + } - return deleteToken(this.firebaseDependencies, swRegistration); + return deleteToken(this.firebaseDependencies, this.swRegistration!); } /** @@ -101,7 +214,6 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } } - // TODO: Deprecate this and make VAPID key a parameter in getToken. usePublicVapidKey(vapidKey: string): void { if (this.vapidKey !== null) { throw ERROR_FACTORY.create(ErrorCode.USE_VAPID_KEY_AFTER_GET_TOKEN); @@ -129,9 +241,9 @@ export class WindowController implements FirebaseMessaging, FirebaseService { /** * @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: NextFn | Observer): Unsubscribe { this.onMessageCallback = typeof nextOrObserver === 'function' @@ -147,64 +259,20 @@ export class WindowController implements FirebaseMessaging, FirebaseService { throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_SW); } - // Unimplemented - onTokenRefresh(): Unsubscribe { - return () => {}; + onBackgroundMessage( + nextOrObserver: NextFn | Observer, + error?: ErrorFn, + completed?: CompleteFn + ): Unsubscribe { + throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_SW); } /** - * Creates or updates the default service worker registration. - * @return The service worker registration to be used for the push service. + * No-op. It was initially designed with token rotation requests from server in mind. However, the plan to implement such feature was abandoned. + * @deprecated */ - private async getServiceWorkerRegistration(): Promise< - ServiceWorkerRegistration - > { - if (!this.swRegistration) { - try { - this.swRegistration = await navigator.serviceWorker.register( - DEFAULT_SW_PATH, - { - scope: DEFAULT_SW_SCOPE - } - ); - - // The timing when browser updates sw when sw has an update is unreliable by my experiment. - // It leads to version conflict when the SDK upgrades to a newer version in the main page, but - // sw is stuck with the old version. For example, https://github.com/firebase/firebase-js-sdk/issues/2590 - // The following line reliably updates sw if there was an update. - this.swRegistration.update().catch(() => { - /* it is non blocking and we don't care if it failed */ - }); - } catch (e) { - throw ERROR_FACTORY.create(ErrorCode.FAILED_DEFAULT_REGISTRATION, { - browserErrorMessage: e.message - }); - } - } - - return this.swRegistration; - } - - private async messageEventListener(event: MessageEvent): Promise { - if (!event.data?.firebaseMessaging) { - // Not a message from FCM - return; - } - - const { type, payload } = (event.data as InternalMessage).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); - } + onTokenRefresh(): Unsubscribe { + return () => {}; } private async logEvent( diff --git a/packages/messaging/src/core/token-management.test.ts b/packages/messaging/src/core/token-management.test.ts index 26421971f0f..db9f3e39268 100644 --- a/packages/messaging/src/core/token-management.test.ts +++ b/packages/messaging/src/core/token-management.test.ts @@ -16,20 +16,23 @@ */ import '../testing/setup'; + +import * as apiModule from './api'; + +import { SubscriptionOptions, TokenDetails } from '../interfaces/token-details'; +import { dbGet, dbSet } from '../helpers/idb-manager'; +import { deleteToken, getToken } from './token-management'; import { spy, stub, useFakeTimers } from 'sinon'; -import { getToken, deleteToken } from './token-management'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; -import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; + import { DEFAULT_VAPID_KEY } from '../util/constants'; -import { expect } from 'chai'; import { ErrorCode } from '../util/errors'; -import { dbGet, dbSet } from '../helpers/idb-manager'; -import * as apiModule from './api'; +import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { Stub } from '../testing/sinon-types'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { TokenDetails, SubscriptionOptions } from '../interfaces/token-details'; import { arrayToBase64 } from '../helpers/array-base64-translator'; +import { expect } from 'chai'; +import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; +import { getFakeTokenDetails } from '../testing/fakes/token-details'; describe('Token Management', () => { let tokenDetails: TokenDetails; diff --git a/packages/messaging/src/interfaces/message-payload.ts b/packages/messaging/src/interfaces/message-payload.ts index 96dfd76b5b3..686f9b09ebd 100644 --- a/packages/messaging/src/interfaces/message-payload.ts +++ b/packages/messaging/src/interfaces/message-payload.ts @@ -16,24 +16,29 @@ */ import { + CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_TIME, CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED + CONSOLE_CAMPAIGN_TIME } from '../util/constants'; -export interface NotificationDetails extends NotificationOptions { +export interface NotificationPayload extends NotificationOptions { title: string; - click_action?: string; // eslint-disable-line camelcase + + // eslint-disable-next-line camelcase + click_action?: string; } export interface FcmOptions { link?: string; + + // eslint-disable-next-line camelcase + analytics_label?: string; } export interface MessagePayload { fcmOptions?: FcmOptions; - notification?: NotificationDetails; + notification?: NotificationPayload; data?: unknown; } diff --git a/packages/messaging/src/testing/fakes/service-worker.ts b/packages/messaging/src/testing/fakes/service-worker.ts index ca46cbb50a2..729212e616e 100644 --- a/packages/messaging/src/testing/fakes/service-worker.ts +++ b/packages/messaging/src/testing/fakes/service-worker.ts @@ -98,7 +98,6 @@ export class FakeServiceWorkerRegistration active = null; installing = null; waiting = null; - onupdatefound = null; pushManager = new FakePushManager(); scope = '/scope-value'; diff --git a/packages/messaging/src/util/constants.ts b/packages/messaging/src/util/constants.ts index 1c3a83765f2..4bafce33b0a 100644 --- a/packages/messaging/src/util/constants.ts +++ b/packages/messaging/src/util/constants.ts @@ -31,3 +31,4 @@ export const CONSOLE_CAMPAIGN_NAME = 'google.c.a.c_l'; export const CONSOLE_CAMPAIGN_TIME = 'google.c.a.ts'; /** Set to '1' if Analytics is enabled for the campaign */ export const CONSOLE_CAMPAIGN_ANALYTICS_ENABLED = 'google.c.a.e'; +export const TAG = 'FirebaseMessaging: '; From 78b0e73c223417aef2abf132a69cc44cda0825c2 Mon Sep 17 00:00:00 2001 From: kai Date: Sun, 28 Jun 2020 13:07:04 -0700 Subject: [PATCH 02/12] Refactor FCM typing --- packages/messaging/package.json | 15 +- .../src/controllers/sw-controller.test.ts | 75 +++---- .../src/controllers/sw-controller.ts | 199 +++++++----------- .../src/controllers/window-controller.test.ts | 133 ++++++------ .../src/controllers/window-controller.ts | 30 +-- .../src/helpers/externalizePayload.test.ts | 94 +++++++++ .../src/helpers/externalizePayload.ts | 96 +++++++++ .../src/interfaces/internal-message.ts | 30 --- .../src/interfaces/message-payload.ts | 36 +++- 9 files changed, 425 insertions(+), 283 deletions(-) create mode 100644 packages/messaging/src/helpers/externalizePayload.test.ts create mode 100644 packages/messaging/src/helpers/externalizePayload.ts delete mode 100644 packages/messaging/src/interfaces/internal-message.ts diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 4cb48dc845b..04493772c3f 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/messaging", - "version": "0.6.17", + "version": "0.6.19", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -15,7 +15,8 @@ "build": "rollup -c", "build:deps": "lerna run --scope @firebase/'{app,messaging}' --include-dependencies build", "dev": "rollup -c -w", - "test": "run-p test:karma type-check lint && cd ../../integration/messaging && npm run-script test", + "test": "run-p test:karma type-check lint ", + "test:integration": "run-p test:karma type-check lint && cd ../../integration/messaging && npm run-script test", "test:ci": "node ../../scripts/run_tests_in_ci.js", "test:karma": "karma start --single-run", "test:debug": "karma start --browsers=Chrome --auto-watch", @@ -28,17 +29,19 @@ "@firebase/app-types": "0.x" }, "dependencies": { - "@firebase/installations": "0.4.11", + "@firebase/component": "0.1.15", + "@firebase/installations": "0.4.13", "@firebase/messaging-types": "0.4.5", - "@firebase/util": "0.2.48", - "@firebase/component": "0.1.13", + "@firebase/util": "0.2.50", + "eslint": "^7.3.1", "idb": "3.0.2", + "npm-run-all": "^4.1.5", "tslib": "^1.11.1" }, "devDependencies": { "rollup": "2.7.6", "rollup-plugin-typescript2": "0.27.0", - "ts-essentials": "6.0.4", + "ts-essentials": "^6.0.7", "typescript": "3.8.3" }, "repository": { diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts index 3e6a60d3606..3f19da8babd 100644 --- a/packages/messaging/src/controllers/sw-controller.test.ts +++ b/packages/messaging/src/controllers/sw-controller.test.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import '../testing/setup'; import * as tokenManagementModule from '../core/token-management'; @@ -18,11 +20,13 @@ import { mockServiceWorker, restoreServiceWorker } from '../testing/fakes/service-worker'; -import { InternalMessage, MessageType } from '../interfaces/internal-message'; +import { + MessagePayloadInternal, + MessageType +} from '../interfaces/message-payload'; import { spy, stub } from 'sinon'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { MessagePayload } from '../interfaces/message-payload'; import { Stub } from '../testing/sinon-types'; import { dbSet } from '../helpers/idb-manager'; /** @@ -48,20 +52,19 @@ import { getFakeTokenDetails } from '../testing/fakes/token-details'; // Add fake SW types. declare const self: Window & Writable; -const NOTIFICATION_MESSAGE_PAYLOAD: MessagePayload = { +// internal message payload (parsed directly from the push event) that contains and only contains notification payload. +const DISPLAY_MESSAGE: MessagePayloadInternal = { notification: { - title: 'message title', - body: 'message body', - data: { - key: 'value' - } + title: 'title', + body: 'body' }, fcmOptions: { link: 'https://example.org' } }; -const DATA_MESSAGE_PAYLOAD: MessagePayload = { +// internal message payload (parsed directly from the push event) that contains and only contains data payload. +const DATA_MESSAGE: MessagePayloadInternal = { data: { key: 'value' } @@ -80,10 +83,7 @@ describe('SwController', () => { stub(Notification, 'permission').value('granted'); - // Instead of calling actual addEventListener, add the event to the - // eventListeners list. - // Actual event listeners can't be used as the tests are not running in a - // Service Worker, which means Push events do not exist. + // Instead of calling actual addEventListener, add the event to the eventListeners list. Actual event listeners can't be used as the tests are not running in a Service Worker, which means Push events do not exist. addEventListenerStub = stub(self, 'addEventListener').callsFake( (type, listener) => { eventListenerMap.set(type, listener); @@ -206,16 +206,14 @@ describe('SwController', () => { await callEventListener( makeEvent('push', { data: { - json: () => NOTIFICATION_MESSAGE_PAYLOAD + json: () => DISPLAY_MESSAGE } }) ); - const expectedMessage: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: NOTIFICATION_MESSAGE_PAYLOAD - } + const expectedMessage: MessagePayloadInternal = { + ...DISPLAY_MESSAGE, + messageType: MessageType.PUSH_RECEIVED }; expect(postMessageSpy).to.have.been.calledOnceWith(expectedMessage); }); @@ -228,17 +226,16 @@ describe('SwController', () => { await callEventListener( makeEvent('push', { data: { - json: () => NOTIFICATION_MESSAGE_PAYLOAD + json: () => DISPLAY_MESSAGE } }) ); expect(postMessageSpy).not.to.have.been.called; - expect(showNotificationSpy).to.have.been.calledWith('message title', { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification, + expect(showNotificationSpy).to.have.been.calledWith('title', { + ...DISPLAY_MESSAGE.notification, data: { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification!.data, - [FCM_MSG]: NOTIFICATION_MESSAGE_PAYLOAD + [FCM_MSG]: DISPLAY_MESSAGE } }); }); @@ -249,16 +246,16 @@ describe('SwController', () => { await callEventListener( makeEvent('push', { data: { - json: () => NOTIFICATION_MESSAGE_PAYLOAD + json: () => DISPLAY_MESSAGE } }) ); - expect(showNotificationSpy).to.have.been.calledWith('message title', { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification, + expect(showNotificationSpy).to.have.been.calledWith('title', { + ...DISPLAY_MESSAGE.notification, data: { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification!.data, - [FCM_MSG]: NOTIFICATION_MESSAGE_PAYLOAD + ...DISPLAY_MESSAGE.notification!.data, + [FCM_MSG]: DISPLAY_MESSAGE } }); }); @@ -270,7 +267,7 @@ describe('SwController', () => { await callEventListener( makeEvent('push', { data: { - json: () => DATA_MESSAGE_PAYLOAD + json: () => DATA_MESSAGE } }) ); @@ -293,7 +290,7 @@ describe('SwController', () => { data: { json: () => ({ notification: { - ...NOTIFICATION_MESSAGE_PAYLOAD, + ...DISPLAY_MESSAGE, actions: [ { action: 'like', title: 'Like' }, { action: 'favorite', title: 'Favorite' } @@ -310,7 +307,7 @@ describe('SwController', () => { }); }); - describe('setBackgrounMessageHandler', () => { + describe('setBackgroundMessageHandler', () => { it('throws on invalid input', () => { expect(() => swController.setBackgroundMessageHandler( @@ -352,11 +349,11 @@ describe('SwController', () => { beforeEach(() => { NOTIFICATION_CLICK_PAYLOAD = { - notification: new Notification('message title', { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification, + notification: new Notification('title', { + ...DISPLAY_MESSAGE.notification, data: { - ...NOTIFICATION_MESSAGE_PAYLOAD.notification!.data, - [FCM_MSG]: NOTIFICATION_MESSAGE_PAYLOAD + ...DISPLAY_MESSAGE.notification!.data, + [FCM_MSG]: DISPLAY_MESSAGE } }) }; @@ -439,10 +436,8 @@ describe('SwController', () => { expect(openWindowSpy).not.to.have.been.called; expect(focusSpy).to.have.been.called; expect(postMessageSpy).to.have.been.calledWith({ - firebaseMessaging: { - type: MessageType.NOTIFICATION_CLICKED, - payload: NOTIFICATION_MESSAGE_PAYLOAD - } + ...DISPLAY_MESSAGE, + messageType: MessageType.NOTIFICATION_CLICKED }); }); diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index 41121126906..e1bc60e15d5 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ /** * @license * Copyright 2017 Google LLC @@ -15,12 +16,13 @@ * limitations under the License. */ -import { DEFAULT_VAPID_KEY, FCM_MSG } from '../util/constants'; +import { DEFAULT_VAPID_KEY, FCM_MSG, TAG } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { InternalMessage, MessageType } from '../interfaces/internal-message'; import { MessagePayload, - NotificationPayload + MessagePayloadInternal, + MessageType, + NotificationPayloadInternal } from '../interfaces/message-payload'; import { NextFn, Observer, Unsubscribe } from '@firebase/util'; import { deleteToken, getToken } from '../core/token-management'; @@ -30,6 +32,7 @@ import { FirebaseInternalDependencies } from '../interfaces/internal-dependencie import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; import { dbGet } from '../helpers/idb-manager'; +import { externalizePayload } from '../helpers/externalizePayload'; import { isConsoleMessage } from '../helpers/is-console-message'; import { sleep } from '../helpers/sleep'; @@ -40,11 +43,11 @@ export type BgMessageHandler = (payload: MessagePayload) => unknown; export class SwController implements FirebaseMessaging, FirebaseService { private vapidKey: string | null = null; - private bgMessageHandler: BgMessageHandler | null = null; - private onBackgroundMessageCallback: + private bgMessageHandler: + | BgMessageHandler + | null | NextFn - | Observer - | null = null; + | Observer = null; constructor( private readonly firebaseDependencies: FirebaseInternalDependencies @@ -67,6 +70,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { /** * Calling setBackgroundMessageHandler will opt in to some specific * behaviors. + * * 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 @@ -90,18 +94,10 @@ export class SwController implements FirebaseMessaging, FirebaseService { onBackgroundMessage( nextOrObserver: NextFn | Observer ): Unsubscribe { - if (typeof nextOrObserver === 'function') { - this.onBackgroundMessageCallback = nextOrObserver; - } else if (typeof nextOrObserver.next === 'function') { - this.onBackgroundMessageCallback = nextOrObserver.next; - } else { - this.onBackgroundMessageCallback = nextOrObserver as Observer< - MessagePayload - >; - } + this.bgMessageHandler = nextOrObserver; return () => { - this.onBackgroundMessageCallback = null; + this.bgMessageHandler = null; }; } @@ -109,10 +105,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { // Calling this from an old SW can cause all kinds of trouble. async getToken(): Promise { 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. + // 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 = tokenDetails?.subscriptionOptions?.vapidKey ?? DEFAULT_VAPID_KEY; @@ -125,8 +118,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { ); } - // TODO: Remove deleteToken from SW Controller. - // Calling this from an old SW can cause all kinds of trouble. + // TODO: Remove deleteToken from SW Controller. Calling this from an old SW can cause all kinds of trouble. deleteToken(): Promise { return deleteToken(this.firebaseDependencies, self.registration); } @@ -161,48 +153,39 @@ export class SwController implements FirebaseMessaging, FirebaseService { } /** - * A handler for push events that shows notifications based on the content of - * the payload. + * 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. + * 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. + * If there is no notification data in the payload then no notification will be shown. */ async onPush(event: PushEvent): Promise { - const payload = getMessagePayload(event); - if (!payload) { + const internalPayload = getMessagePayloadInternal(event); + if (!internalPayload) { + console.debug( + TAG + + 'failed to get parse MessagePayload from the PushEvent. Skip handling the push.' + ); return; } - // foreground handling + // foreground handling: eventually passed to onMessage hook const clientList = await getClientList(); if (hasVisibleClients(clientList)) { - return sendMessageToWindowClients(clientList, payload); + return sendMessagePayloadInternalToWindows(clientList, internalPayload); } - // background handling - const notificationPayload = getNotificationPayload(payload); - if (notificationPayload) { - await showNotification(notificationPayload); + // background handling: display and pass to onBackgroundMessage + if (!!internalPayload.notification) { + await showNotification(wrapInternalPayload(internalPayload)); } else if (this.bgMessageHandler) { - await this.bgMessageHandler(payload); - } - - if (this.onBackgroundMessageCallback) { - if (this.onBackgroundMessageCallback as NextFn) { - await (this.onBackgroundMessageCallback as NextFn)( - payload - ); - return; + const payload = externalizePayload(internalPayload); + if (typeof this.bgMessageHandler === 'function') { + this.bgMessageHandler(payload); + } else { + this.bgMessageHandler.next(payload); } - - await (this.onBackgroundMessageCallback as Observer).next( - payload - ); } } @@ -224,14 +207,13 @@ export class SwController implements FirebaseMessaging, FirebaseService { } async onNotificationClick(event: NotificationEvent): Promise { - const payload: MessagePayload = event.notification?.data?.[FCM_MSG]; - if (!payload) { - // Not an FCM notification, do nothing. + const internalPayload: MessagePayloadInternal = + event.notification?.data?.[FCM_MSG]; + + if (!internalPayload) { 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. + // User clicked on an action button. This will allow developers to act on action button clicks by using a custom onNotificationClick listener that they define. return; } @@ -239,18 +221,16 @@ export class SwController implements FirebaseMessaging, FirebaseService { event.stopImmediatePropagation(); event.notification.close(); - const link = getLink(payload); + const link = getLink(internalPayload); 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. + // 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. + // 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(); @@ -261,12 +241,30 @@ export class SwController implements FirebaseMessaging, FirebaseService { return; } - const message = createNewMessage(MessageType.NOTIFICATION_CLICKED, payload); - return client.postMessage(message); + internalPayload.messageType = MessageType.NOTIFICATION_CLICKED; + internalPayload.isFirebaseMessaging = true; + return client.postMessage(internalPayload); } } -function getMessagePayload({ data }: PushEvent): MessagePayload | null { +function wrapInternalPayload( + internalPayload: MessagePayloadInternal +): NotificationPayloadInternal { + const wrappedInternalPayload: NotificationPayloadInternal = { + ...((internalPayload.notification as unknown) as NotificationPayloadInternal) + }; + + // 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). + wrappedInternalPayload.data = { + [FCM_MSG]: internalPayload + }; + + return wrappedInternalPayload; +} + +function getMessagePayloadInternal({ + data +}: PushEvent): MessagePayloadInternal | null { if (!data) { return null; } @@ -279,36 +277,12 @@ function getMessagePayload({ data }: PushEvent): MessagePayload | null { } } -function getNotificationPayload( - payload: MessagePayload -): NotificationPayload | undefined { - if (!payload || typeof payload.notification !== 'object') { - return; - } - - const notificationInformation: NotificationPayload = { - ...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 = { - ...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: string): Promise { - // Use URL to normalize the URL when comparing to windowClients. - // This at least handles whether to include trailing slashes or not + // 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); const clientList = await getClientList(); @@ -331,26 +305,20 @@ function hasVisibleClients(clientList: WindowClient[]): boolean { 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. + // 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( +function sendMessagePayloadInternalToWindows( clientList: WindowClient[], - payload: MessagePayload + internalPayload: MessagePayloadInternal ): void { - const message = createNewMessage(MessageType.PUSH_RECEIVED, payload); + internalPayload.isFirebaseMessaging = true; + internalPayload.messageType = MessageType.PUSH_RECEIVED; for (const client of clientList) { - client.postMessage(message); + client.postMessage(internalPayload); } } @@ -362,21 +330,11 @@ function getClientList(): Promise { }) as Promise; } -function createNewMessage( - type: MessageType, - payload: MessagePayload -): InternalMessage { - return { - firebaseMessaging: { type, payload } - }; -} - -function showNotification(details: NotificationPayload): Promise { - const title = details.title ?? ''; - - const { actions } = details; - // Note: Firefox does not support the maxActions property. - // https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions +function showNotification( + notificationPayloadInternal: NotificationPayloadInternal +): Promise { + // Note: Firefox does not support the maxActions property. https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions + const { actions } = notificationPayloadInternal; const { maxActions } = Notification; if (actions && maxActions && actions.length > maxActions) { console.warn( @@ -384,10 +342,13 @@ function showNotification(details: NotificationPayload): Promise { ); } - return self.registration.showNotification(title, details); + return self.registration.showNotification( + /* title= */ notificationPayloadInternal.title ?? '', + notificationPayloadInternal + ); } -function getLink(payload: MessagePayload): string | null { +function getLink(payload: MessagePayloadInternal): string | null { // eslint-disable-next-line camelcase const link = payload.fcmOptions?.link ?? payload.notification?.click_action; if (link) { diff --git a/packages/messaging/src/controllers/window-controller.test.ts b/packages/messaging/src/controllers/window-controller.test.ts index 45c8a888de8..0767f808bcb 100644 --- a/packages/messaging/src/controllers/window-controller.test.ts +++ b/packages/messaging/src/controllers/window-controller.test.ts @@ -11,7 +11,10 @@ import { DEFAULT_SW_SCOPE, DEFAULT_VAPID_KEY } from '../util/constants'; -import { InternalMessage, MessageType } from '../interfaces/internal-message'; +import { + MessagePayloadInternal, + MessageType +} from '../interfaces/message-payload'; import { SinonFakeTimers, SinonSpy, spy, stub, useFakeTimers } from 'sinon'; import { Spy, Stub } from '../testing/sinon-types'; /** @@ -37,6 +40,7 @@ import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; import { FirebaseAnalyticsInternal } from '@firebase/analytics-interop-types'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { WindowController } from './window-controller'; +import { externalizePayload } from '../helpers/externalizePayload'; import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; type MessageEventListener = (event: Event) => Promise; @@ -128,7 +132,7 @@ describe('WindowController', () => { ); }); - it('uses previsouly stored sw if non is provided in the option parameter', async () => { + it('uses previously stored sw if non is provided in the option parameter', async () => { windowController.useServiceWorker(swRegistration); assert.strictEqual( JSON.stringify(windowController.getSwReg()), @@ -375,15 +379,14 @@ describe('WindowController', () => { const onMessageCallback = spy(); windowController.onMessage(onMessageCallback); - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: { notification: { title: 'hello', body: 'world' } } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + messageType: MessageType.PUSH_RECEIVED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageCallback).to.have.been.called; @@ -397,34 +400,32 @@ describe('WindowController', () => { complete: () => {} }); - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: { notification: { title: 'hello', body: 'world' } } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + messageType: MessageType.PUSH_RECEIVED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageCallback).to.have.been.called; }); - it('returns a function that unsets the onMessage callback', async () => { + it('returns a function that clears the onMessage callback', async () => { const onMessageCallback = spy(); const unsubscribe = windowController.onMessage(onMessageCallback); unsubscribe(); - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: { notification: { title: 'hello', body: 'world' } } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + messageType: MessageType.PUSH_RECEIVED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageCallback).not.to.have.been.called; @@ -508,33 +509,31 @@ describe('WindowController', () => { }); it('calls onMessage callback when it receives a PUSH_RECEIVED message', async () => { - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: { notification: { title: 'hello', body: 'world' } } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + messageType: MessageType.PUSH_RECEIVED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageSpy).to.have.been.calledOnceWith( - message.firebaseMessaging.payload + externalizePayload(internalPayload) ); expect(logEventSpy).not.to.have.been.called; }); it('does not call onMessage callback when it receives a NOTIFICATION_CLICKED message', async () => { - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.NOTIFICATION_CLICKED, - payload: { notification: { title: 'hello', body: 'world' } } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + messageType: MessageType.NOTIFICATION_CLICKED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageSpy).not.to.have.been.called; @@ -542,36 +541,32 @@ describe('WindowController', () => { }); it('calls analytics.logEvent if the message has analytics enabled for PUSH_RECEIVED', async () => { - const data = { - [CONSOLE_CAMPAIGN_ID]: '123456', - [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', - [CONSOLE_CAMPAIGN_TIME]: '1234567890', - [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' - }; - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.PUSH_RECEIVED, - payload: { - notification: { title: 'hello', body: 'world' }, - data - } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + data: { + [CONSOLE_CAMPAIGN_ID]: '123456', + [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', + [CONSOLE_CAMPAIGN_TIME]: '1234567890', + [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' + }, + messageType: MessageType.PUSH_RECEIVED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageSpy).to.have.been.calledOnceWith( - message.firebaseMessaging.payload + externalizePayload(internalPayload) ); expect(logEventSpy).to.have.been.calledOnceWith( 'notification_foreground', { /* eslint-disable camelcase */ - message_id: data[CONSOLE_CAMPAIGN_ID], - message_name: data[CONSOLE_CAMPAIGN_NAME], - message_time: data[CONSOLE_CAMPAIGN_TIME], + message_id: '123456', + message_name: 'Campaign Name', + message_time: '1234567890', message_device_time: clock.now /* eslint-enable camelcase */ } @@ -579,32 +574,28 @@ describe('WindowController', () => { }); it('calls analytics.logEvent if the message has analytics enabled for NOTIFICATION_CLICKED', async () => { - const data = { - [CONSOLE_CAMPAIGN_ID]: '123456', - [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', - [CONSOLE_CAMPAIGN_TIME]: '1234567890', - [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' - }; - const message: InternalMessage = { - firebaseMessaging: { - type: MessageType.NOTIFICATION_CLICKED, - payload: { - notification: { title: 'hello', body: 'world' }, - data - } - } + const internalPayload: MessagePayloadInternal = { + notification: { title: 'hello', body: 'world' }, + data: { + [CONSOLE_CAMPAIGN_ID]: '123456', + [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', + [CONSOLE_CAMPAIGN_TIME]: '1234567890', + [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' + }, + messageType: MessageType.NOTIFICATION_CLICKED, + isFirebaseMessaging: true }; await messageEventListener( - new MessageEvent('message', { data: message }) + new MessageEvent('message', { data: internalPayload }) ); expect(onMessageSpy).not.to.have.been.called; expect(logEventSpy).to.have.been.calledOnceWith('notification_open', { /* eslint-disable camelcase */ - message_id: data[CONSOLE_CAMPAIGN_ID], - message_name: data[CONSOLE_CAMPAIGN_NAME], - message_time: data[CONSOLE_CAMPAIGN_TIME], + message_id: '123456', + message_name: 'Campaign Name', + message_time: '1234567890', message_device_time: clock.now /* eslint-enable camelcase */ }); diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index 0de689f848f..f25e174dc59 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable @typescript-eslint/no-unused-vars */ /** * @license @@ -35,16 +36,18 @@ import { } from '@firebase/util'; import { ConsoleMessageData, - MessagePayload + MessagePayload, + MessagePayloadInternal, + MessageType } from '../interfaces/message-payload'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { InternalMessage, MessageType } from '../interfaces/internal-message'; import { deleteToken, getToken } from '../core/token-management'; import { FirebaseApp } from '@firebase/app-types'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; +import { externalizePayload } from '../helpers/externalizePayload'; import { isConsoleMessage } from '../helpers/is-console-message'; export class WindowController implements FirebaseMessaging, FirebaseService { @@ -65,24 +68,25 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } private async messageEventListener(event: MessageEvent): Promise { - if (!event.data?.firebaseMessaging) { - // Not a message from FCM + const internalPayload = event.data as MessagePayloadInternal; + + if (!internalPayload.isFirebaseMessaging) { return; } - const { type, payload } = (event.data as InternalMessage).firebaseMessaging; - - if (this.onMessageCallback && type === MessageType.PUSH_RECEIVED) { - this.onMessageCallback(payload); + if ( + this.onMessageCallback && + internalPayload.messageType === MessageType.PUSH_RECEIVED + ) { + this.onMessageCallback(externalizePayload(internalPayload)); } - const { data } = payload; + const dataPayload = internalPayload.data; if ( - isConsoleMessage(data) && - data[CONSOLE_CAMPAIGN_ANALYTICS_ENABLED] === '1' + isConsoleMessage(dataPayload) && + dataPayload[CONSOLE_CAMPAIGN_ANALYTICS_ENABLED] === '1' ) { - // Analytics is enabled on this message, so we should log it. - await this.logEvent(type, data); + await this.logEvent(internalPayload.messageType!, dataPayload); } } diff --git a/packages/messaging/src/helpers/externalizePayload.test.ts b/packages/messaging/src/helpers/externalizePayload.test.ts new file mode 100644 index 00000000000..1680ef506e5 --- /dev/null +++ b/packages/messaging/src/helpers/externalizePayload.test.ts @@ -0,0 +1,94 @@ +/* eslint-disable camelcase */ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + +import { + MessagePayload, + MessagePayloadInternal +} from '../interfaces/message-payload'; + +import { expect } from 'chai'; +import { externalizePayload } from './externalizePayload'; + +describe('externalizePayload', () => { + it('externalizes internalMessage with only notification payload', () => { + const internalPayload: MessagePayloadInternal = { + notification: { + title: 'title', + body: 'body', + image: 'image' + } + }; + + const payload: MessagePayload = { + notification: { title: 'title', body: 'body', image: 'image' } + }; + expect(externalizePayload(internalPayload)).to.deep.equal(payload); + }); + + it('externalizes internalMessage with only data payload', () => { + const internalPayload: MessagePayloadInternal = { + data: { + foo: 'foo', + bar: 'bar', + baz: 'baz' + } + }; + + const payload: MessagePayload = { + data: { foo: 'foo', bar: 'bar', baz: 'baz' } + }; + expect(externalizePayload(internalPayload)).to.deep.equal(payload); + }); + + it('externalizes internalMessage with all three payloads', () => { + const internalPayload: MessagePayloadInternal = { + notification: { + title: 'title', + body: 'body', + image: 'image' + }, + data: { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }, + fcmOptions: { + link: 'link', + analytics_label: 'label' + } + }; + + const payload: MessagePayload = { + notification: { + title: 'title', + body: 'body', + image: 'image' + }, + data: { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }, + fcmOptions: { + link: 'link', + analyticsLabel: 'label' + } + }; + expect(externalizePayload(internalPayload)).to.deep.equal(payload); + }); +}); diff --git a/packages/messaging/src/helpers/externalizePayload.ts b/packages/messaging/src/helpers/externalizePayload.ts new file mode 100644 index 00000000000..683eb74bdb1 --- /dev/null +++ b/packages/messaging/src/helpers/externalizePayload.ts @@ -0,0 +1,96 @@ +/* eslint-disable camelcase */ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + +import { + MessagePayload, + MessagePayloadInternal +} from '../interfaces/message-payload'; + +export function externalizePayload( + internalPayload: MessagePayloadInternal +): MessagePayload { + const payload: MessagePayload = { + notification: {}, + data: {}, + fcmOptions: {} + } as MessagePayload; + + propagateNotificationPayload(payload, internalPayload); + propagateDataPayload(payload, internalPayload); + propagateFcmOptions(payload, internalPayload); + + return payload; +} + +function propagateNotificationPayload( + payload: MessagePayload, + messagePayloadInternal: MessagePayloadInternal +): void { + if (!messagePayloadInternal.notification) { + delete payload.notification; + return; + } + + const title = messagePayloadInternal.notification!.title; + if (!!title) { + payload.notification!.title = title; + } + + const body = messagePayloadInternal.notification!.body; + if (!!body) { + payload.notification!.body = body; + } + + const image = messagePayloadInternal.notification!.image; + if (!!image) { + payload.notification!.image = image; + } +} + +function propagateDataPayload( + payload: MessagePayload, + messagePayloadInternal: MessagePayloadInternal +): void { + if (!messagePayloadInternal.data) { + delete payload.data; + return; + } + + payload.data = messagePayloadInternal.data as { [key: string]: string }; +} + +function propagateFcmOptions( + payload: MessagePayload, + messagePayloadInternal: MessagePayloadInternal +): void { + if (!messagePayloadInternal.fcmOptions) { + delete payload.fcmOptions; + return; + } + + const link = messagePayloadInternal.fcmOptions!.link; + if (!!link) { + payload.fcmOptions!.link = link; + } + + // eslint-disable-next-line camelcase + const analyticsLabel = messagePayloadInternal.fcmOptions!.analytics_label; + if (!!analyticsLabel) { + payload.fcmOptions!.analyticsLabel = analyticsLabel; + } +} diff --git a/packages/messaging/src/interfaces/internal-message.ts b/packages/messaging/src/interfaces/internal-message.ts deleted file mode 100644 index 41f0299664a..00000000000 --- a/packages/messaging/src/interfaces/internal-message.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * 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. - */ - -import { MessagePayload } from './message-payload'; - -export enum MessageType { - PUSH_RECEIVED = 'push-received', - NOTIFICATION_CLICKED = 'notification-clicked' -} - -export interface InternalMessage { - firebaseMessaging: { - type: MessageType; - payload: MessagePayload; - }; -} diff --git a/packages/messaging/src/interfaces/message-payload.ts b/packages/messaging/src/interfaces/message-payload.ts index 686f9b09ebd..632e0b320fd 100644 --- a/packages/messaging/src/interfaces/message-payload.ts +++ b/packages/messaging/src/interfaces/message-payload.ts @@ -22,24 +22,52 @@ import { CONSOLE_CAMPAIGN_TIME } from '../util/constants'; -export interface NotificationPayload extends NotificationOptions { +export interface NotificationPayloadInternal extends NotificationOptions { title: string; + // Supported in the Legacy Send API. See:https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref. // eslint-disable-next-line camelcase click_action?: string; } -export interface FcmOptions { +// Defined in https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions. Note that the keys are sent to the clients in snake cases which we need to convert to camel so it can be exposed as a type to match the Firebase API convention. +export interface FcmOptionsInternal { link?: string; // eslint-disable-next-line camelcase analytics_label?: string; } +// Represents a raw message payload from a push event +export interface MessagePayloadInternal { + notification?: NotificationPayloadInternal; + data?: unknown; + fcmOptions?: FcmOptionsInternal; + messageType?: MessageType; + isFirebaseMessaging?: boolean; +} + +export enum MessageType { + PUSH_RECEIVED = 'push-received', + NOTIFICATION_CLICKED = 'notification-clicked' +} + +// Currently supported fcm notification display parameters. Note that {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/notifications/NotificationOptions} defines a full list of display notification parameters. This interface we only include what the SEND API support for clarity. +export interface NotificationPayload { + title: string; + body: string; + image: string; +} + +export interface FcmOptions { + link?: string; + analyticsLabel?: string; +} + export interface MessagePayload { - fcmOptions?: FcmOptions; notification?: NotificationPayload; - data?: unknown; + data?: { [key: string]: string }; + fcmOptions?: FcmOptions; } /** Additional data of a message sent from the FN Console. */ From 3e4f2d6c9f64cf48a62eb9e963c57d067d7457d2 Mon Sep 17 00:00:00 2001 From: kai Date: Sun, 28 Jun 2020 21:18:24 -0700 Subject: [PATCH 03/12] Polish javascript doc --- packages/firebase/index.d.ts | 48 +++++++++++++++---- .../src/controllers/window-controller.ts | 2 +- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 4c6e8dc041c..651c92fb1c0 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -7014,9 +7014,6 @@ declare namespace firebase.messaging { * for your origin, the message is passed to the page and an `onMessage()` * event is dispatched with the payload of the push message. * - * NOTE: These events are dispatched when you have called - * `setBackgroundMessageHandler()` in your service worker. - * * @param * nextOrObserver This function, or observer object with `next` defined, * is called when a message is received and the user is currently viewing your page. @@ -7030,8 +7027,7 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * Called when a message is received while the app is in the background. An app is considered - * as a background app if no active window is displayed. + * Called when a message is received while the app is in the background. An app is considered as a background app if no active window is displayed. * * @param * nextOrObserver This function, or observer object with `next` defined, @@ -7041,7 +7037,9 @@ declare namespace firebase.messaging { * execute this returned function */ onBackgroundMessage( - nextOrObserver: firebase.NextFn | firebase.Observer, + nextOrObserver: + | firebase.NextFn + | firebase.Observer, error?: firebase.ErrorFn, completed?: firebase.CompleteFn ): firebase.Unsubscribe; @@ -7120,20 +7118,50 @@ declare namespace firebase.messaging { * Message payload that contains the notification payload that is represented with {@link firebase.Messaging.NotificationPayload} and the data payload that contains an arbitrary number of key-value pairs sent by developers through the {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} */ export interface MessagePayload { + /** + * See {@link firebase.Messaging.NotificationPayload}. + */ notification?: NotificationPayload; + + /** + * Arbitrary key/value payload + */ + data?: { [key: string]: string }; + + /** + * See {@link firebase.Messaging.FcmOptions}. + */ + + fcmOptions?: FcmOptions; } /** - * Notification parameters that define the display and behavior properties of a push notification. + * Options for features provided by the FCM SDK for Web. See more {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions}. + */ + export interface FcmOptions { + link?: string; + analyticsLabel?: string; + } + + /** + * Parameters that define how a push notification is displayed to users. */ export interface NotificationPayload { + /** + * The title of a notification. + */ title?: string; + + /** + * The body of a notification. + */ body?: string; + + /** + * The URL of the image that is shown with the notification. See {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification} for supported image format. + */ image?: string; - clickAction?: string; - link?: string; - analyticsLabel?: string; } function isSupported(): boolean; diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index f25e174dc59..3e51765f4b1 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -248,7 +248,7 @@ export class WindowController implements FirebaseMessaging, FirebaseService { * * @return The unsubscribe function for the observer. */ - onMessage(nextOrObserver: NextFn | Observer): Unsubscribe { + onMessage(nextOrObserver: NextFn | Observer, ): Unsubscribe { this.onMessageCallback = typeof nextOrObserver === 'function' ? nextOrObserver From d5047727ecd6872c349709bc05c1bebf9b9b8e0f Mon Sep 17 00:00:00 2001 From: kai Date: Mon, 29 Jun 2020 11:05:25 -0700 Subject: [PATCH 04/12] Update index.d.ts --- packages/messaging-types/index.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/messaging-types/index.d.ts b/packages/messaging-types/index.d.ts index 06b87e6e3fc..3b0229b2c03 100644 --- a/packages/messaging-types/index.d.ts +++ b/packages/messaging-types/index.d.ts @@ -53,6 +53,11 @@ export interface FirebaseMessaging { error?: ErrorFn, completed?: CompleteFn ): Unsubscribe; + /** + * @deprecated Use Notification.requestPermission() instead. + * https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission + */ + requestPermission(): Promise; setBackgroundMessageHandler( callback: (payload: any) => Promise | void ): void; From 0dfecf6b98f715ebaaa74e825f9605f3a07bd45a Mon Sep 17 00:00:00 2001 From: kai Date: Mon, 29 Jun 2020 11:23:31 -0700 Subject: [PATCH 05/12] Add Changeset --- .changeset/strange-crabs-tell.md | 5 +++++ packages/firebase/index.d.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/strange-crabs-tell.md diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md new file mode 100644 index 00000000000..cdcc9922bfd --- /dev/null +++ b/.changeset/strange-crabs-tell.md @@ -0,0 +1,5 @@ +--- +'@firebase/messaging': major +--- + +Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`. diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 1e9079eb957..b8ccccd58c3 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -7124,7 +7124,7 @@ declare namespace firebase.messaging { notification?: NotificationPayload; /** - * Arbitrary key/value payload + * Arbitrary key/value pairs. */ data?: { [key: string]: string }; From 31fa1966f21350dd0d56067984794e8f1fb6d031 Mon Sep 17 00:00:00 2001 From: kai Date: Mon, 29 Jun 2020 11:32:35 -0700 Subject: [PATCH 06/12] Fix changeset from major to minor --- .changeset/strange-crabs-tell.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md index cdcc9922bfd..a6ef5e88ccf 100644 --- a/.changeset/strange-crabs-tell.md +++ b/.changeset/strange-crabs-tell.md @@ -1,5 +1,6 @@ --- -'@firebase/messaging': major +'firebase': minor +'@firebase/messaging': minor, --- Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`. From d058ba4cbb29a2db04f525e8cb14df4be1b8bc9c Mon Sep 17 00:00:00 2001 From: kai Date: Mon, 29 Jun 2020 11:41:17 -0700 Subject: [PATCH 07/12] Add to changeset --- .changeset/strange-crabs-tell.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md index a6ef5e88ccf..10f1bb0c5e2 100644 --- a/.changeset/strange-crabs-tell.md +++ b/.changeset/strange-crabs-tell.md @@ -4,3 +4,4 @@ --- Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`. +Deprecate `setBackgroundHandler`, `onTokenRefresh`, `useVapidKey`, `useServiceWorker`, `getToken`. From 8aedf9282063f0b5595e7f78ce4f489902346de1 Mon Sep 17 00:00:00 2001 From: kai Date: Tue, 7 Jul 2020 13:47:21 -0700 Subject: [PATCH 08/12] Polished index.d.ts based on Arthur's feedback --- packages/firebase/index.d.ts | 46 +++++++++++++++---- .../src/controllers/sw-controller.test.ts | 10 +++- .../src/controllers/sw-controller.ts | 29 ++++++++---- .../src/controllers/window-controller.test.ts | 35 +++++++++++--- .../src/controllers/window-controller.ts | 7 ++- .../src/helpers/externalizePayload.test.ts | 24 +++++++--- .../src/helpers/externalizePayload.ts | 4 +- .../src/interfaces/message-payload.ts | 5 ++ 8 files changed, 125 insertions(+), 35 deletions(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index b8ccccd58c3..96a40c53465 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -6950,7 +6950,7 @@ declare namespace firebase.messaging { */ interface Messaging { /** - * Deletes the only registration token associated with this messaging instance and unsubscribes + * Deletes the registration token associated with this messaging instance and unsubscribes * this messaging instance from the push subscription. * * @return The promise resolves when the token has been successfully deleted. @@ -6970,19 +6970,26 @@ declare namespace firebase.messaging { deleteToken(token: string): Promise; /** - * Subscribes the user to push notifications. Returns an FCM registration + * Subscribes the messaging instance to push notifications. Returns an FCM registration * token that can be used to send push messages to the user. * * If notification permission isn't already granted, this method asks the * user for permission. The returned promise rejects if the user does not * allow the app to show notifications. * - * @param options.vapidKey the public server key provided to push services. It is used to authenticate - * the push subscribers to receive push messages only from sending servers that holds the corresponding private key. If it is not provided, a default VAPID key will be be used. Note that some push services (Chrome Push Service) require a non-default VAPID key. Therefore, it is recommended to to generate and import a VAPID key for your project with {@link https://firebase.google.com/docs/cloud-messaging/js/client#configure_web_credentials_with_fcm Configure Web Credentials with FCM}. Also See {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} for details on web push services.} + * @param options.vapidKey the public server key provided to push services. It is used to + * authenticate the push subscribers to receive push messages only from sending servers that + * holds the corresponding private key. If it is not provided, a default VAPID key will be used. + * Note that some push services (Chrome Push Service) require a non-default VAPID key. + * Therefore, it is recommended to generate and import a VAPID key for your project with + * {@link https://firebase.google.com/docs/cloud-messaging/js/client#configure_web_credentials_with_fcm Configure Web Credentials with FCM}. + * See {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} + * for details on web push services.} * * @param options.serviceWorkerRegistration the service worker registration for receiving push messaging. * If it is not provided explicitly, you need to have a `firebase-messaging-sw.js` at your root location. - * See {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} for more details. + * See {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} + * for more details. * * @return The promise resolves with the FCM token string. * @@ -7027,7 +7034,8 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * Called when a message is received while the app is in the background. An app is considered as a background app if no active window is displayed. + * Called when a message is received while the app is in the background. An app is considered + * as a background app if no active window is displayed. * * @param * nextOrObserver This function, or observer object with `next` defined, @@ -7115,7 +7123,10 @@ declare namespace firebase.messaging { } /** - * Message payload that contains the notification payload that is represented with {@link firebase.Messaging.NotificationPayload} and the data payload that contains an arbitrary number of key-value pairs sent by developers through the {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} + * Message payload that contains the notification payload that is represented with + * {@link firebase.Messaging.NotificationPayload} and the data payload that contains an + * arbitrary number of key-value pairs sent by developers through + * the {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} */ export interface MessagePayload { /** @@ -7126,21 +7137,38 @@ declare namespace firebase.messaging { /** * Arbitrary key/value pairs. */ - data?: { [key: string]: string }; /** * See {@link firebase.Messaging.FcmOptions}. */ - fcmOptions?: FcmOptions; + + /** + * The sender of this message. + */ + from: string; + + /** + * The collapse key of this message. See more {@link https://firebase.google.com/docs/cloud-messaging/concept-options#collapsible_and_non-collapsible_messages}. + */ + collapseKey: string; } /** * Options for features provided by the FCM SDK for Web. See more {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions}. */ export interface FcmOptions { + /** + * The link to open when the user clicks on the notification. For all URL values, HTTPS is required. + * For example, by setting this value to your app's URL, a notification click click event will + * put your app in focus for the user. + */ link?: string; + + /** + * Label associated with the message's analytics data. + */ analyticsLabel?: string; } diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts index 3f19da8babd..94fcf29758f 100644 --- a/packages/messaging/src/controllers/sw-controller.test.ts +++ b/packages/messaging/src/controllers/sw-controller.test.ts @@ -60,14 +60,20 @@ const DISPLAY_MESSAGE: MessagePayloadInternal = { }, fcmOptions: { link: 'https://example.org' - } + }, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; // internal message payload (parsed directly from the push event) that contains and only contains data payload. const DATA_MESSAGE: MessagePayloadInternal = { data: { key: 'value' - } + }, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; describe('SwController', () => { diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index e1bc60e15d5..5d62a8ff0ee 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -105,7 +105,9 @@ export class SwController implements FirebaseMessaging, FirebaseService { // Calling this from an old SW can cause all kinds of trouble. async getToken(): Promise { 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. + // 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 = tokenDetails?.subscriptionOptions?.vapidKey ?? DEFAULT_VAPID_KEY; @@ -156,7 +158,9 @@ export class SwController implements FirebaseMessaging, FirebaseService { * 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. + * 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. */ @@ -176,7 +180,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { return sendMessagePayloadInternalToWindows(clientList, internalPayload); } - // background handling: display and pass to onBackgroundMessage + // background handling: display and pass to onBackgroundMessage hook if (!!internalPayload.notification) { await showNotification(wrapInternalPayload(internalPayload)); } else if (this.bgMessageHandler) { @@ -213,7 +217,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { if (!internalPayload) { return; } else if (event.action) { - // User clicked on an action button. This will allow developers to act on action button clicks by using a custom onNotificationClick listener that they define. + // User clicked on an action button. This will allow developers to act on action button clicks + // by using a custom onNotificationClick listener that they define. return; } @@ -230,7 +235,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { 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. + // 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(); @@ -254,7 +260,9 @@ function wrapInternalPayload( ...((internalPayload.notification as unknown) as NotificationPayloadInternal) }; - // 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). + // 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). wrappedInternalPayload.data = { [FCM_MSG]: internalPayload }; @@ -282,7 +290,8 @@ function getMessagePayloadInternal({ * @return Returns an existing window client or a newly opened WindowClient. */ async function getWindowClient(url: string): Promise { - // Use URL to normalize the URL when comparing to windowClients. This at least handles whether to include trailing slashes or not + // 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); const clientList = await getClientList(); @@ -305,7 +314,8 @@ function hasVisibleClients(clientList: WindowClient[]): boolean { 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. + // 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://') ); } @@ -333,7 +343,8 @@ function getClientList(): Promise { function showNotification( notificationPayloadInternal: NotificationPayloadInternal ): Promise { - // Note: Firefox does not support the maxActions property. https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions + // Note: Firefox does not support the maxActions property. + // https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions const { actions } = notificationPayloadInternal; const { maxActions } = Notification; if (actions && maxActions && actions.length > maxActions) { diff --git a/packages/messaging/src/controllers/window-controller.test.ts b/packages/messaging/src/controllers/window-controller.test.ts index 0767f808bcb..d5c4b1126af 100644 --- a/packages/messaging/src/controllers/window-controller.test.ts +++ b/packages/messaging/src/controllers/window-controller.test.ts @@ -382,7 +382,10 @@ describe('WindowController', () => { const internalPayload: MessagePayloadInternal = { notification: { title: 'hello', body: 'world' }, messageType: MessageType.PUSH_RECEIVED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -403,7 +406,10 @@ describe('WindowController', () => { const internalPayload: MessagePayloadInternal = { notification: { title: 'hello', body: 'world' }, messageType: MessageType.PUSH_RECEIVED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -421,7 +427,10 @@ describe('WindowController', () => { const internalPayload: MessagePayloadInternal = { notification: { title: 'hello', body: 'world' }, messageType: MessageType.PUSH_RECEIVED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -512,7 +521,10 @@ describe('WindowController', () => { const internalPayload: MessagePayloadInternal = { notification: { title: 'hello', body: 'world' }, messageType: MessageType.PUSH_RECEIVED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -529,7 +541,10 @@ describe('WindowController', () => { const internalPayload: MessagePayloadInternal = { notification: { title: 'hello', body: 'world' }, messageType: MessageType.NOTIFICATION_CLICKED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -550,7 +565,10 @@ describe('WindowController', () => { [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' }, messageType: MessageType.PUSH_RECEIVED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( @@ -583,7 +601,10 @@ describe('WindowController', () => { [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' }, messageType: MessageType.NOTIFICATION_CLICKED, - isFirebaseMessaging: true + isFirebaseMessaging: true, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' }; await messageEventListener( diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index 4abf591dde4..cc01427f929 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -74,11 +74,16 @@ export class WindowController implements FirebaseMessaging, FirebaseService { return; } + // onMessageCallback is either a function or observer/subscriber. if ( this.onMessageCallback && internalPayload.messageType === MessageType.PUSH_RECEIVED ) { - this.onMessageCallback(externalizePayload(internalPayload)); + if (typeof this.onMessageCallback === 'function') { + this.onMessageCallback(externalizePayload(internalPayload)); + } else { + this.onMessageCallback.next(externalizePayload(internalPayload)); + } } const dataPayload = internalPayload.data; diff --git a/packages/messaging/src/helpers/externalizePayload.test.ts b/packages/messaging/src/helpers/externalizePayload.test.ts index 1680ef506e5..d28d5df870a 100644 --- a/packages/messaging/src/helpers/externalizePayload.test.ts +++ b/packages/messaging/src/helpers/externalizePayload.test.ts @@ -31,11 +31,15 @@ describe('externalizePayload', () => { title: 'title', body: 'body', image: 'image' - } + }, + from: 'from', + collapse_key: 'collapse' }; const payload: MessagePayload = { - notification: { title: 'title', body: 'body', image: 'image' } + notification: { title: 'title', body: 'body', image: 'image' }, + from: 'from', + collapseKey: 'collapse' }; expect(externalizePayload(internalPayload)).to.deep.equal(payload); }); @@ -46,11 +50,15 @@ describe('externalizePayload', () => { foo: 'foo', bar: 'bar', baz: 'baz' - } + }, + from: 'from', + collapse_key: 'collapse' }; const payload: MessagePayload = { - data: { foo: 'foo', bar: 'bar', baz: 'baz' } + data: { foo: 'foo', bar: 'bar', baz: 'baz' }, + from: 'from', + collapseKey: 'collapse' }; expect(externalizePayload(internalPayload)).to.deep.equal(payload); }); @@ -70,7 +78,9 @@ describe('externalizePayload', () => { fcmOptions: { link: 'link', analytics_label: 'label' - } + }, + from: 'from', + collapse_key: 'collapse' }; const payload: MessagePayload = { @@ -87,7 +97,9 @@ describe('externalizePayload', () => { fcmOptions: { link: 'link', analyticsLabel: 'label' - } + }, + from: 'from', + collapseKey: 'collapse' }; expect(externalizePayload(internalPayload)).to.deep.equal(payload); }); diff --git a/packages/messaging/src/helpers/externalizePayload.ts b/packages/messaging/src/helpers/externalizePayload.ts index 683eb74bdb1..a5730ebd739 100644 --- a/packages/messaging/src/helpers/externalizePayload.ts +++ b/packages/messaging/src/helpers/externalizePayload.ts @@ -27,7 +27,9 @@ export function externalizePayload( const payload: MessagePayload = { notification: {}, data: {}, - fcmOptions: {} + fcmOptions: {}, + from: internalPayload.from, + collapseKey: internalPayload.collapse_key } as MessagePayload; propagateNotificationPayload(payload, internalPayload); diff --git a/packages/messaging/src/interfaces/message-payload.ts b/packages/messaging/src/interfaces/message-payload.ts index 632e0b320fd..a9c8422d48b 100644 --- a/packages/messaging/src/interfaces/message-payload.ts +++ b/packages/messaging/src/interfaces/message-payload.ts @@ -45,6 +45,9 @@ export interface MessagePayloadInternal { fcmOptions?: FcmOptionsInternal; messageType?: MessageType; isFirebaseMessaging?: boolean; + from: string; + // eslint-disable-next-line camelcase + collapse_key: string; } export enum MessageType { @@ -68,6 +71,8 @@ export interface MessagePayload { notification?: NotificationPayload; data?: { [key: string]: string }; fcmOptions?: FcmOptions; + from: string; + collapseKey: string; } /** Additional data of a message sent from the FN Console. */ From 0403b847af94ff9322869e3d7d98cfbfce3b92cc Mon Sep 17 00:00:00 2001 From: kai Date: Thu, 9 Jul 2020 16:01:50 -0700 Subject: [PATCH 09/12] Polish PR based on feedbacks --- integration/messaging/download-browsers.js | 5 +- integration/messaging/manual-test-server.js | 2 +- integration/messaging/test/static/helpers.js | 3 +- .../messaging/test/test-deleteToken.js | 2 +- integration/messaging/test/test-send.js | 51 +++--- .../messaging/test/test-updateToken.js | 2 +- .../utils/getReceivedBackgroundMessages.js | 5 +- .../messaging/test/utils/sendMessage.js | 4 +- .../messaging/test/utils/test-server.js | 4 +- packages/firebase/index.d.ts | 161 ++++++++---------- packages/installations/package.json | 3 +- packages/messaging-types/index.d.ts | 26 ++- packages/messaging/package.json | 8 +- .../src/controllers/sw-controller.test.ts | 72 +++++--- .../src/controllers/sw-controller.ts | 78 +++++---- .../src/controllers/window-controller.test.ts | 99 ++++++----- .../src/controllers/window-controller.ts | 90 +++++----- packages/messaging/src/core/api.test.ts | 18 +- packages/messaging/src/core/api.ts | 7 +- .../messaging/src/core/token-management.ts | 19 ++- .../helpers/array-base64-translator.test.ts | 4 +- .../src/helpers/externalizePayload.test.ts | 14 +- .../src/helpers/externalizePayload.ts | 20 +-- .../src/helpers/extract-app-config.test.ts | 9 +- .../src/helpers/extract-app-config.ts | 5 +- .../messaging/src/helpers/idb-manager.test.ts | 17 +- packages/messaging/src/helpers/idb-manager.ts | 12 +- .../src/helpers/is-console-message.ts | 5 +- .../src/helpers/migrate-old-database.test.ts | 12 +- .../src/helpers/migrate-old-database.ts | 12 +- packages/messaging/src/helpers/sleep.test.ts | 6 +- packages/messaging/src/index.ts | 24 +-- .../src/interfaces/internal-dependencies.ts | 6 +- .../interfaces/internal-message-payload.ts | 64 +++++++ .../src/interfaces/message-payload.ts | 84 --------- .../src/testing/compare-headers.test.ts | 4 +- .../messaging/src/testing/compare-headers.ts | 6 +- .../testing/fakes/firebase-dependencies.ts | 11 +- .../src/testing/fakes/service-worker.ts | 21 ++- .../src/testing/fakes/token-details.ts | 2 +- packages/messaging/src/testing/setup.ts | 5 +- packages/messaging/src/testing/sinon-types.ts | 2 +- packages/messaging/src/util/sw-types.ts | 8 +- 43 files changed, 531 insertions(+), 481 deletions(-) create mode 100644 packages/messaging/src/interfaces/internal-message-payload.ts delete mode 100644 packages/messaging/src/interfaces/message-payload.ts diff --git a/integration/messaging/download-browsers.js b/integration/messaging/download-browsers.js index 9dd38250ca6..c8b5de3f7d7 100644 --- a/integration/messaging/download-browsers.js +++ b/integration/messaging/download-browsers.js @@ -18,8 +18,9 @@ const seleniumAssistant = require('selenium-assistant'); console.log('Starting browser download - this may take some time.'); -// TODO: enable firefox testing once figure out how to give notification permission with SE webdriver. -// TODO: Run the integration test against multiple major chrome versions to ensure backward compatibility +// TODO: enable firefox testing once figure out how to give notification permission with SE +// webdriver. TODO: Run the integration test against multiple major chrome versions to ensure +// backward compatibility Promise.all([seleniumAssistant.downloadLocalBrowser('chrome', 'stable', 80)]) .then(() => { console.log('Browser download complete.'); diff --git a/integration/messaging/manual-test-server.js b/integration/messaging/manual-test-server.js index 34a5ca05948..9532b74c584 100644 --- a/integration/messaging/manual-test-server.js +++ b/integration/messaging/manual-test-server.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration/messaging/test/static/helpers.js b/integration/messaging/test/static/helpers.js index 4589f75389f..7712a636bea 100644 --- a/integration/messaging/test/static/helpers.js +++ b/integration/messaging/test/static/helpers.js @@ -40,7 +40,8 @@ async function addPayloadToDb(payload) { } async function addPayloadToDbInternal(db, payload) { - // onsuccess might race with onupgradeneeded. Consequently causing "object stores was not found" error. Therefore, wait briefly for db.createObjectStore to complete + // onsuccess might race with onupgradeneeded. Consequently causing "object stores was not found" + // error. Therefore, wait briefly for db.createObjectStore to complete const delay = ms => new Promise(res => setTimeout(res, ms)); await delay(/* milliseconds= */ 30000); diff --git a/integration/messaging/test/test-deleteToken.js b/integration/messaging/test/test-deleteToken.js index 3eea890b98d..d3c760a22ea 100644 --- a/integration/messaging/test/test-deleteToken.js +++ b/integration/messaging/test/test-deleteToken.js @@ -41,7 +41,7 @@ describe('Firebase Messaging Integration Tests > get and delete token', function const availableBrowsers = seleniumAssistant.getLocalBrowsers(); availableBrowsers.forEach(assistantBrowser => { - //TODO: enable testing for firefox + // TODO: enable testing for firefox if (assistantBrowser.getId() !== 'chrome') { return; } diff --git a/integration/messaging/test/test-send.js b/integration/messaging/test/test-send.js index ff342f4e592..f6f819affdd 100644 --- a/integration/messaging/test/test-send.js +++ b/integration/messaging/test/test-send.js @@ -29,16 +29,21 @@ const TEST_DOMAINS = ['valid-vapid-key', 'valid-vapid-key-modern-sw']; const TEST_PROJECT_SENDER_ID = '750970317741'; const DEFAULT_COLLAPSE_KEY_VALUE = 'do_not_collapse'; const FIELD_FROM = 'from'; -const FIELD_COLLAPSE_KEY = 'collapse_key'; +const FIELD_COLLAPSE_KEY_LEGACY = 'collapse_key'; +const FIELD_COLLAPSE_KEY = 'collapseKey'; + const FIELD_DATA = 'data'; const FIELD_NOTIFICATION = 'notification'; -// 4 minutes. The fact that the flow includes making a request to the Send Service, storing/retrieving form indexedDb asynchronously makes these test units to have a execution time variance. Therefore, allowing these units to have a longer time to work is crucial. +// 4 minutes. The fact that the flow includes making a request to the Send Service, +// storing/retrieving form indexedDb asynchronously makes these test units to have a execution time +// variance. Therefore, allowing these units to have a longer time to work is crucial. const TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 240000; const TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 120000; -// 1 minute. Wait for object store to be created and received message to be stored in idb. This waiting time MUST be longer than the wait time for adding to db in the sw. +// 1 minute. Wait for object store to be created and received message to be stored in idb. This +// waiting time MUST be longer than the wait time for adding to db in the sw. const WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS = 60000; const wait = ms => new Promise(res => setTimeout(res, ms)); @@ -55,7 +60,7 @@ describe('Starting Integration Test > Sending and Receiving ', function() { await testServer.stop(); }); - //TODO: enable testing for firefox + // TODO: enable testing for firefox seleniumAssistant.getLocalBrowsers().forEach(assistantBrowser => { if (assistantBrowser.getId() !== 'chrome') { return; @@ -72,7 +77,10 @@ describe('Starting Integration Test > Sending and Receiving ', function() { it('Background app can receive a {} empty message from sw', async function() { this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - // Clearing the cache and db data by killing the previously instantiated driver. Note that ideally this call is placed inside the after/before hooks. However, Mocha forbids operations longer than 2s in hooks. Hence, this clearing call needs to be inside the test unit. + // Clearing the cache and db data by killing the previously instantiated driver. Note that + // ideally this call is placed inside the after/before hooks. However, Mocha forbids + // operations longer than 2s in hooks. Hence, this clearing call needs to be inside the + // test unit. await seleniumAssistant.killWebDriver(globalWebDriver); globalWebDriver = createPermittedWebDriver( @@ -94,7 +102,8 @@ describe('Starting Integration Test > Sending and Receiving ', function() { checkMessageReceived( await getReceivedBackgroundMessages(globalWebDriver), /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ null + /* expectedDataPayload= */ null, + /* isLegacyPayload= */ false ); }); @@ -137,9 +146,7 @@ describe('Starting Integration Test > Sending and Receiving ', function() { /* browser= */ assistantBrowser.getId() ); - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); let token = await retrieveToken(globalWebDriver); checkSendResponse( @@ -164,9 +171,7 @@ describe('Starting Integration Test > Sending and Receiving ', function() { /* browser= */ assistantBrowser.getId() ); - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); checkSendResponse( await sendMessage({ @@ -191,9 +196,7 @@ describe('Starting Integration Test > Sending and Receiving ', function() { /* browser= */ assistantBrowser.getId() ); - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); checkSendResponse( await sendMessage({ @@ -218,9 +221,7 @@ describe('Starting Integration Test > Sending and Receiving ', function() { /* browser= */ assistantBrowser.getId() ); - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); checkSendResponse( await sendMessage({ @@ -250,7 +251,10 @@ function checkMessageReceived( const message = receivedMessages[0]; expect(message[FIELD_FROM]).to.equal(TEST_PROJECT_SENDER_ID); - expect(message[FIELD_COLLAPSE_KEY]).to.equal(DEFAULT_COLLAPSE_KEY_VALUE); + const collapseKey = !!message[FIELD_COLLAPSE_KEY_LEGACY] + ? message[FIELD_COLLAPSE_KEY_LEGACY] + : message[FIELD_COLLAPSE_KEY]; + expect(collapseKey).to.equal(DEFAULT_COLLAPSE_KEY_VALUE); if (expectedNotificationPayload) { expect(message[FIELD_NOTIFICATION]).to.deep.equal( @@ -285,13 +289,14 @@ function getTestDataPayload() { async function prepareBackgroundApp(globalWebDriver, domain) { await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - // TODO: remove the try/catch block once the underlying bug has been resolved. - // Shift window focus away from app window so that background messages can be received/processed + // TODO: remove the try/catch block once the underlying bug has been resolved. Shift window focus + // away from app window so that background messages can be received/processed try { await openNewTab(globalWebDriver); } catch (err) { - // ChromeDriver seems to have an open bug which throws "JavascriptError: javascript error: circular reference". - // Nevertheless, a new tab can still be opened. Hence, just catch and continue here. + // ChromeDriver seems to have an open bug which throws "JavascriptError: javascript error: + // circular reference". Nevertheless, a new tab can still be opened. Hence, just catch and + // continue here. console.log('FCM (ignored on purpose): ' + err); } } diff --git a/integration/messaging/test/test-updateToken.js b/integration/messaging/test/test-updateToken.js index bc7676b74f7..1eb88533780 100644 --- a/integration/messaging/test/test-updateToken.js +++ b/integration/messaging/test/test-updateToken.js @@ -43,7 +43,7 @@ describe('Firebase Messaging Integration Tests > update a token', function() { }); const availableBrowsers = seleniumAssistant.getLocalBrowsers(); - //TODO: enable testing for edge and firefox if applicable + // TODO: enable testing for edge and firefox if applicable availableBrowsers.forEach(assistantBrowser => { if (assistantBrowser.getId() !== 'chrome') { return; diff --git a/integration/messaging/test/utils/getReceivedBackgroundMessages.js b/integration/messaging/test/utils/getReceivedBackgroundMessages.js index 34dcbb26a26..3f20e4cb946 100644 --- a/integration/messaging/test/utils/getReceivedBackgroundMessages.js +++ b/integration/messaging/test/utils/getReceivedBackgroundMessages.js @@ -18,7 +18,10 @@ const TEST_DB = 'FCM_INTEGRATION_TEST_DB'; const BACKGROUND_MESSAGES_OBJECT_STORE = 'background_messages'; -/** Getting received background messages are trickier than getting foreground messages from app. It requires idb object store creation with the service worker. Idb operations are fired as async events. This method needs to be called after the idb operations inside sw is done. In tests, consider adding a brief timeout before calling the method to give sw some time to work. +/** Getting received background messages are trickier than getting foreground messages from app. It + * requires idb object store creation with the service worker. Idb operations are fired as async + * events. This method needs to be called after the idb operations inside sw is done. In tests, + * consider adding a brief timeout before calling the method to give sw some time to work. */ module.exports = async webdriver => { console.log('Getting received background messages from idb: '); diff --git a/integration/messaging/test/utils/sendMessage.js b/integration/messaging/test/utils/sendMessage.js index 7565cc5d44c..9d3b93986f1 100644 --- a/integration/messaging/test/utils/sendMessage.js +++ b/integration/messaging/test/utils/sendMessage.js @@ -17,7 +17,9 @@ const fetch = require('node-fetch'); const FCM_SEND_ENDPOINT = 'https://fcm.googleapis.com/fcm/send'; -// Rotatable fcm server key. It's generally a bad idea to expose server keys. The reason is to simplify testing process (no need to implement server side decryption of git secret). The justification is that a) this is a disposable test project b) the key itself is rotatable. +// Rotatable fcm server key. It's generally a bad idea to expose server keys. The reason is to +// simplify testing process (no need to implement server side decryption of git secret). The +// justification is that a) this is a disposable test project b) the key itself is rotatable. const FCM_KEY = 'AAAArtlRq60:APA91bHFulW1dBpIPbArYXPbFtO9M_a9ZNXhnj9hGArfGK55g8fv5s5Qset6984xRIrqhZ_3IlKcG9LgSk3DiTdHMDIOkxboNJquNK1SChC7J0ULTvHPg7t0V6AjR1UEA21DXI22BM5N'; diff --git a/integration/messaging/test/utils/test-server.js b/integration/messaging/test/utils/test-server.js index 9268e7e8508..c178f0c72d4 100644 --- a/integration/messaging/test/utils/test-server.js +++ b/integration/messaging/test/utils/test-server.js @@ -61,8 +61,8 @@ class MessagingTestServer { }); } - // Sometimes the server doesn't trigger the callback due to - // currently open sockets. So call close this._server + // Sometimes the server doesn't trigger the callback due to currently open sockets. So call close + // this._server async stop() { if (this._server) { this._server.close(); diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 96a40c53465..f05c5e6073d 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -6941,57 +6941,54 @@ declare namespace firebase.messaging { * Do not call this constructor directly. Instead, use * {@link firebase.messaging `firebase.messaging()`}. * - * See - * {@link - * https://firebase.google.com/docs/cloud-messaging/js/client - * Set Up a JavaScript Firebase Cloud Messaging Client App} - * for a full guide on how to use the Firebase Messaging service. + * See {@link https://firebase.google.com/docs/cloud-messaging/js/client Set Up a JavaScript + * Firebase Cloud Messaging Client App} for a full guide on how to use the Firebase Messaging + * service. * */ interface Messaging { /** - * Deletes the registration token associated with this messaging instance and unsubscribes - * this messaging instance from the push subscription. + * Deletes the registration token associated with this messaging instance and unsubscribes the + * messaging instance from the push subscription. * * @return The promise resolves when the token has been successfully deleted. */ deleteToken(): Promise; /** - * To forcibly stop a registration token from being used, delete it - * by calling this method. + * To forcibly stop a registration token from being used, delete it by calling this method. * * @param token The token to delete. - * @return The promise resolves when the token has been - * successfully deleted. + * @return The promise resolves when the token has been successfully deleted. * * @deprecated Use deleteToken() instead. */ deleteToken(token: string): Promise; /** - * Subscribes the messaging instance to push notifications. Returns an FCM registration - * token that can be used to send push messages to the user. + * Subscribes the messaging instance to push notifications. Returns an FCM registration token + * that can be used to send push messages to that messaging instance. * - * If notification permission isn't already granted, this method asks the - * user for permission. The returned promise rejects if the user does not - * allow the app to show notifications. + * If a notification permission isn't already granted, this method asks the user for permission. + * The returned promise rejects if the user does not allow the app to show notifications. * - * @param options.vapidKey the public server key provided to push services. It is used to + * @param options.vapidKey The public server key provided to push services. It is used to * authenticate the push subscribers to receive push messages only from sending servers that - * holds the corresponding private key. If it is not provided, a default VAPID key will be used. - * Note that some push services (Chrome Push Service) require a non-default VAPID key. - * Therefore, it is recommended to generate and import a VAPID key for your project with + * hold the corresponding private key. If it is not provided, a default VAPID key is used. Note + * that some push services (Chrome Push Service) require a non-default VAPID key. Therefore, it + * is recommended to generate and import a VAPID key for your project with * {@link https://firebase.google.com/docs/cloud-messaging/js/client#configure_web_credentials_with_fcm Configure Web Credentials with FCM}. - * See {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} + * See + * {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} * for details on web push services.} * - * @param options.serviceWorkerRegistration the service worker registration for receiving push messaging. - * If it is not provided explicitly, you need to have a `firebase-messaging-sw.js` at your root location. - * See {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} + * @param options.serviceWorkerRegistration The service worker registration for receiving push + * messaging. If the registration is not provided explicitly, you need to have a + * `firebase-messaging-sw.js` at your root location. See + * {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} * for more details. * - * @return The promise resolves with the FCM token string. + * @return The promise resolves with an FCM registration token. * */ getToken(options?: { @@ -7000,32 +6997,14 @@ declare namespace firebase.messaging { }): Promise; /** - * Subscribes the user to push notifications and returns an FCM registration - * token that can be used to send push messages to the user. - * - * If notification permission isn't already granted, this method asks the - * user for permission. The returned promise rejects if the user does not - * allow the app to show notifications. - * - * @return The promise resolves with the FCM token string. - * - * @deprecated Use getToken(options?: { - vapidKey?: string; - serviceWorkerRegistration?: ServiceWorkerRegistration; - }): Promise;. - */ - getToken(): Promise; - - /** - * When a push message is received and the user is currently on a page - * for your origin, the message is passed to the page and an `onMessage()` - * event is dispatched with the payload of the push message. + * When a push message is received and the user is currently on a page for your origin, the + * message is passed to the page and an `onMessage()` event is dispatched with the payload of + * the push message. * * @param * nextOrObserver This function, or observer object with `next` defined, * is called when a message is received and the user is currently viewing your page. - * @return To stop listening for messages - * execute this returned function. + * @return To stop listening for messages execute this returned function. */ onMessage( nextOrObserver: firebase.NextFn | firebase.Observer, @@ -7034,15 +7013,14 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * Called when a message is received while the app is in the background. An app is considered - * as a background app if no active window is displayed. + * Called when a message is received while the app is in the background. An app is to be in the + * background if no active window is displayed. * * @param * nextOrObserver This function, or observer object with `next` defined, * is called when a message is received and the app is currently in the background. * - * @return To stop listening for messages - * execute this returned function + * @return To stop listening for messages execute this returned function */ onBackgroundMessage( nextOrObserver: @@ -7053,15 +7031,13 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * You should listen for token refreshes so your web app knows when FCM - * has invalidated your existing token and you need to call `getToken()` - * to get a new token. + * You should listen for token refreshes so your web app knows when FCM has invalidated your + * existing token and you need to call `getToken()` to get a new token. * * @param * nextOrObserver This function, or observer object with `next` defined, * is called when a token refresh has occurred. - * @return To stop listening for token - * refresh events execute this returned function. + * @return To stop listening for token refresh events execute this returned function. * * @deprecated There is no need to handle token rotation. */ @@ -7072,61 +7048,59 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * Notification permissions are required to send a user push messages. - * Calling this method displays the permission dialog to the user and - * resolves if the permission is granted. It is not necessary to call this - * method, as `getToken()` will do this automatically if required. + * Notification permissions are required to send a user push messages. Calling this method + * displays the permission dialog to the user and resolves if the permission is granted. It is + * not necessary to call this method, as `getToken()` will do this automatically if required. * - * @return The promise resolves if permission is - * granted. Otherwise, the promise is rejected with an error. + * @return The promise resolves if permission is granted. Otherwise, the promise is rejected + * with an error. * - * @deprecated Use {@link https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission Notification.requestPermission()} instead. + * @deprecated Use + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission Notification.requestPermission()} + * instead. */ requestPermission(): Promise; /** - * FCM directs push messages to your web page's `onMessage()` callback - * if the user currently has it open. Otherwise, it calls - * your callback passed into `setBackgroundMessageHandler()`. + * FCM directs push messages to your web page's `onMessage()` callback if the user currently has + * it open. Otherwise, it calls your callback passed into `setBackgroundMessageHandler()`. * - * Your callback should return a promise that, once resolved, has - * shown a notification. + * Your callback should return a promise that, once resolved, has shown a notification. * * @param callback The function to handle the push message. + * + * @deprecated onBackgroundMessage(nextOrObserver: firebase.NextFn| + * firebase.Observer, error?: firebase.ErrorFn,completed?: firebase.CompleteFn): + * firebase.Unsubscribe. */ setBackgroundMessageHandler( callback: (payload: any) => Promise | void ): void; /** - * To use your own service worker for receiving push messages, you - * can pass in your service worker registration in this method. + * To use your own service worker for receiving push messages, you can pass in your service + * worker registration in this method. * - * @param registration The service worker - * registration you wish to use for push messaging. - * - * @deprecated Use getToken(options?: { - vapidKey?: string; - serviceWorkerRegistration?: ServiceWorkerRegistration; - }): Promise;. + * @param registration The service worker registration you wish to use for push messaging. + * + * @deprecated Use getToken(options?: {vapidKey?: string; serviceWorkerRegistration?: + * ServiceWorkerRegistration;}: Promise;. */ useServiceWorker(registration: ServiceWorkerRegistration): void; /** - * @deprecated Use getToken(options?: { - vapidKey?: string; - serviceWorkerRegistration?: ServiceWorkerRegistration; - }): Promise;. + * @deprecated Use getToken(options?: {vapidKey?: string; serviceWorkerRegistration?: + * ServiceWorkerRegistration;}): Promise;. */ usePublicVapidKey(b64PublicKey: string): void; } /** * Message payload that contains the notification payload that is represented with - * {@link firebase.Messaging.NotificationPayload} and the data payload that contains an - * arbitrary number of key-value pairs sent by developers through - * the {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} + * {@link firebase.Messaging.NotificationPayload} and the data payload that contains an arbitrary + * number of key-value pairs sent by developers through the + * {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification Send API} */ export interface MessagePayload { /** @@ -7150,24 +7124,27 @@ declare namespace firebase.messaging { from: string; /** - * The collapse key of this message. See more {@link https://firebase.google.com/docs/cloud-messaging/concept-options#collapsible_and_non-collapsible_messages}. + * The collapse key of this message. See more + * {@link https://firebase.google.com/docs/cloud-messaging/concept-options#collapsible_and_non-collapsible_messages}. */ collapseKey: string; } /** - * Options for features provided by the FCM SDK for Web. See more {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions}. + * Options for features provided by the FCM SDK for Web. See more + * {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions}. */ export interface FcmOptions { /** - * The link to open when the user clicks on the notification. For all URL values, HTTPS is required. - * For example, by setting this value to your app's URL, a notification click click event will - * put your app in focus for the user. + * The link to open when the user clicks on the notification. For all URL values, HTTPS is + * required. For example, by setting this value to your app's URL, a notification click event + * will put your app in focus for the user. */ link?: string; /** - * Label associated with the message's analytics data. + * Label associated with the message's analytics data. See more + * {@link https://firebase.google.com/docs/cloud-messaging/understand-delivery#adding-analytics-labels-to-messages}. */ analyticsLabel?: string; } @@ -7187,7 +7164,9 @@ declare namespace firebase.messaging { body?: string; /** - * The URL of the image that is shown with the notification. See {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification} for supported image format. + * The URL of the image that is shown with the notification. See + * {@link https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification} + * for supported image format. */ image?: string; } diff --git a/packages/installations/package.json b/packages/installations/package.json index 277f6fbb083..89f5d8e7d47 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -44,9 +44,10 @@ "@firebase/app-types": "0.x" }, "dependencies": { + "@firebase/app": "^0.6.7", + "@firebase/component": "0.1.15", "@firebase/installations-types": "0.3.4", "@firebase/util": "0.2.50", - "@firebase/component": "0.1.15", "idb": "3.0.2", "tslib": "^1.11.1" } diff --git a/packages/messaging-types/index.d.ts b/packages/messaging-types/index.d.ts index 3b0229b2c03..0d69cd19bc7 100644 --- a/packages/messaging-types/index.d.ts +++ b/packages/messaging-types/index.d.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types'; import { Observer, Unsubscribe, @@ -23,7 +22,29 @@ import { ErrorFn, CompleteFn } from '@firebase/util'; -import { MessagePayload } from '../messaging/src/interfaces/message-payload'; + +// Currently supported fcm notification display parameters. Note that +// {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/notifications/NotificationOptions} +// defines a full list of display notification parameters. This interface we only include what the +// SEND API support for clarity. +export interface NotificationPayload { + title?: string; + body?: string; + image?: string; +} + +export interface FcmOptions { + link?: string; + analyticsLabel?: string; +} + +export interface MessagePayload { + notification?: NotificationPayload; + data?: { [key: string]: string }; + fcmOptions?: FcmOptions; + from: string; + collapseKey: string; +} export interface FirebaseMessaging { /** window controller */ @@ -47,7 +68,6 @@ export interface FirebaseMessaging { /** @deprecated */ deleteToken(token: string): Promise; - getToken(): Promise; onTokenRefresh( nextOrObserver: NextFn | Observer, error?: ErrorFn, diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 04493772c3f..da4a73b9a94 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -33,16 +33,14 @@ "@firebase/installations": "0.4.13", "@firebase/messaging-types": "0.4.5", "@firebase/util": "0.2.50", - "eslint": "^7.3.1", "idb": "3.0.2", - "npm-run-all": "^4.1.5", - "tslib": "^1.11.1" + "tslib": "1.11.1" }, "devDependencies": { "rollup": "2.7.6", "rollup-plugin-typescript2": "0.27.0", - "ts-essentials": "^6.0.7", - "typescript": "3.8.3" + "ts-essentials": "6.0.7", + "typescript": "3.9.5" }, "repository": { "directory": "packages/messaging", diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts index 94fcf29758f..5692a1f9a82 100644 --- a/packages/messaging/src/controllers/sw-controller.test.ts +++ b/packages/messaging/src/controllers/sw-controller.test.ts @@ -1,5 +1,20 @@ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + import '../testing/setup'; import * as tokenManagementModule from '../core/token-management'; @@ -23,28 +38,12 @@ import { import { MessagePayloadInternal, MessageType -} from '../interfaces/message-payload'; +} from '../interfaces/internal-message-payload'; import { spy, stub } from 'sinon'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { Stub } from '../testing/sinon-types'; import { dbSet } from '../helpers/idb-manager'; -/** - * @license - * Copyright 2017 Google LLC - * - * 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. - */ import { expect } from 'chai'; import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; import { getFakeTokenDetails } from '../testing/fakes/token-details'; @@ -52,7 +51,8 @@ import { getFakeTokenDetails } from '../testing/fakes/token-details'; // Add fake SW types. declare const self: Window & Writable; -// internal message payload (parsed directly from the push event) that contains and only contains notification payload. +// internal message payload (parsed directly from the push event) that contains and only contains +// notification payload. const DISPLAY_MESSAGE: MessagePayloadInternal = { notification: { title: 'title', @@ -66,7 +66,8 @@ const DISPLAY_MESSAGE: MessagePayloadInternal = { collapse_key: 'collapse' }; -// internal message payload (parsed directly from the push event) that contains and only contains data payload. +// internal message payload (parsed directly from the push event) that contains and only contains +// data payload. const DATA_MESSAGE: MessagePayloadInternal = { data: { key: 'value' @@ -89,7 +90,9 @@ describe('SwController', () => { stub(Notification, 'permission').value('granted'); - // Instead of calling actual addEventListener, add the event to the eventListeners list. Actual event listeners can't be used as the tests are not running in a Service Worker, which means Push events do not exist. + // Instead of calling actual addEventListener, add the event to the eventListeners list. Actual + // event listeners can't be used as the tests are not running in a Service Worker, which means + // Push events do not exist. addEventListenerStub = stub(self, 'addEventListener').callsFake( (type, listener) => { eventListenerMap.set(type, listener); @@ -281,6 +284,31 @@ describe('SwController', () => { expect(bgMessageHandlerSpy).to.have.been.calledWith(); }); + it('forwards MessagePayload with a notification payload to onBackgroundMessage', async () => { + const bgMessageHandlerSpy = spy(); + const showNotificationSpy = spy(self.registration, 'showNotification'); + + swController.setBackgroundMessageHandler(bgMessageHandlerSpy); + + await callEventListener( + makeEvent('push', { + data: { + json: () => ({ + notification: { + ...DISPLAY_MESSAGE + }, + data: { + ...DATA_MESSAGE + } + }) + } + }) + ); + + expect(bgMessageHandlerSpy).to.have.been.called; + expect(showNotificationSpy).to.have.been.called; + }); + it('warns if there are more action buttons than the browser limit', async () => { // This doesn't exist on Firefox: // https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index 5d62a8ff0ee..a7c0fd1a6da 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ /** * @license * Copyright 2017 Google LLC @@ -18,18 +17,17 @@ import { DEFAULT_VAPID_KEY, FCM_MSG, TAG } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { FirebaseMessaging, MessagePayload } from '@firebase/messaging-types'; import { - MessagePayload, MessagePayloadInternal, MessageType, NotificationPayloadInternal -} from '../interfaces/message-payload'; +} from '../interfaces/internal-message-payload'; import { NextFn, Observer, Unsubscribe } from '@firebase/util'; import { deleteToken, getToken } from '../core/token-management'; import { FirebaseApp } from '@firebase/app-types'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; import { dbGet } from '../helpers/idb-manager'; import { externalizePayload } from '../helpers/externalizePayload'; @@ -42,6 +40,11 @@ declare const self: ServiceWorkerGlobalScope; export type BgMessageHandler = (payload: MessagePayload) => unknown; export class SwController implements FirebaseMessaging, FirebaseService { + // A boolean flag to determine wether an app is using onBackgroundMessage or + // setBackgroundMessageHandler. onBackgroundMessage will receive a MessagePayload regardless of if + // a notification is displayed. Whereas, setBackgroundMessageHandler will swallow the + // MessagePayload if a NotificationPayload is included. + private isOnBackgroundMessageUsed: boolean | null = null; private vapidKey: string | null = null; private bgMessageHandler: | BgMessageHandler @@ -68,20 +71,19 @@ export class SwController implements FirebaseMessaging, FirebaseService { } /** - * Calling setBackgroundMessageHandler will opt in to some specific - * behaviors. + * @deprecated. Use onBackgroundMessage(nextOrObserver: NextFn | Observer): + * Unsubscribe instead. * - * 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 + * Calling setBackgroundMessageHandler will opt in to some specific behaviors. * - * @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. + * 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: BgMessageHandler): void { if (!callback || typeof callback !== 'function') { @@ -94,6 +96,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { onBackgroundMessage( nextOrObserver: NextFn | Observer ): Unsubscribe { + this.isOnBackgroundMessageUsed = true; this.bgMessageHandler = nextOrObserver; return () => { @@ -101,8 +104,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { }; } - // TODO: Remove getToken from SW Controller. - // Calling this from an old SW can cause all kinds of trouble. + // TODO: Remove getToken from SW Controller. Calling this from an old SW can cause all kinds of + // trouble. async getToken(): Promise { if (!this.vapidKey) { // Call getToken using the current VAPID key if there already is a token. This is needed @@ -120,7 +123,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { ); } - // TODO: Remove deleteToken from SW Controller. Calling this from an old SW can cause all kinds of trouble. + // TODO: Remove deleteToken from SW Controller. Calling this from an old SW can cause all kinds of + // trouble. deleteToken(): Promise { return deleteToken(this.firebaseDependencies, self.registration); } @@ -157,8 +161,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { /** * 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 + * 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. * @@ -181,10 +185,22 @@ export class SwController implements FirebaseMessaging, FirebaseService { } // background handling: display and pass to onBackgroundMessage hook + let isNotificationShown = false; if (!!internalPayload.notification) { await showNotification(wrapInternalPayload(internalPayload)); - } else if (this.bgMessageHandler) { + isNotificationShown = true; + } + + if ( + isNotificationShown === true && + this.isOnBackgroundMessageUsed === false + ) { + return; + } + + if (!!this.bgMessageHandler) { const payload = externalizePayload(internalPayload); + if (typeof this.bgMessageHandler === 'function') { this.bgMessageHandler(payload); } else { @@ -260,9 +276,9 @@ function wrapInternalPayload( ...((internalPayload.notification as unknown) as NotificationPayloadInternal) }; - // 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). + // 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). wrappedInternalPayload.data = { [FCM_MSG]: internalPayload }; @@ -290,8 +306,8 @@ function getMessagePayloadInternal({ * @return Returns an existing window client or a newly opened WindowClient. */ async function getWindowClient(url: string): Promise { - // Use URL to normalize the URL when comparing to windowClients. This at least handles whether - // to include trailing slashes or not + // 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); const clientList = await getClientList(); @@ -307,15 +323,15 @@ async function getWindowClient(url: string): Promise { } /** - * @returns If there is currently a visible WindowClient, this method will - * resolve to true, otherwise false. + * @returns If there is currently a visible WindowClient, this method will resolve to true, + * otherwise false. */ function hasVisibleClients(clientList: WindowClient[]): boolean { 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. + // 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://') ); } diff --git a/packages/messaging/src/controllers/window-controller.test.ts b/packages/messaging/src/controllers/window-controller.test.ts index d5c4b1126af..373b4eb42f7 100644 --- a/packages/messaging/src/controllers/window-controller.test.ts +++ b/packages/messaging/src/controllers/window-controller.test.ts @@ -1,3 +1,20 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + import '../testing/setup'; import * as tokenManagementModule from '../core/token-management'; @@ -14,33 +31,16 @@ import { import { MessagePayloadInternal, MessageType -} from '../interfaces/message-payload'; +} from '../interfaces/internal-message-payload'; import { SinonFakeTimers, SinonSpy, spy, stub, useFakeTimers } from 'sinon'; import { Spy, Stub } from '../testing/sinon-types'; -/** - * @license - * Copyright 2017 Google LLC - * - * 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. - */ -import { assert, expect } from 'chai'; import { ErrorCode } from '../util/errors'; import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; import { FirebaseAnalyticsInternal } from '@firebase/analytics-interop-types'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { WindowController } from './window-controller'; -import { externalizePayload } from '../helpers/externalizePayload'; +import { expect } from 'chai'; import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; type MessageEventListener = (event: Event) => Promise; @@ -109,7 +109,7 @@ describe('WindowController', () => { describe('getToken', () => { it('uses default sw if none was registered nor provided', async () => { - assert.isUndefined(windowController.getSwReg()); + expect(windowController.getSwReg()).to.be.undefined; await windowController.getToken({}); @@ -119,7 +119,7 @@ describe('WindowController', () => { }); it('uses option-provided swReg if non was registered', async () => { - assert.isUndefined(windowController.getSwReg()); + expect(windowController.getSwReg()).to.be.undefined; await windowController.getToken({ serviceWorkerRegistration: swRegistration @@ -134,10 +134,7 @@ describe('WindowController', () => { it('uses previously stored sw if non is provided in the option parameter', async () => { windowController.useServiceWorker(swRegistration); - assert.strictEqual( - JSON.stringify(windowController.getSwReg()), - JSON.stringify(swRegistration) - ); + expect(windowController.getSwReg()).to.be.deep.equal(swRegistration); await windowController.getToken({}); @@ -150,10 +147,7 @@ describe('WindowController', () => { it('new swReg overrides existing swReg ', async () => { windowController.useServiceWorker(swRegistration); - assert.strictEqual( - JSON.stringify(windowController.getSwReg()), - JSON.stringify(swRegistration) - ); + expect(windowController.getSwReg()).to.be.deep.equal(swRegistration); const otherSwReg = new FakeServiceWorkerRegistration(); @@ -168,12 +162,13 @@ describe('WindowController', () => { ); }); - it('uses default VAPID if: a) no VAPID was stored and b) non iss provided in option', async () => { - assert.strictEqual(windowController.getVapidKey(), null); + it('uses default VAPID if: a) no VAPID was stored and b) none is provided in option', async () => { + expect(windowController.getVapidKey()).is.null; await windowController.getToken({}); - assert.strictEqual(windowController.getVapidKey(), DEFAULT_VAPID_KEY); + expect(windowController.getVapidKey()).to.equal(DEFAULT_VAPID_KEY); + expect(getTokenStub).to.have.been.calledOnceWith( firebaseDependencies, swRegistration, @@ -182,11 +177,11 @@ describe('WindowController', () => { }); it('uses option-provided VAPID if no VAPID has been registered', async () => { - assert.strictEqual(windowController.getVapidKey(), null); + expect(windowController.getVapidKey()).is.null; await windowController.getToken({ vapidKey: 'test_vapid_key' }); - assert.strictEqual(windowController.getVapidKey(), 'test_vapid_key'); + expect(windowController.getVapidKey()).to.equal('test_vapid_key'); expect(getTokenStub).to.have.been.calledOnceWith( firebaseDependencies, swRegistration, @@ -196,11 +191,11 @@ describe('WindowController', () => { it('uses option-provided VAPID if it is different from currently registered VAPID', async () => { windowController.usePublicVapidKey('old_key'); - assert.strictEqual(windowController.getVapidKey(), 'old_key'); + expect(windowController.getVapidKey()).to.equal('old_key'); await windowController.getToken({ vapidKey: 'new_key' }); - assert.strictEqual(windowController.getVapidKey(), 'new_key'); + expect(windowController.getVapidKey()).to.equal('new_key'); expect(getTokenStub).to.have.been.calledOnceWith( firebaseDependencies, swRegistration, @@ -210,11 +205,11 @@ describe('WindowController', () => { it('uses existing VAPID if newly provided has the same value', async () => { windowController.usePublicVapidKey('key'); - assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(windowController.getVapidKey()).to.equal('key'); await windowController.getToken({ vapidKey: 'key' }); - assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(windowController.getVapidKey()).to.equal('key'); expect(getTokenStub).to.have.been.calledOnceWith( firebaseDependencies, swRegistration, @@ -224,11 +219,11 @@ describe('WindowController', () => { it('uses existing VAPID if non is provided in the option parameter', async () => { windowController.usePublicVapidKey('key'); - assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(windowController.getVapidKey()).to.equal('key'); await windowController.getToken({}); - assert.strictEqual(windowController.getVapidKey(), 'key'); + expect(windowController.getVapidKey()).to.equal('key'); expect(getTokenStub).to.have.been.calledOnceWith( firebaseDependencies, swRegistration, @@ -531,9 +526,12 @@ describe('WindowController', () => { new MessageEvent('message', { data: internalPayload }) ); - expect(onMessageSpy).to.have.been.calledOnceWith( - externalizePayload(internalPayload) - ); + expect(onMessageSpy).to.have.been.calledOnceWith({ + notification: { title: 'hello', body: 'world' }, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' + }); expect(logEventSpy).not.to.have.been.called; }); @@ -575,9 +573,18 @@ describe('WindowController', () => { new MessageEvent('message', { data: internalPayload }) ); - expect(onMessageSpy).to.have.been.calledOnceWith( - externalizePayload(internalPayload) - ); + expect(onMessageSpy).to.have.been.calledOnceWith({ + notification: { title: 'hello', body: 'world' }, + data: { + [CONSOLE_CAMPAIGN_ID]: '123456', + [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', + [CONSOLE_CAMPAIGN_TIME]: '1234567890', + [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' + }, + from: 'from', + // eslint-disable-next-line camelcase + collapse_key: 'collapse' + }); expect(logEventSpy).to.have.been.calledOnceWith( 'notification_foreground', { diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index cc01427f929..dedb6c95c30 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -1,5 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * @license * Copyright 2017 Google LLC @@ -24,30 +22,21 @@ import { CONSOLE_CAMPAIGN_TIME, DEFAULT_SW_PATH, DEFAULT_SW_SCOPE, - DEFAULT_VAPID_KEY, - TAG + DEFAULT_VAPID_KEY } from '../util/constants'; -import { - CompleteFn, - ErrorFn, - NextFn, - Observer, - Unsubscribe -} from '@firebase/util'; import { ConsoleMessageData, - MessagePayload, MessagePayloadInternal, MessageType -} from '../interfaces/message-payload'; +} from '../interfaces/internal-message-payload'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { NextFn, Observer, Unsubscribe } from '@firebase/util'; import { deleteToken, getToken } from '../core/token-management'; import { FirebaseApp } from '@firebase/app-types'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { FirebaseMessaging } from '@firebase/messaging-types'; import { FirebaseService } from '@firebase/app-types/private'; -import { externalizePayload } from '../helpers/externalizePayload'; import { isConsoleMessage } from '../helpers/is-console-message'; export class WindowController implements FirebaseMessaging, FirebaseService { @@ -75,18 +64,23 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } // onMessageCallback is either a function or observer/subscriber. + // TODO: in the modularization release, have onMessage handle type MessagePayload as supposed to + // the legacy payload where some fields are in snake cases. if ( this.onMessageCallback && internalPayload.messageType === MessageType.PUSH_RECEIVED ) { if (typeof this.onMessageCallback === 'function') { - this.onMessageCallback(externalizePayload(internalPayload)); + this.onMessageCallback( + stripInternalFields(Object.assign({}, internalPayload)) + ); } else { - this.onMessageCallback.next(externalizePayload(internalPayload)); + this.onMessageCallback.next(Object.assign({}, internalPayload)); } } const dataPayload = internalPayload.data; + if ( isConsoleMessage(dataPayload) && dataPayload[CONSOLE_CAMPAIGN_ANALYTICS_ENABLED] === '1' @@ -118,14 +112,6 @@ export class WindowController implements FirebaseMessaging, FirebaseService { await this.updateVapidKey(options?.vapidKey); await this.updateSwReg(options?.serviceWorkerRegistration); - if (!this.swRegistration) { - console.debug( - TAG + - 'no sw has been provided explicitly. Attempting to find firebase-messaging-sw.js in default directory.' - ); - await this.registerDefaultSw(); - } - return getToken( this.firebaseDependencies, this.swRegistration!, @@ -133,12 +119,8 @@ export class WindowController implements FirebaseMessaging, FirebaseService { ); } - async updateVapidKey(vapidKey: string | undefined): Promise { + async updateVapidKey(vapidKey?: string | undefined): Promise { if (!!this.vapidKey && !!vapidKey && this.vapidKey !== vapidKey) { - console.debug( - TAG + - 'newly provided VapidKey is different from previously stored VapidKey. New VapidKey is overriding.' - ); this.vapidKey = vapidKey; } @@ -147,18 +129,18 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } if (!this.vapidKey && !vapidKey) { - console.debug( - TAG + - 'no VapidKey is provided. Using the default VapidKey. Note that Push will NOT work in Chrome without a non-default VapidKey.' - ); this.vapidKey = DEFAULT_VAPID_KEY; } } async updateSwReg( - swRegistration: ServiceWorkerRegistration | undefined + swRegistration?: ServiceWorkerRegistration | undefined ): Promise { - if (!swRegistration) { + if (!swRegistration && !this.swRegistration) { + await this.registerDefaultSw(); + } + + if (!swRegistration && !!this.swRegistration) { return; } @@ -178,10 +160,11 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } ); - // The timing when browser updates sw when sw has an update is unreliable by my experiment. - // It leads to version conflict when the SDK upgrades to a newer version in the main page, but - // sw is stuck with the old version. For example, https://github.com/firebase/firebase-js-sdk/issues/2590 - // The following line reliably updates sw if there was an update. + // The timing when browser updates sw when sw has an update is unreliable by my experiment. It + // leads to version conflict when the SDK upgrades to a newer version in the main page, but sw + // is stuck with the old version. For example, + // https://github.com/firebase/firebase-js-sdk/issues/2590 The following line reliably updates + // sw if there was an update. this.swRegistration.update().catch(() => { /* it is non blocking and we don't care if it failed */ }); @@ -223,6 +206,10 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } } + /** + * @deprecated. Use getToken(options?: {vapidKey?: string; serviceWorkerRegistration?: + * ServiceWorkerRegistration;}): Promise instead. + */ usePublicVapidKey(vapidKey: string): void { if (this.vapidKey !== null) { throw ERROR_FACTORY.create(ErrorCode.USE_VAPID_KEY_AFTER_GET_TOKEN); @@ -235,6 +222,10 @@ export class WindowController implements FirebaseMessaging, FirebaseService { this.vapidKey = vapidKey; } + /** + * @deprecated. Use getToken(options?: {vapidKey?: string; serviceWorkerRegistration?: + * ServiceWorkerRegistration;}): Promise instead. + */ useServiceWorker(swRegistration: ServiceWorkerRegistration): void { if (!(swRegistration instanceof ServiceWorkerRegistration)) { throw ERROR_FACTORY.create(ErrorCode.INVALID_SW_REGISTRATION); @@ -248,8 +239,7 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } /** - * @param nextOrObserver An observer object or a function triggered on - * message. + * @param nextOrObserver An observer object or a function triggered on message. * * @return The unsubscribe function for the observer. */ @@ -265,17 +255,13 @@ export class WindowController implements FirebaseMessaging, FirebaseService { throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_SW); } - onBackgroundMessage( - nextOrObserver: NextFn | Observer, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe { + onBackgroundMessage(): Unsubscribe { throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_SW); } /** - * No-op. It was initially designed with token rotation requests from server in mind. However, the plan to implement such feature was abandoned. - * @deprecated + * @deprecated No-op. It was initially designed with token rotation requests from server in mind. + * However, the plan to implement such feature was abandoned. */ onTokenRefresh(): Unsubscribe { return () => {}; @@ -308,3 +294,11 @@ function getEventType(messageType: MessageType): string { throw new Error(); } } + +function stripInternalFields( + internalPayload: MessagePayloadInternal +): MessagePayloadInternal { + delete internalPayload.messageType; + delete internalPayload.isFirebaseMessaging; + return internalPayload; +} diff --git a/packages/messaging/src/core/api.test.ts b/packages/messaging/src/core/api.test.ts index 12343359e0e..da3368191c8 100644 --- a/packages/messaging/src/core/api.test.ts +++ b/packages/messaging/src/core/api.test.ts @@ -15,22 +15,24 @@ * limitations under the License. */ -import { expect } from 'chai'; -import { stub } from 'sinon'; import '../testing/setup'; -import { Stub } from '../testing/sinon-types'; + import { ApiRequestBody, + requestDeleteToken, requestGetToken, - requestUpdateToken, - requestDeleteToken + requestUpdateToken } from './api'; -import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; + +import { ENDPOINT } from '../util/constants'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { Stub } from '../testing/sinon-types'; import { TokenDetails } from '../interfaces/token-details'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { ENDPOINT } from '../util/constants'; import { compareHeaders } from '../testing/compare-headers'; +import { expect } from 'chai'; +import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; +import { getFakeTokenDetails } from '../testing/fakes/token-details'; +import { stub } from 'sinon'; describe('API', () => { let tokenDetails: TokenDetails; diff --git a/packages/messaging/src/core/api.ts b/packages/messaging/src/core/api.ts index 6fb0fd1ae77..b93f8623601 100644 --- a/packages/messaging/src/core/api.ts +++ b/packages/messaging/src/core/api.ts @@ -15,11 +15,12 @@ * limitations under the License. */ -import { ErrorCode, ERROR_FACTORY } from '../util/errors'; import { DEFAULT_VAPID_KEY, ENDPOINT } from '../util/constants'; -import { TokenDetails, SubscriptionOptions } from '../interfaces/token-details'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { SubscriptionOptions, TokenDetails } from '../interfaces/token-details'; + import { AppConfig } from '../interfaces/app-config'; +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; export interface ApiResponse { token?: string; diff --git a/packages/messaging/src/core/token-management.ts b/packages/messaging/src/core/token-management.ts index 99fdc179be9..b314c89fc77 100644 --- a/packages/messaging/src/core/token-management.ts +++ b/packages/messaging/src/core/token-management.ts @@ -15,15 +15,16 @@ * limitations under the License. */ -import { dbGet, dbSet, dbRemove } from '../helpers/idb-manager'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { TokenDetails, SubscriptionOptions } from '../interfaces/token-details'; -import { requestUpdateToken, requestGetToken, requestDeleteToken } from './api'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { SubscriptionOptions, TokenDetails } from '../interfaces/token-details'; import { arrayToBase64, base64ToArray } from '../helpers/array-base64-translator'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { dbGet, dbRemove, dbSet } from '../helpers/idb-manager'; +import { requestDeleteToken, requestGetToken, requestUpdateToken } from './api'; + +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; /** UpdateRegistration will be called once every week. */ const TOKEN_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days @@ -37,8 +38,8 @@ export async function getToken( throw ERROR_FACTORY.create(ErrorCode.PERMISSION_BLOCKED); } - // If a PushSubscription exists it's returned, otherwise a new subscription - // is generated and returned. + // 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); @@ -83,8 +84,8 @@ export async function getToken( } /** - * This method deletes the token from the database, unsubscribes the token from - * FCM, and unregisters the push subscription if it exists. + * This method deletes the token from the database, unsubscribes the token from FCM, and unregisters + * the push subscription if it exists. */ export async function deleteToken( firebaseDependencies: FirebaseInternalDependencies, diff --git a/packages/messaging/src/helpers/array-base64-translator.test.ts b/packages/messaging/src/helpers/array-base64-translator.test.ts index 1274311e0cb..c161b365dbc 100644 --- a/packages/messaging/src/helpers/array-base64-translator.test.ts +++ b/packages/messaging/src/helpers/array-base64-translator.test.ts @@ -15,9 +15,11 @@ * limitations under the License. */ +import '../testing/setup'; + import { arrayToBase64, base64ToArray } from './array-base64-translator'; + import { expect } from 'chai'; -import '../testing/setup'; // prettier-ignore const TEST_P256_ARRAY = new Uint8Array([ diff --git a/packages/messaging/src/helpers/externalizePayload.test.ts b/packages/messaging/src/helpers/externalizePayload.test.ts index d28d5df870a..c42fb662cac 100644 --- a/packages/messaging/src/helpers/externalizePayload.test.ts +++ b/packages/messaging/src/helpers/externalizePayload.test.ts @@ -1,7 +1,6 @@ -/* eslint-disable camelcase */ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +15,8 @@ * limitations under the License. */ -import { - MessagePayload, - MessagePayloadInternal -} from '../interfaces/message-payload'; - +import { MessagePayload } from '@firebase/messaging-types'; +import { MessagePayloadInternal } from '../interfaces/internal-message-payload'; import { expect } from 'chai'; import { externalizePayload } from './externalizePayload'; @@ -33,6 +29,7 @@ describe('externalizePayload', () => { image: 'image' }, from: 'from', + // eslint-disable-next-line camelcase collapse_key: 'collapse' }; @@ -52,6 +49,7 @@ describe('externalizePayload', () => { baz: 'baz' }, from: 'from', + // eslint-disable-next-line camelcase collapse_key: 'collapse' }; @@ -77,9 +75,11 @@ describe('externalizePayload', () => { }, fcmOptions: { link: 'link', + // eslint-disable-next-line camelcase analytics_label: 'label' }, from: 'from', + // eslint-disable-next-line camelcase collapse_key: 'collapse' }; diff --git a/packages/messaging/src/helpers/externalizePayload.ts b/packages/messaging/src/helpers/externalizePayload.ts index a5730ebd739..f86cafd089e 100644 --- a/packages/messaging/src/helpers/externalizePayload.ts +++ b/packages/messaging/src/helpers/externalizePayload.ts @@ -1,7 +1,6 @@ -/* eslint-disable camelcase */ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +15,15 @@ * limitations under the License. */ -import { - MessagePayload, - MessagePayloadInternal -} from '../interfaces/message-payload'; +import { MessagePayload } from '@firebase/messaging-types'; +import { MessagePayloadInternal } from '../interfaces/internal-message-payload'; export function externalizePayload( internalPayload: MessagePayloadInternal ): MessagePayload { const payload: MessagePayload = { - notification: {}, - data: {}, - fcmOptions: {}, from: internalPayload.from, + // eslint-disable-next-line camelcase collapseKey: internalPayload.collapse_key } as MessagePayload; @@ -44,10 +39,11 @@ function propagateNotificationPayload( messagePayloadInternal: MessagePayloadInternal ): void { if (!messagePayloadInternal.notification) { - delete payload.notification; return; } + payload.notification = {}; + const title = messagePayloadInternal.notification!.title; if (!!title) { payload.notification!.title = title; @@ -69,7 +65,6 @@ function propagateDataPayload( messagePayloadInternal: MessagePayloadInternal ): void { if (!messagePayloadInternal.data) { - delete payload.data; return; } @@ -81,10 +76,11 @@ function propagateFcmOptions( messagePayloadInternal: MessagePayloadInternal ): void { if (!messagePayloadInternal.fcmOptions) { - delete payload.fcmOptions; return; } + payload.fcmOptions = {}; + const link = messagePayloadInternal.fcmOptions!.link; if (!!link) { payload.fcmOptions!.link = link; diff --git a/packages/messaging/src/helpers/extract-app-config.test.ts b/packages/messaging/src/helpers/extract-app-config.test.ts index 63d27bb463f..0d8d5c08561 100644 --- a/packages/messaging/src/helpers/extract-app-config.test.ts +++ b/packages/messaging/src/helpers/extract-app-config.test.ts @@ -15,12 +15,13 @@ * limitations under the License. */ -import { expect } from 'chai'; -import { AppConfig } from '../interfaces/app-config'; -import { getFakeApp } from '../testing/fakes/firebase-dependencies'; import '../testing/setup'; -import { extractAppConfig } from './extract-app-config'; + +import { AppConfig } from '../interfaces/app-config'; import { FirebaseApp } from '@firebase/app-types'; +import { expect } from 'chai'; +import { extractAppConfig } from './extract-app-config'; +import { getFakeApp } from '../testing/fakes/firebase-dependencies'; describe('extractAppConfig', () => { it('returns AppConfig if the argument is a FirebaseApp object that includes an appId', () => { diff --git a/packages/messaging/src/helpers/extract-app-config.ts b/packages/messaging/src/helpers/extract-app-config.ts index 67033bffa55..e95a45ced7e 100644 --- a/packages/messaging/src/helpers/extract-app-config.ts +++ b/packages/messaging/src/helpers/extract-app-config.ts @@ -15,10 +15,11 @@ * limitations under the License. */ +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; -import { FirebaseError } from '@firebase/util'; + import { AppConfig } from '../interfaces/app-config'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { FirebaseError } from '@firebase/util'; export function extractAppConfig(app: FirebaseApp): AppConfig { if (!app || !app.options) { diff --git a/packages/messaging/src/helpers/idb-manager.test.ts b/packages/messaging/src/helpers/idb-manager.test.ts index 9783d486917..95155b000d8 100644 --- a/packages/messaging/src/helpers/idb-manager.test.ts +++ b/packages/messaging/src/helpers/idb-manager.test.ts @@ -15,16 +15,19 @@ * limitations under the License. */ -import { expect } from 'chai'; -import { stub } from 'sinon'; import '../testing/setup'; -import { TokenDetails } from '../interfaces/token-details'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; -import { dbSet, dbGet, dbRemove } from './idb-manager'; + import * as migrateOldDatabaseModule from './migrate-old-database'; -import { Stub } from '../testing/sinon-types'; + +import { dbGet, dbRemove, dbSet } from './idb-manager'; + import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; +import { Stub } from '../testing/sinon-types'; +import { TokenDetails } from '../interfaces/token-details'; +import { expect } from 'chai'; +import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; +import { getFakeTokenDetails } from '../testing/fakes/token-details'; +import { stub } from 'sinon'; describe('idb manager', () => { let firebaseDependencies: FirebaseInternalDependencies; diff --git a/packages/messaging/src/helpers/idb-manager.ts b/packages/messaging/src/helpers/idb-manager.ts index 96c0b524f4c..dbe957de244 100644 --- a/packages/messaging/src/helpers/idb-manager.ts +++ b/packages/messaging/src/helpers/idb-manager.ts @@ -15,10 +15,11 @@ * limitations under the License. */ -import { DB, openDb, deleteDb } from 'idb'; +import { DB, deleteDb, openDb } from 'idb'; + +import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { TokenDetails } from '../interfaces/token-details'; import { migrateOldDatabase } from './migrate-old-database'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; // Exported for tests. export const DATABASE_NAME = 'firebase-messaging-database'; @@ -29,10 +30,9 @@ let dbPromise: Promise | null = null; function getDbPromise(): Promise { 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. + // 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: diff --git a/packages/messaging/src/helpers/is-console-message.ts b/packages/messaging/src/helpers/is-console-message.ts index c2ed882ae73..151713be132 100644 --- a/packages/messaging/src/helpers/is-console-message.ts +++ b/packages/messaging/src/helpers/is-console-message.ts @@ -15,11 +15,10 @@ * limitations under the License. */ -import { ConsoleMessageData } from '../interfaces/message-payload'; import { CONSOLE_CAMPAIGN_ID } from '../util/constants'; +import { ConsoleMessageData } from '../interfaces/internal-message-payload'; export function isConsoleMessage(data: unknown): data is ConsoleMessageData { - // This message has a campaign ID, meaning it was sent using the - // Firebase Console. + // This message has a campaign ID, meaning it was sent using the Firebase Console. return typeof data === 'object' && !!data && CONSOLE_CAMPAIGN_ID in data; } diff --git a/packages/messaging/src/helpers/migrate-old-database.test.ts b/packages/messaging/src/helpers/migrate-old-database.test.ts index 0ff131621ad..020295ca2fd 100644 --- a/packages/messaging/src/helpers/migrate-old-database.test.ts +++ b/packages/messaging/src/helpers/migrate-old-database.test.ts @@ -15,18 +15,20 @@ * limitations under the License. */ -import { expect } from 'chai'; import '../testing/setup'; -import { openDb } from 'idb'; + import { - migrateOldDatabase, V2TokenDetails, V3TokenDetails, - V4TokenDetails + V4TokenDetails, + migrateOldDatabase } from './migrate-old-database'; + import { FakePushSubscription } from '../testing/fakes/service-worker'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; import { base64ToArray } from './array-base64-translator'; +import { expect } from 'chai'; +import { getFakeTokenDetails } from '../testing/fakes/token-details'; +import { openDb } from 'idb'; describe('migrateOldDb', () => { it("does nothing if old DB didn't exist", async () => { diff --git a/packages/messaging/src/helpers/migrate-old-database.ts b/packages/messaging/src/helpers/migrate-old-database.ts index 8a935eedd2b..40fdece6171 100644 --- a/packages/messaging/src/helpers/migrate-old-database.ts +++ b/packages/messaging/src/helpers/migrate-old-database.ts @@ -15,7 +15,8 @@ * limitations under the License. */ -import { openDb, deleteDb } from 'idb'; +import { deleteDb, openDb } from 'idb'; + import { TokenDetails } from '../interfaces/token-details'; import { arrayToBase64 } from './array-base64-translator'; @@ -60,8 +61,8 @@ export interface V4TokenDetails { 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. + * 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'; @@ -70,9 +71,8 @@ export async function migrateOldDatabase( senderId: string ): Promise { 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. + // 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 as { databases(): Promise>; }).databases(); diff --git a/packages/messaging/src/helpers/sleep.test.ts b/packages/messaging/src/helpers/sleep.test.ts index 6dfc4b328ee..b7c4e228f10 100644 --- a/packages/messaging/src/helpers/sleep.test.ts +++ b/packages/messaging/src/helpers/sleep.test.ts @@ -15,9 +15,11 @@ * limitations under the License. */ -import { expect } from 'chai'; -import { SinonFakeTimers, useFakeTimers } from 'sinon'; import '../testing/setup'; + +import { SinonFakeTimers, useFakeTimers } from 'sinon'; + +import { expect } from 'chai'; import { sleep } from './sleep'; describe('sleep', () => { diff --git a/packages/messaging/src/index.ts b/packages/messaging/src/index.ts index 9435f177260..4b0ba76a7c5 100644 --- a/packages/messaging/src/index.ts +++ b/packages/messaging/src/index.ts @@ -15,23 +15,25 @@ * limitations under the License. */ -import firebase from '@firebase/app'; import '@firebase/installations'; -import { - _FirebaseNamespace, - FirebaseService -} from '@firebase/app-types/private'; -import { FirebaseMessaging } from '@firebase/messaging-types'; + import { Component, - ComponentType, - ComponentContainer + ComponentContainer, + ComponentType } from '@firebase/component'; -import { extractAppConfig } from './helpers/extract-app-config'; -import { FirebaseInternalDependencies } from './interfaces/internal-dependencies'; import { ERROR_FACTORY, ErrorCode } from './util/errors'; -import { WindowController } from './controllers/window-controller'; +import { + FirebaseService, + _FirebaseNamespace +} from '@firebase/app-types/private'; + +import { FirebaseInternalDependencies } from './interfaces/internal-dependencies'; +import { FirebaseMessaging } from '@firebase/messaging-types'; import { SwController } from './controllers/sw-controller'; +import { WindowController } from './controllers/window-controller'; +import { extractAppConfig } from './helpers/extract-app-config'; +import firebase from '@firebase/app'; const MESSAGING_NAME = 'messaging'; function factoryMethod( diff --git a/packages/messaging/src/interfaces/internal-dependencies.ts b/packages/messaging/src/interfaces/internal-dependencies.ts index 9ef1e4963b7..ecc380f2089 100644 --- a/packages/messaging/src/interfaces/internal-dependencies.ts +++ b/packages/messaging/src/interfaces/internal-dependencies.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { FirebaseInstallations } from '@firebase/installations-types'; -import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; -import { Provider } from '@firebase/component'; import { AppConfig } from './app-config'; +import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; import { FirebaseApp } from '@firebase/app-types'; +import { FirebaseInstallations } from '@firebase/installations-types'; +import { Provider } from '@firebase/component'; export interface FirebaseInternalDependencies { app: FirebaseApp; diff --git a/packages/messaging/src/interfaces/internal-message-payload.ts b/packages/messaging/src/interfaces/internal-message-payload.ts new file mode 100644 index 00000000000..92f7b19fa02 --- /dev/null +++ b/packages/messaging/src/interfaces/internal-message-payload.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2018 Google LLC + * + * 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. + */ + +import { + CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, + CONSOLE_CAMPAIGN_ID, + CONSOLE_CAMPAIGN_NAME, + CONSOLE_CAMPAIGN_TIME +} from '../util/constants'; + +export interface MessagePayloadInternal { + notification?: NotificationPayloadInternal; + data?: unknown; + fcmOptions?: FcmOptionsInternal; + messageType?: MessageType; + isFirebaseMessaging?: boolean; + from: string; + // eslint-disable-next-line camelcase + collapse_key: string; +} + +export interface NotificationPayloadInternal extends NotificationOptions { + title: string; + // Supported in the Legacy Send API. + // See:https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref. + // eslint-disable-next-line camelcase + click_action?: string; +} + +// Defined in +// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions. Note +// that the keys are sent to the clients in snake cases which we need to convert to camel so it can +// be exposed as a type to match the Firebase API convention. +export interface FcmOptionsInternal { + link?: string; + + // eslint-disable-next-line camelcase + analytics_label?: string; +} + +export enum MessageType { + PUSH_RECEIVED = 'push-received', + NOTIFICATION_CLICKED = 'notification-clicked' +} + +/** Additional data of a message sent from the FN Console. */ +export interface ConsoleMessageData { + [CONSOLE_CAMPAIGN_ID]: string; + [CONSOLE_CAMPAIGN_TIME]: string; + [CONSOLE_CAMPAIGN_NAME]?: string; + [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]?: '1'; +} diff --git a/packages/messaging/src/interfaces/message-payload.ts b/packages/messaging/src/interfaces/message-payload.ts deleted file mode 100644 index a9c8422d48b..00000000000 --- a/packages/messaging/src/interfaces/message-payload.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * 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. - */ - -import { - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, - CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_TIME -} from '../util/constants'; - -export interface NotificationPayloadInternal extends NotificationOptions { - title: string; - - // Supported in the Legacy Send API. See:https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref. - // eslint-disable-next-line camelcase - click_action?: string; -} - -// Defined in https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions. Note that the keys are sent to the clients in snake cases which we need to convert to camel so it can be exposed as a type to match the Firebase API convention. -export interface FcmOptionsInternal { - link?: string; - - // eslint-disable-next-line camelcase - analytics_label?: string; -} - -// Represents a raw message payload from a push event -export interface MessagePayloadInternal { - notification?: NotificationPayloadInternal; - data?: unknown; - fcmOptions?: FcmOptionsInternal; - messageType?: MessageType; - isFirebaseMessaging?: boolean; - from: string; - // eslint-disable-next-line camelcase - collapse_key: string; -} - -export enum MessageType { - PUSH_RECEIVED = 'push-received', - NOTIFICATION_CLICKED = 'notification-clicked' -} - -// Currently supported fcm notification display parameters. Note that {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/notifications/NotificationOptions} defines a full list of display notification parameters. This interface we only include what the SEND API support for clarity. -export interface NotificationPayload { - title: string; - body: string; - image: string; -} - -export interface FcmOptions { - link?: string; - analyticsLabel?: string; -} - -export interface MessagePayload { - notification?: NotificationPayload; - data?: { [key: string]: string }; - fcmOptions?: FcmOptions; - from: string; - collapseKey: string; -} - -/** Additional data of a message sent from the FN Console. */ -export interface ConsoleMessageData { - [CONSOLE_CAMPAIGN_ID]: string; - [CONSOLE_CAMPAIGN_TIME]: string; - [CONSOLE_CAMPAIGN_NAME]?: string; - [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]?: '1'; -} diff --git a/packages/messaging/src/testing/compare-headers.test.ts b/packages/messaging/src/testing/compare-headers.test.ts index 8bd6fb81203..21fe9551874 100644 --- a/packages/messaging/src/testing/compare-headers.test.ts +++ b/packages/messaging/src/testing/compare-headers.test.ts @@ -15,8 +15,10 @@ * limitations under the License. */ -import { AssertionError, expect } from 'chai'; import '../testing/setup'; + +import { AssertionError, expect } from 'chai'; + import { compareHeaders } from './compare-headers'; describe('compareHeaders', () => { diff --git a/packages/messaging/src/testing/compare-headers.ts b/packages/messaging/src/testing/compare-headers.ts index 4d4aeb52c0c..98bb73259df 100644 --- a/packages/messaging/src/testing/compare-headers.ts +++ b/packages/messaging/src/testing/compare-headers.ts @@ -15,16 +15,16 @@ * limitations under the License. */ -import { expect } from 'chai'; import '../testing/setup'; +import { expect } from 'chai'; + // Trick TS since it's set to target ES5. declare class HeadersWithEntries extends Headers { entries?(): Iterable<[string, string]>; } -// Chai doesn't check if Headers objects contain the same entries, -// so we need to do that manually. +// Chai doesn't check if Headers objects contain the same entries, so we need to do that manually. export function compareHeaders( expectedHeaders: HeadersWithEntries, actualHeaders: HeadersWithEntries diff --git a/packages/messaging/src/testing/fakes/firebase-dependencies.ts b/packages/messaging/src/testing/fakes/firebase-dependencies.ts index cde6318d4d6..58b0a6812c1 100644 --- a/packages/messaging/src/testing/fakes/firebase-dependencies.ts +++ b/packages/messaging/src/testing/fakes/firebase-dependencies.ts @@ -15,14 +15,15 @@ * limitations under the License. */ -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; -import { FirebaseInstallations } from '@firebase/installations-types'; import { - FirebaseAnalyticsInternalName, - FirebaseAnalyticsInternal + FirebaseAnalyticsInternal, + FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; -import { Provider } from '@firebase/component'; +import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; + +import { FirebaseInstallations } from '@firebase/installations-types'; import { FirebaseInternalDependencies } from '../../interfaces/internal-dependencies'; +import { Provider } from '@firebase/component'; import { extractAppConfig } from '../../helpers/extract-app-config'; export function getFakeFirebaseDependencies( diff --git a/packages/messaging/src/testing/fakes/service-worker.ts b/packages/messaging/src/testing/fakes/service-worker.ts index 729212e616e..0e9a31ad9d0 100644 --- a/packages/messaging/src/testing/fakes/service-worker.ts +++ b/packages/messaging/src/testing/fakes/service-worker.ts @@ -20,15 +20,14 @@ import { Writable } from 'ts-essentials'; // Add fake SW types. declare const self: Window & Writable; -// When trying to stub self.clients self.registration, Sinon complains that -// these properties do not exist. This is because we're not actually running -// these tests in a service worker context. -// -// Here we're adding placeholders for Sinon to overwrite, which prevents the -// "Cannot stub non-existent own property" errors. -// -// Casting to any is needed because TS also thinks that we're in a SW -// context and considers these properties readonly. +// When trying to stub self.clients self.registration, Sinon complains that these properties do not +// exist. This is because we're not actually running these tests in a service worker context. + +// Here we're adding placeholders for Sinon to overwrite, which prevents the "Cannot stub +// non-existent own property" errors. + +// Casting to any is needed because TS also thinks that we're in a SW context and considers these +// properties readonly. // Missing function types are implemented from interfaces, so types are actually defined. /* eslint-disable @typescript-eslint/explicit-function-return-type */ @@ -166,8 +165,8 @@ export class FakePushSubscription implements PushSubscription { } /** - * Most of the fields in here are unused / deprecated. - * They are only added here to match the TS Event interface. + * Most of the fields in here are unused / deprecated. They are only added here to match the TS + * Event interface. */ export class FakeEvent implements ExtendableEvent { NONE = Event.NONE; diff --git a/packages/messaging/src/testing/fakes/token-details.ts b/packages/messaging/src/testing/fakes/token-details.ts index f40ff7ce459..73eea06b2e5 100644 --- a/packages/messaging/src/testing/fakes/token-details.ts +++ b/packages/messaging/src/testing/fakes/token-details.ts @@ -15,8 +15,8 @@ * limitations under the License. */ -import { TokenDetails } from '../../interfaces/token-details'; import { FakePushSubscription } from './service-worker'; +import { TokenDetails } from '../../interfaces/token-details'; import { arrayToBase64 } from '../../helpers/array-base64-translator'; export function getFakeTokenDetails(): TokenDetails { diff --git a/packages/messaging/src/testing/setup.ts b/packages/messaging/src/testing/setup.ts index 29546a8ad23..0d55cf43ef2 100644 --- a/packages/messaging/src/testing/setup.ts +++ b/packages/messaging/src/testing/setup.ts @@ -15,12 +15,13 @@ * limitations under the License. */ -import { use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import { restore } from 'sinon'; import * as sinonChai from 'sinon-chai'; + import { dbDelete } from '../helpers/idb-manager'; import { deleteDb } from 'idb'; +import { restore } from 'sinon'; +import { use } from 'chai'; use(chaiAsPromised); use(sinonChai); diff --git a/packages/messaging/src/testing/sinon-types.ts b/packages/messaging/src/testing/sinon-types.ts index decd2506161..13eafbac969 100644 --- a/packages/messaging/src/testing/sinon-types.ts +++ b/packages/messaging/src/testing/sinon-types.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { SinonStub, SinonSpy } from 'sinon'; +import { SinonSpy, SinonStub } from 'sinon'; // Helper types for Sinon stubs and spies. diff --git a/packages/messaging/src/util/sw-types.ts b/packages/messaging/src/util/sw-types.ts index 0c11f7ba84f..9c54a5ccc03 100644 --- a/packages/messaging/src/util/sw-types.ts +++ b/packages/messaging/src/util/sw-types.ts @@ -19,14 +19,14 @@ * Subset of Web Worker types from lib.webworker.d.ts * https://github.com/Microsoft/TypeScript/blob/master/lib/lib.webworker.d.ts * - * Since it's not possible to have both "dom" and "webworker" libs in a single - * project, we have to manually declare the web worker types we need. + * Since it's not possible to have both "dom" and "webworker" libs in a single project, we have to + * manually declare the web worker types we need. */ /* eslint-disable @typescript-eslint/no-explicit-any */ // These types are from TS -// Not the whole interface, just the parts we're currently using. -// If TS claims that something does not exist on this, feel free to add it. +// Not the whole interface, just the parts we're currently using. If TS claims that something does +// not exist on this, feel free to add it. interface ServiceWorkerGlobalScope { readonly location: WorkerLocation; readonly clients: Clients; From 436fa54172d5e3a77c88fec5655e4b857718faa1 Mon Sep 17 00:00:00 2001 From: kai Date: Thu, 9 Jul 2020 16:04:10 -0700 Subject: [PATCH 10/12] Update Changeset --- .changeset/strange-crabs-tell.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md index 10f1bb0c5e2..b7bde3c18c0 100644 --- a/.changeset/strange-crabs-tell.md +++ b/.changeset/strange-crabs-tell.md @@ -1,7 +1,10 @@ --- 'firebase': minor '@firebase/messaging': minor, +'@firebase/messaging-types': minor --- Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`. Deprecate `setBackgroundHandler`, `onTokenRefresh`, `useVapidKey`, `useServiceWorker`, `getToken`. + +Add Typing `MessagePayload`, `NotificationPayload`, `FcmOptions`. From 6921af8d1d92e66038092759b3fe397926a58220 Mon Sep 17 00:00:00 2001 From: kai Date: Fri, 10 Jul 2020 10:27:46 -0700 Subject: [PATCH 11/12] Fix wording --- packages/firebase/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index f05c5e6073d..6edb7c14151 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -7013,8 +7013,8 @@ declare namespace firebase.messaging { ): firebase.Unsubscribe; /** - * Called when a message is received while the app is in the background. An app is to be in the - * background if no active window is displayed. + * Called when a message is received while the app is in the background. An app is considered to + * be in the background if no active window is displayed. * * @param * nextOrObserver This function, or observer object with `next` defined, From 374dc78172c2ff44ba80c74843cf0d793921c39c Mon Sep 17 00:00:00 2001 From: kai Date: Tue, 11 Aug 2020 12:24:57 -0700 Subject: [PATCH 12/12] Polishing --- .changeset/strange-crabs-tell.md | 2 +- packages/installations/package.json | 2 -- packages/messaging/package.json | 2 +- .../messaging/src/controllers/sw-controller.test.ts | 2 +- packages/messaging/src/controllers/sw-controller.ts | 6 +++++- .../messaging/src/controllers/window-controller.ts | 10 ++-------- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md index b7bde3c18c0..d4b305b279c 100644 --- a/.changeset/strange-crabs-tell.md +++ b/.changeset/strange-crabs-tell.md @@ -5,6 +5,6 @@ --- Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`. -Deprecate `setBackgroundHandler`, `onTokenRefresh`, `useVapidKey`, `useServiceWorker`, `getToken`. +Deprecate `setBackgroundMessageHandler`, `onTokenRefresh`, `useVapidKey`, `useServiceWorker`, `getToken`. Add Typing `MessagePayload`, `NotificationPayload`, `FcmOptions`. diff --git a/packages/installations/package.json b/packages/installations/package.json index 35c8b851893..6b037e64a22 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -44,8 +44,6 @@ "@firebase/app-types": "0.x" }, "dependencies": { - "@firebase/app": "^0.6.7", - "@firebase/component": "0.1.15", "@firebase/installations-types": "0.3.4", "@firebase/util": "0.2.50", "@firebase/component": "0.1.16", diff --git a/packages/messaging/package.json b/packages/messaging/package.json index f5614f4d4c4..da4ad1ae7f4 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -32,7 +32,7 @@ "@firebase/util": "0.2.50", "@firebase/component": "0.1.16", "idb": "3.0.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "devDependencies": { "rollup": "2.21.0", diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts index 5692a1f9a82..cfc796e074a 100644 --- a/packages/messaging/src/controllers/sw-controller.test.ts +++ b/packages/messaging/src/controllers/sw-controller.test.ts @@ -288,7 +288,7 @@ describe('SwController', () => { const bgMessageHandlerSpy = spy(); const showNotificationSpy = spy(self.registration, 'showNotification'); - swController.setBackgroundMessageHandler(bgMessageHandlerSpy); + swController.onBackgroundMessage(bgMessageHandlerSpy); await callEventListener( makeEvent('push', { diff --git a/packages/messaging/src/controllers/sw-controller.ts b/packages/messaging/src/controllers/sw-controller.ts index a7c0fd1a6da..164ec9d80e7 100644 --- a/packages/messaging/src/controllers/sw-controller.ts +++ b/packages/messaging/src/controllers/sw-controller.ts @@ -86,6 +86,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { * must be shown. The callback will be given the data from the push message. */ setBackgroundMessageHandler(callback: BgMessageHandler): void { + this.isOnBackgroundMessageUsed = false; + if (!callback || typeof callback !== 'function') { throw ERROR_FACTORY.create(ErrorCode.INVALID_BG_HANDLER); } @@ -173,7 +175,7 @@ export class SwController implements FirebaseMessaging, FirebaseService { if (!internalPayload) { console.debug( TAG + - 'failed to get parse MessagePayload from the PushEvent. Skip handling the push.' + 'failed to get parsed MessagePayload from the PushEvent. Skip handling the push.' ); return; } @@ -191,6 +193,8 @@ export class SwController implements FirebaseMessaging, FirebaseService { isNotificationShown = true; } + // MessagePayload is only passed to `onBackgroundMessage`. Skip passing MessagePayload for + // the legacy `setBackgroundMessageHandler` to preserve the SDK behaviors. if ( isNotificationShown === true && this.isOnBackgroundMessageUsed === false diff --git a/packages/messaging/src/controllers/window-controller.ts b/packages/messaging/src/controllers/window-controller.ts index dedb6c95c30..0ac24de9fe4 100644 --- a/packages/messaging/src/controllers/window-controller.ts +++ b/packages/messaging/src/controllers/window-controller.ts @@ -120,15 +120,9 @@ export class WindowController implements FirebaseMessaging, FirebaseService { } async updateVapidKey(vapidKey?: string | undefined): Promise { - if (!!this.vapidKey && !!vapidKey && this.vapidKey !== vapidKey) { + if (!!vapidKey) { this.vapidKey = vapidKey; - } - - if (!this.vapidKey && !!vapidKey) { - this.vapidKey = vapidKey; - } - - if (!this.vapidKey && !vapidKey) { + } else if (!this.vapidKey) { this.vapidKey = DEFAULT_VAPID_KEY; } }