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
|
## 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
|
### 2.6.0
|
||||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.5.3...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 = {
|
var auth = {
|
||||||
isMaster: true
|
isMaster: true
|
||||||
}
|
}
|
||||||
@@ -913,4 +913,70 @@ describe('PushController', () => {
|
|||||||
done();
|
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 PushWorker = require('../src').PushWorker;
|
||||||
|
var PushUtils = require('../src/Push/utils');
|
||||||
var Config = require('../src/Config');
|
var Config = require('../src/Config');
|
||||||
|
|
||||||
describe('PushWorker', () => {
|
describe('PushWorker', () => {
|
||||||
@@ -54,4 +55,105 @@ describe('PushWorker', () => {
|
|||||||
jfail(err);
|
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<*> {
|
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> {
|
||||||
pushStatus = pushStatusHandler(config, pushStatus.objectId);
|
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)) {
|
if (!utils.isPushIncrementing(body)) {
|
||||||
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
|
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
|
||||||
return pushStatus.trackSent(results);
|
return pushStatus.trackSent(results);
|
||||||
|
|||||||
@@ -8,6 +8,81 @@ export function isPushIncrementing(body) {
|
|||||||
body.data.badge.toLowerCase() == "increment"
|
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.
|
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||||
* @param {Object} where A query condition
|
* @param {Object} where A query condition
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class PushRouter extends PromiseRouter {
|
|||||||
result: true
|
result: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}).catch(req.config.loggerController.error);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user