Skip to content

Commit e9b97ad

Browse files
authored
Merge pull request bitpay#3012 from rastajpa/feat/push-notifications
[FEAT][BWS]: fcm data messages
2 parents af0dafc + da2bcd7 commit e9b97ad

File tree

8 files changed

+473
-129
lines changed

8 files changed

+473
-129
lines changed

packages/bitcore-wallet-service/src/lib/common/defaults.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,5 +223,8 @@ module.exports = {
223223
MIN_GAS_LIMIT: 21000,
224224

225225
// XRP has a non-refundable mininum activation fee / balance
226-
MIN_XRP_BALANCE: 20000000
226+
MIN_XRP_BALANCE: 20000000,
227+
228+
// Time to get the latest push notification subscriptions. In ms.
229+
PUSH_NOTIFICATION_SUBS_TIME: 10 * 60 * 1000 // 10 min.
227230
};

packages/bitcore-wallet-service/src/lib/pushnotificationsservice.ts

Lines changed: 184 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,69 @@ const Utils = require('./common/utils');
1616
const Defaults = require('./common/defaults');
1717
const Constants = require('./common/constants');
1818
const sjcl = require('sjcl');
19+
const $ = require('preconditions').singleton();
1920

2021
const PUSHNOTIFICATIONS_TYPES = {
2122
NewCopayer: {
22-
filename: 'new_copayer'
23+
filename: 'new_copayer',
24+
notifyCreator: false,
25+
notifyCreatorForegroundOnly: true,
26+
notifyOthers: true
2327
},
2428
WalletComplete: {
25-
filename: 'wallet_complete'
29+
filename: 'wallet_complete',
30+
notifyCreator: true,
31+
notifyOthers: true
2632
},
2733
NewTxProposal: {
28-
filename: 'new_tx_proposal'
34+
filename: 'new_tx_proposal',
35+
notifyCreator: false,
36+
notifyCreatorForegroundOnly: true,
37+
notifyOthers: true
2938
},
3039
NewOutgoingTx: {
31-
filename: 'new_outgoing_tx'
40+
filename: 'new_outgoing_tx',
41+
notifyCreator: true,
42+
notifyOthers: true
3243
},
3344
NewIncomingTx: {
34-
filename: 'new_incoming_tx'
45+
filename: 'new_incoming_tx',
46+
notifyCreator: true,
47+
notifyOthers: true
3548
},
3649
TxProposalFinallyRejected: {
37-
filename: 'txp_finally_rejected'
50+
filename: 'txp_finally_rejected',
51+
notifyCreator: false,
52+
notifyCreatorForegroundOnly: true,
53+
notifyOthers: true
3854
},
3955
TxConfirmation: {
4056
filename: 'tx_confirmation',
41-
notifyCreatorOnly: true
57+
notifyCreator: true
58+
},
59+
NewAddress: {
60+
filename: 'empty', // TODO: create templates in case of implement in app notification (eg toast)
61+
notifyCreatorForegroundOnly: true
62+
},
63+
NewBlock: {
64+
notifyCreatorForegroundOnly: true,
65+
filename: 'empty' // TODO: ^
66+
},
67+
TxProposalAcceptedBy: {
68+
notifyCreatorForegroundOnly: true,
69+
filename: 'empty' // TODO: ^
70+
},
71+
TxProposalFinallyAccepted: {
72+
notifyCreatorForegroundOnly: true,
73+
filename: 'empty' // TODO: ^
74+
},
75+
TxProposalRejectedBy: {
76+
notifyCreatorForegroundOnly: true,
77+
filename: 'empty' // TODO: ^
78+
},
79+
TxProposalRemoved: {
80+
notifyCreatorForegroundOnly: true,
81+
filename: 'empty' // TODO: ^
4282
}
4383
};
4484

