Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[firebase_messaging] Handle background messages on iOS #2016

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8e841b3
added ios support
tobiasjunsten Dec 29, 2019
4d0844a
- renamed variable and removed commented log
tobiasjunsten Jan 8, 2020
3ca64a2
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Jan 8, 2020
08dcf2e
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Jan 22, 2020
44b68ba
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 4, 2020
a87c807
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 5, 2020
79e3fa8
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 17, 2020
07fc453
[firebase_messaging] Added instructions for ios background handling i…
tobiasjunsten Feb 18, 2020
1824a2a
[firebase_messaging] Fixed some formatting
tobiasjunsten Feb 27, 2020
0225706
[firebase_messaging] Formatted the objective-c code
tobiasjunsten Feb 27, 2020
24cbd6f
Merge master into ios-background-support
tobiasjunsten Mar 27, 2020
34ad709
[firebase_messaging] Fixed readme after merge
tobiasjunsten Mar 27, 2020
afcb41b
[firebase_messaging] updated versions
tobiasjunsten Mar 27, 2020
7132d34
Merge branch 'master' into ios-background-support
tobiasjunsten Jun 17, 2020
566a977
[firebase_messaging] Added import firebase_messaging to the readme.
tobiasjunsten Jun 17, 2020
685f549
Merge branch 'master' into ios-background-support
tobiasjunsten Aug 18, 2020
9d495bf
[firebase_messaging] Updated readme with suggestions from @oskara
tobiasjunsten Aug 18, 2020
43deb43
Merge branch 'master' into ios-background-support
tobiasjunsten Sep 1, 2020
5b71c9a
- fixed spelling mistake in readme
tobiasjunsten Sep 1, 2020
e31f2cd
Merge branch 'master' into ios-background-support
tobiasjunsten Oct 23, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/firebase_messaging/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 6.0.17
* Added iOS support for background message handling.

## 6.0.16

* Update lower bound of dart dependency to 2.0.0.
Expand Down
103 changes: 61 additions & 42 deletions packages/firebase_messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,46 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
```
#### 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
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
```

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<UNUserNotificationCenterDelegate>) 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)
Expand Down Expand Up @@ -110,6 +148,22 @@ By default background messaging is not enabled. To handle messages in the backgr
<application android:name=".Application" ...>
```

#### 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`:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should specify where in AppDelegate.swift? E.g. didFinishLaunchingWithOptions if that is the case.


```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
Expand Down Expand Up @@ -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
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
```

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<UNUserNotificationCenterDelegate>) self;
}
```

Swift:
```swift
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
```

### Dart/Flutter Integration

Expand All @@ -207,12 +226,12 @@ Next, you should probably request permissions for receiving Push Notifications.

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:

| | 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 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. |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? Aren't background messages delivered through onBackgroundMessage after this patch?


Additional reading: Firebase's [About FCM Messages](https://firebase.google.com/docs/cloud-messaging/concept-options).

Expand Down
161 changes: 157 additions & 4 deletions packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ @interface FLTFirebaseMessagingPlugin () <FIRMessagingDelegate>
@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]
Expand All @@ -26,17 +31,29 @@ @interface FLTFirebaseMessagingPlugin () <FIRMessagingDelegate>

@implementation FLTFirebaseMessagingPlugin {
FlutterMethodChannel *_channel;
FlutterMethodChannel *_backgroundChannel;
NSUserDefaults *_userDefaults;
NSObject<FlutterPluginRegistrar> *_registrar;
NSDictionary *_launchNotification;
NSMutableArray *_eventQueue;
BOOL _resumingFromBackground;
FlutterEngine *_headlessRunner;
BOOL initialized;
FetchCompletionHandler fetchCompletionHandler;
}

+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback {
registerPlugins = callback;
}

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)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];

Expand All @@ -46,7 +63,8 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
}
}

- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];

if (self) {
Expand All @@ -58,12 +76,24 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name);
}
[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, *)) {
Expand Down Expand Up @@ -137,6 +167,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];
Expand Down Expand Up @@ -226,6 +280,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
#endif

- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo {
NSLog(@"didReceiveRemoteNotification");
if (_resumingFromBackground) {
[_channel invokeMethod:@"onResume" arguments:userInfo];
} else {
Expand Down Expand Up @@ -269,8 +324,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;
}

Expand Down Expand Up @@ -308,4 +379,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
2 changes: 1 addition & 1 deletion packages/firebase_messaging/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 6.0.16
version: 6.0.17

flutter:
plugin:
Expand Down