Skip to content

Commit 8987b85

Browse files
authored
feat: Add support for Expo push notifications (#243)
1 parent 0982bb5 commit 8987b85

8 files changed

+407
-24
lines changed

README.md

+21-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The official Push Notification adapter for Parse Server. See [Parse Server Push
2020
- [Using a Custom Version on Parse Server](#using-a-custom-version-on-parse-server)
2121
- [Install Push Adapter](#install-push-adapter)
2222
- [Configure Parse Server](#configure-parse-server)
23+
- [Expo Push Options](#expo-push-options)
2324

2425
# Silent Notifications
2526

@@ -57,16 +58,32 @@ const parseServerOptions = {
5758
push: {
5859
adapter: new PushAdapter({
5960
ios: {
60-
/* Apple push notification options */
61+
/* Apple push options */
6162
},
6263
android: {
6364
/* Android push options */
64-
}
65+
},
6566
web: {
6667
/* Web push options */
67-
}
68-
})
68+
},
69+
expo: {
70+
/* Expo push options */
71+
},
72+
}),
6973
},
7074
/* Other Parse Server options */
7175
}
7276
```
77+
78+
### Expo Push Options
79+
80+
Example options:
81+
82+
```js
83+
expo: {
84+
accessToken: '<EXPO_ACCESS_TOKEN>',
85+
},
86+
```
87+
88+
For more information see the [Expo docs](https://docs.expo.dev/push-notifications/overview/).
89+

package-lock.json

+82-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"dependencies": {
2626
"@parse/node-apn": "6.0.1",
2727
"@parse/node-gcm": "1.0.2",
28+
"expo-server-sdk": "3.10.0",
2829
"firebase-admin": "12.1.0",
2930
"npmlog": "7.0.1",
3031
"parse": "5.0.0",

spec/EXPO.spec.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
const EXPO = require('../src/EXPO').default;
2+
const Expo = require('expo-server-sdk').Expo;
3+
4+
function mockSender(success) {
5+
return spyOn(EXPO.prototype, 'sendNotifications').and.callFake((payload, tokens) => {
6+
return Promise.resolve(tokens.map(() => ({ status: success ? 'ok' : 'error' })));
7+
});
8+
}
9+
10+
function mockExpoPush(success) {
11+
return spyOn(Expo.prototype, 'sendPushNotificationsAsync').and.callFake((deviceToken) => {
12+
if (success) {
13+
return Promise.resolve(deviceToken.map(() => ({ status: 'ok' })));
14+
}
15+
return Promise.resolve(deviceToken.map(() => ({ status: 'error', message: 'Failed to send' })));
16+
});
17+
}
18+
19+
describe('EXPO', () => {
20+
it('can initialize', () => {
21+
const args = { };
22+
new EXPO(args);
23+
});
24+
25+
it('can throw on initializing with invalid args', () => {
26+
expect(function() { new EXPO(123); }).toThrow();
27+
expect(function() { new EXPO(undefined); }).toThrow();
28+
});
29+
30+
it('can send successful EXPO request', async () => {
31+
const log = require('npmlog');
32+
const spy = spyOn(log, 'verbose');
33+
34+
const expo = new EXPO({ vapidDetails: 'apiKey' });
35+
spyOn(EXPO.prototype, 'sendNotifications').and.callFake(() => {
36+
return Promise.resolve([{ status: 'ok' }]);
37+
});
38+
const data = { data: { alert: 'alert' } };
39+
const devices = [{ deviceToken: 'token' }];
40+
const response = await expo.send(data, devices);
41+
expect(EXPO.prototype.sendNotifications).toHaveBeenCalled();
42+
const args = EXPO.prototype.sendNotifications.calls.first().args;
43+
expect(args.length).toEqual(2);
44+
expect(args[0]).toEqual(data.data);
45+
expect(args[1]).toEqual(['token']);
46+
expect(spy).toHaveBeenCalled();
47+
expect(response).toEqual([{
48+
device: { deviceToken: 'token', pushType: 'expo' },
49+
response: { status: 'ok' },
50+
transmitted: true
51+
}]);
52+
});
53+
54+
it('can send failed EXPO request', async () => {
55+
const log = require('npmlog');
56+
const expo = new EXPO({ vapidDetails: 'apiKey' });
57+
spyOn(EXPO.prototype, 'sendNotifications').and.callFake(() => {
58+
return Promise.resolve([{ status: 'error', message: 'DeviceNotRegistered' }])});
59+
const data = { data: { alert: 'alert' } };
60+
const devices = [{ deviceToken: 'token' }];
61+
const response = await expo.send(data, devices);
62+
63+
expect(EXPO.prototype.sendNotifications).toHaveBeenCalled();
64+
const args = EXPO.prototype.sendNotifications.calls.first().args;
65+
expect(args.length).toEqual(2);
66+
expect(args[0]).toEqual(data.data);
67+
expect(args[1]).toEqual(['token']);
68+
69+
expect(response).toEqual([{
70+
device: { deviceToken: 'token', pushType: 'expo' },
71+
response: { status: 'error', message: 'DeviceNotRegistered', error: 'NotRegistered' },
72+
transmitted: false
73+
}]);
74+
});
75+
76+
it('can send multiple successful EXPO request', async () => {
77+
const expo = new EXPO({ });
78+
const data = { data: { alert: 'alert' } };
79+
const devices = [
80+
{ deviceToken: 'token1', deviceType: 'ios' },
81+
{ deviceToken: 'token2', deviceType: 'ios' },
82+
{ deviceToken: 'token3', deviceType: 'ios' },
83+
{ deviceToken: 'token4', deviceType: 'ios' },
84+
{ deviceToken: 'token5', deviceType: 'ios' },
85+
];
86+
mockSender(true);
87+
const response = await expo.send(data, devices);
88+
89+
expect(Array.isArray(response)).toBe(true);
90+
expect(response.length).toEqual(devices.length);
91+
response.forEach((res, index) => {
92+
expect(res.transmitted).toEqual(true);
93+
expect(res.device.deviceToken).toEqual(devices[index].deviceToken);
94+
});
95+
});
96+
97+
it('can send multiple failed EXPO request', async () => {
98+
const expo = new EXPO({ });
99+
const data = { data: { alert: 'alert' } };
100+
const devices = [
101+
{ deviceToken: 'token1' },
102+
{ deviceToken: 'token2' },
103+
{ deviceToken: 'token3' },
104+
{ deviceToken: 'token4' },
105+
{ deviceToken: 'token5' },
106+
];
107+
mockSender(false);
108+
const response = await expo.send(data, devices);
109+
expect(Array.isArray(response)).toBe(true);
110+
expect(response.length).toEqual(devices.length);
111+
response.forEach((res, index) => {
112+
expect(res.transmitted).toEqual(false);
113+
expect(res.device.deviceToken).toEqual(devices[index].deviceToken);
114+
});
115+
});
116+
117+
it('can run successful payload', async () => {
118+
const payload = { alert: 'alert' };
119+
const deviceTokens = ['ExpoPush[1]'];
120+
mockExpoPush(true);
121+
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
122+
expect(response.length).toEqual(1);
123+
expect(response[0].status).toEqual('ok');
124+
});
125+
126+
it('can run failed payload', async () => {
127+
const payload = { alert: 'alert' };
128+
const deviceTokens = ['ExpoPush[1]'];
129+
mockExpoPush(false);
130+
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
131+
expect(response.length).toEqual(1);
132+
expect(response[0].status).toEqual('error');
133+
});
134+
135+
it('can run successful payload with wrong types', async () => {
136+
const payload = JSON.stringify({ alert: 'alert' });
137+
const deviceTokens = ['ExpoPush[1]'];
138+
mockExpoPush(true);
139+
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
140+
expect(response.length).toEqual(1);
141+
expect(response[0].status).toEqual('ok');
142+
});
143+
});

0 commit comments

Comments
 (0)