@@ -18,37 +18,39 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
1818 required this .projectId,
1919 required HttpClient httpClient,
2020 required Logger log,
21- }) : _httpClient = httpClient,
22- _log = log;
21+ }) : _httpClient = httpClient,
22+ _log = log;
2323
2424 /// The Firebase Project ID for push notifications.
2525 final String projectId;
2626 final HttpClient _httpClient;
2727 final Logger _log;
2828
2929 @override
30- Future <void > sendNotification ({
30+ Future <PushNotificationResult > sendNotification ({
3131 required String deviceToken,
3232 required PushNotificationPayload payload,
33- }) async {
33+ }) {
3434 // For consistency, the single send method now delegates to the bulk
3535 // method with a list containing just one token.
36- await sendBulkNotifications (
36+ return sendBulkNotifications (
3737 deviceTokens: [deviceToken],
3838 payload: payload,
3939 );
4040 }
4141
4242 @override
43- Future <void > sendBulkNotifications ({
43+ Future <PushNotificationResult > sendBulkNotifications ({
4444 required List <String > deviceTokens,
4545 required PushNotificationPayload payload,
4646 }) async {
4747 if (deviceTokens.isEmpty) {
4848 _log.info ('No device tokens provided for Firebase bulk send. Aborting.' );
49- return ;
49+ return const PushNotificationResult (
50+ sentTokens: [],
51+ failedTokens: [],
52+ );
5053 }
51-
5254 _log.info (
5355 'Sending Firebase bulk notification to ${deviceTokens .length } devices '
5456 'for project "$projectId ".' ,
@@ -57,6 +59,9 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
5759 // The FCM v1 batch API has a limit of 500 messages per request.
5860 // We must chunk the tokens into batches of this size.
5961 const batchSize = 500 ;
62+ final allSentTokens = < String > [];
63+ final allFailedTokens = < String > [];
64+
6065 for (var i = 0 ; i < deviceTokens.length; i += batchSize) {
6166 final batch = deviceTokens.sublist (
6267 i,
@@ -66,22 +71,29 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
6671 );
6772
6873 // Send each chunk as a separate batch request.
69- await _sendBatch (
74+ final batchResult = await _sendBatch (
7075 batchNumber: (i ~ / batchSize) + 1 ,
7176 totalBatches: (deviceTokens.length / batchSize).ceil (),
7277 deviceTokens: batch,
7378 payload: payload,
7479 );
80+
81+ allSentTokens.addAll (batchResult.sentTokens);
82+ allFailedTokens.addAll (batchResult.failedTokens);
7583 }
84+
85+ return PushNotificationResult (
86+ sentTokens: allSentTokens,
87+ failedTokens: allFailedTokens,
88+ );
7689 }
7790
7891 /// Sends a batch of notifications by dispatching individual requests in
7992 /// parallel.
8093 ///
81- /// This approach is simpler and more robust than using the `batch` endpoint,
82- /// as it avoids the complexity of constructing a multipart request body and
83- /// provides clearer error handling for individual message failures.
84- Future <void > _sendBatch ({
94+ /// This method processes the results to distinguish between successful and
95+ /// failed sends, returning a [PushNotificationResult] .
96+ Future <PushNotificationResult > _sendBatch ({
8597 required int batchNumber,
8698 required int totalBatches,
8799 required List <String > deviceTokens,
@@ -114,63 +126,52 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
114126 return _httpClient.post <void >(url, data: requestBody);
115127 }).toList ();
116128
117- try {
118- // `eagerError: false` ensures that all futures complete, even if some
119- // fail. The results list will contain Exception objects for failures.
120- final results = await Future .wait <dynamic >(
121- sendFutures,
122- eagerError: false ,
123- );
129+ // `eagerError: false` ensures that all futures complete, even if some
130+ // fail. The results list will contain Exception objects for failures.
131+ final results = await Future .wait <dynamic >(
132+ sendFutures,
133+ eagerError: false ,
134+ );
124135
125- final failedResults = results.whereType <Exception >().toList ();
136+ final sentTokens = < String > [];
137+ final failedTokens = < String > [];
126138
127- if (failedResults.isEmpty) {
128- _log.info (
129- 'Successfully sent Firebase batch of ${deviceTokens .length } '
130- 'notifications for project "$projectId ".' ,
131- );
132- } else {
133- _log.warning (
134- 'Batch $batchNumber /$totalBatches : '
135- '${failedResults .length } of ${deviceTokens .length } Firebase '
136- 'notifications failed to send for project "$projectId ".' ,
137- );
138- for (final error in failedResults) {
139- if (error is HttpException ) {
140- // Downgrade log level for invalid tokens (NotFoundException), which
141- // is an expected occurrence. Other HTTP errors are still severe.
142- if (error is NotFoundException ) {
143- _log.info (
144- 'Batch $batchNumber /$totalBatches : Failed to send to an '
145- 'invalid/unregistered token: ${error .message }' ,
146- );
147- } else {
148- _log.severe (
149- 'Batch $batchNumber /$totalBatches : HTTP error sending '
150- 'Firebase notification: ${error .message }' ,
151- error,
152- );
153- }
154- } else {
155- _log.severe (
156- 'Unexpected error sending Firebase notification.' ,
157- error,
158- );
159- }
139+ for (var i = 0 ; i < results.length; i++ ) {
140+ final result = results[i];
141+ final token = deviceTokens[i];
142+
143+ if (result is Exception ) {
144+ failedTokens.add (token);
145+ if (result is NotFoundException ) {
146+ // This is an expected failure when a token is unregistered (e.g.,
147+ // app uninstalled). Log it as info for cleanup purposes.
148+ _log.info (
149+ 'Batch $batchNumber /$totalBatches : Failed to send to an '
150+ 'invalid/unregistered token: ${result .message }' ,
151+ );
152+ } else if (result is HttpException ) {
153+ _log.severe (
154+ 'Batch $batchNumber /$totalBatches : HTTP error sending '
155+ 'Firebase notification to token "$token ": ${result .message }' ,
156+ result,
157+ );
158+ } else {
159+ _log.severe (
160+ 'Unexpected error sending Firebase notification to token "$token ".' ,
161+ result,
162+ );
160163 }
161- // Throw an exception to indicate that the batch send was not fully successful.
162- throw OperationFailedException (
163- 'Failed to send ${failedResults .length } Firebase notifications.' ,
164- );
164+ } else {
165+ // If there's no exception, the send was successful.
166+ sentTokens.add (token);
165167 }
166- } catch (e, s) {
167- _log.severe (
168- 'Unexpected error processing Firebase batch $batchNumber /$totalBatches '
169- 'results.' ,
170- e,
171- s,
172- );
173- throw OperationFailedException ('Failed to process Firebase batch: $e ' );
174168 }
169+ _log.info (
170+ 'Firebase batch $batchNumber /$totalBatches complete. Success: ${sentTokens .length }, Failed: ${failedTokens .length }.' ,
171+ );
172+ return PushNotificationResult (
173+ sentTokens: sentTokens,
174+ failedTokens: failedTokens,
175+ );
175176 }
176177}
0 commit comments