From 65a1bfc50f3c5f4acde7b2c8ce147abf5412c12d Mon Sep 17 00:00:00 2001 From: Alexander Friedl Date: Thu, 27 Nov 2025 23:29:44 +0100 Subject: [PATCH] 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); + }); + }); });