From b5df67db9a9ab686661c0daea2f78855339b0a22 Mon Sep 17 00:00:00 2001 From: Roger Hu Date: Sun, 28 Feb 2016 13:49:20 -0800 Subject: [PATCH 1/5] Refactoring GCM. Fix GCM breakage --- spec/GCM.spec.js | 2 +- spec/ParsePushAdapter.spec.js | 2 +- src/Adapters/Push/ParsePushAdapter.js | 2 +- src/GCM.js | 15 ++++++++++----- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/spec/GCM.spec.js b/spec/GCM.spec.js index ceb1536820..23a2e87c98 100644 --- a/spec/GCM.spec.js +++ b/spec/GCM.spec.js @@ -1,4 +1,4 @@ -var GCM = require('../src/GCM'); +var GCM = require('../src/GCM').GCM; describe('GCM', () => { it('can initialize', (done) => { diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index e21a9dbb21..57c481b59c 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -1,6 +1,6 @@ var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter'); var APNS = require('../src/APNS'); -var GCM = require('../src/GCM'); +var GCM = require('../src/GCM').GCM; describe('ParsePushAdapter', () => { it('can be initialized', (done) => { diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js index 72cd57ed1b..f1784296da 100644 --- a/src/Adapters/Push/ParsePushAdapter.js +++ b/src/Adapters/Push/ParsePushAdapter.js @@ -4,7 +4,7 @@ // for ios push. const Parse = require('parse/node').Parse; -const GCM = require('../../GCM'); +const GCM = require('../../GCM').GCM; const APNS = require('../../APNS'); import PushAdapter from './PushAdapter'; import { classifyInstallations } from './PushAdapterUtils'; diff --git a/src/GCM.js b/src/GCM.js index e3df597976..c279d581c0 100644 --- a/src/GCM.js +++ b/src/GCM.js @@ -7,7 +7,7 @@ const cryptoUtils = require('./cryptoUtils'); const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks const GCMRegistrationTokensMax = 1000; -function GCM(args) { +export function GCM(args) { if (typeof args !== 'object' || !args.apiKey) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'GCM Configuration is invalid'); @@ -52,7 +52,8 @@ GCM.prototype.send = function(data, devices) { expirationTime = data['expiration_time']; } // Generate gcm payload - let gcmPayload = generateGCMPayload(data.data, timestamp, expirationTime); + let gcmPayload = generateGCMPayload(data.data, null, data.expirationTime); + // Make and send gcm request let message = new gcm.Message(gcmPayload); @@ -107,17 +108,21 @@ GCM.prototype.send = function(data, devices) { * @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined * @returns {Object} A promise which is resolved after we get results from gcm */ -function generateGCMPayload(coreData, timeStamp, expirationTime) { +export function generateGCMPayload(coreData, timeStamp, expirationTime) { + timeStamp = timeStamp || Date.now(); + let payloadData = { 'time': new Date(timeStamp).toISOString(), 'data': JSON.stringify(coreData) } + let payload = { priority: 'normal', data: payloadData }; + if (expirationTime) { - // The timeStamp and expiration is in milliseconds but gcm requires second + // The timeStamp and expiration is in milliseconds but gcm requires second let timeToLive = Math.floor((expirationTime - timeStamp) / 1000); if (timeToLive < 0) { timeToLive = 0; @@ -127,6 +132,7 @@ function generateGCMPayload(coreData, timeStamp, expirationTime) { } payload.timeToLive = timeToLive; } + return payload; } @@ -148,4 +154,3 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { GCM.generateGCMPayload = generateGCMPayload; GCM.sliceDevices = sliceDevices; } -module.exports = GCM; From 700bc4e109891026f97d0916ce7df4b2b13b2ffa Mon Sep 17 00:00:00 2001 From: Roger Hu Date: Fri, 26 Feb 2016 10:52:15 -0800 Subject: [PATCH 2/5] Add baseline code --- spec/SNSPushAdapter.spec.js | 240 ++++++++++++++++++++++++++++ src/Adapters/Push/SNSPushAdapter.js | 186 +++++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 spec/SNSPushAdapter.spec.js create mode 100644 src/Adapters/Push/SNSPushAdapter.js diff --git a/spec/SNSPushAdapter.spec.js b/spec/SNSPushAdapter.spec.js new file mode 100644 index 0000000000..46892a02df --- /dev/null +++ b/spec/SNSPushAdapter.spec.js @@ -0,0 +1,240 @@ +var SNSPushAdapter = require('../src/Adapters/Push/SNSPushAdapter'); +describe('SNSPushAdapter', () => { + + var pushConfig; + var snsPushAdapter; + + beforeEach(function() { + pushConfig = { + pushTypes: { + ios: "APNS_ID", + android: "GCM_ID" + }, + accessKey: "accessKey", + secretKey: "secretKey", + region: "region" + }; + snsPushAdapter = new SNSPushAdapter(pushConfig); + }); + + it('can be initialized', (done) => { + // Make mock config + var arnMap = snsPushAdapter.arnMap; + + expect(arnMap.ios).toEqual("APNS_ID"); + expect(arnMap.android).toEqual("GCM_ID"); + + done(); + }); + + it('can get valid push types', (done) => { + expect(snsPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']); + done(); + }); + + it('can classify installation', (done) => { + // Mock installations + var validPushTypes = ['ios', 'android']; + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + }, + { + deviceType: 'ios', + deviceToken: 'iosToken' + }, + { + deviceType: 'win', + deviceToken: 'winToken' + }, + { + deviceType: 'android', + deviceToken: undefined + } + ]; + var deviceMap = SNSPushAdapter.classifyInstallations(installations, validPushTypes); + expect(deviceMap['android']).toEqual([makeDevice('androidToken')]); + expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]); + expect(deviceMap['win']).toBe(undefined); + done(); + }); + + it('can send push notifications', (done) => { + // Mock SNS sender + var snsSender = jasmine.createSpyObj('sns', ['createPlatformEndpoint', 'publish']); + snsPushAdapter.sns = snsSender; + + // Mock android ios senders + var androidSender = jasmine.createSpy('send') + var iosSender = jasmine.createSpy('send') + + var senderMap = { + ios: iosSender, + android: androidSender + }; + snsPushAdapter.senderMap = senderMap; + + // Mock installations + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + }, + { + deviceType: 'ios', + deviceToken: 'iosToken' + }, + { + deviceType: 'win', + deviceToken: 'winToken' + }, + { + deviceType: 'android', + deviceToken: undefined + } + ]; + var data = {}; + + snsPushAdapter.send(data, installations); + // Check SNS sender + expect(androidSender).toHaveBeenCalled(); + var args = androidSender.calls.first().args; + expect(args[0]).toEqual(data); + expect(args[1]).toEqual([ + makeDevice('androidToken') + ]); + // Check ios sender + expect(iosSender).toHaveBeenCalled(); + args = iosSender.calls.first().args; + expect(args[0]).toEqual(data); + expect(args[1]).toEqual([ + makeDevice('iosToken') + ]); + done(); + }); + + it('can generate the right Android payload', (done) => { + var data = {"action": "com.example.UPDATE_STATUS"}; + var pushId = '123'; + var timeStamp = 1456728000; + + var returnedData = SNSPushAdapter.generateAndroidPayload(data, pushId, timeStamp); + var expectedData = {GCM: '{"priority":"normal","data":{"time":"1970-01-17T20:38:48.000Z","push_id":"123"}}'}; + expect(returnedData).toEqual(expectedData) + done(); + }); + + it('can generate the right iOS payload', (done) => { + var data = {"aps": {"alert": "Check out these awesome deals!"}}; + var pushId = '123'; + var timeStamp = 1456728000; + + var returnedData = SNSPushAdapter.generateiOSPayload(data); + var expectedData = {APNS: '{"aps":{"alert":"Check out these awesome deals!"}}'}; + expect(returnedData).toEqual(expectedData) + done(); + }); + + it('can exchange device tokens for an Amazon Resource Number (ARN)', (done) => { + // Mock out Amazon SNS token exchange + var snsSender = jasmine.createSpyObj('sns', ['createPlatformEndpoint']); + snsPushAdapter.sns = snsSender; + + // Mock installations + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + } + ]; + + snsSender.createPlatformEndpoint.and.callFake(function(object, callback) { + callback(null, {'EndpointArn' : 'ARN'}); + }); + + var promise = snsPushAdapter.exchangeTokenPromise(makeDevice("androidToken"), "android"); + + promise.then(function() { + expect(snsSender.createPlatformEndpoint).toHaveBeenCalled(); + args = snsSender.createPlatformEndpoint.calls.first().args; + expect(args[0].PlatformApplicationArn).toEqual("GCM_ID"); + expect(args[0].Token).toEqual("androidToken"); + done(); + }); + }); + + it('can send SNS Payload', (done) => { + // Mock out Amazon SNS token exchange + var snsSender = jasmine.createSpyObj('sns', ['publish']) + snsSender.publish.and.callFake(function (object, callback) { + callback(null, '123'); + }); + + snsPushAdapter.sns = snsSender; + + // Mock installations + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + } + ]; + + var promise = snsPushAdapter.sendSNSPayload("123", {"test": "hello"}); + + var callback = jasmine.createSpy(); + promise.then(function () { + expect(snsSender.publish).toHaveBeenCalled(); + args = snsSender.publish.calls.first().args; + expect(args[0].MessageStructure).toEqual("json"); + expect(args[0].TargetArn).toEqual("123"); + expect(args[0].Message).toEqual('{"test":"hello"}'); + done(); + }); + }); + + it('can send SNS Payload to Android and iOS', (done) => { + // Mock out Amazon SNS token exchange + var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); + + snsSender.createPlatformEndpoint.and.callFake(function (object, callback) { + callback(null, {'EndpointArn': 'ARN'}); + }); + + snsSender.publish.and.callFake(function (object, callback) { + callback(null, '123'); + }); + + snsPushAdapter.sns = snsSender; + + // Mock installations + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + }, + { + deviceType: 'ios', + deviceToken: 'iosToken' + } + ]; + + var promise = snsPushAdapter.send({"test": "hello"}, installations); + + var callback = jasmine.createSpy(); + promise.then(function () { + expect(snsSender.publish).toHaveBeenCalled(); + expect(snsSender.publish.calls.count()).toEqual(2); + done(); + }); + }); + + function makeDevice(deviceToken, appIdentifier) { + return { + deviceToken: deviceToken, + appIdentifier: appIdentifier + }; + } + +}); diff --git a/src/Adapters/Push/SNSPushAdapter.js b/src/Adapters/Push/SNSPushAdapter.js new file mode 100644 index 0000000000..5f5bfe8cae --- /dev/null +++ b/src/Adapters/Push/SNSPushAdapter.js @@ -0,0 +1,186 @@ +"use strict"; +// SNSAdapter +// +// Uses SNS for push notification +import PushAdapter from './PushAdapter'; + +const Parse = require('parse/node').Parse; +const GCM = require('../../GCM'); + +const AWS = require('aws-sdk'); + +var DEFAULT_REGION = "us-east-1"; +import { classifyInstallations } from './PushAdapterUtils'; + +export class SNSPushAdapter extends PushAdapter { + + // Publish to an SNS endpoint + // Providing AWS access and secret keys is mandatory + // Region will use sane defaults if omitted + constructor(pushConfig = {}) { + super(pushConfig); + this.validPushTypes = ['ios', 'android']; + this.availablePushTypes = []; + this.arnMap = {}; + this.senderMap = {}; + + if (!pushConfig.accessKey || !pushConfig.secretKey) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Need to provide AWS keys'); + } + + if (pushConfig.pushTypes) { + let pushTypes = Object.keys(pushConfig.pushTypes); + for (let pushType of pushTypes) { + if (this.validPushTypes.indexOf(pushType) < 0) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push to ' + pushTypes + ' is not supported'); + } + this.availablePushTypes.push(pushType); + switch (pushType) { + case 'ios': + this.senderMap[pushType] = this.sendToAPNS.bind(this); + this.arnMap[pushType] = pushConfig.pushTypes[pushType]; + break; + case 'android': + this.senderMap[pushType] = this.sendToGCM.bind(this); + this.arnMap[pushType] = pushConfig.pushTypes[pushType]; + break; + } + } + } + + AWS.config.update({ + accessKeyId: pushConfig.accessKey, + secretAccessKey: pushConfig.secretKey, + region: pushConfig.region || DEFAULT_REGION + }); + + // Instantiate after config is setup. + this.sns = new AWS.SNS(); + } + + getValidPushTypes() { + return this.availablePushTypes; + } + + static classifyInstallations(installations, validTypes) { + return classifyInstallations(installations, validTypes) + } + + //Generate proper json for APNS message + static generateiOSPayload(data) { + return { + 'APNS': JSON.stringify(data) + }; + } + + // Generate proper json for GCM message + static generateAndroidPayload(data, pushId, timeStamp) { + var payload = GCM.generateGCMPayload(data.data, pushId, timeStamp, data.expirationTime); + + // SNS is verify sensitive to the body being JSON stringified but not GCM key. + return { + 'GCM': JSON.stringify(payload) + }; + } + + sendToAPNS(data, devices) { + var payload = SNSPushAdapter.generateiOSPayload(data); + + return this.sendToSNS(payload, devices, 'ios'); + } + + sendToGCM(data, devices) { + var payload = SNSPushAdapter.generateAndroidPayload(data); + return this.sendToSNS(payload, devices, 'android'); + } + + sendToSNS(payload, devices, pushType) { + // Exchange the device token for the Amazon resource ID + let exchangePromises = devices.map((device) => { + return this.exchangeTokenPromise(device, pushType); + }); + + // Publish off to SNS! + // Bulk publishing is not yet supported on Amazon SNS. + let promises = Parse.Promise.when(exchangePromises).then(arns => { + arns.map((arn) => { + return this.sendSNSPayload(arn, payload); + }); + }); + + return promises; + } + + + /** + * Request a Amazon Resource Identifier if one is not set. + */ + getPlatformArn(device, pushType, callback) { + var params = { + PlatformApplicationArn: this.arnMap[pushType], + Token: device.deviceToken + }; + + this.sns.createPlatformEndpoint(params, callback); + } + + /** + * Exchange the device token for an ARN + */ + exchangeTokenPromise(device, pushType) { + return new Parse.Promise((resolve, reject) => { + this.getPlatformArn(device, pushType, (err, data) => { + if (data.EndpointArn) { + resolve(data.EndpointArn); + } else { + console.error(err); + reject(err); + } + }); + }); + } + + /** + * Send the Message, MessageStructure, and Target Amazon Resource Number (ARN) to SNS + * @param arn Amazon Resource ID + * @param payload JSON-encoded message + * @returns {Parse.Promise} + */ + sendSNSPayload(arn, payload) { + + var object = { + Message: JSON.stringify(payload), + MessageStructure: 'json', + TargetArn: arn + }; + + return new Parse.Promise((resolve, reject) => { + this.sns.publish(object, (err, data) => { + if (err != null) { + console.error("Error sending push " + err); + return reject(err); + } + resolve(object); + }); + }); + } + + // For a given config object, endpoint and payload, publish via SNS + // Returns a promise containing the SNS object publish response + send(data, installations) { + let deviceMap = classifyInstallations(installations, this.availablePushTypes); + + let sendPromises = Object.keys(deviceMap).forEach((pushType) => { + var devices = deviceMap[pushType]; + var sender = this.senderMap[pushType]; + return sender(data, devices); + }); + + return Parse.Promise.when(sendPromises); + } +} + +export default SNSPushAdapter; +module.exports = SNSPushAdapter; From 11f3e529e6e480a7e48f6453f13d1109dcc92ea9 Mon Sep 17 00:00:00 2001 From: Roger Hu Date: Sat, 12 Mar 2016 13:03:45 -0800 Subject: [PATCH 3/5] Add extra test --- spec/SNSPushAdapter.spec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/SNSPushAdapter.spec.js b/spec/SNSPushAdapter.spec.js index 46892a02df..a4b78fafad 100644 --- a/spec/SNSPushAdapter.spec.js +++ b/spec/SNSPushAdapter.spec.js @@ -194,6 +194,18 @@ describe('SNSPushAdapter', () => { }); }); + it('errors sending SNS Payload to Android and iOS', (done) => { + // Mock out Amazon SNS token exchange + var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); + + snsSender.createPlatformEndpoint.and.callFake(function (object, callback) { + callback("error", {}); + }); + + var promise = snsSender.getPlatformArn(makeDevice("android"), "android", function(err, data)); + done(); + }); + it('can send SNS Payload to Android and iOS', (done) => { // Mock out Amazon SNS token exchange var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); @@ -222,7 +234,6 @@ describe('SNSPushAdapter', () => { var promise = snsPushAdapter.send({"test": "hello"}, installations); - var callback = jasmine.createSpy(); promise.then(function () { expect(snsSender.publish).toHaveBeenCalled(); expect(snsSender.publish.calls.count()).toEqual(2); From 2945c116f89f210840b30c184f1a470bba4ff02e Mon Sep 17 00:00:00 2001 From: Roger Hu Date: Sun, 13 Mar 2016 22:16:03 -0700 Subject: [PATCH 4/5] Add support for APNS --- spec/APNS.spec.js | 2 +- spec/ParsePushAdapter.spec.js | 2 +- spec/SNSPushAdapter.spec.js | 108 ++++++++++++++++++++++---- src/APNS.js | 5 +- src/Adapters/Push/ParsePushAdapter.js | 2 +- src/Adapters/Push/SNSPushAdapter.js | 78 +++++++++++++++---- 6 files changed, 162 insertions(+), 35 deletions(-) diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index c56e35d550..30fc4fc4f6 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -1,4 +1,4 @@ -var APNS = require('../src/APNS'); +var APNS = require('../src/APNS').APNS; describe('APNS', () => { diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index 57c481b59c..0f29305e5d 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -1,5 +1,5 @@ var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter'); -var APNS = require('../src/APNS'); +var APNS = require('../src/APNS').APNS; var GCM = require('../src/GCM').GCM; describe('ParsePushAdapter', () => { diff --git a/spec/SNSPushAdapter.spec.js b/spec/SNSPushAdapter.spec.js index a4b78fafad..99a842b318 100644 --- a/spec/SNSPushAdapter.spec.js +++ b/spec/SNSPushAdapter.spec.js @@ -7,8 +7,8 @@ describe('SNSPushAdapter', () => { beforeEach(function() { pushConfig = { pushTypes: { - ios: "APNS_ID", - android: "GCM_ID" + ios: {ARN : "APNS_ID", production: false, bundleId: 'com.parseplatform.myapp'}, + android: {ARN: "GCM_ID"} }, accessKey: "accessKey", secretKey: "secretKey", @@ -19,10 +19,9 @@ describe('SNSPushAdapter', () => { it('can be initialized', (done) => { // Make mock config - var arnMap = snsPushAdapter.arnMap; + var snsPushConfig = snsPushAdapter.snsConfig; - expect(arnMap.ios).toEqual("APNS_ID"); - expect(arnMap.android).toEqual("GCM_ID"); + expect(snsPushConfig).toEqual(pushConfig.pushTypes); done(); }); @@ -126,13 +125,17 @@ describe('SNSPushAdapter', () => { }); it('can generate the right iOS payload', (done) => { - var data = {"aps": {"alert": "Check out these awesome deals!"}}; + var data = {data : {"alert": "Check out these awesome deals!"}}; var pushId = '123'; var timeStamp = 1456728000; - var returnedData = SNSPushAdapter.generateiOSPayload(data); + var returnedData = SNSPushAdapter.generateiOSPayload(data, true); var expectedData = {APNS: '{"aps":{"alert":"Check out these awesome deals!"}}'}; - expect(returnedData).toEqual(expectedData) + + var returnedData = SNSPushAdapter.generateiOSPayload(data, false); + var expectedData = {APNS_SANDBOX: '{"aps":{"alert":"Check out these awesome deals!"}}'}; + + expect(returnedData).toEqual(expectedData); done(); }); @@ -153,11 +156,11 @@ describe('SNSPushAdapter', () => { callback(null, {'EndpointArn' : 'ARN'}); }); - var promise = snsPushAdapter.exchangeTokenPromise(makeDevice("androidToken"), "android"); + var promise = snsPushAdapter.exchangeTokenPromise(makeDevice("androidToken"), "GCM_ID"); promise.then(function() { expect(snsSender.createPlatformEndpoint).toHaveBeenCalled(); - args = snsSender.createPlatformEndpoint.calls.first().args; + var args = snsSender.createPlatformEndpoint.calls.first().args; expect(args[0].PlatformApplicationArn).toEqual("GCM_ID"); expect(args[0].Token).toEqual("androidToken"); done(); @@ -186,7 +189,7 @@ describe('SNSPushAdapter', () => { var callback = jasmine.createSpy(); promise.then(function () { expect(snsSender.publish).toHaveBeenCalled(); - args = snsSender.publish.calls.first().args; + var args = snsSender.publish.calls.first().args; expect(args[0].MessageStructure).toEqual("json"); expect(args[0].TargetArn).toEqual("123"); expect(args[0].Message).toEqual('{"test":"hello"}'); @@ -202,8 +205,10 @@ describe('SNSPushAdapter', () => { callback("error", {}); }); - var promise = snsSender.getPlatformArn(makeDevice("android"), "android", function(err, data)); - done(); + snsPushAdapter.getPlatformArn(makeDevice("android"), "android", function(err, data) { + expect(err).not.toBe(null); + done(); + }); }); it('can send SNS Payload to Android and iOS', (done) => { @@ -241,6 +246,83 @@ describe('SNSPushAdapter', () => { }); }); + it('can send to APNS with known identifier', (done) => { + var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); + + snsSender.createPlatformEndpoint.and.callFake(function (object, callback) { + callback(null, {'EndpointArn': 'ARN'}); + }); + + snsSender.publish.and.callFake(function (object, callback) { + callback(null, '123'); + }); + + snsPushAdapter.sns = snsSender; + + var promises = snsPushAdapter.sendToAPNS({"test": "hello"}, [makeDevice("ios", "com.parseplatform.myapp")]); + expect(promises.length).toEqual(1); + + Promise.all(promises).then(function () { + expect(snsSender.publish).toHaveBeenCalled(); + done(); + }); + + }); + + it('can send to APNS with unknown identifier', (done) => { + var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); + + snsSender.createPlatformEndpoint.and.callFake(function (object, callback) { + callback(null, {'EndpointArn': 'ARN'}); + }); + + snsSender.publish.and.callFake(function (object, callback) { + callback(null, '123'); + }); + + snsPushAdapter.sns = snsSender; + + var promises = snsPushAdapter.sendToAPNS({"test": "hello"}, [makeDevice("ios", "com.parseplatform.unknown")]); + expect(promises.length).toEqual(0); + done(); + }); + + it('can send to APNS with multiple identifiers', (done) => { + pushConfig = { + pushTypes: { + ios: [{ARN : "APNS_SANDBOX_ID", production: false, bundleId: 'beta.parseplatform.myapp'}, + {ARN : "APNS_PROD_ID", production: true, bundleId: 'com.parseplatform.myapp'}], + android: {ARN: "GCM_ID"} + }, + accessKey: "accessKey", + secretKey: "secretKey", + region: "region" + }; + + snsPushAdapter = new SNSPushAdapter(pushConfig); + + var snsSender = jasmine.createSpyObj('sns', ['publish', 'createPlatformEndpoint']); + + snsSender.createPlatformEndpoint.and.callFake(function (object, callback) { + callback(null, {'EndpointArn': 'APNS_PROD_ID'}); + }); + + snsSender.publish.and.callFake(function (object, callback) { + callback(null, '123'); + }); + + snsPushAdapter.sns = snsSender; + + var promises = snsPushAdapter.sendToAPNS({"test": "hello"}, [makeDevice("ios", "beta.parseplatform.myapp")]); + expect(promises.length).toEqual(1); + Promise.all(promises).then(function () { + expect(snsSender.publish).toHaveBeenCalled(); + var args = snsSender.publish.calls.first().args[0]; + expect(args.Message).toEqual("{\"APNS_SANDBOX\":\"{}\"}"); + done(); + }); + }); + function makeDevice(deviceToken, appIdentifier) { return { deviceToken: deviceToken, diff --git a/src/APNS.js b/src/APNS.js index 69389ce8f7..f9e633ee73 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -16,7 +16,7 @@ const apn = require('apn'); * @param {String} args.bundleId The bundleId for cert * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox */ -function APNS(args) { +export function APNS(args) { // Since for ios, there maybe multiple cert/key pairs, // typePushConfig can be an array. let apnsArgsList = []; @@ -187,7 +187,7 @@ function chooseConns(conns, device) { * @param {Object} coreData The data field under api request body * @returns {Object} A apns notification */ -function generateNotification(coreData, expirationTime) { +export function generateNotification(coreData, expirationTime) { let notification = new apn.notification(); let payload = {}; for (let key in coreData) { @@ -224,4 +224,3 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { APNS.chooseConns = chooseConns; APNS.handleTransmissionError = handleTransmissionError; } -module.exports = APNS; diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js index f1784296da..4ae06cccc4 100644 --- a/src/Adapters/Push/ParsePushAdapter.js +++ b/src/Adapters/Push/ParsePushAdapter.js @@ -5,7 +5,7 @@ const Parse = require('parse/node').Parse; const GCM = require('../../GCM').GCM; -const APNS = require('../../APNS'); +const APNS = require('../../APNS').APNS; import PushAdapter from './PushAdapter'; import { classifyInstallations } from './PushAdapterUtils'; diff --git a/src/Adapters/Push/SNSPushAdapter.js b/src/Adapters/Push/SNSPushAdapter.js index 5f5bfe8cae..3d1833bc22 100644 --- a/src/Adapters/Push/SNSPushAdapter.js +++ b/src/Adapters/Push/SNSPushAdapter.js @@ -6,6 +6,7 @@ import PushAdapter from './PushAdapter'; const Parse = require('parse/node').Parse; const GCM = require('../../GCM'); +const APNS = require('../../APNS'); const AWS = require('aws-sdk'); @@ -21,7 +22,7 @@ export class SNSPushAdapter extends PushAdapter { super(pushConfig); this.validPushTypes = ['ios', 'android']; this.availablePushTypes = []; - this.arnMap = {}; + this.snsConfig = pushConfig.pushTypes; this.senderMap = {}; if (!pushConfig.accessKey || !pushConfig.secretKey) { @@ -40,11 +41,9 @@ export class SNSPushAdapter extends PushAdapter { switch (pushType) { case 'ios': this.senderMap[pushType] = this.sendToAPNS.bind(this); - this.arnMap[pushType] = pushConfig.pushTypes[pushType]; break; case 'android': this.senderMap[pushType] = this.sendToGCM.bind(this); - this.arnMap[pushType] = pushConfig.pushTypes[pushType]; break; } } @@ -69,10 +68,20 @@ export class SNSPushAdapter extends PushAdapter { } //Generate proper json for APNS message - static generateiOSPayload(data) { - return { - 'APNS': JSON.stringify(data) - }; + static generateiOSPayload(data, production) { + var prefix = ""; + + if (production) { + prefix = "APNS"; + } else { + prefix = "APNS_SANDBOX" + } + + var notification = APNS.generateNotification(data.data, data.expirationTime); + + var payload = {}; + payload[prefix] = notification.compile(); + return payload; } // Generate proper json for GCM message @@ -86,20 +95,55 @@ export class SNSPushAdapter extends PushAdapter { } sendToAPNS(data, devices) { - var payload = SNSPushAdapter.generateiOSPayload(data); - return this.sendToSNS(payload, devices, 'ios'); + var iosPushConfig = this.snsConfig['ios']; + + let iosConfigs = []; + if (Array.isArray(iosPushConfig)) { + iosConfigs = iosConfigs.concat(iosPushConfig); + } else { + iosConfigs.push(iosPushConfig) + } + + let promises = []; + + for (let iosConfig of iosConfigs) { + + let production = iosConfig.production || false; + var payload = SNSPushAdapter.generateiOSPayload(data, production); + + var deviceSends = []; + for (let device of devices) { + + // Follow the same logic as APNS service. If no appIdentifier, send it! + if (!device.appIdentifier || device.appIdentifier === '') { + deviceSends.push(device); + } + + else if (device.appIdentifier === iosConfig.bundleId) { + deviceSends.push(device); + } + } + if (deviceSends.length > 0) { + promises.push(this.sendToSNS(payload, deviceSends, iosConfig.ARN)); + } + } + + return promises; } sendToGCM(data, devices) { var payload = SNSPushAdapter.generateAndroidPayload(data); - return this.sendToSNS(payload, devices, 'android'); + var pushConfig = this.snsConfig['android']; + + return this.sendToSNS(payload, devices, pushConfig.ARN); } - sendToSNS(payload, devices, pushType) { + sendToSNS(payload, devices, platformArn) { // Exchange the device token for the Amazon resource ID + let exchangePromises = devices.map((device) => { - return this.exchangeTokenPromise(device, pushType); + return this.exchangeTokenPromise(device, platformArn); }); // Publish off to SNS! @@ -117,9 +161,9 @@ export class SNSPushAdapter extends PushAdapter { /** * Request a Amazon Resource Identifier if one is not set. */ - getPlatformArn(device, pushType, callback) { + getPlatformArn(device, arn, callback) { var params = { - PlatformApplicationArn: this.arnMap[pushType], + PlatformApplicationArn: arn, Token: device.deviceToken }; @@ -129,9 +173,10 @@ export class SNSPushAdapter extends PushAdapter { /** * Exchange the device token for an ARN */ - exchangeTokenPromise(device, pushType) { + exchangeTokenPromise(device, platformARN) { return new Parse.Promise((resolve, reject) => { - this.getPlatformArn(device, pushType, (err, data) => { + + this.getPlatformArn(device, platformARN, (err, data) => { if (data.EndpointArn) { resolve(data.EndpointArn); } else { @@ -174,6 +219,7 @@ export class SNSPushAdapter extends PushAdapter { let sendPromises = Object.keys(deviceMap).forEach((pushType) => { var devices = deviceMap[pushType]; + var sender = this.senderMap[pushType]; return sender(data, devices); }); From 16c236b2e35da48854e371b47f4b1562c156bd1d Mon Sep 17 00:00:00 2001 From: Roger Hu Date: Thu, 17 Mar 2016 15:08:18 -0700 Subject: [PATCH 5/5] collapse stuff --- spec/SNSPushAdapter.spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/SNSPushAdapter.spec.js b/spec/SNSPushAdapter.spec.js index 99a842b318..13a3168eb4 100644 --- a/spec/SNSPushAdapter.spec.js +++ b/spec/SNSPushAdapter.spec.js @@ -115,18 +115,16 @@ describe('SNSPushAdapter', () => { it('can generate the right Android payload', (done) => { var data = {"action": "com.example.UPDATE_STATUS"}; - var pushId = '123'; var timeStamp = 1456728000; - var returnedData = SNSPushAdapter.generateAndroidPayload(data, pushId, timeStamp); - var expectedData = {GCM: '{"priority":"normal","data":{"time":"1970-01-17T20:38:48.000Z","push_id":"123"}}'}; + var returnedData = SNSPushAdapter.generateAndroidPayload(data, timeStamp); + var expectedData = {GCM: '{"priority":"normal","data":{"time":"1970-01-17T20:38:48.000Z"}}'}; expect(returnedData).toEqual(expectedData) done(); }); it('can generate the right iOS payload', (done) => { var data = {data : {"alert": "Check out these awesome deals!"}}; - var pushId = '123'; var timeStamp = 1456728000; var returnedData = SNSPushAdapter.generateiOSPayload(data, true);