diff --git a/.changeset/strange-crabs-tell.md b/.changeset/strange-crabs-tell.md
new file mode 100644
index 00000000000..d4b305b279c
--- /dev/null
+++ b/.changeset/strange-crabs-tell.md
@@ -0,0 +1,10 @@
+---
+'firebase': minor
+'@firebase/messaging': minor,
+'@firebase/messaging-types': minor
+---
+
+Add `getToken(options:{serviceWorkerRegistration, vapidKey})`,`onBackgroundMessage`.
+Deprecate `setBackgroundMessageHandler`, `onTokenRefresh`, `useVapidKey`, `useServiceWorker`, `getToken`.
+
+Add Typing `MessagePayload`, `NotificationPayload`, `FcmOptions`.
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
new file mode 100644
index 00000000000..7712a636bea
--- /dev/null
+++ b/integration/messaging/test/static/helpers.js
@@ -0,0 +1,52 @@
+/**
+ * @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/packages/messaging/src/interfaces/internal-message.ts b/integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js
similarity index 53%
rename from packages/messaging/src/interfaces/internal-message.ts
rename to integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js
index 41f0299664a..459e83e948a 100644
--- a/packages/messaging/src/interfaces/internal-message.ts
+++ b/integration/messaging/test/static/valid-vapid-key-modern-sw/sw.js
@@ -15,16 +15,23 @@
* limitations under the License.
*/
-import { MessagePayload } from './message-payload';
+importScripts('../constants.js');
+importScripts('../helpers.js');
-export enum MessageType {
- PUSH_RECEIVED = 'push-received',
- NOTIFICATION_CLICKED = 'notification-clicked'
-}
+// HEAD targets served through express
+importScripts('/firebase-app.js');
+importScripts('/firebase-messaging.js');
-export interface InternalMessage {
- firebaseMessaging: {
- type: MessageType;
- payload: MessagePayload;
- };
-}
+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-deleteToken.js b/integration/messaging/test/test-deleteToken.js
index 65dcab7e251..cb6aa341088 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 9c196e0825e..2f15519b807 100644
--- a/integration/messaging/test/test-send.js
+++ b/integration/messaging/test/test-send.js
@@ -25,20 +25,25 @@ const getReceivedForegroundMessages = require('./utils/getReceivedForegroundMess
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';
-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,75 +60,82 @@ 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;
}
- 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);
+ 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,
+ /* isLegacyPayload= */ false
+ );
+ });
+
+ 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()
+ );
+ });
- 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()
- );
});
it('Foreground app can receive a {} empty message in onMessage', async function () {
@@ -135,9 +147,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(
@@ -162,9 +172,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({
@@ -189,9 +197,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({
@@ -216,9 +222,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({
@@ -248,7 +252,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(
@@ -280,16 +287,17 @@ 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
+ // 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 a18311f8d28..2de606be2a9 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 83488fa28b3..8d5d4db66fa 100644
--- a/packages/firebase/index.d.ts
+++ b/packages/firebase/index.d.ts
@@ -6941,106 +6941,236 @@ 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 {
/**
- * To forcibly stop a registration token from being used, delete it
- * by calling this method.
+ * 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.
*
* @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 user to push notifications and 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
+ * authenticate the push subscribers to receive push messages only from sending servers that
+ * 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}
+ * for details on web push services.}
+ *
+ * @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 an FCM registration token.
*
- * @return The promise resolves with the FCM token string.
*/
- getToken(): Promise;
+ getToken(options?: {
+ vapidKey?: string;
+ serviceWorkerRegistration?: ServiceWorkerRegistration;
+ }): 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.
- *
- * NOTE: These events are dispatched when you have called
- * `setBackgroundMessageHandler()` in your service worker.
+ * 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,
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 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
+ */
+ 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()`
- * 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.
*/
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
- * 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 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
- * 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.
+ * @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 {
+ /**
+ * See {@link firebase.Messaging.NotificationPayload}.
+ */
+ notification?: NotificationPayload;
+
+ /**
+ * 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 event
+ * will put your app in focus for the user.
+ */
+ link?: string;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+
function isSupported(): boolean;
}
diff --git a/packages/messaging-types/index.d.ts b/packages/messaging-types/index.d.ts
index 4c056594553..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,
@@ -24,16 +23,51 @@ import {
CompleteFn
} from '@firebase/util';
+// 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 {
- // 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;
onTokenRefresh(
nextOrObserver: NextFn | Observer,
error?: ErrorFn,
diff --git a/packages/messaging/src/controllers/sw-controller.test.ts b/packages/messaging/src/controllers/sw-controller.test.ts
index 45a90c22aab..cfc796e074a 100644
--- a/packages/messaging/src/controllers/sw-controller.test.ts
+++ b/packages/messaging/src/controllers/sw-controller.test.ts
@@ -14,55 +14,67 @@
* See the License for the specific language governing permissions and
* 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 { BgMessageHandler, SwController } from './sw-controller';
import {
- FCM_MSG,
- DEFAULT_VAPID_KEY,
+ CONSOLE_CAMPAIGN_ANALYTICS_ENABLED,
CONSOLE_CAMPAIGN_ID,
CONSOLE_CAMPAIGN_NAME,
CONSOLE_CAMPAIGN_TIME,
- CONSOLE_CAMPAIGN_ANALYTICS_ENABLED
+ 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 {
+ MessagePayloadInternal,
+ MessageType
+} 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';
+import { expect } from 'chai';
+import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies';
import { getFakeTokenDetails } from '../testing/fakes/token-details';
-import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies';
// 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'
- }
+ },
+ from: 'from',
+ // eslint-disable-next-line camelcase
+ collapse_key: 'collapse'
};
-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'
- }
+ },
+ from: 'from',
+ // eslint-disable-next-line camelcase
+ collapse_key: 'collapse'
};
describe('SwController', () => {
@@ -78,10 +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);
@@ -204,16 +215,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);
});
@@ -226,17 +235,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
}
});
});
@@ -247,16 +255,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
}
});
});
@@ -268,7 +276,7 @@ describe('SwController', () => {
await callEventListener(
makeEvent('push', {
data: {
- json: () => DATA_MESSAGE_PAYLOAD
+ json: () => DATA_MESSAGE
}
})
);
@@ -276,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.onBackgroundMessage(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
@@ -291,7 +324,7 @@ describe('SwController', () => {
data: {
json: () => ({
notification: {
- ...NOTIFICATION_MESSAGE_PAYLOAD,
+ ...DISPLAY_MESSAGE,
actions: [
{ action: 'like', title: 'Like' },
{ action: 'favorite', title: 'Favorite' }
@@ -308,7 +341,7 @@ describe('SwController', () => {
});
});
- describe('setBackgrounMessageHandler', () => {
+ describe('setBackgroundMessageHandler', () => {
it('throws on invalid input', () => {
expect(() =>
swController.setBackgroundMessageHandler(
@@ -350,11 +383,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
}
})
};
@@ -437,10 +470,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 ff719e199c4..164ec9d80e7 100644
--- a/packages/messaging/src/controllers/sw-controller.ts
+++ b/packages/messaging/src/controllers/sw-controller.ts
@@ -15,22 +15,24 @@
* 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, TAG } from '../util/constants';
import { ERROR_FACTORY, ErrorCode } from '../util/errors';
+import { FirebaseMessaging, MessagePayload } from '@firebase/messaging-types';
import {
- MessagePayload,
- NotificationDetails
-} 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';
+ MessagePayloadInternal,
+ MessageType,
+ NotificationPayloadInternal
+} 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 { isConsoleMessage } from '../helpers/is-console-message';
+import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies';
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';
// Let TS know that this is a service worker
declare const self: ServiceWorkerGlobalScope;
@@ -38,8 +40,17 @@ 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 | null = null;
+ private bgMessageHandler:
+ | BgMessageHandler
+ | null
+ | NextFn
+ | Observer = null;
constructor(
private readonly firebaseDependencies: FirebaseInternalDependencies
@@ -60,21 +71,23 @@ export class SwController implements FirebaseMessaging, FirebaseService {
}
/**
- * Calling setBackgroundMessageHandler will opt in to some specific
- * behaviours.
- * 1.) If a notification doesn't need to be shown due to a window already
- * being visible, then push messages will be sent to the page.
- * 2.) If a notification needs to be shown, and the message contains no
- * notification data this method will be called
- * and the promise it returns will be passed to event.waitUntil.
- * If you do not set this callback then all push messages will let and the
- * developer can handle them in a their own 'push' event callback
+ * @deprecated. Use onBackgroundMessage(nextOrObserver: NextFn