Skip to content

Commit 6df9447

Browse files
authored
Adds support for localized push notification in push payload (#4129)
* Adds support for localized push data keys - passign alert-[lang|locale] or title-[lang|locale] will inject the proper locale on the push body based on the installation * Better handling of the default cases * Updates changelog * nits * nits
1 parent 540daa4 commit 6df9447

File tree

6 files changed

+267
-2
lines changed

6 files changed

+267
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## Parse Server Changelog
22

3+
### master
4+
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.6.0...master)
5+
6+
#### New Features
7+
* Adds ability to send localized pushes according to the _Installation localeIdentifier
8+
39
### 2.6.0
410
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.5.3...2.6.0)
511

spec/PushController.spec.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ describe('PushController', () => {
847847
});
848848
});
849849

850-
it('should mark the _PushStatus as succeeded when audience has no deviceToken', (done) => {
850+
it('should mark the _PushStatus as failed when audience has no deviceToken', (done) => {
851851
var auth = {
852852
isMaster: true
853853
}
@@ -913,4 +913,70 @@ describe('PushController', () => {
913913
done();
914914
});
915915
});
916+
917+
it('should support localized payload data', (done) => {
918+
var payload = {data: {
919+
alert: 'Hello!',
920+
'alert-fr': 'Bonjour',
921+
'alert-es': 'Ola'
922+
}}
923+
924+
var pushAdapter = {
925+
send: function(body, installations) {
926+
return successfulTransmissions(body, installations);
927+
},
928+
getValidPushTypes: function() {
929+
return ["ios"];
930+
}
931+
}
932+
933+
var config = new Config(Parse.applicationId);
934+
var auth = {
935+
isMaster: true
936+
}
937+
938+
const where = {
939+
'deviceType': 'ios'
940+
}
941+
spyOn(pushAdapter, 'send').and.callThrough();
942+
var pushController = new PushController();
943+
reconfigureServer({
944+
push: { adapter: pushAdapter }
945+
}).then(() => {
946+
var installations = [];
947+
while (installations.length != 5) {
948+
const installation = new Parse.Object("_Installation");
949+
installation.set("installationId", "installation_" + installations.length);
950+
installation.set("deviceToken", "device_token_" + installations.length)
951+
installation.set("badge", installations.length);
952+
installation.set("originalBadge", installations.length);
953+
installation.set("deviceType", "ios");
954+
installations.push(installation);
955+
}
956+
installations[0].set('localeIdentifier', 'fr-CA');
957+
installations[1].set('localeIdentifier', 'fr-FR');
958+
installations[2].set('localeIdentifier', 'en-US');
959+
return Parse.Object.saveAll(installations);
960+
}).then(() => {
961+
return pushController.sendPush(payload, where, config, auth)
962+
}).then(() => {
963+
// Wait so the push is completed.
964+
return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
965+
}).then(() => {
966+
expect(pushAdapter.send.calls.count()).toBe(2);
967+
const firstCall = pushAdapter.send.calls.first();
968+
expect(firstCall.args[0].data).toEqual({
969+
alert: 'Hello!'
970+
});
971+
expect(firstCall.args[1].length).toBe(3); // 3 installations
972+
973+
const lastCall = pushAdapter.send.calls.mostRecent();
974+
expect(lastCall.args[0].data).toEqual({
975+
alert: 'Bonjour'
976+
});
977+
expect(lastCall.args[1].length).toBe(2); // 2 installations
978+
// No installation is in es so only 1 call for fr, and another for default
979+
done();
980+
}).catch(done.fail);
981+
});
916982
});

spec/PushWorker.spec.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var PushWorker = require('../src').PushWorker;
2+
var PushUtils = require('../src/Push/utils');
23
var Config = require('../src/Config');
34

45
describe('PushWorker', () => {
@@ -54,4 +55,105 @@ describe('PushWorker', () => {
5455
jfail(err);
5556
})
5657
});
58+
59+
describe('localized push', () => {
60+
it('should return locales', () => {
61+
const locales = PushUtils.getLocalesFromPush({
62+
data: {
63+
'alert-fr': 'french',
64+
'alert': 'Yo!',
65+
'alert-en-US': 'English',
66+
}
67+
});
68+
expect(locales).toEqual(['fr', 'en-US']);
69+
});
70+
71+
it('should return and empty array if no locale is set', () => {
72+
const locales = PushUtils.getLocalesFromPush({
73+
data: {
74+
'alert': 'Yo!',
75+
}
76+
});
77+
expect(locales).toEqual([]);
78+
});
79+
80+
it('should deduplicate locales', () => {
81+
const locales = PushUtils.getLocalesFromPush({
82+
data: {
83+
'alert': 'Yo!',
84+
'alert-fr': 'french',
85+
'title-fr': 'french'
86+
}
87+
});
88+
expect(locales).toEqual(['fr']);
89+
});
90+
91+
it('transforms body appropriately', () => {
92+
const cleanBody = PushUtils.transformPushBodyForLocale({
93+
data: {
94+
alert: 'Yo!',
95+
'alert-fr': 'frenchy!',
96+
'alert-en': 'english',
97+
}
98+
}, 'fr');
99+
expect(cleanBody).toEqual({
100+
data: {
101+
alert: 'frenchy!'
102+
}
103+
});
104+
});
105+
106+
it('transforms body appropriately', () => {
107+
const cleanBody = PushUtils.transformPushBodyForLocale({
108+
data: {
109+
alert: 'Yo!',
110+
'alert-fr': 'frenchy!',
111+
'alert-en': 'english',
112+
'title-fr': 'french title'
113+
}
114+
}, 'fr');
115+
expect(cleanBody).toEqual({
116+
data: {
117+
alert: 'frenchy!',
118+
title: 'french title'
119+
}
120+
});
121+
});
122+
123+
it('maps body on all provided locales', () => {
124+
const bodies = PushUtils.bodiesPerLocales({
125+
data: {
126+
alert: 'Yo!',
127+
'alert-fr': 'frenchy!',
128+
'alert-en': 'english',
129+
'title-fr': 'french title'
130+
}
131+
}, ['fr', 'en']);
132+
expect(bodies).toEqual({
133+
fr: {
134+
data: {
135+
alert: 'frenchy!',
136+
title: 'french title'
137+
}
138+
},
139+
en: {
140+
data: {
141+
alert: 'english',
142+
}
143+
},
144+
default: {
145+
data: {
146+
alert: 'Yo!'
147+
}
148+
}
149+
});
150+
});
151+
152+
it('should properly handle default cases', () => {
153+
expect(PushUtils.transformPushBodyForLocale({})).toEqual({});
154+
expect(PushUtils.stripLocalesFromBody({})).toEqual({});
155+
expect(PushUtils.bodiesPerLocales({where: {}})).toEqual({default: {where: {}}});
156+
expect(PushUtils.groupByLocaleIdentifier([])).toEqual({default: []});
157+
});
158+
});
57159
});

