Skip to content

Commit 91d8aa7

Browse files
chore(firebase_messaging): update example app to prove iOS message handlers work as intended (#9292)
1 parent c5318ea commit 91d8aa7

File tree

7 files changed

+235
-62
lines changed

7 files changed

+235
-62
lines changed

docs/cloud-messaging/receive.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,68 @@ Next, the worker must be registered. Within the entry file, **after** the `main.
200200

201201
Next restart your Flutter application. The worker will be registered and any background messages will be handled via this file.
202202

203+
### Handling Interaction
204+
205+
Since notifications are a visible cue, it is common for users to interact with them (by pressing). The default behavior on both Android & iOS is to open the
206+
application. If the application is terminated it will be started, if it is in the background it will be brought to the foreground.
207+
208+
Depending on the content of a notification, you may wish to handle the users interaction when the application opens. For example, if a new chat message is sent via
209+
a notification and the user presses it, you may want to open the specific conversation when the application opens.
210+
211+
The `firebase-messaging` package provides two ways to handle this interaction:
212+
213+
1. `getInitialMessage()`: If the application is opened from a terminated state a `Future` containing a `RemoteMessage` will be returned. Once consumed, the `RemoteMessage` will be removed.
214+
2. `onMessageOpenedApp`: A `Stream` which posts a `RemoteMessage` when the application is opened from a background state.
215+
216+
It is recommended that both scenarios are handled to ensure a smooth UX for your users. The code example below outlines how this can be achieved:
217+
218+
```dart
219+
class Application extends StatefulWidget {
220+
@override
221+
State<StatefulWidget> createState() => _Application();
222+
}
223+
224+
class _Application extends State<Application> {
225+
// It is assumed that all messages contain a data field with the key 'type'
226+
Future<void> setupInteractedMessage() async {
227+
// Get any messages which caused the application to open from
228+
// a terminated state.
229+
RemoteMessage? initialMessage =
230+
await FirebaseMessaging.instance.getInitialMessage();
231+
232+
// If the message also contains a data property with a "type" of "chat",
233+
// navigate to a chat screen
234+
if (initialMessage != null) {
235+
_handleMessage(initialMessage);
236+
}
237+
238+
// Also handle any interaction when the app is in the background via a
239+
// Stream listener
240+
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
241+
}
242+
243+
void _handleMessage(RemoteMessage message) {
244+
if (message.data['type'] == 'chat') {
245+
Navigator.pushNamed(context, '/chat',
246+
arguments: ChatArguments(message),
247+
);
248+
}
249+
}
250+
251+
@override
252+
void initState() {
253+
super.initState();
254+
255+
// Run code required to handle interacted messages in an async function
256+
// as initState() must not be async
257+
setupInteractedMessage();
258+
}
259+
260+
@override
261+
Widget build(BuildContext context) {
262+
return Text("...");
263+
}
264+
}
265+
```
266+
267+
How you handle interaction depends on your application setup, the above example shows a basic illustration using a StatefulWidget.

packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 50;
6+
objectVersion = 51;
77
objects = {
88

99
/* Begin PBXBuildFile section */
1010
00E92990C987F9E25B63A112 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45E51992A527D76267EB20C4 /* Pods_Runner.framework */; };
1111
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1212
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
13-
465BDD1E283BB5B000437DF4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 465BDD1D283BB5B000437DF4 /* GoogleService-Info.plist */; };
1413
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
1514
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
1615
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -37,7 +36,6 @@
3736
27715A442538A1AE00757C2A /* Firebase Cloud Messaging Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Firebase Cloud Messaging Example.entitlements"; sourceTree = "<group>"; };
3837
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3938
45E51992A527D76267EB20C4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
40-
465BDD1D283BB5B000437DF4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
4139
5213D4DB21693B7FDB92C6A0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
4240
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
4341
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
@@ -90,7 +88,6 @@
9088
97C146E51CF9000F007C117D = {
9189
isa = PBXGroup;
9290
children = (
93-
465BDD1D283BB5B000437DF4 /* GoogleService-Info.plist */,
9491
9740EEB11CF90186004384FC /* Flutter */,
9592
97C146F01CF9000F007C117D /* Runner */,
9693
97C146EF1CF9000F007C117D /* Products */,
@@ -204,7 +201,6 @@
204201
files = (
205202
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
206203
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
207-
465BDD1E283BB5B000437DF4 /* GoogleService-Info.plist in Resources */,
208204
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
209205
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
210206
);

packages/firebase_messaging/firebase_messaging/example/lib/firebase_options.dart

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
// File generated by FlutterFire CLI.
2+
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
23
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
34
import 'package:flutter/foundation.dart'
45
show defaultTargetPlatform, kIsWeb, TargetPlatform;
56

7+
/// Default [FirebaseOptions] for use with your Firebase apps.
8+
///
9+
/// Example:
10+
/// ```dart
11+
/// import 'firebase_options.dart';
12+
/// // ...
13+
/// await Firebase.initializeApp(
14+
/// options: DefaultFirebaseOptions.currentPlatform,
15+
/// );
16+
/// ```
617
class DefaultFirebaseOptions {
718
static FirebaseOptions get currentPlatform {
819
if (kIsWeb) {
920
return web;
1021
}
11-
// ignore: missing_enum_constant_in_switch
1222
switch (defaultTargetPlatform) {
1323
case TargetPlatform.android:
1424
return android;
1525
case TargetPlatform.iOS:
1626
return ios;
1727
case TargetPlatform.macOS:
1828
return macos;
29+
case TargetPlatform.windows:
30+
throw UnsupportedError(
31+
'DefaultFirebaseOptions have not been configured for windows - '
32+
'you can reconfigure this by running the FlutterFire CLI again.',
33+
);
34+
case TargetPlatform.linux:
35+
throw UnsupportedError(
36+
'DefaultFirebaseOptions have not been configured for linux - '
37+
'you can reconfigure this by running the FlutterFire CLI again.',
38+
);
39+
default:
40+
throw UnsupportedError(
41+
'DefaultFirebaseOptions are not supported for this platform.',
42+
);
1943
}
20-
21-
throw UnsupportedError(
22-
'DefaultFirebaseOptions are not supported for this platform.',
23-
);
2444
}
2545

2646
static const FirebaseOptions web = FirebaseOptions(

packages/firebase_messaging/firebase_messaging/example/lib/main.dart

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,57 +18,111 @@ import 'permissions.dart';
1818
import 'token_monitor.dart';
1919
import 'firebase_options.dart';
2020

21+
/// Working example of FirebaseMessaging.
22+
/// Please use this in order to verify messages are working in foreground, background & terminated state.
23+
/// Setup your app following this guide:
24+
/// https://firebase.google.com/docs/cloud-messaging/flutter/client#platform-specific_setup_and_requirements):
25+
///
26+
/// Once you've completed platform specific requirements, follow these instructions:
27+
/// 1. Install melos tool by running `flutter pub global activate melos`.
28+
/// 2. Run `melos bootstrap` in FlutterFire project.
29+
/// 3. In your terminal, root to ./packages/firebase_messaging/firebase_messaging/example directory.
30+
/// 4. Run `flutterfire configure` in the example/ directory to setup your app with your Firebase project.
31+
/// 5. Run the app on an actual device for iOS, android is fine to run on an emulator.
32+
/// 6. Use the following script to send a message to your device: scripts/send-message.js. To run this script,
33+
/// you will need nodejs installed on your computer. Then the following:
34+
/// a. Download a service account key (JSON file) from your Firebase console, rename it to "google-services.json" and add to the example/scripts directory.
35+
/// b. Ensure your device/emulator is running, and run the FirebaseMessaging example app using `flutter run --no-pub`.
36+
/// c. Copy the token that is printed in the console and paste it here: https://github.com/firebase/flutterfire/blob/01b4d357e1/packages/firebase_messaging/firebase_messaging/example/lib/main.dart#L32
37+
/// c. From your terminal, root to example/scripts directory & run `npm install`.
38+
/// d. Run `npm run send-message` in the example/scripts directory and your app will receive messages in any state; foreground, background, terminated.
39+
/// Note: Flutter API documentation for receiving messages: https://firebase.google.com/docs/cloud-messaging/flutter/receive
40+
/// Note: If you find your messages have stopped arriving, it is extremely likely they are being throttled by the platform. iOS in particular
41+
/// are aggressive with their throttling policy.
42+
///
43+
/// To verify that your messages are being received, you ought to see a notification appearon your device/emulator via the flutter_local_notifications plugin.
2144
/// Define a top-level named handler which background/terminated messages will
2245
/// call.
23-
///
24-
/// To verify things are working, check out the native platform logs.
2546
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
47+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
48+
await setupFlutterNotifications();
49+
showFlutterNotification(message);
2650
// If you're going to use other Firebase services in the background, such as Firestore,
2751
// make sure you call `initializeApp` before using other Firebase services.
28-
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
2952
print('Handling a background message ${message.messageId}');
3053
}
3154

3255
/// Create a [AndroidNotificationChannel] for heads up notifications
3356
late AndroidNotificationChannel channel;
3457

58+
bool isFlutterLocalNotificationsInitialized = false;
59+
60+
Future<void> setupFlutterNotifications() async {
61+
if (isFlutterLocalNotificationsInitialized) {
62+
return;
63+
}
64+
channel = const AndroidNotificationChannel(
65+
'high_importance_channel', // id
66+
'High Importance Notifications', // title
67+
description:
68+
'This channel is used for important notifications.', // description
69+
importance: Importance.high,
70+
);
71+
72+
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
73+
74+
/// Create an Android Notification Channel.
75+
///
76+
/// We use this channel in the `AndroidManifest.xml` file to override the
77+
/// default FCM channel to enable heads up notifications.
78+
await flutterLocalNotificationsPlugin
79+
.resolvePlatformSpecificImplementation<
80+
AndroidFlutterLocalNotificationsPlugin>()
81+
?.createNotificationChannel(channel);
82+
83+
/// Update the iOS foreground notification presentation options to allow
84+
/// heads up notifications.
85+
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
86+
alert: true,
87+
badge: true,
88+
sound: true,
89+
);
90+
isFlutterLocalNotificationsInitialized = true;
91+
}
92+
93+
void showFlutterNotification(RemoteMessage message) {
94+
RemoteNotification? notification = message.notification;
95+
AndroidNotification? android = message.notification?.android;
96+
if (notification != null && android != null && !kIsWeb) {
97+
flutterLocalNotificationsPlugin.show(
98+
notification.hashCode,
99+
notification.title,
100+
notification.body,
101+
NotificationDetails(
102+
android: AndroidNotificationDetails(
103+
channel.id,
104+
channel.name,
105+
channelDescription: channel.description,
106+
// TODO add a proper drawable resource to android, for now using
107+
// one that already exists in example app.
108+
icon: 'launch_background',
109+
),
110+
),
111+
);
112+
}
113+
}
114+
35115
/// Initialize the [FlutterLocalNotificationsPlugin] package.
36116
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
37117

38118
Future<void> main() async {
39119
WidgetsFlutterBinding.ensureInitialized();
40-
await Firebase.initializeApp();
120+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
41121
// Set the background messaging handler early on, as a named top-level function
42122
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
43123

44124
if (!kIsWeb) {
45-
channel = const AndroidNotificationChannel(
46-
'high_importance_channel', // id
47-
'High Importance Notifications', // title
48-
description:
49-
'This channel is used for important notifications.', // description
50-
importance: Importance.high,
51-
);
52-
53-
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
54-
55-
/// Create an Android Notification Channel.
56-
///
57-
/// We use this channel in the `AndroidManifest.xml` file to override the
58-
/// default FCM channel to enable heads up notifications.
59-
await flutterLocalNotificationsPlugin
60-
.resolvePlatformSpecificImplementation<
61-
AndroidFlutterLocalNotificationsPlugin>()
62-
?.createNotificationChannel(channel);
63-
64-
/// Update the iOS foreground notification presentation options to allow
65-
/// heads up notifications.
66-
await FirebaseMessaging.instance
67-
.setForegroundNotificationPresentationOptions(
68-
alert: true,
69-
badge: true,
70-
sound: true,
71-
);
125+
await setupFlutterNotifications();
72126
}
73127

74128
runApp(MessagingExampleApp());
@@ -132,27 +186,7 @@ class _Application extends State<Application> {
132186
}
133187
});
134188

