From 9e8da4ec3dc0bea9850ad622c0aa3cefe6541a18 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 1 Sep 2017 11:01:27 -0400 Subject: [PATCH 1/5] 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 --- spec/PushController.spec.js | 68 +++++++++++++++++++++- spec/PushWorker.spec.js | 95 +++++++++++++++++++++++++++++++ src/Controllers/PushController.js | 2 +- src/Push/PushWorker.js | 30 ++++++++-- src/Push/utils.js | 75 ++++++++++++++++++++++++ src/Routers/PushRouter.js | 2 + 6 files changed, 265 insertions(+), 7 deletions(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 57c9de60d2..e159dd8235 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -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); + }); }); diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index 8b392d12e2..b9b826b2a7 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -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,98 @@ 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!' + } + } + }); + }); + }); }); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 3cae892bc3..9cc5c09d79 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -61,7 +61,7 @@ export class PushController { }).catch((err) => { return pushStatus.fail(err).then(() => { throw err; - }); + }) }); } diff --git a/src/Push/PushWorker.js b/src/Push/PushWorker.js index 1c50a1e0d5..0260e744c3 100644 --- a/src/Push/PushWorker.js +++ b/src/Push/PushWorker.js @@ -12,15 +12,19 @@ import { PushQueue } from './PushQueue'; const UNSUPPORTED_BADGE_KEY = "unsupported"; -function groupByBadge(installations) { - return installations.reduce((map, installation) => { - const badge = installation.badge + ''; - map[badge] = map[badge] || []; - map[badge].push(installation); +function groupBy(key, objects) { + return objects.reduce((map, object) => { + const value = object[key] + ''; + map[value] = map[value] || []; + map[value].push(object); return map; }, {}); } +function groupByBadge(installations) { + return groupBy('badge', installations); +} + export class PushWorker { subscriber: ?any; adapter: any; @@ -65,6 +69,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); diff --git a/src/Push/utils.js b/src/Push/utils.js index 364003a5a4..1d21c236bd 100644 --- a/src/Push/utils.js +++ b/src/Push/utils.js @@ -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 diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index ace3cf80b0..a042aadc30 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -28,6 +28,8 @@ export class PushRouter extends PromiseRouter { result: true } }); + }).catch((err) => { + req.config.loggerController.error(err); }); return promise; } From e48b8e3dee774a06093be3d08800679ad31bca7f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 1 Sep 2017 14:20:55 -0400 Subject: [PATCH 2/5] Better handling of the default cases --- spec/PushWorker.spec.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index b9b826b2a7..f5c5a3843e 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -148,5 +148,12 @@ describe('PushWorker', () => { } }); }); + + it('should properly handle defaut cases', () => { + expect(PushUtils.transformPushBodyForLocale({})).toEqual({}); + expect(PushUtils.stripLocalesFromBody({})).toEqual({}); + expect(PushUtils.bodiesPerLocales({where: {}})).toEqual({default: {where: {}}}); + expect(PushUtils.groupByLocaleIdentifier([])).toEqual({default: []}); + }); }); }); From e1b592ab3b79eae35cc907a5c22119039416c7b0 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 1 Sep 2017 14:22:04 -0400 Subject: [PATCH 3/5] Updates changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3689fb9ea..f8e8b219cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) From 59322cf8b31c154398d81c6ad13ea67aaed9b9db Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 1 Sep 2017 14:44:47 -0400 Subject: [PATCH 4/5] nits --- src/Controllers/PushController.js | 2 +- src/Push/PushWorker.js | 14 +++++--------- src/Routers/PushRouter.js | 4 +--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 9cc5c09d79..3cae892bc3 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -61,7 +61,7 @@ export class PushController { }).catch((err) => { return pushStatus.fail(err).then(() => { throw err; - }) + }); }); } diff --git a/src/Push/PushWorker.js b/src/Push/PushWorker.js index 0260e744c3..b6d23955cb 100644 --- a/src/Push/PushWorker.js +++ b/src/Push/PushWorker.js @@ -12,19 +12,15 @@ import { PushQueue } from './PushQueue'; const UNSUPPORTED_BADGE_KEY = "unsupported"; -function groupBy(key, objects) { - return objects.reduce((map, object) => { - const value = object[key] + ''; - map[value] = map[value] || []; - map[value].push(object); +function groupByBadge(installations) { + return installations.reduce((map, installation) => { + const badge = installation.badge + ''; + map[badge] = map[badge] || []; + map[badge].push(installation); return map; }, {}); } -function groupByBadge(installations) { - return groupBy('badge', installations); -} - export class PushWorker { subscriber: ?any; adapter: any; diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index a042aadc30..03889c1c6f 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -28,9 +28,7 @@ export class PushRouter extends PromiseRouter { result: true } }); - }).catch((err) => { - req.config.loggerController.error(err); - }); + }).catch(req.config.loggerController.error); return promise; } From 4887f884ea336e91a0cbc4f79aa7c4eca0738144 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 1 Sep 2017 14:59:59 -0400 Subject: [PATCH 5/5] nits --- spec/PushWorker.spec.js | 2 +- src/Push/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index f5c5a3843e..b186c6e3f8 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -149,7 +149,7 @@ describe('PushWorker', () => { }); }); - it('should properly handle defaut cases', () => { + it('should properly handle default cases', () => { expect(PushUtils.transformPushBodyForLocale({})).toEqual({}); expect(PushUtils.stripLocalesFromBody({})).toEqual({}); expect(PushUtils.bodiesPerLocales({where: {}})).toEqual({default: {where: {}}}); diff --git a/src/Push/utils.js b/src/Push/utils.js index 1d21c236bd..ed33a66a14 100644 --- a/src/Push/utils.js +++ b/src/Push/utils.js @@ -70,7 +70,7 @@ export function groupByLocaleIdentifier(installations, locales = []) { if (added) { return; } - if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) == 0) { + if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) === 0) { added = true; map[locale] = map[locale] || []; map[locale].push(installation);