Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: APN notification topic not composed based on push type #347

Merged
merged 6 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions spec/APNS.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
mtrezza marked this conversation as resolved.
Show resolved Hide resolved
});

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
Expand Down
68 changes: 53 additions & 15 deletions src/APNS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
mtrezza marked this conversation as resolved.
Show resolved Hide resolved
}

const provider = new apn.Provider(apnsArgs);
Expand Down Expand Up @@ -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;
Expand All @@ -221,32 +231,60 @@ 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.
*
* @param {String} appIdentifier appIdentifier for required provider
* @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 === '') {
mtrezza marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down
Loading