@parse/push-adapter 6.2.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/EXPO.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+
3
+ import Parse from 'parse';
4
+ import log from 'npmlog';
5
+ import { Expo } from 'expo-server-sdk';
6
+
7
+ const LOG_PREFIX = 'parse-server-push-adapter EXPO';
8
+
9
+ function expoResultToParseResponse(result) {
10
+ if (result.status === 'ok') {
11
+ return result;
12
+ } else {
13
+ // ParseServer looks for "error", and supports ceratin codes like 'NotRegistered' for
14
+ // cleanup. Expo returns slighyly different ones so changing to match what is expected
15
+ // This can be taken out if the responsibility gets moved to the adapter itself.
16
+ const error = result.message === 'DeviceNotRegistered' ?
17
+ 'NotRegistered' : result.message;
18
+ return {
19
+ error,
20
+ ...result
21
+ }
22
+ }
23
+ }
24
+
25
+ export class EXPO {
26
+ expo = undefined;
27
+ /**
28
+ * Create a new EXPO push adapter. Based on Web Adapter.
29
+ *
30
+ * @param {Object} args https://github.com/expo/expo-server-sdk-node / https://docs.expo.dev/push-notifications/sending-notifications/
31
+ */
32
+ constructor(args) {
33
+ if (typeof args !== 'object') {
34
+ throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'EXPO Push Configuration is invalid');
35
+ }
36
+
37
+ this.expo = new Expo(args)
38
+ this.options = args;
39
+ }
40
+
41
+ /**
42
+ * Send Expo push notification request.
43
+ *
44
+ * @param {Object} data The data we need to send, the format is the same with api request body
45
+ * @param {Array} devices An array of devices
46
+ * @returns {Object} A promise which is resolved immediately
47
+ */
48
+ async send(data, devices) {
49
+ const coreData = data && data.data;
50
+
51
+ if (!coreData || !devices || !Array.isArray(devices)) {
52
+ log.warn(LOG_PREFIX, 'invalid push payload');
53
+ return;
54
+ }
55
+ const devicesMap = devices.reduce((memo, device) => {
56
+ memo[device.deviceToken] = device;
57
+ return memo;
58
+ }, {});
59
+ const deviceTokens = Object.keys(devicesMap);
60
+
61
+ const resolvers = [];
62
+ const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
63
+ let length = deviceTokens.length;
64
+
65
+ log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
66
+
67
+ const response = await this.sendNotifications(coreData, deviceTokens);
68
+
69
+ log.verbose(LOG_PREFIX, `EXPO Response: %d sent`, response.length);
70
+
71
+ deviceTokens.forEach((token, index) => {
72
+ const resolve = resolvers[index];
73
+ const result = response[index];
74
+ const device = devicesMap[token];
75
+ const resolution = {
76
+ transmitted: result.status === 'ok',
77
+ device: {
78
+ ...device,
79
+ pushType: 'expo'
80
+ },
81
+ response: expoResultToParseResponse(result),
82
+ };
83
+ resolve(resolution);
84
+ });
85
+ return Promise.all(promises);
86
+ }
87
+
88
+ /**
89
+ * Send multiple Expo push notification request.
90
+ *
91
+ * @param {Object} payload The data we need to send, the format is the same with api request body
92
+ * @param {Array} deviceTokens An array of devicesTokens
93
+ * @param {Object} options The options for the request
94
+ * @returns {Object} A promise which is resolved immediately
95
+ */
96
+ async sendNotifications({alert, body, ...payload}, deviceTokens) {
97
+ const messages = deviceTokens.map((token) => ({
98
+ to: token,
99
+ body: body || alert,
100
+ ...payload
101
+ }));
102
+
103
+ return await this.expo.sendPushNotificationsAsync(messages);
104
+ }
105
+ }
106
+
107
+ export default EXPO;
@@ -1,48 +1,37 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
-
7
- var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8
-
9
- var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
10
-
11
- exports.default = FCM;
12
-
13
- var _parse = require('parse');
14
-
15
- var _parse2 = _interopRequireDefault(_parse);
16
-
17
- var _npmlog = require('npmlog');
18
-
19
- var _npmlog2 = _interopRequireDefault(_npmlog);
20
-
21
- var _app = require('firebase-admin/app');
22
-
23
- var _messaging = require('firebase-admin/messaging');
24
-
25
- var _PushAdapterUtils = require('./PushAdapterUtils');
26
-
27
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
-
29
- var LOG_PREFIX = 'parse-server-push-adapter FCM';
30
- var FCMRegistrationTokensMax = 500;
31
- var FCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // FCM allows a max of 4 weeks
32
- var apnsIntegerDataKeys = ['badge', 'content-available', 'mutable-content', 'priority', 'expiration_time'];
33
-
34
- function FCM(args, pushType) {
35
- if ((typeof args === 'undefined' ? 'undefined' : _typeof(args)) !== 'object' || !args.firebaseServiceAccount) {
36
- throw new _parse2.default.Error(_parse2.default.Error.PUSH_MISCONFIGURED, 'FCM Configuration is invalid');
3
+ import Parse from 'parse';
4
+ import log from 'npmlog';
5
+ import { initializeApp, cert, getApps, getApp } from 'firebase-admin/app';
6
+ import { getMessaging } from 'firebase-admin/messaging';
7
+ import { randomString } from './PushAdapterUtils.js';
8
+
9
+ const LOG_PREFIX = 'parse-server-push-adapter FCM';
10
+ const FCMRegistrationTokensMax = 500;
11
+ const FCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // FCM allows a max of 4 weeks
12
+ const apnsIntegerDataKeys = [
13
+ 'badge',
14
+ 'content-available',
15
+ 'mutable-content',
16
+ 'priority',
17
+ 'expiration_time',
18
+ ];
19
+
20
+ export default function FCM(args, pushType) {
21
+ if (typeof args !== 'object' || !args.firebaseServiceAccount) {
22
+ throw new Parse.Error(
23
+ Parse.Error.PUSH_MISCONFIGURED,
24
+ 'FCM Configuration is invalid',
25
+ );
37
26
  }
38
27
 
39
- var app = void 0;
40
- if ((0, _app.getApps)().length === 0) {
41
- app = (0, _app.initializeApp)({ credential: (0, _app.cert)(args.firebaseServiceAccount) });
28
+ let app;
29
+ if (getApps().length === 0) {
30
+ app = initializeApp({ credential: cert(args.firebaseServiceAccount) });
42
31
  } else {
43
- app = (0, _app.getApp)();
32
+ app = getApp();
44
33
  }
45
- this.sender = (0, _messaging.getMessaging)(app);
34
+ this.sender = getMessaging(app);
46
35
  this.pushType = pushType; // Push type is only used to remain backwards compatible with APNS and GCM
47
36
  }
48
37
 
@@ -56,84 +45,111 @@ FCM.FCMRegistrationTokensMax = FCMRegistrationTokensMax;
56
45
  */
57
46
 
58
47
  FCM.prototype.send = function (data, devices) {
59
- var _this = this;
60
-
61
48
  if (!data || !devices || !Array.isArray(devices)) {
62
- _npmlog2.default.warn(LOG_PREFIX, 'invalid push payload');
49
+ log.warn(LOG_PREFIX, 'invalid push payload');
63
50
  return;
64
51
  }
65
52
 
66
53
  // We can only have 500 recepients per send, so we need to slice devices to
67
54
  // chunk if necessary
68
- var slices = sliceDevices(devices, FCM.FCMRegistrationTokensMax);
55
+ const slices = sliceDevices(devices, FCM.FCMRegistrationTokensMax);
69
56
 
70
- var sendToDeviceSlice = function sendToDeviceSlice(deviceSlice, pushType) {
71
- var pushId = (0, _PushAdapterUtils.randomString)(10);
72
- var timestamp = Date.now();
57
+ const sendToDeviceSlice = (deviceSlice, pushType) => {
58
+ const pushId = randomString(10);
59
+ const timestamp = Date.now();
73
60
 
74
61
  // Build a device map
75
- var devicesMap = deviceSlice.reduce(function (memo, device) {
62
+ const devicesMap = deviceSlice.reduce((memo, device) => {
76
63
  memo[device.deviceToken] = device;
77
64
  return memo;
78
65
  }, {});
79
66
 
80
- var deviceTokens = Object.keys(devicesMap);
81
-
82
- var fcmPayload = generateFCMPayload(data, pushId, timestamp, deviceTokens, pushType);
83
- var length = deviceTokens.length;
84
- _npmlog2.default.info(LOG_PREFIX, 'sending push to ' + length + ' devices');
85
-
86
- return _this.sender.sendEachForMulticast(fcmPayload.data).then(function (response) {
87
- var promises = [];
88
- var failedTokens = [];
89
- var successfulTokens = [];
90
-
91
- response.responses.forEach(function (resp, idx) {
92
- if (resp.success) {
93
- successfulTokens.push(deviceTokens[idx]);
94
- promises.push(createSuccessfulPromise(deviceTokens[idx], devicesMap[deviceTokens[idx]].deviceType));
95
- } else {
96
- failedTokens.push(deviceTokens[idx]);
97
- promises.push(createErrorPromise(deviceTokens[idx], devicesMap[deviceTokens[idx]].deviceType, resp.error));
98
- _npmlog2.default.error(LOG_PREFIX, 'failed to send to ' + deviceTokens[idx] + ' with error: ' + JSON.stringify(resp.error));
67
+ const deviceTokens = Object.keys(devicesMap);
68
+
69
+ const fcmPayload = generateFCMPayload(
70
+ data,
71
+ pushId,
72
+ timestamp,
73
+ deviceTokens,
74
+ pushType,
75
+ );
76
+ const length = deviceTokens.length;
77
+ log.info(LOG_PREFIX, `sending push to ${length} devices`);
78
+
79
+ return this.sender
80
+ .sendEachForMulticast(fcmPayload.data)
81
+ .then((response) => {
82
+ const promises = [];
83
+ const failedTokens = [];
84
+ const successfulTokens = [];
85
+
86
+ response.responses.forEach((resp, idx) => {
87
+ if (resp.success) {
88
+ successfulTokens.push(deviceTokens[idx]);
89
+ promises.push(
90
+ createSuccessfulPromise(
91
+ deviceTokens[idx],
92
+ devicesMap[deviceTokens[idx]].deviceType,
93
+ ),
94
+ );
95
+ } else {
96
+ failedTokens.push(deviceTokens[idx]);
97
+ promises.push(
98
+ createErrorPromise(
99
+ deviceTokens[idx],
100
+ devicesMap[deviceTokens[idx]].deviceType,
101
+ resp.error,
102
+ ),
103
+ );
104
+ log.error(
105
+ LOG_PREFIX,
106
+ `failed to send to ${deviceTokens[idx]} with error: ${JSON.stringify(resp.error)}`,
107
+ );
108
+ }
109
+ });
110
+
111
+ if (failedTokens.length) {
112
+ log.error(
113
+ LOG_PREFIX,
114
+ `tokens with failed pushes: ${JSON.stringify(failedTokens)}`,
115
+ );
99
116
  }
100
- });
101
117
 
102
- if (failedTokens.length) {
103
- _npmlog2.default.error(LOG_PREFIX, 'tokens with failed pushes: ' + JSON.stringify(failedTokens));
104
- }
105
-
106
- if (successfulTokens.length) {
107
- _npmlog2.default.verbose(LOG_PREFIX, 'tokens with successful pushes: ' + JSON.stringify(successfulTokens));
108
- }
118
+ if (successfulTokens.length) {
119
+ log.verbose(
120
+ LOG_PREFIX,
121
+ `tokens with successful pushes: ${JSON.stringify(successfulTokens)}`,
122
+ );
123
+ }
109
124
 
110
- return Promise.all(promises);
111
- });
125
+ return Promise.all(promises);
126
+ });
112
127
  };
113
128
 
114
- var allPromises = Promise.all(slices.map(function (slice) {
115
- return sendToDeviceSlice(slice, _this.pushType);
116
- })).catch(function (err) {
117
- _npmlog2.default.error(LOG_PREFIX, 'error sending push: ' + err);
129
+ const allPromises = Promise.all(
130
+ slices.map((slice) => sendToDeviceSlice(slice, this.pushType)),
131
+ ).catch((err) => {
132
+ log.error(LOG_PREFIX, `error sending push: ${err}`);
118
133
  });
119
134
 
120
135
  return allPromises;
121
136
  };
122
137
 
123
138
  function _APNSToFCMPayload(requestData) {
124
- var coreData = requestData;
139
+ let coreData = requestData;
125
140
 
126
141
  if (requestData.hasOwnProperty('data')) {
127
142
  coreData = requestData.data;
128
143
  }
129
144
 
130
- var expirationTime = requestData['expiration_time'] || coreData['expiration_time'];
131
- var collapseId = requestData['collapse_id'] || coreData['collapse_id'];
132
- var pushType = requestData['push_type'] || coreData['push_type'];
133
- var priority = requestData['priority'] || coreData['priority'];
145
+ let expirationTime =
146
+ requestData['expiration_time'] || coreData['expiration_time'];
147
+ let collapseId = requestData['collapse_id'] || coreData['collapse_id'];
148
+ let pushType = requestData['push_type'] || coreData['push_type'];
149
+ let priority = requestData['priority'] || coreData['priority'];
134
150
 
135
- var apnsPayload = { apns: { payload: { aps: {} } } };
136
- var headers = {};
151
+ let apnsPayload = { apns: { payload: { aps: {} } } };
152
+ let headers = {};
137
153
 
138
154
  // Set to alert by default if not set explicitly
139
155
  headers['apns-push-type'] = 'alert';
@@ -156,13 +172,13 @@ function _APNSToFCMPayload(requestData) {
156
172
  apnsPayload.apns.headers = headers;
157
173
  }
158
174
 
159
- for (var key in coreData) {
175
+ for (let key in coreData) {
160
176
  switch (key) {
161
177
  case 'aps':
162
178
  apnsPayload['apns']['payload']['aps'] = coreData.aps;
163
179
  break;
164
180
  case 'alert':
165
- if (_typeof(coreData.alert) == 'object') {
181
+ if (typeof coreData.alert == 'object') {
166
182
  // When we receive a dictionary, use as is to remain
167
183
  // compatible with how the APNS.js + node-apn work
168
184
  apnsPayload['apns']['payload']['aps']['alert'] = coreData.alert;
@@ -188,16 +204,20 @@ function _APNSToFCMPayload(requestData) {
188
204
  apnsPayload['apns']['payload']['aps']['sound'] = coreData.sound;
189
205
  break;
190
206
  case 'content-available':
191
- apnsPayload['apns']['payload']['aps']['content-available'] = coreData['content-available'];
207
+ apnsPayload['apns']['payload']['aps']['content-available'] =
208
+ coreData['content-available'];
192
209
  break;
193
210
  case 'mutable-content':
194
- apnsPayload['apns']['payload']['aps']['mutable-content'] = coreData['mutable-content'];
211
+ apnsPayload['apns']['payload']['aps']['mutable-content'] =
212
+ coreData['mutable-content'];
195
213
  break;
196
214
  case 'targetContentIdentifier':
197
- apnsPayload['apns']['payload']['aps']['target-content-id'] = coreData.targetContentIdentifier;
215
+ apnsPayload['apns']['payload']['aps']['target-content-id'] =
216
+ coreData.targetContentIdentifier;
198
217
  break;
199
218
  case 'interruptionLevel':
200
- apnsPayload['apns']['payload']['aps']['interruption-level'] = coreData.interruptionLevel;
219
+ apnsPayload['apns']['payload']['aps']['interruption-level'] =
220
+ coreData.interruptionLevel;
201
221
  break;
202
222
  case 'category':
203
223
  apnsPayload['apns']['payload']['aps']['category'] = coreData.category;
@@ -205,8 +225,7 @@ function _APNSToFCMPayload(requestData) {
205
225
  case 'threadId':
206
226
  apnsPayload['apns']['payload']['aps']['thread-id'] = coreData.threadId;
207
227
  break;
208
- case 'expiration_time':
209
- // Exclude header-related fields as these are set above
228
+ case 'expiration_time': // Exclude header-related fields as these are set above
210
229
  break;
211
230
  case 'collapse_id':
212
231
  break;
@@ -224,10 +243,10 @@ function _APNSToFCMPayload(requestData) {
224
243
 
225
244
  function _GCMToFCMPayload(requestData, pushId, timeStamp) {
226
245
 
227
- var androidPayload = {
246
+ const androidPayload = {
228
247
  android: {
229
- priority: 'high'
230
- }
248
+ priority: 'high',
249
+ },
231
250
  };
232
251
 
233
252
  if (requestData.hasOwnProperty('notification')) {
@@ -236,44 +255,22 @@ function _GCMToFCMPayload(requestData, pushId, timeStamp) {
236
255
 
237
256
  if (requestData.hasOwnProperty('data')) {
238
257
  // FCM gives an error on send if we have apns keys that should have integer values
239
- var _iteratorNormalCompletion = true;
240
- var _didIteratorError = false;
241
- var _iteratorError = undefined;
242
-
243
- try {
244
- for (var _iterator = apnsIntegerDataKeys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
245
- var key = _step.value;
246
-
247
- if (requestData.data.hasOwnProperty(key)) {
248
- delete requestData.data[key];
249
- }
250
- }
251
- } catch (err) {
252
- _didIteratorError = true;
253
- _iteratorError = err;
254
- } finally {
255
- try {
256
- if (!_iteratorNormalCompletion && _iterator.return) {
257
- _iterator.return();
258
- }
259
- } finally {
260
- if (_didIteratorError) {
261
- throw _iteratorError;
262
- }
258
+ for (const key of apnsIntegerDataKeys) {
259
+ if (requestData.data.hasOwnProperty(key)) {
260
+ delete requestData.data[key]
263
261
  }
264
262
  }
265
-
266
263
  androidPayload.android.data = {
267
264
  push_id: pushId,
268
265
  time: new Date(timeStamp).toISOString(),
269
- data: JSON.stringify(requestData.data)
270
- };
266
+ data: JSON.stringify(requestData.data),
267
+ }
271
268
  }
272
269
 
273
270
  if (requestData['expiration_time']) {
274
- var expirationTime = requestData['expiration_time'];
271
+ const expirationTime = requestData['expiration_time'];
275
272
  // Convert to seconds
276
- var timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
273
+ let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
277
274
  if (timeToLive < 0) {
278
275
  timeToLive = 0;
279
276
  }
@@ -307,7 +304,10 @@ function payloadConverter(requestData, pushType, pushId, timeStamp) {
307
304
  } else if (pushType === 'android') {
308
305
  return _GCMToFCMPayload(requestData, pushId, timeStamp);
309
306
  } else {
310
- throw new _parse2.default.Error(_parse2.default.Error.PUSH_MISCONFIGURED, 'Unsupported push type, apple or android only.');
307
+ throw new Parse.Error(
308
+ Parse.Error.PUSH_MISCONFIGURED,
309
+ 'Unsupported push type, apple or android only.',
310
+ );
311
311
  }
312
312
  }
313
313
 
@@ -320,17 +320,24 @@ function payloadConverter(requestData, pushType, pushId, timeStamp) {
320
320
  * @param {String} pushType Either apple or android
321
321
  * @returns {Object} A payload for FCM
322
322
  */
323
- function generateFCMPayload(requestData, pushId, timeStamp, deviceTokens, pushType) {
323
+ function generateFCMPayload(
324
+ requestData,
325
+ pushId,
326
+ timeStamp,
327
+ deviceTokens,
328
+ pushType,
329
+ ) {
324
330
  delete requestData['where'];
325
331
 
326
- var payloadToUse = {
332
+ const payloadToUse = {
327
333
  data: {}
328
334
  };
329
335
 
330
- var fcmPayload = payloadConverter(requestData, pushType, pushId, timeStamp);
331
- payloadToUse.data = _extends({}, fcmPayload, {
332
- tokens: deviceTokens
333
- });
336
+ const fcmPayload = payloadConverter(requestData, pushType, pushId, timeStamp);
337
+ payloadToUse.data = {
338
+ ...fcmPayload,
339
+ tokens: deviceTokens,
340
+ };
334
341
 
335
342
  return payloadToUse;
336
343
  }
@@ -342,7 +349,7 @@ function generateFCMPayload(requestData, pushId, timeStamp, deviceTokens, pushTy
342
349
  * @returns {Array} An array which contains several arrays of devices with fixed chunk size
343
350
  */
344
351
  function sliceDevices(devices, chunkSize) {
345
- var chunkDevices = [];
352
+ const chunkDevices = [];
346
353
  while (devices.length > 0) {
347
354
  chunkDevices.push(devices.splice(0, chunkSize));
348
355
  }
@@ -361,9 +368,9 @@ function createErrorPromise(token, deviceType, errorMessage) {
361
368
  transmitted: false,
362
369
  device: {
363
370
  deviceToken: token,
364
- deviceType: deviceType
371
+ deviceType: deviceType,
365
372
  },
366
- response: { error: errorMessage }
373
+ response: { error: errorMessage },
367
374
  });
368
375
  }
369
376
 
@@ -378,8 +385,8 @@ function createSuccessfulPromise(token, deviceType) {
378
385
  transmitted: true,
379
386
  device: {
380
387
  deviceToken: token,
381
- deviceType: deviceType
382
- }
388
+ deviceType: deviceType,
389
+ },
383
390
  });
384
391
  }
385
392
 
@@ -388,4 +395,4 @@ FCM.generateFCMPayload = generateFCMPayload;
388
395
  /* istanbul ignore else */
389
396
  if (process.env.TESTING) {
390
397
  FCM.sliceDevices = sliceDevices;
391
- }
398
+ }