FCM Background notifications works in Android; not in iOS

Viewed 681

I am developing a flutter app with Firebase messaging involved. The app is for both iOS and Android.

In the Android version, I can get data messages pretty well and the Firebase Messaging background listener works well. The background listener get fired every time when a message is sent.

However in iOS the background listener never get fired. But if I send a push notification with notification section included then the default notification is displayed by the SDK, still nothing happens in the background messaging listener.

Reading many questions I figured many has faced this issue. Below is my code.

Main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  FirebaseMessaging.instance
        .getInitialMessage()
        .then((RemoteMessage? message) {
      developer.log("REMOTE MESSAGE");
      if (message != null) {
        developer.log(message.data.toString());
      }
    });

  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    RemoteNotification? notification = message.notification;
    AndroidNotification? android = message.notification?.android;
    developer.log("REMOTE MESSAGE LISTENER");
    if (message.data["data_type"] == "TEXT") {
      await AwesomeNotifications().createNotification(
        content: NotificationContent(
            id: UniqueKey().hashCode,
            groupKey: message.data["senderUid"],
            channelKey: 'basic_channel',
            title: message.data["title"],
            body: message.data["body"],
            summary: message.data["body"], // Anything you want here
            notificationLayout: NotificationLayout.Messaging,
            displayOnBackground: true,
            displayOnForeground: true),
      );
    } else if (message.data["data_type"] == "IMAGE") {
      await AwesomeNotifications().createNotification(
        content: NotificationContent(
            id: UniqueKey().hashCode,
            groupKey: message.data["senderUid"],
            channelKey: 'basic_channel',
            title: message.data["title"],
            body: Emojis.art_framed_picture + " " + message.data["body"],
            summary: message.data["body"], // Anything you want here
            notificationLayout: NotificationLayout.Messaging,
            displayOnBackground: true,
            displayOnForeground: true),
      );
    }
  });

  //Request permission for firebase messaging

  NotificationSettings settings =
      await FirebaseMessaging.instance.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false,
    sound: true,
  );

  print('User granted permission: ${settings.authorizationStatus}');

  /// Update the iOS foreground notification presentation options to allow
  /// heads up notifications.
  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
    alert: true,
    badge: true,
    sound: true,
  );

  AwesomeNotifications().initialize('resource://drawable/logo', [
    NotificationChannel(
        channelGroupKey: 'basic_tests',
        channelKey: 'basic_channel',
        channelName: 'Basic notifications',
        channelDescription: 'Notification channel for basic tests',
        defaultColor: Color(0xFF9D50DD),
        ledColor: Colors.white,
        importance: NotificationImportance.High),
  ]);

  runApp(MultiProvider(
    child: MyApp(),
  ));
}

AppDeligate.swift

import UIKit
import Flutter
import GoogleMaps
import FirebaseMessaging

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      GMSServices.provideAPIKey("xxxxxxx-xxxx")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

       Messaging.messaging().apnsToken = deviceToken
       super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
     }

}

pubspec.yaml

name: project
description: A new Flutter project.

version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"



dependencies:
  intl: ^0.17.0

  firebase_auth: ^3.1.0
  firebase_core: ^1.6.0
  firebase_messaging: ^11.4.1
  cloud_firestore:  ^3.1.9
  firebase_storage: ^10.2.8
  awesome_notifications: ^0.6.21

dependency_overrides:
  firebase_messaging_platform_interface: ^3.5.1
  firebase_crashlytics_platform_interface: 3.1.13
  cloud_firestore_platform_interface: 5.4.13
  firebase_auth_platform_interface: 6.1.11
  firebase_storage_platform_interface: 4.0.14
  cloud_functions_platform_interface: 5.0.21
  firebase_analytics_platform_interface: 3.0.5
  firebase_remote_config_platform_interface: 1.0.5
  firebase_dynamic_links_platform_interface: 0.2.0+5
  firebase_performance_platform_interface: 0.1.0+5
  firebase_app_installations_platform_interface: 0.1.0+6

dev_dependencies:
  flutter_test:
    sdk: flutter

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/graphics/
    - assets/icons_chat/
    - assets/lottie/
 

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>customer</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>$(FLUTTER_BUILD_NAME)</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>$(FLUTTER_BUILD_NUMBER)</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UIViewControllerBasedStatusBarAppearance</key>
    <false/>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>Use your device location to find nearby sellers</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>Use your device location to find nearby sellers</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Use your device location to find nearby sellers</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Take image from your gallery</string>
    <key>NSCameraUsageDescription</key>
    <string>Take image from your gallery</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Take image from your gallery</string>
    <key>UIBackgroundModes</key>
    <array>
        <string>fetch</string>
        <string>remote-notification</string>
    </array>
    <key>GoogleUtilitiesAppDelegateProxyEnabled</key>
    <false/>
    <key>FirebaseAppDelegateProxyEnabled</key>
    <false/>
    <key>CADisableMinimumFrameDurationOnPhone</key>
    <true/>
</dict>
</plist>

As you can see here I have enabled Background Fetch and Remote Notifications as well.

