PeakGen
PeakGen

Reputation: 23025

FCM Background notifications works in Android; not in iOS

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?

Upvotes: 6

Views: 4299

Answers (6)

Tom
Tom

Reputation: 1

I got notification in IOS device when app (flutter) is terminated but BackgroundHandler handler did not invoked first time. For second notification worked ok.

I send FCM via postman and here is a JSON-formatted:

{ 
"priority": "high",
"content_available": true,
"notification":{
  "title": "this is test",
  "body": "works",
},
"data" : {
  "type" :  "0",
  "title" : "this is test",
  "body" :  "works",
"image":"https://static.vecteezy.com/system/resources/previews/006/416/647/non_2x/yellow-face-emoji-smiley-emoticon-icon-free-vector.jpg",
  "timestamp" : "627E1D36",
  "source" : "postman",
  "sound" : "default",
  "vibration" : "150"
},
"to":"token"
}

My workaround:

I catch notification in native IOS swift and convert it in FMC json, if applicationState is background mode then I sink it in FlutterEventSink.

IOS swift

  1. catch notification & sink to FlutterStreamHandler
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate,MessagingDelegate {
private var eventNotificationChannel: FlutterEventChannel?
private let notificationStreamHandler = NotificationStreamHandler()


 override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      eventNotificationChannel = FlutterEventChannel(name: "testapp.my.de/notification/events", binaryMessenger: controller.binaryMessenger)
      GeneratedPluginRegistrant.register(with: self)
 
      eventNotificationChannel?.setStreamHandler(notificationStreamHandler)
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
   NSLog("notification: %@", userInfo);
   let endcodeFCMJsonString = convertToFCMJson(userInfo: userInfo)
         
   if(endcodeFCMJsonString != nil){
       let state = UIApplication.shared.applicationState
       if state == .background || state == .inactive {
             NSLog("aplication state background")     
eventNotificationChannel?.setStreamHandler(notificationStreamHandler)              notificationStreamHandler.handleNotification(endcodeFCMJsonString ?? "")
       } else if state == .active {
         NSLog("aplication state foreground")
       }
   }
}

convert in FCMJson

//FCMClass
struct FCMClass: Codable {
    let notification: NotificationClass
    let data: DataClass
}

//DataClass
struct DataClass: Codable {
    let type, title, body: String
    let image: String
    let timestamp, source, sound, vibration: String
}

//NotificationClass
struct NotificationClass: Codable {
    let title, body: String
}

func convertToFCMJson(userInfo: [AnyHashable : Any]) -> String? {
   var notifBody = "", notifTitle = ""
   if let aps = userInfo["aps"] as? NSDictionary {
       if let alert = aps["alert"] as? NSDictionary {
           notifBody = alert["body"] as? String ?? ""
           notifTitle = alert["title"] as? String ?? ""
       }
   }
      
   let notif = NotificationClass(title: notifTitle, body:  notifBody)
   let data = DataClass(type: userInfo["type"] as? String ?? "",
                        title: userInfo["title"] as? String ?? "",
                        body: userInfo["body"] as? String ?? "",
                        image: userInfo["image"] as? String ?? "",
                        timestamp: userInfo["timestamp"] as? String ?? "",
                        source: userInfo["source"] as? String ?? "",
                        sound: userInfo["sound"] as? String ?? "",
                        vibration: userInfo["vibration"] as? String ?? "")
   let fcm = FCMClass(notification: notif, data: data)

   let jsonEncoder = JSONEncoder()
   jsonEncoder.outputFormatting = .prettyPrinted
   do {
      let encodeFMCJson = try jsonEncoder.encode(fcm)
      let endcodeFCMJsonString = String(data: encodeFMCJson, encoding: .utf8)!
      NSLog("json: %@", endcodeFCMJsonString);
      return endcodeFCMJsonString
      } catch {
         NSLog(error.localizedDescription)
      }
      return nil
}

FlutterStreamHandler

class NotificationStreamHandler:NSObject, FlutterStreamHandler {

  var eventSink: FlutterEventSink?

  // notification will be added to this queue until the sink is ready to process them
  var queuedNotification = [String]()

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    self.eventSink = events
    queuedNotification.forEach({ events($0) })
    queuedNotification.removeAll()
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    self.eventSink = nil
    return nil
  }
    
  func handleNotification(_ notification: String) -> Bool {
    guard let eventSink = eventSink else {
      queuedNotification.append(notification)
      return false
    }
    eventSink(notification)
    return true
  }
}

FLUTTER

  1. Checking broadcast stream (notification from native)
  2. Redirect into StreamController
  3. listen appNotificationBloc stream and store notification
AppNotificationBloc? _appNotificationBloc;
_appNotificationBloc = AppNotificationBloc();
AppNotificationBloc? get appNotificationBloc => _appNotificationBloc;
 _appBloc.appNotificationBloc?.state.listen((event) async {
      Map<String, dynamic> valueMap = json.decode(event);
      RemoteMessage test = RemoteMessage.fromMap(valueMap);
      saveNotificationMassages(test);
    });
class AppNotificationBloc implements BlocBase {

   //Event Channels creation
  static const stream = EventChannel('testapp.my.de/notification/events');

  final StreamController<String> _stateController = StreamController();
  Stream<String> get state => _stateController.stream;
  Sink<String> get stateSink => _stateController.sink;

  AppNotificationBloc() {
    //Checking broadcast stream, if get notification then redirect it to stream
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }

  @override
  void dispose() {
      _stateController.close();
  }

  _onRedirected(String notification) {
    stateSink.add(notification);
  }
}

Now I don't need FirebaseMessaging.onBackgroundMessage on IOS anymore. And works perfect. Finally! :)

Upvotes: 0

Sadhik
Sadhik

Reputation: 300

You need to set the content_available property to true like so:

{  
   "data":{  
      "title":"mytitle",
      "body":"mybody",
      "url":"myurl"
   },
   "notification":{  
      "title":"mytitle",
      "body":"mybody",
      "content_available": true
   },
   "to":"/topics/topic"
}

There is a blue note box on in this section that states this: https://firebase.google.com/docs/cloud-messaging/concept-options#notifications

Upvotes: 0

Ice
Ice

Reputation: 757

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
            }
        }
    }

Upvotes: 0

Jay Nirmal
Jay Nirmal

Reputation: 400

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

Upvotes: 3

Mukund Jogi
Mukund Jogi

Reputation: 1395

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.

Upvotes: 1

Kaushik Chandru
Kaushik Chandru

Reputation: 17732

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

Upvotes: 0

Related Questions