diff --git a/packages/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/CHANGELOG.md index 834f14e6c54d..d9b063b7a47e 100644 --- a/packages/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.0.4 + +* Added iOS support for background message handling. + ## 7.0.3 - Update a dependency to the latest release. diff --git a/packages/firebase_messaging/README.md b/packages/firebase_messaging/README.md index 14313e6b489e..228be891e4a6 100644 --- a/packages/firebase_messaging/README.md +++ b/packages/firebase_messaging/README.md @@ -52,8 +52,46 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se ``` -#### Optionally handle background messages +### iOS Integration + +To integrate your plugin into the iOS part of your app, follow these steps: + +1. Generate the certificates required by Apple for receiving push notifications following [this guide](https://firebase.google.com/docs/cloud-messaging/ios/certs) in the Firebase docs. You can skip the section titled "Create the Provisioning Profile". + +1. Using the [Firebase Console](https://console.firebase.google.com/) add an iOS app to your project: Follow the assistant, download the generated `GoogleService-Info.plist` file, open `ios/Runner.xcworkspace` with Xcode, and within Xcode place the file inside `ios/Runner`. **Don't** follow the steps named "Add Firebase SDK" and "Add initialization code" in the Firebase assistant. + +1. In Xcode, select `Runner` in the Project Navigator. In the Capabilities Tab turn on `Push Notifications` and `Background Modes`, and enable `Background fetch` and `Remote notifications` under `Background Modes`. + +1. Follow the steps in the "[Upload your APNs certificate](https://firebase.google.com/docs/cloud-messaging/ios/client#upload_your_apns_certificate)" section of the Firebase docs. + +1. If you need to disable the method swizzling done by the FCM iOS SDK (e.g. so that you can use this plugin with other notification plugins) then add the following to your application's `Info.plist` file. + +```xml +FirebaseAppDelegateProxyEnabled + +``` + +After that, add the following lines to the `(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions` +method in the `AppDelegate.m`/`AppDelegate.swift` of your iOS project. + +Objective-C: +```objectivec +if (@available(iOS 10.0, *)) { + [UNUserNotificationCenter currentNotificationCenter].delegate = (id) self; +} +``` + +Swift: +```swift +if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate +} +``` + +### Handle background messages (Optional) + +#### Android configuration >Background message handling is intended to be performed quickly. Do not perform long running tasks as they may not be allowed to finish by the Android system. See [Background Execution Limits](https://developer.android.com/about/versions/oreo/background) @@ -110,6 +148,22 @@ By default background messaging is not enabled. To handle messages in the backgr ``` +#### iOS configuration (Swift) +1. In the top of `AppDelegate.swift`, add the import of firebase_messaging: + + ```swift + import firebase_messaging + ``` + +1. Then add the following code to `AppDelegate.swift` (E.g. last in the function `application` right before the return statement)): + + ```swift + FLTFirebaseMessagingPlugin.setPluginRegistrantCallback({ (registry: FlutterPluginRegistry) -> Void in + GeneratedPluginRegistrant.register(with: registry); + }); + ``` + +#### Usage in the common Dart code 1. Define a **TOP-LEVEL** or **STATIC** function to handle background messages ```dart @@ -155,41 +209,6 @@ By default background messaging is not enabled. To handle messages in the backgr so that it can be ready to receive messages as early as possible. See the [example app](https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging/example) for a demonstration. -### iOS Integration - -To integrate your plugin into the iOS part of your app, follow these steps: - -1. Generate the certificates required by Apple for receiving push notifications following [this guide](https://firebase.google.com/docs/cloud-messaging/ios/certs) in the Firebase docs. You can skip the section titled "Create the Provisioning Profile". - -1. Using the [Firebase Console](https://console.firebase.google.com/) add an iOS app to your project: Follow the assistant, download the generated `GoogleService-Info.plist` file, open `ios/Runner.xcworkspace` with Xcode, and within Xcode place the file inside `ios/Runner`. **Don't** follow the steps named "Add Firebase SDK" and "Add initialization code" in the Firebase assistant. - -1. In Xcode, select `Runner` in the Project Navigator. In the Capabilities Tab turn on `Push Notifications` and `Background Modes`, and enable `Background fetch` and `Remote notifications` under `Background Modes`. - -1. Follow the steps in the "[Upload your APNs certificate](https://firebase.google.com/docs/cloud-messaging/ios/client#upload_your_apns_certificate)" section of the Firebase docs. - -1. If you need to disable the method swizzling done by the FCM iOS SDK (e.g. so that you can use this plugin with other notification plugins) then add the following to your application's `Info.plist` file. - -```xml -FirebaseAppDelegateProxyEnabled - -``` - -After that, add the following lines to the `(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions` -method in the `AppDelegate.m`/`AppDelegate.swift` of your iOS project. - -Objective-C: -```objectivec -if (@available(iOS 10.0, *)) { - [UNUserNotificationCenter currentNotificationCenter].delegate = (id) self; -} -``` - -Swift: -```swift -if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate -} -``` ### Dart/Flutter Integration @@ -205,14 +224,14 @@ Next, you should probably request permissions for receiving Push Notifications. ## Receiving Messages -Messages are sent to your Flutter app via the `onMessage`, `onLaunch`, and `onResume` callbacks that you configured with the plugin during setup. Here is how different message types are delivered on the supported platforms: +Messages are sent to your Flutter app via the `onMessage`, `onLaunch`, `onResume` and `onBackgroundMessage` callbacks that you configured with the plugin during setup. Here is how different message types are delivered on the supported platforms: -| | App in Foreground | App in Background | App Terminated | -| --------------------------: | ----------------- | ----------------- | -------------- | -| **Notification on Android** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | -| **Notification on iOS** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires. | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires. | -| **Data Message on Android** | `onMessage` | `onMessage` while app stays in the background. | *not supported by plugin, message is lost* | -| **Data Message on iOS** | `onMessage` | Message is stored by FCM and delivered to app via `onMessage` when the app is brought back to foreground. | Message is stored by FCM and delivered to app via `onMessage` when the app is brought back to foreground. | +| | App in Foreground | App in Background | App Terminated | +| --------------------------: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Notification on Android** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | +| **Notification on iOS** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires. | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires. | +| **Data Message on Android** | `onMessage` | `onMessage` while app stays in the background. | *not supported by plugin, message is lost* | +| **Data Message on iOS** | `onMessage` | Message is delivered to `onBackgroundMessage`. This is the case both when app is running in background and the system has suspended the app. | When app is force-quit by the user the message is not handled. | Additional reading: Firebase's [About FCM Messages](https://firebase.google.com/docs/cloud-messaging/concept-options). diff --git a/packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m index ec35948f6364..d54ecbbfeaf1 100644 --- a/packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m @@ -15,6 +15,11 @@ @interface FLTFirebaseMessagingPlugin () @end #endif +static NSString *backgroundSetupCallback = @"background_setup_callback"; +static NSString *backgroundMessageCallback = @"background_message_callback"; +static FlutterPluginRegistrantCallback registerPlugins = nil; +typedef void (^FetchCompletionHandler)(UIBackgroundFetchResult result); + static FlutterError *getFlutterError(NSError *error) { if (error == nil) return nil; return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", (long)error.code] @@ -26,17 +31,29 @@ @interface FLTFirebaseMessagingPlugin () @implementation FLTFirebaseMessagingPlugin { FlutterMethodChannel *_channel; + FlutterMethodChannel *_backgroundChannel; + NSUserDefaults *_userDefaults; + NSObject *_registrar; NSDictionary *_launchNotification; + NSMutableArray *_eventQueue; BOOL _resumingFromBackground; + FlutterEngine *_headlessRunner; + BOOL initialized; + FetchCompletionHandler fetchCompletionHandler; +} + ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + registerPlugins = callback; } + (void)registerWithRegistrar:(NSObject *)registrar { + NSLog(@"registerWithRegistrar"); _registrar = registrar; FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging" binaryMessenger:[registrar messenger]]; FLTFirebaseMessagingPlugin *instance = - [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel]; + [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel registrar:registrar]; [registrar addApplicationDelegate:instance]; [registrar addMethodCallDelegate:instance channel:channel]; @@ -46,19 +63,32 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } } -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel + registrar:(NSObject *)registrar { self = [super init]; if (self) { _channel = channel; _resumingFromBackground = NO; [FIRMessaging messaging].delegate = self; + + // Setup background handling + _userDefaults = [NSUserDefaults standardUserDefaults]; + _eventQueue = [[NSMutableArray alloc] init]; + _registrar = registrar; + _headlessRunner = [[FlutterEngine alloc] initWithName:@"firebase_messaging_background" + project:nil + allowHeadlessExecution:YES]; + _backgroundChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/firebase_messaging_background" + binaryMessenger:[_headlessRunner binaryMessenger]]; } return self; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { NSString *method = call.method; + NSLog(@"handleMethodCall : %@", method); if ([@"requestNotificationPermissions" isEqualToString:method]) { NSDictionary *arguments = call.arguments; if (@available(iOS 10.0, *)) { @@ -132,6 +162,30 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[UIApplication sharedApplication] registerForRemoteNotifications]; result([NSNumber numberWithBool:YES]); } + } else if ([@"FcmDartService#start" isEqualToString:method]) { + NSDictionary *arguments = call.arguments; + NSLog(@"FcmDartService#start"); + long setupHandle = [arguments[@"setupHandle"] longValue]; + long backgroundHandle = [arguments[@"backgroundHandle"] longValue]; + NSLog(@"FcmDartService#start with handle : %ld", setupHandle); + [self saveCallbackHandle:backgroundSetupCallback handle:setupHandle]; + [self saveCallbackHandle:backgroundMessageCallback handle:backgroundHandle]; + result(nil); + } else if ([@"FcmDartService#initialized" isEqualToString:method]) { + /** + * Acknowledge that background message handling on the Dart side is ready. This is called by the + * Dart side once all background initialization is complete via `FcmDartService#initialized`. + */ + @synchronized(self) { + initialized = YES; + while ([_eventQueue count] > 0) { + NSArray *call = _eventQueue[0]; + [_eventQueue removeObjectAtIndex:0]; + + [self invokeMethod:call[0] callbackHandle:[call[1] longLongValue] arguments:call[2]]; + } + } + result(nil); } else if ([@"configure" isEqualToString:method]) { [FIRMessaging messaging].shouldEstablishDirectChannel = true; [[UIApplication sharedApplication] registerForRemoteNotifications]; @@ -221,6 +275,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center #endif - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo { + NSLog(@"didReceiveRemoteNotification"); if (_resumingFromBackground) { [_channel invokeMethod:@"onResume" arguments:userInfo]; } else { @@ -252,8 +307,24 @@ - (void)applicationDidBecomeActive:(UIApplication *)application { - (BOOL)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [self didReceiveRemoteNotification:userInfo]; - completionHandler(UIBackgroundFetchResultNoData); + NSLog(@"didReceiveRemoteNotification:completionHandler"); + if (application.applicationState == UIApplicationStateBackground) { + // save this handler for later so it can be completed + fetchCompletionHandler = completionHandler; + + [self queueMethodCall:@"handleBackgroundMessage" + callbackName:backgroundMessageCallback + arguments:userInfo]; + + if (!initialized) { + [self startBackgroundRunner]; + } + + } else { + [self didReceiveRemoteNotification:userInfo]; + completionHandler(UIBackgroundFetchResultNewData); + } + return YES; } @@ -291,4 +362,86 @@ - (void)messaging:(FIRMessaging *)messaging [_channel invokeMethod:@"onMessage" arguments:remoteMessage.appData]; } +- (void)setupBackgroundHandling:(int64_t)handle { + NSLog(@"Setting up Firebase background handling"); + + [self saveCallbackHandle:backgroundSetupCallback handle:handle]; + + NSLog(@"Finished background setup"); +} + +- (void)startBackgroundRunner { + NSLog(@"Starting background runner"); + + int64_t handle = [self getCallbackHandle:backgroundSetupCallback]; + + FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle]; + NSAssert(info != nil, @"failed to find callback"); + NSString *entrypoint = info.callbackName; + NSString *uri = info.callbackLibraryPath; + + [_headlessRunner runWithEntrypoint:entrypoint libraryURI:uri]; + [_registrar addMethodCallDelegate:self channel:_backgroundChannel]; + + // Once our headless runner has been started, we need to register the application's plugins + // with the runner in order for them to work on the background isolate. `registerPlugins` is + // a callback set from AppDelegate.m in the main application. This callback should register + // all relevant plugins (excluding those which require UI). + + NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); + registerPlugins(_headlessRunner); +} + +- (int64_t)getCallbackHandle:(NSString *)key { + NSLog(@"Getting callback handle for key %@", key); + id handle = [_userDefaults objectForKey:key]; + if (handle == nil) { + return 0; + } + return [handle longLongValue]; +} + +- (void)saveCallbackHandle:(NSString *)key handle:(int64_t)handle { + NSLog(@"Saving callback handle for key %@", key); + + [_userDefaults setObject:[NSNumber numberWithLongLong:handle] forKey:key]; +} + +- (void)queueMethodCall:(NSString *)method + callbackName:(NSString *)callback + arguments:(NSDictionary *)arguments { + NSLog(@"Queuing method call: %@", method); + int64_t handle = [self getCallbackHandle:callback]; + + @synchronized(self) { + if (initialized) { + [self invokeMethod:method callbackHandle:handle arguments:arguments]; + } else { + NSArray *call = @[ method, @(handle), arguments ]; + [_eventQueue addObject:call]; + } + } +} + +- (void)invokeMethod:(NSString *)method + callbackHandle:(long)handle + arguments:(NSDictionary *)arguments { + NSLog(@"Invoking method: %@", method); + + NSDictionary *callbackArguments = @{ + @"handle" : @(handle), + @"message" : arguments, + }; + + [_backgroundChannel invokeMethod:method + arguments:callbackArguments + result:^(id _Nullable result) { + NSLog(@"%@ method completed", method); + if (self->fetchCompletionHandler != nil) { + self->fetchCompletionHandler(UIBackgroundFetchResultNewData); + self->fetchCompletionHandler = nil; + } + }]; +} + @end diff --git a/packages/firebase_messaging/pubspec.yaml b/packages/firebase_messaging/pubspec.yaml index 44261112906b..da917d7bca0e 100644 --- a/packages/firebase_messaging/pubspec.yaml +++ b/packages/firebase_messaging/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_messaging description: Flutter plugin for Firebase Cloud Messaging, a cross-platform messaging solution that lets you reliably deliver messages on Android and iOS. homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging -version: 7.0.3 +version: 7.0.4 flutter: plugin: