Skip to content

Commit abe6bb7

Browse files
committed
refactor(push_notifications): improve breaking news notification logic
- Persist InAppNotification before sending push notification - Personalize notification payload for each user - Log errors for individual user notifications - Target specific user devices for notification delivery
1 parent 7f6cf17 commit abe6bb7

File tree

1 file changed

+79
-82
lines changed

1 file changed

+79
-82
lines changed

lib/src/services/push_notification_service.dart

Lines changed: 79 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,26 @@ class DefaultPushNotificationService implements IPushNotificationService {
3232
/// {@macro default_push_notification_service}
3333
DefaultPushNotificationService({
3434
required DataRepository<PushNotificationDevice>
35-
pushNotificationDeviceRepository,
35+
pushNotificationDeviceRepository,
3636
required DataRepository<UserContentPreferences>
37-
userContentPreferencesRepository,
37+
userContentPreferencesRepository,
3838
required DataRepository<RemoteConfig> remoteConfigRepository,
3939
required DataRepository<InAppNotification> inAppNotificationRepository,
4040
required IPushNotificationClient? firebaseClient,
4141
required IPushNotificationClient? oneSignalClient,
4242
required Logger log,
43-
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
44-
_userContentPreferencesRepository = userContentPreferencesRepository,
45-
_remoteConfigRepository = remoteConfigRepository,
46-
_inAppNotificationRepository = inAppNotificationRepository,
47-
_firebaseClient = firebaseClient,
48-
_oneSignalClient = oneSignalClient,
49-
_log = log;
43+
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
44+
_userContentPreferencesRepository = userContentPreferencesRepository,
45+
_remoteConfigRepository = remoteConfigRepository,
46+
_inAppNotificationRepository = inAppNotificationRepository,
47+
_firebaseClient = firebaseClient,
48+
_oneSignalClient = oneSignalClient,
49+
_log = log;
5050

5151
final DataRepository<PushNotificationDevice>
52-
_pushNotificationDeviceRepository;
52+
_pushNotificationDeviceRepository;
5353
final DataRepository<UserContentPreferences>
54-
_userContentPreferencesRepository;
54+
_userContentPreferencesRepository;
5555
final DataRepository<RemoteConfig> _remoteConfigRepository;
5656
final DataRepository<InAppNotification> _inAppNotificationRepository;
5757
final IPushNotificationClient? _firebaseClient;
@@ -113,8 +113,8 @@ class DefaultPushNotificationService implements IPushNotificationService {
113113
// Check if breaking news notifications are enabled.
114114
final isBreakingNewsEnabled =
115115
pushConfig.deliveryConfigs[PushNotificationSubscriptionDeliveryType
116-
.breakingOnly] ??
117-
false;
116+
.breakingOnly] ??
117+
false;
118118

119119
if (!isBreakingNewsEnabled) {
120120
_log.info('Breaking news notifications are disabled. Aborting.');
@@ -123,16 +123,16 @@ class DefaultPushNotificationService implements IPushNotificationService {
123123

124124
// 2. Find all user preferences that contain a saved headline filter
125125
// subscribed to breaking news. This query targets the embedded 'savedHeadlineFilters' array.
126-
final subscribedUserPreferences = await _userContentPreferencesRepository
127-
.readAll(
128-
filter: {
129-
'savedHeadlineFilters.deliveryTypes': {
130-
r'$in': [
131-
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
132-
],
133-
},
134-
},
135-
);
126+
final subscribedUserPreferences =
127+
await _userContentPreferencesRepository.readAll(
128+
filter: {
129+
'savedHeadlineFilters.deliveryTypes': {
130+
r'$in': [
131+
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
132+
],
133+
},
134+
},
135+
);
136136

137137
if (subscribedUserPreferences.items.isEmpty) {
138138
_log.info('No users subscribed to breaking news. Aborting.');
@@ -142,22 +142,21 @@ class DefaultPushNotificationService implements IPushNotificationService {
142142
// 3. Collect all unique user IDs from the preference documents.
143143
// Using a Set automatically handles deduplication.
144144
// The ID of the UserContentPreferences document is the user's ID.
145-
final userIds = subscribedUserPreferences.items
146-
.map((preference) => preference.id)
147-
.toSet();
145+
final userIds =
146+
subscribedUserPreferences.items.map((preference) => preference.id).toSet();
148147

149148
_log.info(
150149
'Found ${subscribedUserPreferences.items.length} users with '
151150
'subscriptions to breaking news.',
152151
);
153152

154153
// 4. Fetch all devices for all subscribed users in a single bulk query.
155-
final allDevicesResponse = await _pushNotificationDeviceRepository
156-
.readAll(
157-
filter: {
158-
'userId': {r'$in': userIds.toList()},
159-
},
160-
);
154+
final allDevicesResponse =
155+
await _pushNotificationDeviceRepository.readAll(
156+
filter: {
157+
'userId': {r'$in': userIds.toList()},
158+
},
159+
);
161160

162161
final allDevices = allDevicesResponse.items;
163162
if (allDevices.isEmpty) {
@@ -183,61 +182,59 @@ class DefaultPushNotificationService implements IPushNotificationService {
183182
return;
184183
}
185184

186-
// 6. Extract the specific tokens for the primary provider.
187-
final tokens = targetedDevices
188-
.map((d) => d.providerTokens[primaryProvider]!)
189-
.toList();
190-
191185
_log.info(
192-
'Found ${tokens.length} devices to target via $primaryProvider.',
186+
'Found ${targetedDevices.length} devices to target via $primaryProvider.',
193187
);
194188

195-
// Before sending the push, persist the InAppNotification record for
196-
// each targeted user. This ensures the notification is available in
197-
// their inbox immediately.
198-
try {
199-
final notificationCreationFutures = userIds.map((userId) {
200-
final notification = InAppNotification(
201-
id: ObjectId().oid,
202-
userId: userId,
203-
payload: PushNotificationPayload(
204-
title: headline.title,
205-
body: headline.excerpt,
206-
imageUrl: headline.imageUrl,
207-
data: {
208-
'headlineId': headline.id,
209-
'contentType': 'headline',
210-
'notificationType':
211-
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
212-
},
213-
),
214-
createdAt: DateTime.now(),
215-
);
216-
return _inAppNotificationRepository.create(item: notification);
217-
});
218-
await Future.wait(notificationCreationFutures);
219-
_log.info('Persisted ${userIds.length} in-app notifications.');
220-
} catch (e, s) {
221-
_log.severe('Failed to persist in-app notifications.', e, s);
222-
}
189+
// 7. Iterate through each subscribed user to create and send a
190+
// personalized notification.
191+
final sendFutures = <Future<void>>[];
192+
for (final userId in userIds) {
193+
// Create the InAppNotification record first to get its unique ID.
194+
final notificationId = ObjectId();
195+
final notification = InAppNotification(
196+
id: notificationId.oid,
197+
userId: userId,
198+
payload: PushNotificationPayload(
199+
title: headline.title,
200+
body: headline.excerpt,
201+
imageUrl: headline.imageUrl,
202+
data: {
203+
'notificationType':
204+
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
205+
'contentType': 'headline',
206+
'headlineId': headline.id,
207+
'notificationId': notificationId.oid,
208+
},
209+
),
210+
createdAt: DateTime.now(),
211+
);
223212

224-
// 7. Construct the notification payload.
225-
final payload = PushNotificationPayload(
226-
title: headline.title,
227-
body: headline.excerpt,
228-
imageUrl: headline.imageUrl,
229-
data: {
230-
'headlineId': headline.id,
231-
'contentType': 'headline',
232-
'notificationType':
233-
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
234-
},
235-
);
213+
try {
214+
await _inAppNotificationRepository.create(item: notification);
236215

237-
await client.sendBulkNotifications(
238-
deviceTokens: tokens,
239-
payload: payload,
240-
);
216+
// Find all device tokens for the current user.
217+
final userDeviceTokens = targetedDevices
218+
.where((d) => d.userId == userId)
219+
.map((d) => d.providerTokens[primaryProvider]!)
220+
.toList();
221+
222+
if (userDeviceTokens.isNotEmpty) {
223+
// Add the send operation to the list of futures.
224+
sendFutures.add(
225+
client.sendBulkNotifications(
226+
deviceTokens: userDeviceTokens,
227+
payload: notification.payload,
228+
),
229+
);
230+
}
231+
} catch (e, s) {
232+
_log.severe('Failed to process notification for user $userId.', e, s);
233+
}
234+
}
235+
236+
// Await all the send operations to complete in parallel.
237+
await Future.wait(sendFutures);
241238

242239
_log.info(
243240
'Successfully dispatched breaking news notification for headline: '

0 commit comments

Comments
 (0)