Fix “Received an invalid notification content” in iOS — FCM Flutter

Arby Azra
4 min readApr 20, 2022

I dont know how long i have been strolling around to solve it, I had tried with the various way by the time, but not with this one, and its worked for me now, at least.

The Main Issue

In my case, i already had log data when FCM payload came :

but when I tried to click the push notification, it prints “Received an invalid notification content”.

A Several Approach Before

UNUserNotificationCenter delegate — I just want to remind you that you need to put :

Objective-C:

if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}

Swift:

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

if you had solved the problem, you may stop reading.

Then if you received redundant “Receive an invalid notification content” until the app got ERR_BAD_ACCESS (forced close), put this following code inside AppDelegate.swift:

@available(iOS 10, *)
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([[.banner, .sound]])
}
@available(iOS 10, *)
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
completionHandler()
}
}

The user notification center will call these methods when notifications arrive or when the user interacts with them. You’ll be working with them a little later.
You may check the detail in FCM guidance in iOS app (https://firebase.google.com/docs/cloud-messaging/ios/client).

if you had solved the problem, you may stop reading.

Payload Content Format

If we read carefully the error message, it seems like we need to pass FCM payload content properly. I already had tried all the github and stackoverflow suggestion at the moment and the problem is not yet solved.

You may check the standard Payload Content Format https://firebase.google.com/docs/cloud-messaging/concept-options#example-notification-message-with-platform-specific-delivery-options

So I assume if you guys had solved the problem by trying all payload format, you may stop read.

Time to solve!

So, the idea was creating `Flutter Native Channel` in `didFinishLaunchingWithOptions` function on AppDelegate.swift.

Here’s the magic! :

  1. Put the following code in a state that you want to listen when the Push Notification will be clicked.
    Lets say "HomePage"
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {  MethodChannel _channel =
const MethodChannel('com.arby.app/backgroundNotifications');
@override
void initState() {
super.initState();
.
.
.
_channel.setMethodCallHandler(setupNotification);
}

Future<dynamic> setupNotification(MethodCall call) async {
print("init state setMethodCallHandler ${call.method}"); // Never comes here
switch (call.method) {
case 'manuallyInterceptNotification':

// variable contains data model obtained from native iOS
// call.arguments would be like {id:1,title:...,body:...,screen:...}

var payload = Map<String,dynamic>.from(call.arguments as Map)

// your navigation code or something goes here...
// Navigate.of(context).pushNamed(payload['screen']);

default:
throw MissingPluginException('notImplemented');
}
}

2. Add FlutterMethodChannel inside didFinisihLaunch in AppDelegate.swift

...
var manualNotificationChannel : FlutterMethodChannel!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

....

let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

manualNotificationChannel = FlutterMethodChannel.init(name: "com.arby.app/backgroundNotifications", binaryMessenger: controller.binaryMessenger)

....

}

3. Add function sendMessageToFlutter below didFinishLaunch in AppDelegate.swift, it will fires state in home.dart when this function get invoked

private func sendMessageToFlutter(result : Dictionary <String, String>){
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
self.manualNotificationChannel.invokeMethod("manuallyInterceptNotification", arguments: result)
}
}

4. Add overriding function based on FCM documentation (https://firebase.google.com/docs/cloud-messaging/ios/client), inside the override function, we’ll put the following codes :

@available(iOS 10, *)
override func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
Messaging.messaging().appDidReceiveMessage(userInfo)
var payload: [String: String] = [:]

// this is the example of key:value data inside your payload

if let id = userInfo["id"] {
payload["id"] = "\(id)"
}
if let body = userInfo["body"] {
payload["body"] = "\(body)"
}
if let title = userInfo["title"] {
payload["title"] = "\(title)"
}
if let screen= userInfo["screen"] {
payload["screen"] = "\(screen)"
}

sendMessageToFlutter(result:payload);
completionHandler()

}

The payload supposed to be like this :

curl -X POST \
https://fcm.googleapis.com/fcm/send \
-H 'Authorization: key=your_server_key' \
-H 'Content-Type: application/json' \
-H 'cache-control: no-cache' \
-d '{
"content_available": true,
"mutable_content": true,
"priority":"high",
"data":
{
"id":"8",
"body":"This is the body",
"title":"Hello Flutter"
"screen":"/detail"
},
"notification":
{
"title": "Title",
"body" : "Test",
"content_available": 1
},
"to": "your_token"
}'

I didnt know whats the best practice to convert AnyHashable to Dictionary (we are using Dictionary due to dart received data-type rules https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-swift-tab#codec)

Yet, we still waiting for the Firebase (i thought) to solve the problem.

--

--

Arby Azra

Gue suka hal yang ribet, bukan berarti bisa nyelesain dengan cepet, tapi gue cinta proses dibelakang hal itu — Flutter Engineer