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:
Florent Vilmart
2017-09-01 15:22:02 -04:00
committed by GitHub
parent 540daa4c4a
commit 6df944704c
6 changed files with 267 additions and 2 deletions

View File

@@ -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)

View File

@@ -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);
});
});

View File

@@ -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: []});
});
});
});

View File

@@ -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);

View File

@@ -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

View File

@@ -28,7 +28,7 @@ export class PushRouter extends PromiseRouter {
result: true
}
});
});
}).catch(req.config.loggerController.error);
return promise;
}