diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 70b3d54..781f859 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -322,6 +322,111 @@ describe('APNS', () => { done(); }); + 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, { 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 () => { + const data = {}; + 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('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'; + 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 b68f2f6..62ad5c9 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); @@ -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,21 +231,53 @@ export class APNS { notification.payload = payload; - notification.topic = headers.topic; - notification.expiry = Math.round(headers.expirationTime / 1000); - notification.collapseId = headers.collapseId; + // 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. - 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 = coreData.pushType ?? headers.pushType ?? 'alert'; + notification.pushType = pushType; + const topic = coreData.topic ?? APNS._determineTopic(headers.topic, pushType); + notification.topic = topic; + let expiry = notification.expiry; + if (headers.expirationTime) { + expiry = Math.round(headers.expirationTime / 1000); } + 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 = coreData.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 +285,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);