Skip to content

Commit

Permalink
feat: Add support for Expo push notifications (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
mortenmo committed May 15, 2024
1 parent 0982bb5 commit 8987b85
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 24 deletions.
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The official Push Notification adapter for Parse Server. See [Parse Server Push
- [Using a Custom Version on Parse Server](#using-a-custom-version-on-parse-server)
- [Install Push Adapter](#install-push-adapter)
- [Configure Parse Server](#configure-parse-server)
- [Expo Push Options](#expo-push-options)

# Silent Notifications

Expand Down Expand Up @@ -57,16 +58,32 @@ const parseServerOptions = {
push: {
adapter: new PushAdapter({
ios: {
/* Apple push notification options */
/* Apple push options */
},
android: {
/* Android push options */
}
},
web: {
/* Web push options */
}
})
},
expo: {
/* Expo push options */
},
}),
},
/* Other Parse Server options */
}
```

### Expo Push Options

Example options:

```js
expo: {
accessToken: '<EXPO_ACCESS_TOKEN>',
},
```

For more information see the [Expo docs](https://docs.expo.dev/push-notifications/overview/).

95 changes: 82 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@parse/node-apn": "6.0.1",
"@parse/node-gcm": "1.0.2",
"expo-server-sdk": "3.10.0",
"firebase-admin": "12.1.0",
"npmlog": "7.0.1",
"parse": "5.0.0",
Expand Down
143 changes: 143 additions & 0 deletions spec/EXPO.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
const EXPO = require('../src/EXPO').default;
const Expo = require('expo-server-sdk').Expo;

function mockSender(success) {
return spyOn(EXPO.prototype, 'sendNotifications').and.callFake((payload, tokens) => {
return Promise.resolve(tokens.map(() => ({ status: success ? 'ok' : 'error' })));
});
}

function mockExpoPush(success) {
return spyOn(Expo.prototype, 'sendPushNotificationsAsync').and.callFake((deviceToken) => {
if (success) {
return Promise.resolve(deviceToken.map(() => ({ status: 'ok' })));
}
return Promise.resolve(deviceToken.map(() => ({ status: 'error', message: 'Failed to send' })));
});
}

describe('EXPO', () => {
it('can initialize', () => {
const args = { };
new EXPO(args);
});

it('can throw on initializing with invalid args', () => {
expect(function() { new EXPO(123); }).toThrow();
expect(function() { new EXPO(undefined); }).toThrow();
});

it('can send successful EXPO request', async () => {
const log = require('npmlog');
const spy = spyOn(log, 'verbose');

const expo = new EXPO({ vapidDetails: 'apiKey' });
spyOn(EXPO.prototype, 'sendNotifications').and.callFake(() => {
return Promise.resolve([{ status: 'ok' }]);
});
const data = { data: { alert: 'alert' } };
const devices = [{ deviceToken: 'token' }];
const response = await expo.send(data, devices);
expect(EXPO.prototype.sendNotifications).toHaveBeenCalled();
const args = EXPO.prototype.sendNotifications.calls.first().args;
expect(args.length).toEqual(2);
expect(args[0]).toEqual(data.data);
expect(args[1]).toEqual(['token']);
expect(spy).toHaveBeenCalled();
expect(response).toEqual([{
device: { deviceToken: 'token', pushType: 'expo' },
response: { status: 'ok' },
transmitted: true
}]);
});

it('can send failed EXPO request', async () => {
const log = require('npmlog');
const expo = new EXPO({ vapidDetails: 'apiKey' });
spyOn(EXPO.prototype, 'sendNotifications').and.callFake(() => {
return Promise.resolve([{ status: 'error', message: 'DeviceNotRegistered' }])});
const data = { data: { alert: 'alert' } };
const devices = [{ deviceToken: 'token' }];
const response = await expo.send(data, devices);

expect(EXPO.prototype.sendNotifications).toHaveBeenCalled();
const args = EXPO.prototype.sendNotifications.calls.first().args;
expect(args.length).toEqual(2);
expect(args[0]).toEqual(data.data);
expect(args[1]).toEqual(['token']);

expect(response).toEqual([{
device: { deviceToken: 'token', pushType: 'expo' },
response: { status: 'error', message: 'DeviceNotRegistered', error: 'NotRegistered' },
transmitted: false
}]);
});

it('can send multiple successful EXPO request', async () => {
const expo = new EXPO({ });
const data = { data: { alert: 'alert' } };
const devices = [
{ deviceToken: 'token1', deviceType: 'ios' },
{ deviceToken: 'token2', deviceType: 'ios' },
{ deviceToken: 'token3', deviceType: 'ios' },
{ deviceToken: 'token4', deviceType: 'ios' },
{ deviceToken: 'token5', deviceType: 'ios' },
];
mockSender(true);
const response = await expo.send(data, devices);

expect(Array.isArray(response)).toBe(true);
expect(response.length).toEqual(devices.length);
response.forEach((res, index) => {
expect(res.transmitted).toEqual(true);
expect(res.device.deviceToken).toEqual(devices[index].deviceToken);
});
});

it('can send multiple failed EXPO request', async () => {
const expo = new EXPO({ });
const data = { data: { alert: 'alert' } };
const devices = [
{ deviceToken: 'token1' },
{ deviceToken: 'token2' },
{ deviceToken: 'token3' },
{ deviceToken: 'token4' },
{ deviceToken: 'token5' },
];
mockSender(false);
const response = await expo.send(data, devices);
expect(Array.isArray(response)).toBe(true);
expect(response.length).toEqual(devices.length);
response.forEach((res, index) => {
expect(res.transmitted).toEqual(false);
expect(res.device.deviceToken).toEqual(devices[index].deviceToken);
});
});

it('can run successful payload', async () => {
const payload = { alert: 'alert' };
const deviceTokens = ['ExpoPush[1]'];
mockExpoPush(true);
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
expect(response.length).toEqual(1);
expect(response[0].status).toEqual('ok');
});

it('can run failed payload', async () => {
const payload = { alert: 'alert' };
const deviceTokens = ['ExpoPush[1]'];
mockExpoPush(false);
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
expect(response.length).toEqual(1);
expect(response[0].status).toEqual('error');
});

it('can run successful payload with wrong types', async () => {
const payload = JSON.stringify({ alert: 'alert' });
const deviceTokens = ['ExpoPush[1]'];
mockExpoPush(true);
const response = await new EXPO({}).sendNotifications(payload, deviceTokens);
expect(response.length).toEqual(1);
expect(response[0].status).toEqual('ok');
});
});
Loading

0 comments on commit 8987b85

Please sign in to comment.