From 65a1bfc50f3c5f4acde7b2c8ce147abf5412c12d Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Thu, 27 Nov 2025 23:29:44 +0100 Subject: [PATCH 1/3] 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 e9907f7..0141f9e 100644 --- a/src/utils/fcmMessage.js +++ b/src/utils/fcmMessage.js @@ -28,7 +28,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 b5a06a8..4b9259c 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -126,11 +126,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 35ae46b..d61f1a9 100644 --- a/test/send/sendFCM.js +++ b/test/send/sendFCM.js @@ -79,4 +79,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 a356291df9224d848fec111dce94c5bd315a9f7e Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Tue, 2 Dec 2025 21:56:11 +0100 Subject: [PATCH 2/3] Fix FCM notification merge: allow partial fcm_notification overrides --- src/utils/tools.js | 7 ++++++- test/send/sendGCM.js | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/utils/tools.js b/src/utils/tools.js index 4b9259c..f493b11 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -87,7 +87,7 @@ const containsValidRecipients = (obj) => { }; const buildGcmNotification = (data) => { - const notification = data.fcm_notification || { + const notification = { title: data.title, body: data.body, icon: data.icon, @@ -107,6 +107,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 da732a6..66b874e 100644 --- a/test/send/sendGCM.js +++ b/test/send/sendGCM.js @@ -910,6 +910,53 @@ describe('push-notifications-gcm', () => { }); }); + 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: { From 6d1fb4a9eeb925c6d4bc358c28f44774e917d633 Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Tue, 2 Dec 2025 21:59:04 +0100 Subject: [PATCH 3/3] Update README --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 45da859..5f46d1e 100644 --- a/README.md +++ b/README.md @@ -580,13 +580,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 {