0

How to implement FCM 9+ to work correctly on IOS versions 14+?

Mayvas
  • 530
  • 4
  • 14
  • While we appriciate knowledge, the formally correct way is to write a question yourself and the self-answer it with the information. This way, you are staying inside the guidelines. Otherwise, your question will probably be closed as not a question/unclear. – nvoigt Oct 09 '21 at 08:01
  • The bureaucracy is a great way to stop distributing useful tips. But you are right. Thank you for your advice. – Mayvas Oct 09 '21 at 08:10
  • 1
    Well, look at it this way... if someone likes it, you can get points on the questions *and* answer :) – nvoigt Oct 09 '21 at 08:11

2 Answers2

3

My previous answer about Flutter FCM 7 implementation was helpful, so I decided to write the same instructions for the new FCM 9+ versions and show how to implement smooth messages delivery in our Flutter App in some minutes.

After migrating to null safety and FCM version 9+ (IOS 14+) situation does not look better. We got the same issues but in a new wrapper :). 

The instruction described below can help with FCM 9+ implementation & provide some code examples. Maybe these instructions can help someone & prevent wasting time. 

XCode Setting

enter image description here

AppDelegate.swift

import UIKit
import Flutter
import Firebase
import FirebaseMessaging

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    FirebaseApp.configure()
    GeneratedPluginRegistrant.register(with: self)
    if #available(iOS 10.0, *) {
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    }

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Info.plist

<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>FirebaseScreenReportingEnabled</key>
<true/>

Message Example (Callable function)

Your message must be sent with these options:

{
   mutableContent: true,
   contentAvailable: true,
   apnsPushType: "background"
}

Just an example to use in callable function

exports.sendNotification = functions.https.onCall(
    async (data) => {
        console.log(data, "send notification");
        var userTokens = [USERTOKEN1,USERTOKEN2,USERTOKEN3];
        var payload = {
            notification: {
                title: '',
                body: '',
                image: '',
            },
            data: {
                type:'',
            },
        };
        
        for (const [userToken,userUID] of Object.entries(userTokens)) {
            admin.messaging().sendToDevice(userToken, payload, {
                mutableContent: true,
                contentAvailable: true,
                apnsPushType: "background"
            });
        }
        
        return {code: 100, message: "notifications send successfully"};
    });

Flutter Message Service

import 'dart:convert' as convert;
import 'dart:io' show Platform;

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:octopoos/entities/notification.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:uuid/uuid.dart';

class MessagingService {
  final Box prefs = Hive.box('preferences');
  final FirebaseMessaging fcm = FirebaseMessaging.instance;
  static final instance = MessagingService._();

  bool debug = true;

  /// Private Singleton Instance
  MessagingService._();

  /// Set FCM Presentation Options
  Future<void> setPresentationOptions() async {
    await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );
  }

  /// Check PUSH permissions for IOS
  Future<bool> requestPermission({bool withDebug = true}) async {
    NotificationSettings settings = await fcm.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    // if (withDebug) debugPrint('[ FCM ] Push: ${settings.authorizationStatus}');
    bool authorized = settings.authorizationStatus == AuthorizationStatus.authorized;
    return (Platform.isIOS && authorized || Platform.isAndroid) ? true : false;
  }

  /// Initialize FCM stream service
  Future<void> initializeFcm() async {
    final String? currentToken = await fcm.getToken();
    final String storedToken = prefs.get('fcmToken', defaultValue: '');

    /// Refresh Device token & resubscribe topics
    if (currentToken != null && currentToken != storedToken) {
      prefs.put('fcmToken', currentToken);
      /// resubscribeTopics();
    }

    if (debug) {
      debugPrint('[ FCM ] token: $currentToken');
      debugPrint('[ FCM ] service initialized');
    }
  }

  /// Store messages to Hive Storage 
  void store(RemoteMessage message) async {
    final FirebaseAuth auth = FirebaseAuth.instance;
    final Map options = message.data['options'] != null && message.data['options'].runtimeType == String
        ? convert.json.decode(message.data['options'])
        : message.data['options'];

    final AppNotification notificationData = AppNotification(
      id: const Uuid().v4(),
      title: message.data['title'] ?? '',
      body: message.data['body'] ?? '',
      image: message.data['image'] ?? '',
      type: message.data['type'] ?? 'notification',
      options: options,
      createdAt: DateTime.now().toString(),
    );

    late Box storage;
    switch (message.data['type']) {
      default:
        storage = Hive.box('notifications');
        break;
    }

    try {
      String id = const Uuid().v4();
      storage.put(id, notificationData.toMap());
      updateAppBadge(id);

      if (debug) debugPrint('Document $id created');
    } catch (error) {
      if (debug) debugPrint('Something wrong! $error');
    }
  }

  /// Update app badge
  Future<void> updateAppBadge(String id) async {
    final bool badgeIsAvailable = await FlutterAppBadger.isAppBadgeSupported();

    if (badgeIsAvailable && id.isNotEmpty) {
      final int count = Hive.box('preferences').get('badgeCount', defaultValue: 0) + 1;
      Hive.box('preferences').put('badgeCount', count);
      FlutterAppBadger.updateBadgeCount(count);
    }
  }

  /// Subscribe topic
  Future<void> subscribeTopic({required String name}) async {
    await fcm.subscribeToTopic(name);
  }

  /// Unsubscribe topic
  Future<void> unsubscribeTopic({required String name}) async {
    await fcm.unsubscribeFromTopic(name);
  }

  /// Resubscribe to topics
  Future<int> resubscribeTopics() async {
    final List topics = prefs.get('topics', defaultValue: []);
    if (topics.isNotEmpty) {
      for (String topic in topics) {
        subscribeTopic(name: topic);
      }
    }

    return topics.length;
  }
}