135-
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
136-
RemoteNotification? notification = message.notification;
137-
AndroidNotification? android = message.notification?.android;
138-
if (notification != null && android != null && !kIsWeb) {
139-
flutterLocalNotificationsPlugin.show(
140-
notification.hashCode,
141-
notification.title,
142-
notification.body,
143-
NotificationDetails(
144-
android: AndroidNotificationDetails(
145-
channel.id,
146-
channel.name,
147-
channelDescription: channel.description,
148-
// TODO add a proper drawable resource to android, for now using
149-
// one that already exists in example app.
150-
icon: 'launch_background',
151-
),
152-
),
153-
);
154-
}
155-
});
189+
FirebaseMessaging.onMessage.listen(showFlutterNotification);
156190

157191
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
158192
print('A new onMessageOpenedApp event was published!');

packages/firebase_messaging/firebase_messaging/example/macos/Runner.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
7373
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7474
3D7BD4B06D0869EA1407E048 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
75+
6BDD63DB43C6689603A39034 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
7576
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7677
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
7778
B550B1FB23F53648007DADD5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
@@ -118,6 +119,7 @@
118119
33CC10EE2044A3C60003C045 /* Products */,
119120
D73912EC22F37F3D000D13A0 /* Frameworks */,
120121
286E7513A68DD39907D77423 /* Pods */,
122+
6BDD63DB43C6689603A39034 /* GoogleService-Info.plist */,
121123
);
122124
sourceTree = "<group>";
123125
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "firebase-messaging-scripts",
3+
"description": "Used to demonstrate sending a RemoteMessage to a client.",
4+
"scripts": {
5+
"send-message": "node send-message.js"
6+
},
7+
"dependencies": {
8+
"firebase-admin": "^11.0.1"
9+
}
10+
}

0 commit comments

Comments
 (0)