Description
Is there an existing issue for this?
- I have searched the existing issues.
Are you aware of the differences between iOS and Android background message handling?
- I understand that iOS and Android background messages behave differently, and I've designed my application with that in mind.
Do you have an active Apple Developer account?
- I have an active Apple Developer account.
Are you using a physical iOS device to test background messages?
- I am using a physical iOS device to test background messages.
Have you enabled "Remote Notifications" & "Background Mode" (Checking options for "Background Processing" & "Remote Notifications") in your app's Xcode project?
Have you created an APNs key in your Apple Developer account & uploaded this APNs key to your Firebase console?
Have you disabled method swizzling for Firebase in your app?
CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName Bahia Principe Residences CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsArbitraryLoadsInWebContent NSCameraUsageDescription Can we access your camera in order to scan your id? NSFaceIDUsageDescription Allow access to FaceID to access BPASS NSLocationAlwaysAndWhenInUseUsageDescription Bahia principe requires access to location information. NSLocationWhenInUseUsageDescription Bahia principe requires access to location information. NSMicrophoneUsageDescription Microphone Access NSPhotoLibraryUsageDescription Can we access your photo library in order to scan your id? UIApplicationSupportsIndirectInputEvents UIBackgroundModes processing fetch remote-notification UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIStatusBarHidden UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance io.flutter.embedded_views_preview YES ITSAppUsesNonExemptEncryption BGTaskSchedulerPermittedIdentifiers com.bahiaprincipe.bahiaprinciperesidencesAre you sending messages to your app from the Firebase Admin SDK?
Sending from postman https://fcm.googleapis.com/v1/projects/bahia-principe-residences/messages:send
{
"message": {
"token": "c7TYmMT5p0zEkW-y89zjiS:APA91bHAUOyCD9jwSJp98WUcSowvu_7Q-yN6q3JTn0zuT3nBVcu0-e9q_vU4QmA5sICBUYLfakSTzpzn-QxSp-5theJAtEbKKYAq5NktJe--aSFmbvdailQ",
//"topic": "test",
"notification": {
"body": "Body23",
"title": "Title23",
"image": "https://www.bahia-principe.com/content/image/1418281867332/imagen-azu-san-valentin.jpg"
},
"apns": {
"payload": {
"aps": {
"mutable-content": 1,
"content-available": 1,
"alert": {
"title": "Hello!",
"body": "This is a test notification with mutable content."
},
"sound": "default"
}
}
},
"data": {
"body": "Body23",
"title": "Title23",
"image_url": "https://www.bahia-principe.com/content/image/1418281867332/imagen-azu-san-valentin.jpg",
"priority": "high"
},
"android": {
"priority": "high"
}
}
}
Have you requested permission from the user to receive notifications?
- I have the relevant permission to receive notifications.
Have you used the 'Console' application on your macOS device to check if the iOS device's system is throttling your background messages?
Additional context and comments
Expected behaviour of FirebaseMessaging.onMessageOpenedApp:
App is on background, tapping the notifications opens the app an executes the FirebaseMessaging.onMessageOpenedApp callback.
Problem: FirebaseMessaging.onMessageOpenedApp is not being called when the notification y tapped and the app is on background.
Expected behaviour of FirebaseMessaging.instance.getInitialMessage:
App is terminated, tapping the notifications opens the app an executes the FirebaseMessaging.instance.getInitialMessage callback.
Problem: FirebaseMessaging.instance.getInitialMessage is not being called when the notification y tapped and the app terminated.
Firebase messaging class:
import 'dart:async';
import 'dart:convert';
import 'package:bpass/src/utils/Constants.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:salesforce_push_notification/sfmc_plugin.dart';
import 'package:provider/provider.dart';
import '../../localization/app_localizations.dart';
import '../bussiness_logic/blocs/notifications_screen/notifications_screen_bloc.dart';
import '../data/models/get_notifications/get_notifications_request.dart';
import '../data/models/insert_notification/insert_notification_request.dart';
import '../data/models/user_model.dart';
import '../presentation/screens/notifications_screen.dart';
import '../presentation/theme/theme_provider.dart';
import '../presentation/widgets/custom_notification.dart';
import '../utils/enums.dart';
import '../utils/global.dart';
import '../utils/preferences.dart';
import '../utils/utils.dart';
import 'package:http/http.dart' as http;
class FirebaseMessagingService {
static FirebaseMessaging messaging = FirebaseMessaging.instance;
static String? token;
static RemoteMessage? currentMessage;
static StreamController<Map<String, dynamic>> _messageStream = new StreamController<Map<String, dynamic>>.broadcast();
static NotificationSettings? settings;
static Stream<Map<String, dynamic>> get messageStream => _messageStream.stream;
@pragma('vm:entry-point')
static Future _backgroundHandler(RemoteMessage message) async {
print('onBackgroundHandler ${MapToJson([message.data])}');
_messageStream.add(message.data);
if (await SfmcPlugin().isSalesforcePush(message.data)){
SfmcPlugin().handleMessage(message.data);
}
}
@pragma('vm:entry-point')
static Future _onMessageHandler(RemoteMessage message) async {
print('onMessageHandler ${MapToJson([message.data])}');
_messageStream.add(message.data);
currentMessage = message;
if (await SfmcPlugin().isSalesforcePush(message.data)){
SfmcPlugin().handleMessage(message.data);
final prefs = new UserPreferences();
await prefs.initPrefs();
InfoUsuario userInfo = InfoUsuario.fromJson(jsonDecode(prefs.user));
if (userInfo.contCodi != null){
_insertSalesforceNotificationForeground(message, userInfo.contCodi!);
}
}
showFlutterNotification(message);
}
static Future _onMessageOpenedApp(RemoteMessage message) async {
print('onMessageOpenedApp ${MapToJson([message.data])}');
_messageStream.add(message.data);
BlocProvider.of(GlobalUtils.navigatorKey.currentState!.context).notifications = [];
showModalBottomSheet(
context: GlobalUtils.navigatorKey.currentState!.context,
enableDrag: false,
showDragHandle: false,
isScrollControlled: true,
builder: (context) {
return Container(
padding: EdgeInsets.only(top: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
height: (MediaQuery.of(context).size.height - MediaQuery.of(context).padding.vertical) * 0.8,
child: NotificationsScreen(isModal: true));
});
print("OPENED APP NOTIFICATION");
}
static Future deleteToken() async {
await FirebaseMessaging.instance.deleteToken();
token = null;
}
static Future initializeApp() async {
await Firebase.initializeApp();
await requestPermission();
getOAuthToken();
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: false,
badge: true,
sound: true,
);
// Save token
token = await FirebaseMessaging.instance.getToken();
UserPreferences().firebaseToken = token ?? "";
print("Firebase Token: $token");
// Handlers
FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
FirebaseMessaging.onMessage.listen(_onMessageHandler);
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
FirebaseMessaging.instance.subscribeToTopic("test");
onOpenedAppMessage();
}
static onOpenedAppMessage () async {
FirebaseMessaging.instance.getInitialMessage().then((message) {
print("GET INITIAL MESSAGE");
if (message != null) {
print("GET INITIAL MESSAGE NOT NULL");
if(GlobalUtils.initNotification) return
GlobalUtils.initNotification = true;
_onMessageOpenedApp(message);
}
});
}
static requestPermission() async {
settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
if (settings!.authorizationStatus == AuthorizationStatus.authorized) {
GlobalUtils.hasNotificationPermission = true;
print('User granted permission');
} else if (settings!.authorizationStatus == AuthorizationStatus.provisional) {
GlobalUtils.hasNotificationPermission = true;
print('User granted provisional permission');
} else {
GlobalUtils.hasNotificationPermission = false;
print('User declined or has not accepted permission');
}
}
static showFlutterNotification(RemoteMessage message) async {
Map<String, dynamic> data = message.data;
String title = data["title"];
String body = data["body"];
String image = data["image_url"] ?? "";
notificationKey.currentState?.playAnimation(title, body, image);
}
static _insertSalesforceNotificationForeground(RemoteMessage message, int contCodi){
final theme = Provider.of(GlobalUtils.scaffoldMessengerKey.currentContext!, listen: false);
BlocProvider.of<NotificationsScreenBloc>(GlobalUtils.navigatorKey.currentContext!).add(AddNotification(
request: InsertNotificationRequest(
contCodi: contCodi,
codeApp: Constants.NOTIFICATIONS_CODE_APP,
copuTipo: NotificationsType.com.requestName,
hoteCodi: getSFResidence(message.data["_mediaResidencial"]),
pumeAsun: message.data["title"],
pumeMens: message.data["alert"],
pumeUrl: message.data["_od"] != null && message.data["_od"].toString().isNotEmpty ? message.data["_od"] : null,
pumeImag: message.data["_mediaUrl"] != null && message.data["_mediaUrl"].toString().isNotEmpty ? message.data["_mediaUrl"] : null,
),
requestGetNotifications: GetNotificationsRequest(
contCodi: contCodi,
codeApp: Constants.NOTIFICATIONS_CODE_APP,
idioma: '${AppLocalizations.of(GlobalUtils.navigatorKey.currentContext!)!.locale.languageCode}'.toUpperCase(),
hoteCodi: theme.getInfoTipoUsuario().hotel!,
)
));
}
static String MapToJson(List<Map<String, dynamic>> map) {
String res = "[";
for (var s in map) {
res += "{";
for (String k in s.keys) {
res += '"';
res += k;
res += '":"';
res += s[k].toString();
res += '",';
}
res = res.substring(0, res.length - 1);
res += "},";
res = res.substring(0, res.length - 1);
}
res += "]";
return res;
}
}
FirebaseMessagingService.initializeApp(); is being called in main.dart
Example of message payload being used:
{
"message": {
"token": "",
//"topic": "test",
"notification": {
"body": "Body23",
"title": "Title23",
"image": "image"
},
"apns": {
"payload": {
"aps": {
"mutable-content": 1,
"content-available": 1,
"alert": {
"title": "Hello!",
"body": "This is a test notification with mutable content."
},
"sound": "default"
}
}
},
"data": {
"body": "Body23",
"title": "Title23",
"image_url": "image",
"priority": "high"
},
"android": {
"priority": "high"
}
}
}
App delegate:
import UIKit
import FirebaseMessaging
import FirebaseCore
import SafariServices
import Flutter
import GoogleMaps
import SFMCSDK
import MarketingCloudSDK
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, URLHandlingDelegate {
var flutterChannel : FlutterMethodChannel!
func sfmc_handleURL(
_ url: URL,
type: String
) {
redirectToUrl(
url: url.absoluteString
)
}
var notificationUserInfo:[AnyHashable:Any]?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
flutterChannel = FlutterMethodChannel.init(
name: "sfmc_plugin",
binaryMessenger: controller.binaryMessenger
)
if let options = launchOptions, let notification = options[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any] {
self.notificationUserInfo = notification
}
GMSServices.provideAPIKey(
"AIzaSyCZYc3w0MpE8qCmyNXh2vQko7aZKGk1PBU"
)
FirebaseApp.configure()
Messaging.messaging().delegate = self
self.configureSDK()
GeneratedPluginRegistrant.register(
with: self
)
if #available(
iOS 10.0,
*
) {
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [
.alert,
.badge,
.sound
]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: {
_,
_ in
}
)
} else {
let settings: UIUserNotificationSettings =
UIUserNotificationSettings(
types: [
.alert,
.badge,
.sound
],
categories: nil
)
application.registerUserNotificationSettings(
settings
)
}
application.registerForRemoteNotifications()
return super.application(
application,
didFinishLaunchingWithOptions: launchOptions
)
}
func extractValue(
key : String,
fromPushNotificationUserInfo userInfo:[AnyHashable: Any]
) -> String? {
var message: String?
if let aps = userInfo["aps"] as? NSDictionary {
if let alert = aps["alert"] as? NSDictionary {
if let alertMessage = alert[key] as? String {
message = alertMessage
}
}
}
return message
}
func getSFResidence(residences : String?) -> Int? {
if(residences == nil || (residences!.contains( "MX") && residences!.contains( "DO")) || residences == "" ){
return nil
}
else if(residences!.contains( "MX")){
return 301
}
else if(residences!.contains( "DO")){
return 302
}
else{
return nil
}
}
// Called when a notification is delivered in the background
override func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (
UIBackgroundFetchResult
) -> Void
) {
let state = UIApplication.shared.applicationState
let title = extractValue(
key: "title",
fromPushNotificationUserInfo: userInfo
)
let body = extractValue(
key: "body",
fromPushNotificationUserInfo: userInfo
)
var imageUrl: String? = nil
var notiUrl: String? = nil
var hotel: Int? = nil
if let imageMesssage = userInfo["_mediaUrl"] as? String {
imageUrl = userInfo["_mediaUrl"] as? String
}
if let urlMessage = userInfo["_od"] as? String {
notiUrl = userInfo["_od"] as? String
}
if let hotelMessage = userInfo["_mediaResidencial"] as? String {
hotel = getSFResidence(residences: userInfo["_mediaResidencial"] as? String)
}
self.insertNotification(
title: title == nil ? "" : title!,
body: body == nil ? "" : body!,
notiUrl: notiUrl == nil ? nil : notiUrl!,
imageUrl: imageUrl == nil ? nil : imageUrl!,
hotel: hotel == nil ? nil : hotel
)
if let messageData = userInfo["yourKey"] as? [String: AnyObject] {
print(
"Background Notification received!!"
)
print(
messageData
)
completionHandler(
.newData
)
} else {
completionHandler(
.noData
)
}
}
@discardableResult
func configureSDK() -> Bool {
// Enable logging for debugging early on. Debug level is not recommended for production apps, as significant data
// about the MobilePush will be logged to the console.
#if DEBUG
SFMCSdk.setLogger(
logLevel: .debug
)
#endif
// To override the Keycahin accessibility attribute (default set by SDK: kSecAttrAccessibleWhenUnlockedThisDeviceOnly)
SFMCSdk.setKeychainAccessibleAttribute(
accessibleAttribute: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
)
// To Override the Keychain Error to be considered fatal or not (Default value is true)
SFMCSdk.setKeychainAccessErrorsAreFatal(
errorsAreFatal: false
)
// Use the Mobile Push Config Builder to configure the Mobile Push Module. This gives you the maximum flexibility in SDK configuration.
// The builder lets you configure the module parameters at runtime.
let mobilePushConfiguration = PushConfigBuilder(
appId: "eb5c8a85-74cc-4c0c-83c3-2a41941e5823"
)
.setAccessToken(
"g3p90MLhUbPoSGk2daaK8O6U"
)
.setMarketingCloudServerUrl(
URL(
string: "https://mcxk2vj-r2bshv49-8z4g6f7zsd1.device.marketingcloudapis.com/"
)!
)
.setMid(
"536006138"
)
.setDelayRegistrationUntilContactKeyIsSet(
false
)
.setAnalyticsEnabled(
true
)
.build()
// Set the completion handler to take action when module initialization is completed. The result indicates if initialization was sucesfull or not.
// Seting the completion handler is optional.
let completionHandler: (
OperationResult
) -> () = { result in
//DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
// }
if result == .success {
print(
"SFMCSdk success"
)
self.setupMobilePush()
} else if result == .error {
print(
"SFMCSdk error"
)
} else if result == .cancelled {
print(
"SFMCSdk cancelled"
)
} else if result == .timeout {
print(
"SFMCSdk timeout"
)
}
}
// Once you've created the mobile push configuration, intialize the SDK.
SFMCSdk.initializeSdk(
ConfigBuilder().setPush(
config: mobilePushConfiguration,
onCompletion: completionHandler
).build()
)
return true
}
func setupMobilePush(){
SFMCSdk.mp.setURLHandlingDelegate(
self
)
// Make sure to dispatch this to the main thread, as UNUserNotificationCenter will present UI.
DispatchQueue.main.async {
// Set the UNUserNotificationCenterDelegate to a class adhering to thie protocol.
// In this exmple, the AppDelegate class adheres to the protocol (see below)
// and handles Notification Center delegate methods from iOS.
UNUserNotificationCenter.current().delegate = self
// Request authorization from the user for push notification alerts.
UNUserNotificationCenter.current().requestAuthorization(options: [
.alert,
.sound,
.badge
],
completionHandler: {
(
_ granted: Bool,
_ error: Error?
) -> Void in
if error == nil {
if granted == true {
// Your application may want to do something specific if the user has granted authorization
// for the notification types specified; it would be done here.
}
}
})
// In any case, your application should register for remote notifications *each time* your application
// launches to ensure that the push token used by MobilePush (for silent push) is updated if necessary.
// Registering in this manner does *not* mean that a user will see a notification - it only means
// that the application will receive a unique push token from iOS.
UIApplication.shared.registerForRemoteNotifications()
}
}
// SDK: OPTIONAL IMPLEMENTATION (if using Data Protection)
override func applicationProtectedDataDidBecomeAvailable(
_ application: UIApplication
) {
if (
SFMCSdk.mp.getStatus() != .operational
) {
self.configureSDK()
}
}
private func redirectToUrl(
url: String
){
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + 2
) {
self.flutterChannel.invokeMethod(
"redirectToUrl",
arguments: url
)
}
/* DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
let urli = URL(string: url)
let safariViewController = SFSafariViewController(url: urli!)
self.window?.rootViewController?.present(safariViewController, animated: true) {
}
}*/
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// tell the SDK about the notification
SFMCSdk.requestPushSdk { mp in
mp.setNotificationRequest(
response.notification.request
)
mp.setURLHandlingDelegate(
self
)
}
completionHandler()
}
func insertNotification(
title: String?,
body: String?,
notiUrl: String?,
imageUrl: String?,
hotel: Int?
){
let defaults = UserDefaults.standard
let contCodi = UserDefaults.standard.integer(
forKey: "flutter.contCodi"
)
let url = URL(
string: "https://hotels-svcext.grupo-pinero.com/inhouse-svc/notificaciones-svc/insertNotification"
)!
var request = URLRequest(
url: url
)
request.httpMethod = "POST"
let message = Message(
contCodi: contCodi,
hoteCodi: hotel,
pumeAsun: title ?? "",
pumeMens: body ?? "",
pumeImag: imageUrl,
codeApp:"RYG",
pumeUrl: notiUrl,
copuTipo: "COMERCIAL"
)
let data = try! JSONEncoder().encode(
message
)
request.httpBody = data
request.setValue(
"application/json",
forHTTPHeaderField: "Content-Type"
)
request.setValue(
"b3b3771ef8094591a0d06614e90bc3c0",
forHTTPHeaderField: "MULESOFT_CLIENT_ID"
)
request.setValue(
"5B9C5214078a4440bE84e6DeB81D239d",
forHTTPHeaderField: "MULESOFT_CLIENT_SECRET"
)
let task = URLSession.shared.dataTask(
with: request
) {
data,
response,
error in
let statusCode = (
response as! HTTPURLResponse
).statusCode
}
task.resume()
}
}
struct Message: Encodable {
let contCodi: Int
// let pumeCodi: Int?
let hoteCodi: Int?
let pumeAsun: String
let pumeMens: String
let pumeImag: String?
let codeApp: String
let pumeUrl: String?
let copuTipo: String
// let copuFech: String?
}
extension AppDelegate : MessagingDelegate {
func messaging(
_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?
) {
print(
"FCM Token",
fcmToken ?? ""
)
print(
"SDK is operational"
)
if let apnsToken = messaging.apnsToken {
print(
"Setting APNS token in MarketingCloudSDK"
)
SFMCSdk.mp.setDeviceToken(
apnsToken
)
} else {
print(
"fcm token is null"
)
}
print(
"SDK is not yet operational"
)
}
}
Flutter doctor -v
[✓] Flutter (Channel stable, 3.27.2, on macOS 15.2 24C101 darwin-arm64, locale en-ES)
• Flutter version 3.27.2 on channel stable at /Users/andonigarcialopez/src/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 68415ad1d9 (9 weeks ago), 2025-01-13 10:22:03 -0800
• Engine revision e672b006cb
• Dart version 3.6.1
• DevTools version 2.40.2
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
• Android SDK at /Users/andonigarcialopez/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16C5032a
• CocoaPods version 1.15.2
[✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome)
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[✓] Android Studio (version 2024.2)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1)
[✓] Connected device (4 available)
• motorola one action (mobile) • ZY326PJXNS • android-arm64 • Android 11 (API 30)
• Andoni (mobile) • 00008110-000925D10A12401E • ios • iOS 18.3.2 22D82
• macOS (desktop) • macos • darwin-arm64 • macOS 15.2 24C101 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 15.2 24C101 darwin-arm64
[✓] Network resources
• All expected network resources are available.
Firebase version:
firebase_messaging: ^15.2.4
firebase_core_platform_interface: ^5.4.0