diff --git a/packages/firebase_messaging/firebase_messaging/README.md b/packages/firebase_messaging/firebase_messaging/README.md index 435bc6ec735a..0ad249bee020 100644 --- a/packages/firebase_messaging/firebase_messaging/README.md +++ b/packages/firebase_messaging/firebase_messaging/README.md @@ -16,6 +16,23 @@ To get started with Firebase Cloud Messaging for Flutter, please [see the docume To use this plugin, please visit the [Cloud Messaging Usage documentation](https://firebase.google.com/docs/cloud-messaging) +### iOS apps using UIScene + +Apps that adopt the UIScene lifecycle register Flutter plugins after +`application:didFinishLaunchingWithOptions:`. Apple requires +`UNUserNotificationCenter.delegate` to be configured before that method returns, so configure +Firebase Messaging explicitly from your app delegate: + +```objectivec +#import + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [FLTFirebaseMessagingPlugin configureNotificationCenterDelegate]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} +``` + ## Issues and feedback Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/firebase/flutterfire/issues/new). diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m index 413a0702dcbc..21cf90bc85af 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m @@ -1,11 +1,12 @@ #import "AppDelegate.h" +#import #import "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Override point for customization after application launch. + [FLTFirebaseMessagingPlugin configureNotificationCenterDelegate]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m index 46684663681b..4a4cd46e616b 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m @@ -45,6 +45,7 @@ @implementation FLTFirebaseMessagingPlugin { // Guard against calling setupNotificationHandling twice BOOL _notificationHandlingSetup; + BOOL _applicationObserverRegistered; #if TARGET_OS_OSX // Tracks when plugin registration occurred after the macOS launch notification. @@ -65,15 +66,39 @@ @implementation FLTFirebaseMessagingPlugin { #pragma mark - FlutterPlugin -- (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel - andFlutterPluginRegistrar:(NSObject *)registrar { +- (instancetype)init { self = [super init]; if (self) { _initialNotificationGathered = NO; _sceneDidConnect = NO; _notificationHandlingSetup = NO; - _channel = channel; - _registrar = registrar; + _applicationObserverRegistered = NO; + } + return self; +} + ++ (instancetype)sharedInstance { + static FLTFirebaseMessagingPlugin *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FLTFirebaseMessagingPlugin alloc] init]; + }); + return sharedInstance; +} + ++ (void)configureNotificationCenterDelegate { +#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM + [[FLTFirebaseMessagingPlugin sharedInstance] configureNotificationCenterDelegate]; +#endif +} + +- (void)configureWithFlutterMethodChannel:(FlutterMethodChannel *)channel + andFlutterPluginRegistrar:(NSObject *)registrar { + _channel = channel; + _registrar = registrar; + + if (!_applicationObserverRegistered) { + _applicationObserverRegistered = YES; // Application // Dart -> `getInitialNotification` // ObjC -> Initialize other delegates & observers @@ -87,16 +112,14 @@ - (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel #endif object:nil]; } - return self; } + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kFLTFirebaseMessagingChannelName binaryMessenger:[registrar messenger]]; - FLTFirebaseMessagingPlugin *instance = - [[FLTFirebaseMessagingPlugin alloc] initWithFlutterMethodChannel:channel - andFlutterPluginRegistrar:registrar]; + FLTFirebaseMessagingPlugin *instance = [FLTFirebaseMessagingPlugin sharedInstance]; + [instance configureWithFlutterMethodChannel:channel andFlutterPluginRegistrar:registrar]; // Register with internal FlutterFire plugin registry. [[FLTFirebasePluginRegistry sharedInstance] registerFirebasePlugin:instance]; @@ -250,6 +273,57 @@ - (void)registerForRemoteNotifications { #endif } +#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM +- (void)configureNotificationCenterDelegate { + // Set UNUserNotificationCenter but preserve original delegate if necessary. + if (@available(iOS 10.0, macOS 10.14, *)) { + BOOL shouldReplaceDelegate = YES; + UNUserNotificationCenter *notificationCenter = + [UNUserNotificationCenter currentNotificationCenter]; + id currentDelegate = notificationCenter.delegate; + + if (currentDelegate == self) { + return; + } + + if (currentDelegate != nil) { +#if !TARGET_OS_OSX + // If a UNUserNotificationCenterDelegate is set and it conforms to + // FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier + // call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls + // to this plugin. If we replace it, it will cause a stack overflow as our original + // delegate forwarding handler below causes an infinite loop of forwarding. See + // https://github.com/firebasefire/issues/4026. + if ([currentDelegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { + // Note this one only executes if Firebase swizzling is **enabled**. + shouldReplaceDelegate = NO; + } +#endif + + if (shouldReplaceDelegate) { + _originalNotificationCenterDelegate = currentDelegate; + _originalNotificationCenterDelegateRespondsTo.openSettingsForNotification = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]; + _originalNotificationCenterDelegateRespondsTo.willPresentNotification = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter:willPresentNotification: + withCompletionHandler:)]; + _originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse: + withCompletionHandler:)]; + } + } + + if (shouldReplaceDelegate) { + __strong FLTFirebasePlugin *strongSelf = self; + notificationCenter.delegate = strongSelf; + } + } +} +#endif + - (void)setupNotificationHandlingWithRemoteNotification:(nullable NSDictionary *)remoteNotification actionIdentifier:(nullable NSString *)actionIdentifier { // If notification handling was already set up (e.g. from @@ -335,48 +409,10 @@ - (void)setupNotificationHandlingWithRemoteNotification:(nullable NSDictionary * [_registrar addApplicationDelegate:self]; #endif - // Set UNUserNotificationCenter but preserve original delegate if necessary. - if (@available(iOS 10.0, macOS 10.14, *)) { - BOOL shouldReplaceDelegate = YES; - UNUserNotificationCenter *notificationCenter = - [UNUserNotificationCenter currentNotificationCenter]; - - if (notificationCenter.delegate != nil) { -#if !TARGET_OS_OSX - // If a UNUserNotificationCenterDelegate is set and it conforms to - // FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier - // call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls - // to this plugin. If we replace it, it will cause a stack overflow as our original - // delegate forwarding handler below causes an infinite loop of forwarding. See - // https://github.com/firebasefire/issues/4026. - if ([notificationCenter.delegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { - // Note this one only executes if Firebase swizzling is **enabled**. - shouldReplaceDelegate = NO; - } +#ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM + [self configureNotificationCenterDelegate]; #endif - if (shouldReplaceDelegate) { - _originalNotificationCenterDelegate = notificationCenter.delegate; - _originalNotificationCenterDelegateRespondsTo.openSettingsForNotification = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]; - _originalNotificationCenterDelegateRespondsTo.willPresentNotification = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter:willPresentNotification: - withCompletionHandler:)]; - _originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse: - withCompletionHandler:)]; - } - } - - if (shouldReplaceDelegate) { - __strong FLTFirebasePlugin *strongSelf = self; - notificationCenter.delegate = strongSelf; - } - } - // We automatically register for remote notifications as // application:didReceiveRemoteNotification:fetchCompletionHandler: will not get called unless // registerForRemoteNotifications is called early on during app initialization, calling this from diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/include/FLTFirebaseMessagingPlugin.h b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/include/FLTFirebaseMessagingPlugin.h index 23125d5dff3a..5d8832535265 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/include/FLTFirebaseMessagingPlugin.h +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/include/FLTFirebaseMessagingPlugin.h @@ -41,6 +41,16 @@ FIRMessagingDelegate, NSApplicationDelegate, UNUserNotificationCenterDelegate> + +/// Returns the shared Firebase Messaging plugin instance. ++ (instancetype)sharedInstance; + +/// Configures Firebase Messaging as the `UNUserNotificationCenter` delegate. +/// +/// Apps that adopt the UIScene lifecycle should call this from +/// `application:didFinishLaunchingWithOptions:` before returning, because Apple requires the +/// notification center delegate to be assigned before application launch completes. ++ (void)configureNotificationCenterDelegate; #else @interface FLTFirebaseMessagingPlugin : FLTFirebasePlugin + +/// Returns the shared Firebase Messaging plugin instance. ++ (instancetype)sharedInstance; + +/// Configures Firebase Messaging as the `UNUserNotificationCenter` delegate. +/// +/// Apps that adopt the UIScene lifecycle should call this from +/// `application:didFinishLaunchingWithOptions:` before returning, because Apple requires the +/// notification center delegate to be assigned before application launch completes. ++ (void)configureNotificationCenterDelegate; #else @interface FLTFirebaseMessagingPlugin : FLTFirebasePlugin