src/Push/PushWorker.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ export class PushWorker {
6565

6666
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> {
6767
pushStatus = pushStatusHandler(config, pushStatus.objectId);
68+
// Check if we have locales in the push body
69+
const locales = utils.getLocalesFromPush(body);
70+
if (locales.length > 0) {
71+
// Get all tranformed bodies for each locale
72+
const bodiesPerLocales = utils.bodiesPerLocales(body, locales);
73+
74+
// Group installations on the specified locales (en, fr, default etc...)
75+
const grouppedInstallations = utils.groupByLocaleIdentifier(installations, locales);
76+
const promises = Object.keys(grouppedInstallations).map((locale) => {
77+
const installations = grouppedInstallations[locale];
78+
const body = bodiesPerLocales[locale];
79+
return this.sendToAdapter(body, installations, pushStatus, config);
80+
});
81+
return Promise.all(promises);
82+
}
83+
6884
if (!utils.isPushIncrementing(body)) {
6985
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
7086
return pushStatus.trackSent(results);

src/Push/utils.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,81 @@ export function isPushIncrementing(body) {
88
body.data.badge.toLowerCase() == "increment"
99
}
1010

11+
const localizableKeys = ['alert', 'title'];
12+
13+
export function getLocalesFromPush(body) {
14+
const data = body.data;
15+
if (!data) {
16+
return [];
17+
}
18+
return [...new Set(Object.keys(data).reduce((memo, key) => {
19+
localizableKeys.forEach((localizableKey) => {
20+
if (key.indexOf(`${localizableKey}-`) == 0) {
21+
memo.push(key.slice(localizableKey.length + 1));
22+
}
23+
});
24+
return memo;
25+
}, []))];
26+
}
27+
28+
export function transformPushBodyForLocale(body, locale) {
29+
const data = body.data;
30+
if (!data) {
31+
return body;
32+
}
33+
body = deepcopy(body);
34+
localizableKeys.forEach((key) => {
35+
const localeValue = body.data[`${key}-${locale}`];
36+
if (localeValue) {
37+
body.data[key] = localeValue;
38+
}
39+
});
40+
return stripLocalesFromBody(body);
41+
}
42+
43+
export function stripLocalesFromBody(body) {
44+
if (!body.data) { return body; }
45+
Object.keys(body.data).forEach((key) => {
46+
localizableKeys.forEach((localizableKey) => {
47+
if (key.indexOf(`${localizableKey}-`) == 0) {
48+
delete body.data[key];
49+
}
50+
});
51+
});
52+
return body;
53+
}
54+
55+
export function bodiesPerLocales(body, locales = []) {
56+
// Get all tranformed bodies for each locale
57+
const result = locales.reduce((memo, locale) => {
58+
memo[locale] = transformPushBodyForLocale(body, locale);
59+
return memo;
60+
}, {});
61+
// Set the default locale, with the stripped body
62+
result.default = stripLocalesFromBody(body);
63+
return result;
64+
}
65+
66+
export function groupByLocaleIdentifier(installations, locales = []) {
67+
return installations.reduce((map, installation) => {
68+
let added = false;
69+
locales.forEach((locale) => {
70+
if (added) {
71+
return;
72+
}
73+
if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) === 0) {
74+
added = true;
75+
map[locale] = map[locale] || [];
76+
map[locale].push(installation);
77+
}
78+
});
79+
if (!added) {
80+
map.default.push(installation);
81+
}
82+
return map;
83+
}, {default: []});
84+
}
85+
1186
/**
1287
* Check whether the deviceType parameter in qury condition is valid or not.
1388
* @param {Object} where A query condition

src/Routers/PushRouter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class PushRouter extends PromiseRouter {
2828
result: true
2929
}
3030
});
31-
});
31+
}).catch(req.config.loggerController.error);
3232
return promise;
3333
}
3434

0 commit comments

Comments
 (0)