@@ -153,51 +193,51 @@ export class PushNotificationsService {
153193
this._readAndApplyTemplates(notification, notifType, recipientsList, next);
154194
},
155195
(contents, next) => {
156-
async.map(
157-
recipientsList,
158-
(recipient: IPreferences, next) => {
159-
const content = contents[recipient.language];
196+
this._getSubscriptions(notification, recipientsList, contents, next);
197+
},
198+
(subs, next) => {
199+
const notifications = _.map(subs, sub => {
200+
const tokenAddress =
201+
notification.data && notification.data.tokenAddress ? notification.data.tokenAddress : null;
202+
const multisigContractAddress =
203+
notification.data && notification.data.multisigContractAddress
204+
? notification.data.multisigContractAddress
205+
: null;
206+
207+
const notificationData: any = {
208+
to: sub.token,
209+
priority: 'high',
210+
restricted_package_name: sub.packageName,
211+
data: {
212+
walletId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(notification.walletId || sub.walletId)),
213+
tokenAddress,
214+
multisigContractAddress,
215+
copayerId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(sub.copayerId)),
216+
title: sub?.plain?.subject,
217+
body: sub?.plain?.body,
218+
notification_type: notification.type
219+
}
220+
};
221+
222+
if (notifType.notifyOthers || notifType.notifyCreator) {
223+
notificationData.notification = {
224+
title: sub?.plain?.subject,
225+
body: sub?.plain?.body,
226+
sound: 'default',
227+
click_action: 'FCM_PLUGIN_ACTIVITY',
228+
icon: 'fcm_push_icon'
229+
};
230+
}
231+
return notificationData;
232+
});
160233

161-
this.storage.fetchPushNotificationSubs(recipient.copayerId, (err, subs) => {
162-
if (err) return next(err);
234+
if (notifications && notifications[0] && notifications[0].notification)
235+
$.checkState(
236+
subs.length < 10,
237+
"'Failed state: The recipient list for this push notification is >= 10'"
238+
);
163239

164-
const notifications = _.map(subs, sub => {
165-
const tokenAddress =
166-
notification.data && notification.data.tokenAddress ? notification.data.tokenAddress : null;
167-
const multisigContractAddress =
168-
notification.data && notification.data.multisigContractAddress
169-
? notification.data.multisigContractAddress
170-
: null;
171-
return {
172-
to: sub.token,
173-
priority: 'high',
174-
restricted_package_name: sub.packageName,
175-
notification: {
176-
title: content.plain.subject,
177-
body: content.plain.body,
178-
sound: 'default',
179-
click_action: 'FCM_PLUGIN_ACTIVITY',
180-
icon: 'fcm_push_icon'
181-
},
182-
data: {
183-
walletId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(notification.walletId)),
184-
tokenAddress,
185-
multisigContractAddress,
186-
copayerId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(recipient.copayerId)),
187-
title: content.plain.subject,
188-
body: content.plain.body,
189-
notification_type: notification.type
190-
}
191-
};
192-
});
193-
return next(err, notifications);
194-
});
195-
},
196-
(err, allNotifications) => {
197-
if (err) return next(err);
198-
return next(null, _.flatten(allNotifications));
199-
}
200-
);
240+
return next(err, notifications);
201241
},
202242
(notifications, next) => {
203243
async.each(
@@ -238,57 +278,63 @@ export class PushNotificationsService {
238278
}
239279

240280
_getRecipientsList(notification, notificationType, cb) {
241-
this.storage.fetchWallet(notification.walletId, (err, wallet) => {
242-
if (err) return cb(err);
243-
244-
let unit;
245-
if (wallet.coin != Defaults.COIN) {
246-
unit = wallet.coin;
247-
}
281+
if (notification.type !== 'NewBlock') {
282+
this.storage.fetchWallet(notification.walletId, (err, wallet) => {
283+
if (err) return cb(err);
248284

249-
this.storage.fetchPreferences(notification.walletId, null, (err, preferences) => {
250-
if (err) logger.error(err);
251-
if (_.isEmpty(preferences)) preferences = [];
285+
let unit;
286+
if (wallet && wallet.coin != Defaults.COIN) {
287+
unit = wallet.coin;
288+
}
252289

253-
const recipientPreferences = _.compact(
254-
_.map(preferences, p => {
255-
if (!_.includes(this.availableLanguages, p.language)) {
256-
if (p.language) logger.warn('Language for notifications "' + p.language + '" not available.');
257-
p.language = this.defaultLanguage;
258-
}
290+
this.storage.fetchPreferences(notification.walletId, null, (err, preferences) => {
291+
if (err) logger.error(err);
292+
if (_.isEmpty(preferences)) preferences = [];
259293

260-
return {
261-
copayerId: p.copayerId,
262-
language: p.language,
263-
unit: unit || p.unit || this.defaultUnit
264-
};
265-
})
266-
);
294+
const recipientPreferences = _.compact(
295+
_.map(preferences, p => {
296+
if (!_.includes(this.availableLanguages, p.language)) {
297+
if (p.language) logger.warn('Language for notifications "' + p.language + '" not available.');
298+
p.language = this.defaultLanguage;
299+
}
267300

268-
const copayers = _.keyBy(recipientPreferences, 'copayerId');
269-
270-
const recipientsList = _.compact(
271-
_.map(wallet.copayers, copayer => {
272-
if (
273-
(copayer.id == notification.creatorId && notificationType.notifyCreatorOnly) ||
274-
(copayer.id != notification.creatorId && !notificationType.notifyCreatorOnly)
275-
) {
276-
const p = copayers[copayer.id] || {
277-
language: this.defaultLanguage,
278-
unit: this.defaultUnit
279-
};
280301
return {
281-
copayerId: copayer.id,
282-
language: p.language || this.defaultLanguage,
302+
copayerId: p.copayerId,
303+
language: p.language,
283304
unit: unit || p.unit || this.defaultUnit
284305
};
285-
}
286-
})
287-
);
288-
289-
return cb(null, recipientsList);
306+
})
307+
);
308+
309+
const copayers = _.keyBy(recipientPreferences, 'copayerId');
310+
311+
const recipientsList = wallet
312+
? _.compact(
313+
_.map(wallet.copayers, copayer => {
314+
if (
315+
(copayer.id == notification.creatorId &&
316+
(notificationType.notifyCreator || notificationType.notifyCreatorForegroundOnly)) ||
317+
(copayer.id != notification.creatorId &&
318+
(!notificationType.notifyCreatorOnly || !notificationType.notifyCreatorForegroundOnly))
319+
) {
320+
const p = copayers[copayer.id] || {
321+
language: this.defaultLanguage,
322+
unit: this.defaultUnit
323+
};
324+
return {
325+
walletId: notification.walletId,
326+
copayerId: copayer.id,
327+
language: p.language || this.defaultLanguage,
328+
unit: unit || p.unit || this.defaultUnit
329+
};
330+
}
331+
})
332+
)
333+
: [];
334+
return cb(null, recipientsList);
335+
});
290336
});
291-
});
337+
} else return cb(null, []);
292338
}
293339

294340
_readAndApplyTemplates(notification, notifType, recipientsList, cb) {
@@ -443,6 +489,51 @@ export class PushNotificationsService {
443489
};
444490
}
445491

492+
_getSubscriptions(notification, recipientsList, contents, cb) {
493+
if (notification.type !== 'NewBlock') {
494+
async.map(
495+
recipientsList,
496+
(recipient: IPreferences, next) => {
497+
const content = contents ? contents[recipient.language] : null;
498+
499+
this.storage.fetchPushNotificationSubs(recipient.copayerId, (err, subs) => {
500+
if (err) return next(err);
501+
502+
subs[0].plain = content.plain;
503+
return next(err, subs);
504+
});
505+
},
506+
(err, allSubs) => {
507+
if (err) return cb(err);
508+
return cb(null, _.flatten(allSubs));
509+
}
510+
);
511+
} else {
512+
this.storage.fetchLatestPushNotificationSubs((err, subs) => {
513+
if (err) return cb(err);
514+
515+
logger.info(
516+
`Sending NewBlock [${notification.data.coin}/${notification.data.network}] notifications to: ${subs.length} subscribers`
517+
);
518+
async.map(
519+
subs,
520+
(sub: any, next) => {
521+
this.storage.fetchCopayerLookup(sub.copayerId, (err, wallet) => {
522+
if (err) return cb(err);
523+
524+
sub.walletId = wallet.walletId;
525+
return next(err, sub);
526+
});
527+
},
528+
(err, subs) => {
529+
if (err) return cb(err);
530+
return cb(null, _.flatten(subs));
531+
}
532+
);
533+
});
534+
}
535+
}
536+
446537
_makeRequest(opts, cb) {
447538
this.request(
448539
{

packages/bitcore-wallet-service/src/lib/storage.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ const collections = {
4141

4242
const Common = require('./common');
4343
const Constants = Common.Constants;
44+
const Defaults = Common.Defaults;
45+
46+
const ObjectID = mongodb.ObjectID;
47+
48+
var objectIdDate = function(date) {
49+
return Math.floor(date / 1000).toString(16) + '0000000000000000';
50+
};
4451
export class Storage {
4552
static BCHEIGHT_KEY = 'bcheight';
4653
static collections = collections;
@@ -1384,6 +1391,28 @@ export class Storage {
13841391
});
13851392
}
13861393

1394+
fetchLatestPushNotificationSubs(cb) {
1395+
const fromDate = new Date().getTime() - Defaults.PUSH_NOTIFICATION_SUBS_TIME;
1396+
this.db
1397+
.collection(collections.PUSH_NOTIFICATION_SUBS)
1398+
.find({
1399+
_id: {
1400+
$gte: new ObjectID(objectIdDate(fromDate))
1401+
}
1402+
})
1403+
.sort({ _id: -1 })
1404+
.toArray((err, result) => {
1405+
if (err) return cb(err);
1406+
1407+
if (!result) return cb();
1408+
1409+
const tokens = _.map([].concat(result), r => {
1410+
return PushNotificationSub.fromObj(r);
1411+
});
1412+
return cb(null, tokens);
1413+
});
1414+
}
1415+
13871416
storePushNotificationSub(pushNotificationSub, cb) {
13881417
this.db.collection(collections.PUSH_NOTIFICATION_SUBS).replaceOne(
13891418
{

packages/bitcore-wallet-service/templates/en/empty.plain

Whitespace-only changes.

packages/bitcore-wallet-service/templates/es/empty.plain

Whitespace-only changes.

packages/bitcore-wallet-service/templates/fr/empty.plain

Whitespace-only changes.

packages/bitcore-wallet-service/templates/ja/empty.plain

Whitespace-only changes.

0 commit comments

Comments
 (0)