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
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### master
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.6.0...master)
|
||||
|
||||
#### New Features
|
||||
* Adds ability to send localized pushes according to the _Installation localeIdentifier
|
||||
|
||||
### 2.6.0
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.5.3...2.6.0)
|
||||
|
||||
|
||||
@@ -847,7 +847,7 @@ describe('PushController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark the _PushStatus as succeeded when audience has no deviceToken', (done) => {
|
||||
it('should mark the _PushStatus as failed when audience has no deviceToken', (done) => {
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
@@ -913,4 +913,70 @@ describe('PushController', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support localized payload data', (done) => {
|
||||
var payload = {data: {
|
||||
alert: 'Hello!',
|
||||
'alert-fr': 'Bonjour',
|
||||
'alert-es': 'Ola'
|
||||
}}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
const where = {
|
||||
'deviceType': 'ios'
|
||||
}
|
||||
spyOn(pushAdapter, 'send').and.callThrough();
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
var installations = [];
|
||||
while (installations.length != 5) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken", "device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
installations[0].set('localeIdentifier', 'fr-CA');
|
||||
installations[1].set('localeIdentifier', 'fr-FR');
|
||||
installations[2].set('localeIdentifier', 'en-US');
|
||||
return Parse.Object.saveAll(installations);
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, where, config, auth)
|
||||
}).then(() => {
|
||||
// Wait so the push is completed.
|
||||
return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
|
||||
}).then(() => {
|
||||
expect(pushAdapter.send.calls.count()).toBe(2);
|
||||
const firstCall = pushAdapter.send.calls.first();
|
||||
expect(firstCall.args[0].data).toEqual({
|
||||
alert: 'Hello!'
|
||||
});
|
||||
expect(firstCall.args[1].length).toBe(3); // 3 installations
|
||||
|
||||
const lastCall = pushAdapter.send.calls.mostRecent();
|
||||
expect(lastCall.args[0].data).toEqual({
|
||||
alert: 'Bonjour'
|
||||
});
|
||||
expect(lastCall.args[1].length).toBe(2); // 2 installations
|
||||
// No installation is in es so only 1 call for fr, and another for default
|
||||
done();
|
||||
}).catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var PushWorker = require('../src').PushWorker;
|
||||
var PushUtils = require('../src/Push/utils');
|
||||
var Config = require('../src/Config');
|
||||
|
||||
describe('PushWorker', () => {
|
||||
@@ -54,4 +55,105 @@ describe('PushWorker', () => {
|
||||
jfail(err);
|
||||
})
|
||||
});
|
||||
|
||||
describe('localized push', () => {
|
||||
it('should return locales', () => {
|
||||
const locales = PushUtils.getLocalesFromPush({
|
||||
data: {
|
||||
'alert-fr': 'french',
|
||||
'alert': 'Yo!',
|
||||
'alert-en-US': 'English',
|
||||
}
|
||||
});
|
||||
expect(locales).toEqual(['fr', 'en-US']);
|
||||
});
|
||||
|
||||
it('should return and empty array if no locale is set', () => {
|
||||
const locales = PushUtils.getLocalesFromPush({
|
||||
data: {
|
||||
'alert': 'Yo!',
|
||||
}
|
||||
});
|
||||
expect(locales).toEqual([]);
|
||||
});
|
||||
|
||||
it('should deduplicate locales', () => {
|
||||
const locales = PushUtils.getLocalesFromPush({
|
||||
data: {
|
||||
'alert': 'Yo!',
|
||||
'alert-fr': 'french',
|
||||
'title-fr': 'french'
|
||||
}
|
||||
});
|
||||
expect(locales).toEqual(['fr']);
|
||||
});
|
||||
|
||||
it('transforms body appropriately', () => {
|
||||
const cleanBody = PushUtils.transformPushBodyForLocale({
|
||||
data: {
|
||||
alert: 'Yo!',
|
||||
'alert-fr': 'frenchy!',
|
||||
'alert-en': 'english',
|
||||
}
|
||||
}, 'fr');
|
||||
expect(cleanBody).toEqual({
|
||||
data: {
|
||||
alert: 'frenchy!'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('transforms body appropriately', () => {
|
||||
const cleanBody = PushUtils.transformPushBodyForLocale({
|
||||
data: {
|
||||
alert: 'Yo!',
|
||||
'alert-fr': 'frenchy!',
|
||||
'alert-en': 'english',
|
||||
'title-fr': 'french title'
|
||||
}
|
||||
}, 'fr');
|
||||
expect(cleanBody).toEqual({
|
||||
data: {
|
||||
alert: 'frenchy!',
|
||||
title: 'french title'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('maps body on all provided locales', () => {
|
||||
const bodies = PushUtils.bodiesPerLocales({
|
||||
data: {
|
||||
alert: 'Yo!',
|
||||
'alert-fr': 'frenchy!',
|
||||
'alert-en': 'english',
|
||||
'title-fr': 'french title'
|
||||
}
|
||||
}, ['fr', 'en']);
|
||||
expect(bodies).toEqual({
|
||||
fr: {
|
||||
data: {
|
||||
alert: 'frenchy!',
|
||||
title: 'french title'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
data: {
|
||||
alert: 'english',
|
||||
}
|
||||
},
|
||||
default: {
|
||||
data: {
|
||||
alert: 'Yo!'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle default cases', () => {
|
||||
expect(PushUtils.transformPushBodyForLocale({})).toEqual({});
|
||||
expect(PushUtils.stripLocalesFromBody({})).toEqual({});
|
||||
expect(PushUtils.bodiesPerLocales({where: {}})).toEqual({default: {where: {}}});
|
||||
expect(PushUtils.groupByLocaleIdentifier([])).toEqual({default: []});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,22 @@ export class PushWorker {
|
||||
|
||||
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> {
|
||||
pushStatus = pushStatusHandler(config, pushStatus.objectId);
|
||||
// Check if we have locales in the push body
|
||||
const locales = utils.getLocalesFromPush(body);
|
||||
if (locales.length > 0) {
|
||||
// Get all tranformed bodies for each locale
|
||||
const bodiesPerLocales = utils.bodiesPerLocales(body, locales);
|
||||
|
||||
// Group installations on the specified locales (en, fr, default etc...)
|
||||
const grouppedInstallations = utils.groupByLocaleIdentifier(installations, locales);
|
||||
const promises = Object.keys(grouppedInstallations).map((locale) => {
|
||||
const installations = grouppedInstallations[locale];
|
||||
const body = bodiesPerLocales[locale];
|
||||
return this.sendToAdapter(body, installations, pushStatus, config);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
if (!utils.isPushIncrementing(body)) {
|
||||
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
|
||||
return pushStatus.trackSent(results);
|
||||
|
||||
@@ -8,6 +8,81 @@ export function isPushIncrementing(body) {
|
||||
body.data.badge.toLowerCase() == "increment"
|
||||
}
|
||||
|
||||
const localizableKeys = ['alert', 'title'];
|
||||
|
||||
export function getLocalesFromPush(body) {
|
||||
const data = body.data;
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return [...new Set(Object.keys(data).reduce((memo, key) => {
|
||||
localizableKeys.forEach((localizableKey) => {
|
||||
if (key.indexOf(`${localizableKey}-`) == 0) {
|
||||
memo.push(key.slice(localizableKey.length + 1));
|
||||
}
|
||||
});
|
||||
return memo;
|
||||
}, []))];
|
||||
}
|
||||
|
||||
export function transformPushBodyForLocale(body, locale) {
|
||||
const data = body.data;
|
||||
if (!data) {
|
||||
return body;
|
||||
}
|
||||
body = deepcopy(body);
|
||||
localizableKeys.forEach((key) => {
|
||||
const localeValue = body.data[`${key}-${locale}`];
|
||||
if (localeValue) {
|
||||
body.data[key] = localeValue;
|
||||
}
|
||||
});
|
||||
return stripLocalesFromBody(body);
|
||||
}
|
||||
|
||||
export function stripLocalesFromBody(body) {
|
||||
if (!body.data) { return body; }
|
||||
Object.keys(body.data).forEach((key) => {
|
||||
localizableKeys.forEach((localizableKey) => {
|
||||
if (key.indexOf(`${localizableKey}-`) == 0) {
|
||||
delete body.data[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return body;
|
||||
}
|
||||
|
||||
export function bodiesPerLocales(body, locales = []) {
|
||||
// Get all tranformed bodies for each locale
|
||||
const result = locales.reduce((memo, locale) => {
|
||||
memo[locale] = transformPushBodyForLocale(body, locale);
|
||||
return memo;
|
||||
}, {});
|
||||
// Set the default locale, with the stripped body
|
||||
result.default = stripLocalesFromBody(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function groupByLocaleIdentifier(installations, locales = []) {
|
||||
return installations.reduce((map, installation) => {
|
||||
let added = false;
|
||||
locales.forEach((locale) => {
|
||||
if (added) {
|
||||
return;
|
||||
}
|
||||
if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) === 0) {
|
||||
added = true;
|
||||
map[locale] = map[locale] || [];
|
||||
map[locale].push(installation);
|
||||
}
|
||||
});
|
||||
if (!added) {
|
||||
map.default.push(installation);
|
||||
}
|
||||
return map;
|
||||
}, {default: []});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||
* @param {Object} where A query condition
|
||||
|
||||
@@ -28,7 +28,7 @@ export class PushRouter extends PromiseRouter {
|
||||
result: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(req.config.loggerController.error);
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user