AppNotification Model

class AppNotification {
  String id;
  String title;
  String body;
  String image;
  String type;
  Map options;
  String createdAt;

  AppNotification({
    this.id = '',
    this.title = '',
    this.body = '',
    this.image = '',
    this.type = 'notification',
    this.options = const {},
    this.createdAt = '',

  });

  AppNotification.fromMap(Map snapshot, this.id)
      : title = snapshot['title'],
        body = snapshot['body'],
        image = snapshot['image'],
        type = snapshot['type'] ?? 'notification',
        options = snapshot['options'] ?? {},
        createdAt = (DateTime.parse(snapshot['createdAt'])).toString();

  Map<String, dynamic> toMap() => {
        "id": id,
        "title": title,
        "body": body,
        "image": image,       
        "type": type,
        "options": options,
        "createdAt": createdAt,
      };
}

main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:provider/provider.dart';
import 'package:octopoos/services/messaging.dart';
import 'package:timezone/data/latest.dart' as tz;

Future<void> fcm(RemoteMessage message) async {
    MessagingService.instance.store(message);

 /// Show foreground Push notification
 /// !!! Flutter Local Notification Plugin REQUIRED
 await notificationsPlugin.show(
      0,
      message.data['title'],
      message.data['body'],
      NotificationDetails(android: androidChannelSpecifics, iOS: iOSChannelSpecifics),
    );
}

Future<void> main() async {
  /// Init TimeZone
  tz.initializeTimeZones();

  /// Init Firebase Core Application
  await Firebase.initializeApp();

  /// FCM Permissions & Background Handler
  MessagingService.instance.setPresentationOptions();
  FirebaseMessaging.onBackgroundMessage(fcm);
  
  runApp(
    MultiProvider(
      providers: kAppProviders,
      child: App(),
    ),
  );
}

app.dart


  @override
  void initState() {
    super.initState();
    initFcmListeners();
  }

  Future<void> initFcmListeners() async {
    MessagingService.instance.initializeFcm();
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) _handleMessage(message);
    });

    FirebaseMessaging.onMessage.listen(_handleMessage);
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
   MessagingService.instance.store(message);
  }
  

That's all. Don't forget to test on a real IOS device. FCM will not work on IOS Simulator.

Dharman
  • 26,923
  • 21
  • 73
  • 125
Mayvas
  • 530
  • 4
  • 14
0

Here is whole process of how to Implement FCM in flutter

First of setup your app on firebase console by follow this link Add Firebase to your Flutter app

Add dependencys

firebase_core: ^1.12.0
firebase_messaging: ^11.2.6

Add configurations to both app side.

Android

Add in your Application class

import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingBackgroundService;

public class Application extends FlutterApplication implements PluginRegistrantCallback {
  // ...
  @Override
  public void onCreate() {
    super.onCreate();
    FlutterFirebaseMessagingBackgroundService.setPluginRegistrant(this);
  }

  @Override
  public void registerWith(PluginRegistry registry) {
    GeneratedPluginRegistrant.registerWith(registry);
  }
  // ...
}

FlutterFirebaseMessagingBackgroundService a callback to call your application's onCreate method.

iOS Integration

for iOS folow this document setup iOS or macOS with Firebase Cloud Messaging.

Adding functionality

here is your main.dart file and Replace the entire code with the following:

import 'dart:async';
import 'dart:convert';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_messaging_example/firebase_config.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

import 'message.dart';
import 'message_list.dart';
import 'permissions.dart';
import 'token_monitor.dart';

/// Define a top-level named handler which background/terminated messages will
/// call.
///
/// To verify things are working, check out the native platform logs.
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp(options: DefaultFirebaseConfig.platformOptions);
  print('Handling a background message ${message.messageId}');
}

/// Create a [AndroidNotificationChannel] for heads up notifications
late AndroidNotificationChannel channel;

/// Initialize the [FlutterLocalNotificationsPlugin] package.
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: const FirebaseOptions(
      apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0',
      appId: '1:448618578101:ios:0b11ed8263232715ac3efc',
      messagingSenderId: '448618578101',
      projectId: 'react-native-firebase-testing',
    ),
  );

  // Set the background messaging handler early on, as a named top-level function
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

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

    flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

    /// Create an Android Notification Channel.
    ///
    /// We use this channel in the `AndroidManifest.xml` file to override the
    /// default FCM channel to enable heads up notifications.
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(channel);

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

  runApp(MessagingExampleApp());
}

