From 4ef59fb9adc0cc7db2afcfbfce5196ee2b5c21a9 Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Thu, 27 Nov 2025 23:29:44 +0100 Subject: [PATCH 1/4] Fix FCM custom data handling --- src/utils/fcmMessage.js | 4 ++- src/utils/tools.js | 14 +++++---- test/send/sendFCM.js | 64 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/utils/fcmMessage.js b/src/utils/fcmMessage.js index f5cfd56..58050c2 100644 --- a/src/utils/fcmMessage.js +++ b/src/utils/fcmMessage.js @@ -27,7 +27,9 @@ class FcmMessage { } static buildAndroidMessage(params, options) { - const message = buildGcmMessage(params, options); + // Mark as FCM so buildGcmMessage doesn't pollute custom data + const fcmOptions = { ...options, fcm: true }; + const message = buildGcmMessage(params, fcmOptions); const androidMessage = message.toJson(); diff --git a/src/utils/tools.js b/src/utils/tools.js index 5bb7eda..0cf391a 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -125,11 +125,15 @@ const buildGcmMessage = (data, options) => { }; } - custom.title = custom.title || data.title; - custom.message = custom.message || data.body; - custom.sound = custom.sound || data.sound; - custom.icon = custom.icon || data.icon; - custom.msgcnt = custom.msgcnt || data.badge; + // Only add notification fields to custom data for GCM (not FCM) + // FCM uses separate notification and data fields + if (!options.fcm) { + custom.title = custom.title || data.title; + custom.message = custom.message || data.body; + custom.sound = custom.sound || data.sound; + custom.icon = custom.icon || data.icon; + custom.msgcnt = custom.msgcnt || data.badge; + } if (options.phonegap === true && data.contentAvailable) { custom["content-available"] = 1; } diff --git a/test/send/sendFCM.js b/test/send/sendFCM.js index 91bb560..b117acc 100644 --- a/test/send/sendFCM.js +++ b/test/send/sendFCM.js @@ -76,4 +76,68 @@ describe("push-notifications-fcm", () => { .catch(done); }); }); + + describe('send push notifications with custom data', () => { + const customDataMessage = { + title: 'Notification Title', + body: 'Notification Body', + custom: { + userId: '12345', + actionId: 'action-001', + deepLink: 'app://section/item', + }, + }; + + let customDataSendMethod; + + function sendCustomDataMethod() { + return sinon.stub( + fbMessaging.prototype, + 'sendEachForMulticast', + function sendFCMWithCustomData(firebaseMessage) { + const { custom } = customDataMessage; + + // Verify custom data is preserved in top-level data field + expect(firebaseMessage.data).to.deep.equal(custom); + + // Verify custom data does NOT pollute the notification + // Note: normalizeDataParams converts all values to strings (FCM requirement) + expect(firebaseMessage.android.data).to.deep.equal(custom); + expect(firebaseMessage.android.data).to.not.have.property('title'); + expect(firebaseMessage.android.data).to.not.have.property('body'); + + // Verify notification has proper fields (separate from data) + expect(firebaseMessage.android.notification).to.include({ + title: customDataMessage.title, + body: customDataMessage.body, + }); + + return Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }); + } + ); + } + + before(() => { + customDataSendMethod = sendCustomDataMethod(); + }); + + after(() => { + customDataSendMethod.restore(); + }); + + it('custom data should be preserved and not mixed with notification fields', (done) => { + pn.send(regIds, customDataMessage) + .then((results) => { + expect(results).to.be.an('array'); + expect(results[0].method).to.equal('fcm'); + expect(results[0].success).to.equal(1); + done(); + }) + .catch(done); + }); + }); }); From f8bee9f0e42902293503f176b50152304faf83ad Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Tue, 2 Dec 2025 21:56:11 +0100 Subject: [PATCH 2/4] Fix FCM notification merge: allow partial fcm_notification overrides --- src/utils/tools.js | 7 ++++++- test/send/sendGCM.js | 49 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/utils/tools.js b/src/utils/tools.js index 0cf391a..68b7a71 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -86,7 +86,7 @@ const containsValidRecipients = (obj) => { }; const buildGcmNotification = (data) => { - const notification = data.fcm_notification || { + const notification = { title: data.title, body: data.body, icon: data.icon, @@ -106,6 +106,11 @@ const buildGcmNotification = (data) => { notification_count: data.notificationCount || data.badge, }; + // Merge with fcm_notification overrides if provided + if (data.fcm_notification) { + return { ...notification, ...data.fcm_notification }; + } + return notification; }; diff --git a/test/send/sendGCM.js b/test/send/sendGCM.js index 6f344ea..1c54fce 100644 --- a/test/send/sendGCM.js +++ b/test/send/sendGCM.js @@ -788,7 +788,54 @@ describe("push-notifications-gcm", () => { }); }); - describe("send push notifications in phonegap-push compatibility mode", () => { + describe('fcm_notification partial override (merge)', () => { + before(() => { + sendMethod = sinon.stub( + gcm.Sender.prototype, + 'send', + (message, recipients, retries, cb) => { + expect(recipients).to.be.instanceOf(Object); + expect(message).to.be.instanceOf(gcm.Message); + // Verify that title and body from main data are preserved + expect(message.params.notification.title).to.equal(data.title); + expect(message.params.notification.body).to.equal(data.body); + // Verify that fcm_notification overrides are applied + expect(message.params.notification.color).to.equal('#FF0000'); + // Verify custom data is present + expect(message.params.data.sender).to.equal(data.custom.sender); + + cb(null, { + multicast_id: 'abc', + success: recipients.registrationTokens.length, + failure: 0, + results: recipients.registrationTokens.map((token) => ({ + message_id: '', + registration_id: token, + error: null, + })), + }); + } + ); + }); + + after(() => { + sendMethod.restore(); + }); + + it('should merge fcm_notification overrides with base notification', (done) => { + const androidData = { + ...data, + fcm_notification: { + color: '#FF0000', // Override only the color + }, + }; + pn.send(regIds, androidData, (err, results) => + testSuccess(err, results, done) + ); + }); + }); + + describe('send push notifications in phonegap-push compatibility mode', () => { const pushPhoneGap = new PN({ gcm: { phonegap: true, From c59122dcfe326fff3496b4a7b74482319e500b76 Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Tue, 2 Dec 2025 21:59:04 +0100 Subject: [PATCH 3/4] Update README --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10c8e58..fbf935d 100644 --- a/README.md +++ b/README.md @@ -578,13 +578,27 @@ pushNotifications.send(tokens, notifications, (error, result) => { }); ``` -`fcm_notification` - object that will be passed to +`fcm_notification` - object that will be **merged** with the notification fields. This allows you to override specific notification properties (like `channelId`, `ttl`, etc.) without duplicating standard fields like `title` and `body`. +For example, to set a channel ID for Android: ```js - new gcm.Message({ ..., notification: data.fcm_notification }) +const data = { + title: 'My Title', + body: 'My Message', + fcm_notification: { + channelId: 'my-channel-id' + }, + custom: { id: 123 } +}; +``` + +The `fcm_notification` object will be passed to + +```js + new gcm.Message({ ..., notification: { ...builtNotification, ...data.fcm_notification } }) ``` -Fcm object that will be sent to provider ([Fcm message format](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Message)) : +FCM object that will be sent to provider ([FCM message format](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Message)) : ```json { From b04de1a681ff7c88df6ae39a06356e76837572d6 Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Sat, 6 Dec 2025 22:04:21 +0100 Subject: [PATCH 4/4] Fix linter and formatting after rebase --- README.md | 9 ++++--- test/send/sendFCM.js | 24 +++++++++--------- test/send/sendGCM.js | 60 ++++++++++++++++++++------------------------ 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index fbf935d..0408443 100644 --- a/README.md +++ b/README.md @@ -581,14 +581,15 @@ pushNotifications.send(tokens, notifications, (error, result) => { `fcm_notification` - object that will be **merged** with the notification fields. This allows you to override specific notification properties (like `channelId`, `ttl`, etc.) without duplicating standard fields like `title` and `body`. For example, to set a channel ID for Android: + ```js const data = { - title: 'My Title', - body: 'My Message', + title: "My Title", + body: "My Message", fcm_notification: { - channelId: 'my-channel-id' + channelId: "my-channel-id", }, - custom: { id: 123 } + custom: { id: 123 }, }; ``` diff --git a/test/send/sendFCM.js b/test/send/sendFCM.js index b117acc..d317e0b 100644 --- a/test/send/sendFCM.js +++ b/test/send/sendFCM.js @@ -77,14 +77,14 @@ describe("push-notifications-fcm", () => { }); }); - describe('send push notifications with custom data', () => { + describe("send push notifications with custom data", () => { const customDataMessage = { - title: 'Notification Title', - body: 'Notification Body', + title: "Notification Title", + body: "Notification Body", custom: { - userId: '12345', - actionId: 'action-001', - deepLink: 'app://section/item', + userId: "12345", + actionId: "action-001", + deepLink: "app://section/item", }, }; @@ -93,7 +93,7 @@ describe("push-notifications-fcm", () => { function sendCustomDataMethod() { return sinon.stub( fbMessaging.prototype, - 'sendEachForMulticast', + "sendEachForMulticast", function sendFCMWithCustomData(firebaseMessage) { const { custom } = customDataMessage; @@ -103,8 +103,8 @@ describe("push-notifications-fcm", () => { // Verify custom data does NOT pollute the notification // Note: normalizeDataParams converts all values to strings (FCM requirement) expect(firebaseMessage.android.data).to.deep.equal(custom); - expect(firebaseMessage.android.data).to.not.have.property('title'); - expect(firebaseMessage.android.data).to.not.have.property('body'); + expect(firebaseMessage.android.data).to.not.have.property("title"); + expect(firebaseMessage.android.data).to.not.have.property("body"); // Verify notification has proper fields (separate from data) expect(firebaseMessage.android.notification).to.include({ @@ -129,11 +129,11 @@ describe("push-notifications-fcm", () => { customDataSendMethod.restore(); }); - it('custom data should be preserved and not mixed with notification fields', (done) => { + it("custom data should be preserved and not mixed with notification fields", (done) => { pn.send(regIds, customDataMessage) .then((results) => { - expect(results).to.be.an('array'); - expect(results[0].method).to.equal('fcm'); + expect(results).to.be.an("array"); + expect(results[0].method).to.equal("fcm"); expect(results[0].success).to.equal(1); done(); }) diff --git a/test/send/sendGCM.js b/test/send/sendGCM.js index 1c54fce..1f3b2cc 100644 --- a/test/send/sendGCM.js +++ b/test/send/sendGCM.js @@ -788,54 +788,48 @@ describe("push-notifications-gcm", () => { }); }); - describe('fcm_notification partial override (merge)', () => { + describe("fcm_notification partial override (merge)", () => { before(() => { - sendMethod = sinon.stub( - gcm.Sender.prototype, - 'send', - (message, recipients, retries, cb) => { - expect(recipients).to.be.instanceOf(Object); - expect(message).to.be.instanceOf(gcm.Message); - // Verify that title and body from main data are preserved - expect(message.params.notification.title).to.equal(data.title); - expect(message.params.notification.body).to.equal(data.body); - // Verify that fcm_notification overrides are applied - expect(message.params.notification.color).to.equal('#FF0000'); - // Verify custom data is present - expect(message.params.data.sender).to.equal(data.custom.sender); - - cb(null, { - multicast_id: 'abc', - success: recipients.registrationTokens.length, - failure: 0, - results: recipients.registrationTokens.map((token) => ({ - message_id: '', - registration_id: token, - error: null, - })), - }); - } - ); + sendMethod = sinon.stub(gcm.Sender.prototype, "send", (message, recipients, retries, cb) => { + expect(recipients).to.be.instanceOf(Object); + expect(message).to.be.instanceOf(gcm.Message); + // Verify that title and body from main data are preserved + expect(message.params.notification.title).to.equal(data.title); + expect(message.params.notification.body).to.equal(data.body); + // Verify that fcm_notification overrides are applied + expect(message.params.notification.color).to.equal("#FF0000"); + // Verify custom data is present + expect(message.params.data.sender).to.equal(data.custom.sender); + + cb(null, { + multicast_id: "abc", + success: recipients.registrationTokens.length, + failure: 0, + results: recipients.registrationTokens.map((token) => ({ + message_id: "", + registration_id: token, + error: null, + })), + }); + }); }); after(() => { sendMethod.restore(); }); - it('should merge fcm_notification overrides with base notification', (done) => { + it("should merge fcm_notification overrides with base notification", (done) => { const androidData = { ...data, fcm_notification: { - color: '#FF0000', // Override only the color + color: "#FF0000", // Override only the color }, }; - pn.send(regIds, androidData, (err, results) => - testSuccess(err, results, done) - ); + pn.send(regIds, androidData, (err, results) => testSuccess(err, results, done)); }); }); - describe('send push notifications in phonegap-push compatibility mode', () => { + describe("send push notifications in phonegap-push compatibility mode", () => { const pushPhoneGap = new PN({ gcm: { phonegap: true,