Skip to content

Adds support for localized push notification in push payload #4129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
68 changes: 67 additions & 1 deletion spec/PushController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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);
});
});
102 changes: 102 additions & 0 deletions spec/PushWorker.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var PushWorker = require('../src').PushWorker;
var PushUtils = require('../src/Push/utils');
var Config = require('../src/Config');

describe('PushWorker', () => {
Expand Down Expand Up @@ -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: []});
});
});
});
16 changes: 16 additions & 0 deletions src/Push/PushWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions src/Push/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slick

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
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/PushRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class PushRouter extends PromiseRouter {
result: true
}
});
});
}).catch(req.config.loggerController.error);
return promise;
}

Expand Down