@@ -4,7 +4,9 @@ import 'package:firebase_core/firebase_core.dart';
4
4
import 'package:firebase_messaging/firebase_messaging.dart' ;
5
5
import 'package:flutter/foundation.dart' ;
6
6
import 'package:flutter/material.dart' ;
7
- import 'package:flutter/scheduler.dart' ;
7
+ import 'package:flutter/scheduler.dart' hide Priority;
8
+ import 'package:flutter_local_notifications/flutter_local_notifications.dart'
9
+ hide Message;
8
10
import 'package:flutter_secure_storage/flutter_secure_storage.dart' ;
9
11
import 'package:go_router/go_router.dart' ;
10
12
import 'package:provider/provider.dart' ;
@@ -23,6 +25,25 @@ import 'package:stream_chat_localizations/stream_chat_localizations.dart';
23
25
import 'package:stream_chat_persistence/stream_chat_persistence.dart' ;
24
26
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart' ;
25
27
28
+ // Define supported notification types
29
+ const expectedNotificationTypes = [
30
+ EventType .messageNew,
31
+ EventType .messageUpdated,
32
+ EventType .reactionNew,
33
+ EventType .reactionUpdated,
34
+ ];
35
+
36
+ const notificationChannelId = 'stream_GetStreamFlutterClient' ;
37
+ const notificationChannelName = 'Stream Notifications' ;
38
+ const notificationChannelDescription = 'Notifications for Stream messages' ;
39
+
40
+ // Define platform constants
41
+ const bool kIsIOS = bool .fromEnvironment ('dart.io.is_ios' );
42
+
43
+ // Initialize FlutterLocalNotificationsPlugin for background messages
44
+ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
45
+ FlutterLocalNotificationsPlugin ();
46
+
26
47
/// Constructs callback for background notification handling.
27
48
///
28
49
/// Will be invoked from another Isolate, that's why it's required to
@@ -36,10 +57,13 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
36
57
final data = message.data;
37
58
// ensure that Push Notification was sent by Stream.
38
59
if (data['sender' ] != 'stream.chat' ) {
60
+ debugPrint ('[onBackgroundMessage] #firebase; not sent by Stream' );
39
61
return ;
40
62
}
41
- // ensure that Push Notification relates to a new message event.
42
- if (data['type' ] != 'message.new' ) {
63
+ final eventType = data['type' ];
64
+ // ensure that Push Notification relates to a supported event type
65
+ if (! expectedNotificationTypes.contains (eventType)) {
66
+ debugPrint ('[onBackgroundMessage] #firebase; unexpected type: $eventType ' );
43
67
return ;
44
68
}
45
69
// If you're going to use Firebase services in the background, make sure
@@ -55,7 +79,12 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
55
79
userId = await secureStorage.read (key: kStreamUserId);
56
80
token = await secureStorage.read (key: kStreamToken);
57
81
}
58
- if (userId == null || token == null ) {
82
+ if (userId == null ) {
83
+ debugPrint ('[onBackgroundMessage] #firebase; user not found' );
84
+ return ;
85
+ }
86
+ if (token == null ) {
87
+ debugPrint ('[onBackgroundMessage] #firebase; token not found' );
59
88
return ;
60
89
}
61
90
final chatClient = buildStreamChatClient (apiKey ?? kDefaultStreamApiKey);
@@ -71,16 +100,142 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
71
100
await chatPersistentClient.connect (userId);
72
101
}
73
102
74
- final messageId = data['id' ];
103
+ final messageId = data['message_id' ];
104
+ if (messageId == null ) {
105
+ debugPrint ('[onBackgroundMessage] #firebase; messageId not found' );
106
+ return ;
107
+ }
75
108
final cid = data['cid' ];
109
+ if (cid == null ) {
110
+ debugPrint ('[onBackgroundMessage] #firebase; cid not found' );
111
+ return ;
112
+ }
76
113
// pre-cache the new message using client and persistence.
77
114
final response = await chatClient.getMessage (messageId);
78
115
await chatPersistentClient.updateMessages (cid, [response.message]);
116
+
117
+ final title = data['title' ];
118
+ final body = data['body' ];
119
+
120
+ // Show Android notification
121
+ if (! kIsWeb && ! kIsIOS) {
122
+ await _showAndroidNotification (
123
+ eventType: eventType,
124
+ cid: cid,
125
+ messageId: messageId,
126
+ title: message.notification? .title ?? title ?? 'Fallback title' ,
127
+ body: message.notification? .body ?? body ?? 'Fallback body' ,
128
+ );
129
+ }
79
130
} catch (e, stk) {
80
131
debugPrint ('[onBackgroundMessage] #firebase; failed: $e ; $stk ' );
81
132
}
82
133
}
83
134
135
+ /// Shows an Android notification for a background message
136
+ Future <void > _showAndroidNotification ({
137
+ required String eventType,
138
+ required String cid,
139
+ required String messageId,
140
+ required String title,
141
+ required String body,
142
+ }) async {
143
+ try {
144
+ // Create notification channel for Android
145
+ await _createNotificationChannel ();
146
+
147
+ // Initialize the Android notification channel
148
+ const androidNotificationDetails = AndroidNotificationDetails (
149
+ notificationChannelId,
150
+ notificationChannelName,
151
+ channelDescription: notificationChannelDescription,
152
+ importance: Importance .high,
153
+ priority: Priority .high,
154
+ showWhen: true ,
155
+ enableVibration: true ,
156
+ playSound: true ,
157
+ // Using default sound instead of custom sound resource
158
+ // sound: RawResourceAndroidNotificationSound('notification_sound'),
159
+ // Using default app icon instead of custom icon
160
+ // icon: 'ic_notification',
161
+ );
162
+
163
+ const notificationDetails = NotificationDetails (
164
+ android: androidNotificationDetails,
165
+ );
166
+
167
+ // Initialize the plugin with click handler
168
+ // Use the default app icon for notifications
169
+ const initializationSettingsAndroid = AndroidInitializationSettings (
170
+ 'ic_notification_in_app' ,
171
+ );
172
+ const initializationSettings = InitializationSettings (
173
+ android: initializationSettingsAndroid,
174
+ );
175
+
176
+ // Initialize the plugin with a notification click handler
177
+ await flutterLocalNotificationsPlugin.initialize (
178
+ initializationSettings,
179
+ onDidReceiveNotificationResponse: (NotificationResponse response) async {
180
+ debugPrint (
181
+ '[onBackgroundMessage] #firebase; notification clicked: ${response .payload }' );
182
+ // The payload contains the channel information (channelType:channelId)
183
+ // This will be handled when the app is opened
184
+ },
185
+ );
186
+
187
+ // Generate a unique notification ID
188
+ final notificationId = (eventType + cid + messageId).hashCode;
189
+
190
+ // Show the notification
191
+ await flutterLocalNotificationsPlugin.show (
192
+ notificationId, // Notification ID
193
+ title, // Notification title
194
+ body, // Notification body
195
+ notificationDetails,
196
+ payload: cid,
197
+ );
198
+
199
+ debugPrint (
200
+ '[onBackgroundMessage] #firebase; android notification shown successfully: ID=$notificationId , Title="$title "' );
201
+ } catch (e) {
202
+ debugPrint (
203
+ '[onBackgroundMessage] #firebase; failed to show notification: $e ' );
204
+ }
205
+ }
206
+
207
+ /// Creates the notification channel for Android
208
+ Future <void > _createNotificationChannel () async {
209
+ try {
210
+ const channel = AndroidNotificationChannel (
211
+ notificationChannelId,
212
+ notificationChannelName,
213
+ description: notificationChannelDescription,
214
+ importance: Importance .high,
215
+ enableVibration: true ,
216
+ playSound: true ,
217
+ showBadge: true ,
218
+ );
219
+
220
+ // Create the channel
221
+ final androidPlugin =
222
+ flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
223
+ AndroidFlutterLocalNotificationsPlugin > ();
224
+
225
+ if (androidPlugin != null ) {
226
+ await androidPlugin.createNotificationChannel (channel);
227
+ debugPrint (
228
+ '[onBackgroundMessage] #firebase; notification channel created' );
229
+ } else {
230
+ debugPrint (
231
+ '[onBackgroundMessage] #firebase; failed to resolve Android plugin' );
232
+ }
233
+ } catch (e) {
234
+ debugPrint (
235
+ '[onBackgroundMessage] #firebase; notification channel failed: $e ' );
236
+ }
237
+ }
238
+
84
239
final chatPersistentClient = StreamChatPersistenceClient (
85
240
logLevel: Level .SEVERE ,
86
241
);
@@ -200,9 +355,10 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
200
355
debugPrint ('[onMessageOpenedApp] #firebase; message: ${message .toMap ()}' );
201
356
// This callback is getting invoked when the user clicks
202
357
// on the notification in case if notification was shown by OS.
203
- final channelType = (message.data['channel_type' ] as String ? ) ?? '' ;
204
- final channelId = (message.data['channel_id' ] as String ? ) ?? '' ;
205
358
final channelCid = (message.data['cid' ] as String ? ) ?? '' ;
359
+ final parts = channelCid.split (':' );
360
+ final channelType = parts[0 ];
361
+ final channelId = parts[1 ];
206
362
var channel = client.state.channels[channelCid];
207
363
if (channel == null ) {
208
364
channel = client.channel (
0 commit comments