When setting up the firebase setup for iOS, I have followed my setups up to here - https://firebase.flutter.dev/docs/messaging/apple-integration/#3-generating-a-provisioning-profile. That means Registering the APN key with Firebase, Registering the App identifier (XCode generated the correct identifier and i am using it), and even generated a Provisioned Profile (However I am using XCode managed provisioned profile). I did not generate any specific certificate anyway, because it is already done by XCode.

Below is my flutter doctor result

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.0.1, on macOS 12.4 21F79 darwin-arm, locale
    en-LK)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[!] Xcode - develop for iOS and macOS (Xcode 13.4.1)
    ! CocoaPods 1.10.1 out of date (1.11.0 is recommended).
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin
        code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To upgrade see
      https://guides.cocoapods.org/using/getting-started.html#installation for
      instructions.
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.67.2)
[✓] Connected device (3 available)
[✓] HTTP Host Availability

This is my JSON Notification Data

"message": {

    "token": recieverFcm,
    "data": {
        "title": senderName,
        "body": message,
        "chatRoomId": chatRoomId,
        "sender_profile_pic": senderProfilePic,
        "senderUid": senderUid,
        "data_type": messageType,
        "click_action": "OPEN_CHAT_ROOM"
    },
    "android": {
        "priority": "high"
    },
    "apns": {
        "payload": {
            "aps": {
                "category": "OPEN_CHAT_ROOM",
                "sound": "enable",
                "content-available": 1,
            },
            "data": {
                "title": senderName,
                "body": message,
                "chatRoomId": chatRoomId,
                "sender_profile_pic": senderProfilePic,
                "senderUid": senderUid,
                "data_type": messageType,
                "click_action": "OPEN_CHAT_ROOM"
            },
        }
    }
}

In above Json I have 2 data sections just for testing. Removing any of them wont make any difference, I have tested.

How can I fix this issue?

5 Answers

Make sure you have selected the Remote notifications and Background fetch in background mode under the signing & capabilities section of XCODE

enter image description here

You can try with request for permission in iOS in android not needed.

FirebaseMessaging.requestNotificationPermissions(
      const IosNotificationSettings(
          sound: true, badge: true, alert: true, provisional: true));

Also you can check about here, how to configure and enable push notifications.

Hope it will help you.

https://developer.apple.com/account/resources/authkeys/list

Please go to this link.. Generate a key

key page

Then generate a new key. select Apple push notification

create new key

Once you create a key you will get a secret and key id. Take a note of these values. Go to firebase console. In project settings you will find cloud messaging on top

cloud messaging

In the bottom most section you will see iOS configuration and an option to add the APN key details. Please add your key secret and key ID here and save it.

Edit

Add priority in apns

"headers": {
         "apns-priority": "5",
       },

and enable background processing in Xcode

Edit 1:

If things appear to look normal in the log but you're still not receiving notifications, try turning off the Notifications switch in Settings, and then turn it back on. That will try to re-establish the device's persistent connection with APNs

Some more technical info here https://developer.apple.com/library/archive/technotes/tn2265/_index.html#:~:text=If%20things%20appear%20to%20look,device's%20persistent%20connection%20with%20APNs.

Also please remove content available as that will make the notification available only once per day. Please check the document above

First of all, you should enable enter image description here

Then add Notification Service Extension XCode->File-New-Target enter image description here

My code of Notification Service, I did add variable

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    
    var receivedRequest: UNNotificationRequest!
    
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        self.receivedRequest = request
        
        if let bestAttemptContent = bestAttemptContent {
            // Modify the notification content here...
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
            contentHandler(bestAttemptContent)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

And the last step inside AppDelegate, need to subscribe UserNotifications

import UIKit
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    let notificationCenter = UNUserNotificationCenter.current()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        initNotification()
        return true
    }
    
    func initNotification() {
        // Notification
        notificationCenter.delegate = self
        
        sendAuthorizationRequiest()
        
    }
    
    func sendAuthorizationRequiest() {
        let options: UNAuthorizationOptions = [.alert, .sound, .badge, .carPlay, .criticalAlert, .providesAppNotificationSettings, .provisional]
        notificationCenter.requestAuthorization(options: options) {
            (didAllow, error) in
            if !didAllow {
                print("User has declined notifications")
            }
        }
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {

        let userInfo = response.notification.request.content.userInfo
        print("Parse notification \(userInfo)")
        
        completionHandler()
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

Inside func userNotificationCenter you can parse all data, the create custom Notifications, and past any data to any screen, but on all screens, you should be subscribed to custom notification

Example of how to send this data:

// Custom Name
extension Notification.Name {

    static let Test = Notification.Name("Test")

}

// Send
let nc = NotificationCenter.default
            nc.post(name: Notification.Name.Test, object: nil, userInfo: userInfo)

And inside some of screen you should be subscribed to Notification

func subscribeToNotifications() {
        
        NotificationCenter.default.addObserver(self, selector: #selector(notificationExist(_:)), name: Notification.Name.Test, object: nil)
    }
    
    @objc func notificationExist(_ notification: NSNotification) {
    
        if let dict = notification.userInfo as Dictionary? {
            if let content = dict["content"] as? String{
                // do something with your data
            }
        }
    }
Related