flutter – FirebaseMessaging notification duplicated on iOS when including an imageUrl


I’m doing a Flutter example app for FirebaseMessaging to test out how server notification would work. So I’ve followed the guide on FlutterFire and setup everything and it works great on Android.

As far as iOS goes, it seems to work great as long as I don’t include images in the notification. When I do include an imageUrl, the app shows a notification with all of the content except for the Image, and immediately after, another with the same content and the image.

I’m quite lost, I don’t understand why it would do that. Whether I use the test notification from the firebase console or my own backend using FirebaseMessaging.send, the behavior is the same.

Here are snippets of the code:

main.dart

import 'dart:developer';
import 'dart:io';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:googlenotifpoc/notification_manager.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    name: "...",
    options: DefaultFirebaseOptions.currentPlatform,
  );
  await FirebaseMessaging.instance.setAutoInitEnabled(true);

  const AndroidNotificationChannel channel = AndroidNotificationChannel(
    'high_importance_channel', // id
    'High Importance Notifications', // title
    description:
        'This channel is used for important notifications.', // description
    importance: Importance.max,
  );

  const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
        'high_importance_channel', // id
        'High Importance Notifications',
        channelDescription: 'This channel is used for important notifications.',
        importance: Importance.max,
        priority: Priority.high,
        ticker: 'ticker',
        icon: "ic_white",
        playSound: true,
    );

  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
    alert: true, // Required to display a heads up notification
    badge: true,
    sound: true,
  ); 

  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(channel);

  FirebaseMessaging.onMessage.listen((event) => NotificationManager.showNotification(event, androidDetails),);

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    FirebaseMessaging messaging = FirebaseMessaging.instance;

    messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: true,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FutureBuilder<String?>(
              future: getFMCToken(),
              builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
                if (snapshot.hasData) {
                  log(snapshot.data ?? "-");
                  print(snapshot.data ?? "-");
                  return Text(
                    "FMC TOKEN: \n ${snapshot.data}",
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                        fontSize: 16, fontWeight: FontWeight.bold),
                  );
                } else {
                  return const Text("Loading your FCM token...");
                }
              },
            ),
          ],
        ),
      ),
    );
  }

  Future<String?> getFMCToken() async {
    String? apnsToken;
    if(Platform.isIOS) {
      apnsToken = await FirebaseMessaging.instance.getAPNSToken();
      log("Got APNS token: $apnsToken");
      print("Got APNS token: $apnsToken");
    }

    if(Platform.isAndroid || apnsToken != null) {
      return FirebaseMessaging.instance.getToken();
    }

    return Future.value(null);
  }
}

NotificationManaging class


import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationManager {

  static Future<void> showNotification(RemoteMessage payload, AndroidNotificationDetails androidDetails) async {
    const android = AndroidInitializationSettings("ic_white");
    const initializationSettingsIOS = DarwinInitializationSettings();
    const initialSetting = InitializationSettings(android: android, iOS: initializationSettingsIOS);
    final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    flutterLocalNotificationsPlugin.initialize(initialSetting);

    
    const iOSDetails = DarwinNotificationDetails();
    NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidDetails, iOS: iOSDetails);

    await flutterLocalNotificationsPlugin.show(0, payload.notification!.title, payload.notification!.body, platformChannelSpecifics);
  }
}

ios notificationService.m

//
//  NotificationService.m
//  ImageNotification
//

#import "NotificationService.h"
#import "FirebaseMessaging.h"

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    // Modify the notification content here...
    //self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
    //self.contentHandler(self.bestAttemptContent);
    
    [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
}

- (void)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.
    self.contentHandler(self.bestAttemptContent);
}

@end

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img