/// Entry point for the example application.
class MessagingExampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Messaging Example App',
      theme: ThemeData.dark(),
      routes: {
        '/': (context) => Application(),
        '/message': (context) => MessageView(),
      },
    );
  }
}

// Crude counter to make messages unique
int _messageCount = 0;

/// The API endpoint here accepts a raw FCM payload for demonstration purposes.
String constructFCMPayload(String? token) {
  _messageCount++;
  return jsonEncode({
    'token': token,
    'data': {
      'via': 'FlutterFire Cloud Messaging!!!',
      'count': _messageCount.toString(),
    },
    'notification': {
      'title': 'Hello FlutterFire!',
      'body': 'This notification (#$_messageCount) was created via FCM!',
    },
  });
}

/// Renders the example application.
class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  String? _token;

  @override
  void initState() {
    super.initState();
    FirebaseMessaging.instance
        .getInitialMessage()
        .then((RemoteMessage? message) {
      if (message != null) {
        Navigator.pushNamed(
          context,
          '/message',
          arguments: MessageArguments(message, true),
        );
      }
    });

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      RemoteNotification? notification = message.notification;
      AndroidNotification? android = message.notification?.android;
      if (notification != null && android != null && !kIsWeb) {
        flutterLocalNotificationsPlugin.show(
          notification.hashCode,
          notification.title,
          notification.body,
          NotificationDetails(
            android: AndroidNotificationDetails(
              channel.id,
              channel.name,
              channel.description,
              // TODO add a proper drawable resource to android, for now using
              //      one that already exists in example app.
              icon: 'launch_background',
            ),
          ),
        );
      }
    });

    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('A new onMessageOpenedApp event was published!');
      Navigator.pushNamed(
        context,
        '/message',
        arguments: MessageArguments(message, true),
      );
    });
  }

  Future<void> sendPushMessage() async {
    if (_token == null) {
      print('Unable to send FCM message, no token exists.');
      return;
    }

    try {
      await http.post(
        Uri.parse('https://api.rnfirebase.io/messaging/send'),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: constructFCMPayload(_token),
      );
      print('FCM request for device sent!');
    } catch (e) {
      print(e);
    }
  }

  Future<void> onActionSelected(String value) async {
    switch (value) {
      case 'subscribe':
        {
          print(
            'FlutterFire Messaging Example: Subscribing to topic "fcm_test".',
          );
          await FirebaseMessaging.instance.subscribeToTopic('fcm_test');
          print(
            'FlutterFire Messaging Example: Subscribing to topic "fcm_test" successful.',
          );
        }
        break;
      case 'unsubscribe':
        {
          print(
            'FlutterFire Messaging Example: Unsubscribing from topic "fcm_test".',
          );
          await FirebaseMessaging.instance.unsubscribeFromTopic('fcm_test');
          print(
            'FlutterFire Messaging Example: Unsubscribing from topic "fcm_test" successful.',
          );
        }
        break;
      case 'get_apns_token':
        {
          if (defaultTargetPlatform == TargetPlatform.iOS ||
              defaultTargetPlatform == TargetPlatform.macOS) {
            print('FlutterFire Messaging Example: Getting APNs token...');
            String? token = await FirebaseMessaging.instance.getAPNSToken();
            print('FlutterFire Messaging Example: Got APNs token: $token');
          } else {
            print(
              'FlutterFire Messaging Example: Getting an APNs token is only supported on iOS and macOS platforms.',
            );
          }
        }
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cloud Messaging'),
        actions: <Widget>[
          PopupMenuButton(
            onSelected: onActionSelected,
            itemBuilder: (BuildContext context) {
              return [
                const PopupMenuItem(
                  value: 'subscribe',
                  child: Text('Subscribe to topic'),
                ),
                const PopupMenuItem(
                  value: 'unsubscribe',
                  child: Text('Unsubscribe to topic'),
                ),
                const PopupMenuItem(
                  value: 'get_apns_token',
                  child: Text('Get APNs token (Apple only)'),
                ),
              ];
            },
          ),
        ],
      ),
      floatingActionButton: Builder(
        builder: (context) => FloatingActionButton(
          onPressed: sendPushMessage,
          backgroundColor: Colors.white,
          child: const Icon(Icons.send),
        ),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            MetaCard('Permissions', Permissions()),
            MetaCard(
              'FCM Token',
              TokenMonitor((token) {
                _token = token;
                return token == null
                    ? const CircularProgressIndicator()
                    : Text(token, style: const TextStyle(fontSize: 12));
              }),
            ),
            MetaCard('Message Stream', MessageList()),
          ],
        ),
      ),
    );
  }
}

/// UI Widget for displaying metadata.
class MetaCard extends StatelessWidget {
  final String _title;
  final Widget _children;

  // ignore: public_member_api_docs
  MetaCard(this._title, this._children);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              Container(
                margin: const EdgeInsets.only(bottom: 16),
                child: Text(_title, style: const TextStyle(fontSize: 18)),
              ),
              _children,
            ],
          ),
        ),
      ),
    );
  }
}

Folow this docs: Cloud Messaging

Paresh Mangukiya
  • 37,512
  • 17
  • 201
  • 182