Reputation: 23025
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
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
@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
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
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
Reputation: 757
First of all, you should enable
Then add Notification Service Extension XCode->File-New-Target
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
Reputation: 400
Make sure you have selected the Remote notifications and Background fetch in background mode under the signing & capabilities section of XCODE
Upvotes: 3
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
Reputation: 17732
https://developer.apple.com/account/resources/authkeys/list
Please go to this link.. Generate a key
Then generate a new key. select Apple push notification
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
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