From bd45b35543ab6585e8187ec9dadd07f6a1c26149 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sat, 28 Dec 2024 22:07:49 -0800 Subject: [PATCH 1/5] feat: Update APN notification topic update based on pushType --- spec/APNS.spec.js | 106 ++++++++++++++++++++++++++++++++++++++++++++++ src/APNS.js | 54 ++++++++++++++++------- 2 files changed, 145 insertions(+), 15 deletions(-) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 70b3d541..d12da43e 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -322,6 +322,112 @@ describe('APNS', () => { done(); }); + it('generating notification prioritizes header information from notification data', async () => { + const data = { + 'alert': 'alert', + 'title': 'title', + 'badge': 100, + 'sound': 'test', + 'content-available': 1, + 'mutable-content': 1, + 'category': 'INVITE_CATEGORY', + 'threadId': 'a-thread-id', + 'key': 'value', + 'keyAgain': 'valueAgain', + 'topic': 'bundle', + 'expiry': 20, + 'collapseId': 'collapse', + 'pushType': 'alert', + 'priority': 7, + }; + const topic = 'bundleId'; + const expirationTime = 1454571491354; + const collapseId = "collapseIdentifier"; + const pushType = "background"; + const priority = 5; + + const notification = APNS._generateNotification(data, { topic: topic, expirationTime: expirationTime, collapseId: collapseId, pushType: pushType, priority: priority }); + expect(notification.topic).toEqual(data.topic); + expect(notification.expiry).toEqual(data.expiry); + expect(notification.collapseId).toEqual(data.collapseId); + expect(notification.pushType).toEqual(data.pushType); + expect(notification.priority).toEqual(data.priority); + }); + + it('generating notification does not override default notification info when header info is missing', async () => { + const data = { + 'alert': 'alert', + 'title': 'title', + 'badge': 100, + 'sound': 'test', + 'content-available': 1, + 'mutable-content': 1, + 'category': 'INVITE_CATEGORY', + 'threadId': 'a-thread-id', + 'key': 'value', + 'keyAgain': 'valueAgain', + }; + const topic = 'bundleId'; + const collapseId = "collapseIdentifier"; + const pushType = "background"; + + const notification = APNS._generateNotification(data, { topic: topic, collapseId: collapseId, pushType: pushType }); + expect(notification.topic).toEqual(topic); + expect(notification.expiry).toEqual(-1); + expect(notification.collapseId).toEqual(collapseId); + expect(notification.pushType).toEqual(pushType); + expect(notification.priority).toEqual(10); + }); + + it('defaults to original topic', async () => { + const topic = 'bundleId'; + const pushType = 'alert'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic); + }); + + it('updates topic based on location pushType', async () => { + const topic = 'bundleId'; + const pushType = 'location'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.location-query'); + }); + + it('updates topic based on voip pushType', async () => { + const topic = 'bundleId'; + const pushType = 'voip'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.voip'); + }); + + it('updates topic based on complication pushType', async () => { + const topic = 'bundleId'; + const pushType = 'complication'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.complication'); + }); + + it('updates topic based on complication pushType', async () => { + const topic = 'bundleId'; + const pushType = 'fileprovider'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.pushkit.fileprovider'); + }); + + it('updates topic based on liveactivity pushType', async () => { + const topic = 'bundleId'; + const pushType = 'liveactivity'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.push-type.liveactivity'); + }); + + it('updates topic based on pushtotalk pushType', async () => { + const topic = 'bundleId'; + const pushType = 'pushtotalk'; + const updatedTopic = APNS._determineTopic(topic, pushType); + expect(updatedTopic).toEqual(topic+'.voip-ptt'); + }); + it('can choose providers for device with valid appIdentifier', (done) => { const appIdentifier = 'topic'; // Mock providers diff --git a/src/APNS.js b/src/APNS.js index b68f2f66..faa1f683 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -151,7 +151,7 @@ export class APNS { static _createProvider(apnsArgs) { // if using certificate, then topic must be defined if (!APNS._validateAPNArgs(apnsArgs)) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs); + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is missing for %j', apnsArgs); } const provider = new apn.Provider(apnsArgs); @@ -221,21 +221,49 @@ export class APNS { notification.payload = payload; - notification.topic = headers.topic; - notification.expiry = Math.round(headers.expirationTime / 1000); - notification.collapseId = headers.collapseId; // set alert as default push type. If push type is not set notifications are not delivered to devices running iOS 13, watchOS 6 and later. - notification.pushType = 'alert'; - if (headers.pushType) { - notification.pushType = headers.pushType; - } - if (headers.priority) { - // if headers priority is not set 'node-apn' defaults it to 5 which is min. required value for background pushes to launch the app in background. - notification.priority = headers.priority + const pushType = payload.pushType ?? headers.pushType ?? 'alert'; + notification.pushType = pushType; + const topic = payload.topic ?? APNS._determineTopic(headers.topic, pushType); + notification.topic = topic; + let defaultExpiry = notification.expiry; + if (headers.expirationTime) { + defaultExpiry = Math.round(headers.expirationTime / 1000); } + notification.expiry = payload.expiry ?? defaultExpiry; + notification.collapseId = payload.collapseId ?? headers.collapseId; + // if headers priority is not set 'node-apn' defaults it to notification's default value. Required value for background pushes to launch the app in background. + notification.priority = payload.priority ?? headers.priority ?? notification.priority; + return notification; } + /** + * Updates the topic based on the pushType. + * + * @param {String} topic The current topic to append additional information to for required provider + * @param {any} pushType The current push type of the notification + * @returns {String} Returns the updated topic + */ + static _determineTopic(topic, pushType) { + switch(pushType) { + case 'location': + return topic+'.location-query'; + case 'voip': + return topic+'.voip'; + case 'complication': + return topic+'.complication'; + case 'fileprovider': + return topic+'.pushkit.fileprovider'; + case 'liveactivity': + return topic+'.push-type.liveactivity'; + case 'pushtotalk': + return topic+'.voip-ptt'; + default: + return topic + } + } + /** * Choose appropriate providers based on device appIdentifier. * @@ -243,10 +271,6 @@ export class APNS { * @returns {Array} Returns Array with appropriate providers */ _chooseProviders(appIdentifier) { - // If the device we need to send to does not have appIdentifier, any provider could be a qualified provider - /*if (!appIdentifier || appIdentifier === '') { - return this.providers.map((provider) => provider.index); - }*/ // Otherwise we try to match the appIdentifier with topic on provider const qualifiedProviders = this.providers.filter((provider) => appIdentifier === provider.topic); From 1f8bebd0f46a8a262e46c03f418e5f6799750c47 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sat, 28 Dec 2024 22:40:45 -0800 Subject: [PATCH 2/5] remove unnecessary obj properties when testing --- spec/APNS.spec.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index d12da43e..86bda267 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -324,16 +324,6 @@ describe('APNS', () => { it('generating notification prioritizes header information from notification data', async () => { const data = { - 'alert': 'alert', - 'title': 'title', - 'badge': 100, - 'sound': 'test', - 'content-available': 1, - 'mutable-content': 1, - 'category': 'INVITE_CATEGORY', - 'threadId': 'a-thread-id', - 'key': 'value', - 'keyAgain': 'valueAgain', 'topic': 'bundle', 'expiry': 20, 'collapseId': 'collapse', @@ -355,18 +345,7 @@ describe('APNS', () => { }); it('generating notification does not override default notification info when header info is missing', async () => { - const data = { - 'alert': 'alert', - 'title': 'title', - 'badge': 100, - 'sound': 'test', - 'content-available': 1, - 'mutable-content': 1, - 'category': 'INVITE_CATEGORY', - 'threadId': 'a-thread-id', - 'key': 'value', - 'keyAgain': 'valueAgain', - }; + const data = {}; const topic = 'bundleId'; const collapseId = "collapseIdentifier"; const pushType = "background"; From 974809d57257953b5ff3789bf7b82e27e7009244 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 29 Dec 2024 08:45:10 -0800 Subject: [PATCH 3/5] never add header info to payload --- spec/APNS.spec.js | 12 +++++++++++- src/APNS.js | 28 +++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 86bda267..8c563842 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -324,24 +324,34 @@ describe('APNS', () => { it('generating notification prioritizes header information from notification data', async () => { const data = { + 'id': 'hello', + 'requestId': 'world', + 'channelId': 'foo', 'topic': 'bundle', 'expiry': 20, 'collapseId': 'collapse', 'pushType': 'alert', 'priority': 7, }; + const id = 'foo'; + const requestId = 'hello'; + const channelId = 'world'; const topic = 'bundleId'; const expirationTime = 1454571491354; const collapseId = "collapseIdentifier"; const pushType = "background"; const priority = 5; - const notification = APNS._generateNotification(data, { topic: topic, expirationTime: expirationTime, collapseId: collapseId, pushType: pushType, priority: priority }); + const notification = APNS._generateNotification(data, { id: id, requestId: requestId, channelId: channelId, topic: topic, expirationTime: expirationTime, collapseId: collapseId, pushType: pushType, priority: priority }); + expect(notification.id).toEqual(data.id); + expect(notification.requestId).toEqual(data.requestId); + expect(notification.channelId).toEqual(data.channelId); expect(notification.topic).toEqual(data.topic); expect(notification.expiry).toEqual(data.expiry); expect(notification.collapseId).toEqual(data.collapseId); expect(notification.pushType).toEqual(data.pushType); expect(notification.priority).toEqual(data.priority); + expect(Object.keys(notification.payload).length).toBe(0); }); it('generating notification does not override default notification info when header info is missing', async () => { diff --git a/src/APNS.js b/src/APNS.js index faa1f683..e54ad331 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -213,6 +213,16 @@ export class APNS { case 'threadId': notification.setThreadId(coreData.threadId); break; + case 'id': + case 'collapseId': + case 'channelId': + case 'requestId': + case 'pushType': + case 'topic': + case 'expiry': + case 'priority': + // Header information is skipped and added later. + break; default: payload[key] = coreData[key]; break; @@ -221,19 +231,23 @@ export class APNS { notification.payload = payload; + // Update header information if necessary. + notification.id = coreData.id ?? headers.id; + notification.collapseId = coreData.collapseId ?? headers.collapseId; + notification.requestId = coreData.requestId ?? headers.requestId; + notification.channelId = coreData.channelId ?? headers.channelId; // set alert as default push type. If push type is not set notifications are not delivered to devices running iOS 13, watchOS 6 and later. - const pushType = payload.pushType ?? headers.pushType ?? 'alert'; + const pushType = coreData.pushType ?? headers.pushType ?? 'alert'; notification.pushType = pushType; - const topic = payload.topic ?? APNS._determineTopic(headers.topic, pushType); + const topic = coreData.topic ?? APNS._determineTopic(headers.topic, pushType); notification.topic = topic; - let defaultExpiry = notification.expiry; + let expiry = notification.expiry; if (headers.expirationTime) { - defaultExpiry = Math.round(headers.expirationTime / 1000); + expiry = Math.round(headers.expirationTime / 1000); } - notification.expiry = payload.expiry ?? defaultExpiry; - notification.collapseId = payload.collapseId ?? headers.collapseId; + notification.expiry = coreData.expiry ?? expiry; // if headers priority is not set 'node-apn' defaults it to notification's default value. Required value for background pushes to launch the app in background. - notification.priority = payload.priority ?? headers.priority ?? notification.priority; + notification.priority = coreData.priority ?? headers.priority ?? notification.priority; return notification; } From 9bea00cf54e940dd49f92819135465ffada3d763 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 29 Dec 2024 08:53:00 -0800 Subject: [PATCH 4/5] lint --- spec/APNS.spec.js | 12 ++++++------ src/APNS.js | 38 +++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 8c563842..db08c986 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -379,42 +379,42 @@ describe('APNS', () => { const topic = 'bundleId'; const pushType = 'location'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.location-query'); + expect(updatedTopic).toEqual(topic + '.location-query'); }); it('updates topic based on voip pushType', async () => { const topic = 'bundleId'; const pushType = 'voip'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.voip'); + expect(updatedTopic).toEqual(topic + '.voip'); }); it('updates topic based on complication pushType', async () => { const topic = 'bundleId'; const pushType = 'complication'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.complication'); + expect(updatedTopic).toEqual(topic + '.complication'); }); it('updates topic based on complication pushType', async () => { const topic = 'bundleId'; const pushType = 'fileprovider'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.pushkit.fileprovider'); + expect(updatedTopic).toEqual(topic + '.pushkit.fileprovider'); }); it('updates topic based on liveactivity pushType', async () => { const topic = 'bundleId'; const pushType = 'liveactivity'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.push-type.liveactivity'); + expect(updatedTopic).toEqual(topic + '.push-type.liveactivity'); }); it('updates topic based on pushtotalk pushType', async () => { const topic = 'bundleId'; const pushType = 'pushtotalk'; const updatedTopic = APNS._determineTopic(topic, pushType); - expect(updatedTopic).toEqual(topic+'.voip-ptt'); + expect(updatedTopic).toEqual(topic + '.voip-ptt'); }); it('can choose providers for device with valid appIdentifier', (done) => { diff --git a/src/APNS.js b/src/APNS.js index e54ad331..62ad5c99 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -214,12 +214,12 @@ export class APNS { notification.setThreadId(coreData.threadId); break; case 'id': - case 'collapseId': - case 'channelId': - case 'requestId': + case 'collapseId': + case 'channelId': + case 'requestId': case 'pushType': - case 'topic': - case 'expiry': + case 'topic': + case 'expiry': case 'priority': // Header information is skipped and added later. break; @@ -261,20 +261,20 @@ export class APNS { */ static _determineTopic(topic, pushType) { switch(pushType) { - case 'location': - return topic+'.location-query'; - case 'voip': - return topic+'.voip'; - case 'complication': - return topic+'.complication'; - case 'fileprovider': - return topic+'.pushkit.fileprovider'; - case 'liveactivity': - return topic+'.push-type.liveactivity'; - case 'pushtotalk': - return topic+'.voip-ptt'; - default: - return topic + case 'location': + return topic + '.location-query'; + case 'voip': + return topic + '.voip'; + case 'complication': + return topic + '.complication'; + case 'fileprovider': + return topic + '.pushkit.fileprovider'; + case 'liveactivity': + return topic + '.push-type.liveactivity'; + case 'pushtotalk': + return topic + '.voip-ptt'; + default: + return topic; } } From dfa241bea4392afa69cf7b072273a9edf50c4e27 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 29 Dec 2024 09:38:01 -0800 Subject: [PATCH 5/5] add test to ensure generate method uses right topic --- spec/APNS.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index db08c986..781f8592 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -368,6 +368,16 @@ describe('APNS', () => { expect(notification.priority).toEqual(10); }); + it('generating notification updates topic properly', async () => { + const data = {}; + const topic = 'bundleId'; + const pushType = "liveactivity"; + + const notification = APNS._generateNotification(data, { topic: topic, pushType: pushType }); + expect(notification.topic).toEqual(topic + '.push-type.liveactivity'); + expect(notification.pushType).toEqual(pushType); + }); + it('defaults to original topic', async () => { const topic = 'bundleId'; const pushType = 'alert';