How to implement FCM 9+ to work correctly on IOS versions 14+?
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
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.
Related
I would like to build an app like when I save a new data with latitude and longitude information to firebase then in my app I calculate the distance between these latitude&longitude and the user's current location. If the distance is less than 60 kilometres then send an onBackgroundMessage notification. I do not store the user's current location on firebase. I get the user's current location with function _getCurrentLocation.
The problem is that I do not undertsand where and how to put the isValidDistance to check if the distance is under 60 kilimetres.
Currently my app send notifications but not by distance.
index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.myFunction = functions.firestore
.document("animal/{message}")
.onCreate((snapshot, context) => {
return admin.messaging().sendToTopic("animal", {
data: {
latitude: snapshot.data()["latitude"].toString(),
longitude: snapshot.data()["longitude"].toString(),
},
notification: {
title: snapshot.data().username,
body: snapshot.data().description,
clickAction: "FLUTTER_NOTIFICATION_CLICK",
},
});
});
main.dart
Future<void> _messageHandler(RemoteMessage message) async {
print('background message ${message.data}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
double? currentLatitude;
double? currentLongitude;
Future<void> _getCurrentLocation() async {
final locData = await Location().getLocation();
setState(() {
currentLatitude = locData.latitude;
currentLongitude = locData.longitude;
});
}
int getDistanceInMeters(currLat, currLng, lat, lng) {
return Geolocator.distanceBetween(
currLat,
currLng,
lat,
lng,
).round();
}
bool isValidDistance(RemoteMessage messaging) {
Map<String, dynamic> data = messaging.data;
var _list = data.values.toList();
var lat = double.parse(_list[0]);
var lng = double.parse(_list[1]);
print(_list);
int distance =
getDistanceInMeters(currentLatitude, currentLongitude, lat, lng);
var distanceInKm = (distance / 1000).round();
print('Distance is: ${distanceInKm.toString()}');
if (distance < 60000) {
return true;
}
return false;
}
#override
void initState() {
super.initState();
_getCurrentLocation();
final messaging = FirebaseMessaging.instance;
messaging.subscribeToTopic('animal');
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (isValidDistance(message)) {
print('onMessageListen');
}
});
FirebaseMessaging.onMessageOpenedApp.listen((message) {
if (isValidDistance(message)) {
print('onMessageOpened');
}
});
FirebaseMessaging.onBackgroundMessage(_messageHandler);
}
...
First you need to change your fcm from notification message to data message to allow app handle messages while in background. Check here.
exports.myFunction = functions.firestore
.document("animal/{message}")
.onCreate((snapshot, context) => {
return admin.messaging().sendToTopic("animal", {
data: {
latitude: snapshot.data()["latitude"].toString(),
longitude: snapshot.data()["longitude"].toString(),
title: snapshot.data().username,
body: snapshot.data().description,
},
});
});
Check here to show push notification while app is open. Your code should like this.
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (isValidDistance(message)) {
print('onMessageListen');
showNotification(message);
}
});
Yo can access the data sent through the following
Map<String, dynamic> data = message.data;
Then the background handler will be as follows.
_messageHandler(RemoteMessaging message){
if (isValidDistance(message)) {
print('onMessageListen');
showNotification(message);
}
}
Or create a Notification class as follows and use.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator/geolocator.dart';
import 'package:location/location.dart';
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.',
importance: Importance.max,
);
Future<LocationData> _getCurrentLocation() => Location().getLocation();
int getDistanceInMeters(currLat, currLng, lat, lng) {
return Geolocator.distanceBetween(
currLat,
currLng,
lat,
lng,
).round();
}
Future<bool> isValidDistance(RemoteMessage messaging) async {
Map<String, dynamic> data = messaging.data;
var _list = data.values.toList();
var lat = double.parse(_list[0]);
var lng = double.parse(_list[1]);
print(_list);
var location = await _getCurrentLocation();
int distance =
getDistanceInMeters(location.latitude, location.longitude, lat, lng);
var distanceInKm = (distance / 1000).round();
print('Distance is: ${distanceInKm.toString()}');
if (distance < 60000) {
return true;
}
return false;
}
class NotificationServices {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
bool isValid = await isValidDistance(message);
if (isValid) {
print('onMessageListen');
showNotification(message);
}
}
backgroundNotification() {
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final AndroidInitializationSettings _initialzationSettingsAndriod =
AndroidInitializationSettings('#mipmap/ic_launcher');
final IOSInitializationSettings _initialzationSettingsIOS =
IOSInitializationSettings();
final InitializationSettings _initializationSettings =
InitializationSettings(
android: _initialzationSettingsAndriod,
iOS: _initialzationSettingsIOS);
_flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
/// Update the iOS foreground notification presentation options to allow
/// heads up notifications.
FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
_flutterLocalNotificationsPlugin.initialize(_initializationSettings);
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage? message) async {
if (message != null) await onClickNotificationHandler(message);
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
bool isValid = await isValidDistance(message);
if (isValid) {
print('onMessageListen');
showNotification(message);
}
});
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
await onClickNotificationHandler(message);
});
}
onClickNotificationHandler(RemoteMessage message) async {
Map<String, dynamic> data = message.data;
print(data);
//you can handle notificationand navigate to necessary screen here.
}
showNotification(RemoteMessage message) {
Map<String, dynamic> data = message.data;
if (data["body"] != null) {
flutterLocalNotificationsPlugin.show(
data.hashCode,
data["title"],
data["body"],
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
icon: '#mipmap/ic_launcher',
),
iOS: IOSNotificationDetails(
presentAlert: true, presentBadge: true, presentSound: true),
),
);
}
}
}
I am working on the chat section of my flutter application. I have a chat_messages collection which contains all of the messages sent by users for all chats in the application. Below is the structure of a chat_messages document:
Here user is the sender of the message. I would like to display the number of unread messages (where the message.seen==false) for a specific user hence i used the query below to get a stram of all messages which where not seen by the user and i listen to that stream for any new messages sent:
unreadMessagesStream = queryChatMessagesRecord(queryBuilder: (query)
{
return query
.where('chat_users', arrayContains: currentUserReference)
.where('user', isNotEqualTo: currentUserReference)
.where('seen', isEqualTo: false);
});
unreadMessagesStream.listen((msgs) {
if (mounted)
setState(() {
unreadMessagesCount = msgs?.length ?? 0;
});
});
Unfortunately, this stream only produces a value once when the app is run, but later on when any new message is sent, new values are not received in the stream and the number of unread messages remain the same.
NB: If I remove the filters and query the whole collection is works perfectly fine.
I give you a snippet of my code to get it done faster: The code juste below is for each user. It's a subcollection of the chat. So a user could have a chatMembre for each chat.
import 'package:cloud_firestore/cloud_firestore.dart';
enum IsDoing { reading, notReading, writing, recording }
class ChatMembre {
final String id;
final DateTime lastReading;
final DateTime lastReceived;
final IsDoing isDoing;
final bool hasSubscribeToTopic;
ChatMembre(
{required this.id,
required this.lastReading,
required this.lastReceived,
required this.isDoing,
required this.hasSubscribeToTopic});
Map<String, dynamic> toMap() {
return {
'id': id,
'lastReading': lastReading == DateTime.now()
? FieldValue.serverTimestamp()
: DateTime.now(),
'lastReceived': lastReceived == DateTime.now()
? FieldValue.serverTimestamp()
: DateTime.now(),
'isDoing':
isDoing.toString().substring(isDoing.toString().indexOf(".") + 1),
'isSubscribeToTopic': hasSubscribeToTopic
};
}
factory ChatMembre.fromMap(Map<String, dynamic>? map) {
if (map == null || map.isEmpty) {
return ChatMembre(
id: '',
lastReading: DateTime.now(),
lastReceived: DateTime.now(),
hasSubscribeToTopic: false,
isDoing: IsDoing.notReading);
}
IsDoing isDoing;
switch (map["isDoing"]) {
case "reading":
isDoing = IsDoing.reading;
break;
case "writing":
isDoing = IsDoing.writing;
break;
case "recording":
isDoing = IsDoing.recording;
break;
default:
isDoing = IsDoing.notReading;
break;
}
return ChatMembre(
id: (map['id'] ?? '') as String,
lastReading:
((map['lastReading'] ?? Timestamp.now()) as Timestamp).toDate(),
lastReceived:
((map['lastReceived'] ?? Timestamp.now()) as Timestamp).toDate(),
isDoing: isDoing,
hasSubscribeToTopic: (map['isSubscribeToTopic'] ?? false) as bool);
}
#override
String toString() {
return 'ChatMembre{id: $id, lastReading: $lastReading, lastReceived: $lastReceived, isDoing: $isDoing, hasSubscribeToTopic: $hasSubscribeToTopic}';
}
}
And under it's to look for the state of the chat page.
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:customer/constants/credentials.dart';
import 'package:customer/constants/firestore_path.dart';
import 'package:customer/domain/repositories/my_chat_repository.dart';
import 'package:customer/services/firestore_service.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:http/http.dart' as http;
class ChatRoomLifeCycle extends StatefulWidget {
final Widget child;
final MyChatRepository chatRepo;
final String chatId;
final String? token;
final String? idTo;
final Timestamp? lastReceivedOfFriend;
const ChatRoomLifeCycle(
{Key? key,
required this.chatId,
required this.chatRepo,
required this.child,
this.token,
this.idTo,
this.lastReceivedOfFriend})
: super(key: key);
#override
_ChatRoomLifeCycleState createState() => _ChatRoomLifeCycleState();
}
class _ChatRoomLifeCycleState extends State<ChatRoomLifeCycle>
with WidgetsBindingObserver {
late GlobalKey<AnimatedListState> listKey;
bool hasSentFcm = false;
#override
void initState() {
super.initState();
sendPushMessage();
WidgetsBinding.instance!.addObserver(this);
widget.chatRepo.setIsReading();
}
#override
void dispose() {
widget.chatRepo.setIsNotReading(isFromDispose: true);
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
widget.chatRepo.setIsNotReading();
break;
case AppLifecycleState.resumed:
widget.chatRepo.setIsReading();
break;
case AppLifecycleState.inactive:
widget.chatRepo.setIsNotReading();
break;
case AppLifecycleState.detached:
widget.chatRepo.setIsNotReading();
break;
}
}
Future<void> sendPushMessage() async {
if (hasSentFcm || widget.idTo == null || widget.token == null) {
return;
}
FirestoreService.instance.updateData(
path: MyPath.myUserStatus(uid: widget.idTo!), data: {'isLogin': false});
try {
await http
.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json',
'Authorization': 'key=$serverToken',
},
body: constructFCMPayload(widget.token!),
)
.catchError((onError) {});
hasSentFcm = true;
} catch (e) {
Fluttertoast.showToast(msg: e.toString());
}
}
// Crude counter to make messages unique
/// The API endpoint here accepts a raw FCM payload for demonstration purposes.
String constructFCMPayload(String token) {
return jsonEncode(<String, dynamic>{
'data': <String, dynamic>{
'test': 'check online',
'chatId': widget.chatId,
'idTo': widget.idTo
},
'to': token,
});
}
}
I am developing an app using Flutter, and I am implementing push notification using FCM with the firebase_messaging: ^10.0.4 Flutter plugin:
I am using Firebase to send notification on mobile app while the mobile app is in terminated state. To get the notification in terminated state, I am using FirebaseMessaging.instance.getInitialMessage() to handle the on click of notification. When the user clicks on the notification, they will be routed to a specific screen (which shows the message passed).
The issue is I am getting the notification in mobile app in terminated state, but when I click on the notification, I am not routed to the specific screen which I passed in from Firebase and FirebaseMessaging.instance.getInitialMessage() value is getting null in message.
Please let me know If anyone have idea about this.
main.dart
checkFirebase() async {
await Firebase.initializeApp();
// 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
description: 'This channel is used for important notifications.',
// description
importance: Importance.max,
);
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.showFrontNotification
await FirebaseMessaging.instance
.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
}
}
class _ProcessAppState extends State<ProcessApp> {
Future<void> _initializeFuture;
Future<void> _initializeServices() async {
await Firebase.initializeApp();
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
var dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
// pass all uncaught errors to crashlytics
Function originalOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails errorDetails) async {
await FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
originalOnError(errorDetails);
};
// 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
description: 'This channel is used for important notifications.',
// description
importance: Importance.max,
);
/*
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,
);
}
}
#override
void initState() {
super.initState();
_initializeFuture = _initializeServices();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: FutureBuilder(
future: _initializeFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
reportError(
error: snapshot.error, stackTrace: snapshot.stackTrace);
return Center(
child: Text(context.translateText(key: "general_error")),
);
}
if (snapshot.connectionState == ConnectionState.done) {
return MyApp();
}
return progressBar();
},
),
),
);
}
}
#override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: gradientTopColor,
statusBarBrightness: Brightness.dark,
),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: primaryColor,
backgroundColor: whiteColor,
fontFamily: 'Roboto'),
supportedLocales: [
Locale('en'),
Locale('ta'),
Locale('ml'),
Locale('kn'),
Locale('te'),
],
localizationsDelegates: [
// for our own localizations
AppLocalizations.delegate,
// localizations for all material widgets provided
GlobalMaterialLocalizations.delegate,
// localizations for all cupertino widgets provided
DefaultCupertinoLocalizations.delegate,
// for rtl, ltr text directions
GlobalWidgetsLocalizations.delegate,
],
locale: _locale,
localeResolutionCallback: (deviceLocale, supportedLocales) {
try {
return Locale(defaultLang);
} catch (e) {
print(e);
return Locale("en");
}
},
// navigation analytics reporting
// navigatorObservers: <NavigatorObserver>[observer],
home: NotificationMessageHandler(child: LauncherScreen()),
builder: EasyLoading.init(),
),
);
}
message_handler.dart
class _NotificationMessageHandlerState extends State<NotificationMessageHandler>
with AfterLayoutMixin<NotificationMessageHandler> {
#override
void initState() {
super.initState();
// _checkForUpdate();
var initializationSettingsAndroid =
AndroidInitializationSettings("#mipmap/ic_launcher");
var initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (payload) async {
log("payload : $payload", name: "onSelectNotification");
handleNotificationClick(context, jsonDecode(payload));
});
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage initialMessage) async {
log("message : $initialMessage", name: "getInitialMessage");
handleNotificationClick(context, jsonDecode(initialMessage.toString()));
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print(message);
showFrontNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
handleNotificationClick(context, message.data);
});
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
When your app is terminated and you want to navigate to another screen you need context from your MaterialApp's navigatorKey and also a keyword where you actually want to go, the keyword we will "click_action" key in the FCM request body.
I would recommend you handle your Firebase Messaging code in a separate file.
fcm_service.dart
This file contains route navigator in foreground, background and after app terminated, also handles notification with an image.
import 'dart:convert';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
import '../main.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'custom_notification_channel_id',
'Notification',
description: 'notifications from Your App Name.',
importance: Importance.high,
);
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
void setupFcm() {
var initializationSettingsAndroid = const AndroidInitializationSettings('#mipmap/ic_launcher');
var initializationSettingsIOs = const IOSInitializationSettings();
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOs,
);
//when the app is in foreground state and you click on notification.
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String payload) {
if (payload != null) {
Map<String, dynamic> data = json.decode(payload);
goToNextScreen(data);
}
});
//When the app is terminated, i.e., app is neither in foreground or background.
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage message) {
//Its compulsory to check if RemoteMessage instance is null or not.
if (message != null) {
goToNextScreen(message.data);
}
});
//When the app is in the background, but not terminated.
FirebaseMessaging.onMessageOpenedApp.listen((event) {
goToNextScreen(event.data);
},
cancelOnError: false,
onDone: () {},
);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
RemoteNotification notification = message.notification;
AndroidNotification android = message.notification?.android;
if (notification != null && android != null) {
if (android.imageUrl != null && android.imageUrl.trim().isNotEmpty) {
final String largeIcon = await _base64encodedImage(
android.imageUrl,
);
final BigPictureStyleInformation bigPictureStyleInformation =
BigPictureStyleInformation(
ByteArrayAndroidBitmap.fromBase64String(largeIcon),
largeIcon: ByteArrayAndroidBitmap.fromBase64String(largeIcon),
contentTitle: notification.title,
htmlFormatContentTitle: true,
summaryText: notification.body,
htmlFormatSummaryText: true,
hideExpandedLargeIcon: true,
);
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
icon: 'custom_notification_icon',
color: primaryColor,
importance: Importance.max,
priority: Priority.high,
largeIcon: ByteArrayAndroidBitmap.fromBase64String(largeIcon),
styleInformation: bigPictureStyleInformation,
),
),
payload: json.encode(message.data),
);
}
else {
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
icon: 'custom_notification_icon',
color: primaryColor,
importance: Importance.max,
priority: Priority.high,
),
),
payload: json.encode(message.data),
);
}
}
});
}
Future<void> deleteFcmToken() async {
return await FirebaseMessaging.instance.deleteToken();
}
Future<String> getFcmToken() async {
String token = await FirebaseMessaging.instance.getToken();
return Future.value(token);
}
void goToNextScreen(Map<String, dynamic> data) {
if (data['click_action'] != null) {
switch (data['click_action']) {
case "first_screen":
navigatorKey.currentState.pushNamed(FirstScreen.routeName,);
break;
case "second_screen":
navigatorKey.currentState.pushNamed(SecondScreen.routeName,);
break;
case "sample_screen":
navigatorKey.currentState.pushNamed(SampleScreen.routeName,);
}
return;
}
//If the payload is empty or no click_action key found then go to Notification Screen if your app has one.
navigatorKey.currentState.pushNamed(NotificationPage.routeName,);
}
Future<String> _base64encodedImage(String url) async {
final http.Response response = await http.get(Uri.parse(url));
final String base64Data = base64Encode(response.bodyBytes);
return base64Data;
}
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
runApp(const MyApp());
}
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatefulWidget {
const MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
setupFcm();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: //Define your named routes.
);
}
}
Also, you need to define the default notification channel id, and optionally default notification icon, default notification color
AndroidManifest.xml
<application>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="custom_notification_channel_id" />
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="#drawable/custom_notification_icon" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming notification message. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="#color/notification_icon_color" />
</application>
Also you can check sample FCM HTTP Request for handling "data" key JsonObject and "click_action" in it.
URL: https://fcm.googleapis.com/fcm/send
Request Method: POST
Header: {
"Authorization": "key={value1}",
"Sender": "id={value2}",
}
Request Body: {
"registration_ids": [
"exwlH32_S0il4ky4ZRXCrg:APA91bHp4kL-IJmtHRGFcQlhUauEY1ZiqZFfWsDkWqsB-yHDUzRVx63e8ehSirUTbSg6NqMqAAfcW16tk4dgs-NtTcCVShipGt9JWIJK_r8b4ldqFYGhzZcNF0VTiVKWzWkRQQIncCoE"
],
"notification": {
"title": "Wear Mask",
"body": "Maintain social distance",
"image": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"imageUrl": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"sound": "default"
},
"data": {
"click_action": "sample_screen",
"custom_key": "custom_value",
"image": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"imageUrl": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1"
}
}
Note: registration_ids key only takes 1000 values in the list.
In "data" JsonObject you can define your custom key-value pair, which will come in handy. e.g., you want to open a specific screen let's say an event_screen.dart and you need to fetch the event details from the server by event id. So you can prepare your "data" object accordingly
"data": {
"click_action": "event_screen",
"event_id": "23"
}
I know I am late to the party but may this help someone in future. I was facing the same issue and I used the above answer by Smeet Bhatt but I was still facing the same issue. What I noticed that I was clicking too fast may be await Firebase.initializeApp(); was not finished yet. When I waited for a while, RemoteMessage was never null on FirebaseMessaging.instance.getInitialMessage().
// ignore_for_file: deprecated_member_use
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:http/http.dart' as http;
class PaymentData {
static Future<void> makePayment() async {
Map<String, dynamic>? paymentIntentData;
final url = Uri.parse(
'firebase function url');
final response =
await http.get(url, headers: {'Content-Type': 'application/json'});
paymentIntentData = jsonDecode(response.body);
print(paymentIntentData);
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: paymentIntentData!['paymentIntent'],
applePay: true,
googlePay: true,
style: ThemeMode.system,
merchantCountryCode: 'US',
merchantDisplayName: 'Raja',
));
//displayPaymentSheet(paymentIntentData);
}
static Future<void> displayPaymentSheet(
Map<String, dynamic>? paymentIntentData) async {
try {
await Stripe.instance.presentPaymentSheet(
parameters: PresentPaymentSheetParameters(
clientSecret: paymentIntentData!['paymentIntent'],
confirmPayment: true));
} catch (e) {
print(e);
}
}
}
I am getting the same error, This is my code, I've tried many other logic but facing the same error.I have used the the flutter dependency flutter_stripe. I've created the Firebase function and also tried without using Firebase.
I have built an application with flutter that works like a reminder.
How can I display notifications to the user even though the app is closed?
For reminders i would recomend Flutter Local Notifications Plugin. It has a powerful scheduling api. From the documentation of local notification:
Scheduling when notifications should appear - Periodically show a
notification (interval-based) - Schedule a notification to be shown
daily at a specified time - Schedule a notification to be shown weekly
on a specified day and time - Ability to handle when a user has tapped on a notification when the app is the foreground, background or terminated
And for push notification, you can use Firebase Cloud Messaging
or one signal plugin or you can implement natively through platform-channels
Edit: You can also fire notifications according to specific conditions even if the app is terminated. This can be achevied by running dart code in the background. Quoting from the official faq:
Can I run Dart code in the background of an Flutter app? Yes, you can
run Dart code in a background process on both iOS and Android. For
more information, see the Medium article Executing Dart in the
Background with Flutter Plugins and Geofencing.
I have found a solution to this problem. We just have to register the Local Notification Plugin in the Application class.
First Create a class FlutterLocalNotificationPluginRegistrant, I have created this in Kotlin.
class FlutterLocalNotificationPluginRegistrant {
companion object {
fun registerWith(registry: PluginRegistry) {
if (alreadyRegisteredWith(registry)) {
Log.d("Local Plugin", "Already Registered");
return
}
FlutterLocalNotificationsPlugin.registerWith(registry.registrarFor("com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin"))
Log.d("Local Plugin", "Registered");
}
private fun alreadyRegisteredWith(registry: PluginRegistry): Boolean {
val key = FlutterLocalNotificationPluginRegistrant::class.java.canonicalName
if (registry.hasPlugin(key)) {
return true
}
registry.registrarFor(key)
return false
}
}}
Now create a Application class extending FlutterApplication and implement PluginRegistry.PluginRegistrantCallback.
class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
}
override fun registerWith(registry: PluginRegistry?) {
if (registry != null) {
FlutterLocalNotificationPluginRegistrant.registerWith(registry)
}
}}
and register the Application class in the AndroidManifest.xml
<application
android:name="com.packagename.Application"/>
All done. Now write a function to show notification and call it from the background handler method of Firebase messaging.
Future _showNotificationWithDefaultSound(String title, String message) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'channel_id', 'channel_name', 'channel_description',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
'$title',
'$message',
platformChannelSpecifics,
payload: 'Default_Sound',
);
}
and call it like this.
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) async {
if (message['data'] != null) {
final data = message['data'];
final title = data['title'];
final body = data['message'];
await _showNotificationWithDefaultSound(title, message);
}
return Future<void>.value();
}
I have also faced this issue, So these are my learnings
In my Case : i am able to get notification in App-Resume or App-background state, but in App-Close state, I am not receiving notifification.
In this case our notification body was :
{notification: {body: null, title: null}, data: {body: hello, title: world}}
To Receive Notification in App-Closed state we changed notification to
{notification: {body: abc, title: abc}, data: {url: string, body: string, title: string}}
You can use scheduled notifications in flutter.
var scheduledNotificationDateTime =
new DateTime.now().add(new Duration(seconds: 5));
var androidPlatformChannelSpecifics =
new AndroidNotificationDetails('your other channel id',
'your other channel name', 'your other channel description');
var iOSPlatformChannelSpecifics =
new IOSNotificationDetails();
NotificationDetails platformChannelSpecifics = new
NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.schedule(
0,
'scheduled title',
'scheduled body',
scheduledNotificationDateTime,
platformChannelSpecifics);
For those who are using the latest version around 2.2 just call the firebaseMessageInstance
FirebaseMessaging.instance.getInitialMessage().then((message) =>
message.messageId.isNotEmpty
? print('we can now navigate to specific screen')
: print('there is no new notification so default screen will be shown when application start from terminated state'));
Don't forget to call the
Navigator.push(
context, MaterialPageRoute(builder: (context) => YourScreenName()));
when message.messageId.isNotEmpty
upvote if you like this approach thanks have a good coding day
If you do not need to connect to the Internet, you can use this packages flutter local notification && flutter native timezone
after add the package to pubspace.ymal
add this code to android/app/src/main/AndroidManifest.xml
<activity
android:showWhenLocked="true"
android:turnScreenOn="true">
also in ios folder open if you used swift Runner/AppDelegate.swift in function didFinishLaunchingWithOptions add
if #available(iOS 10.0, *) {UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate}
if you used Object-C Runner/AppDelegate.m in function didFinishLaunchingWithOptions add
if (#available(iOS 10.0, *)) {[UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}
after that you should add app-icon to drawable folder
then import the packages import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; in file dart create and add
class NotifyHelper {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
String selectedNotificationPayload = '';
final BehaviorSubject<String> selectNotificationSubject =
BehaviorSubject<String>();
initializeNotification() async {
tz.initializeTimeZones();
_configureSelectNotificationSubject();
await _configureLocalTimeZone();
// await requestIOSPermissions(flutterLocalNotificationsPlugin);
final IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings(
requestSoundPermission: false,
requestBadgePermission: false,
requestAlertPermission: false,
onDidReceiveLocalNotification: onDidReceiveLocalNotification,
);
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('appicon');
final InitializationSettings initializationSettings =
InitializationSettings(
iOS: initializationSettingsIOS,
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onSelectNotification: (String? payload) async {
if (payload != null) {
debugPrint('notification payload: ' + payload);
}
selectNotificationSubject.add(payload!);
},
);
}
displayNotification({required String title, required String body}) async {
print('doing test');
var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
'your channel id', 'your channel name', 'your channel description',
importance: Importance.max, priority: Priority.high);
var iOSPlatformChannelSpecifics = const IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title,
body,
platformChannelSpecifics,
payload: 'Default_Sound',
);
}
// this is the scheduled notification
// Task is a model class have a data item like title, desc, start time and end time
scheduledNotification(int hour, int minutes, Task task) async {
await flutterLocalNotificationsPlugin.zonedSchedule(
task.id!,
task.title,
task.note,
//tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
_nextInstanceOfTenAM(hour, minutes),
const NotificationDetails(
android: AndroidNotificationDetails(
'your channel id', 'your channel name', 'your channel description'),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
payload: '${task.title}|${task.note}|${task.startTime}|',
);
}
tz.TZDateTime _nextInstanceOfTenAM(int hour, int minutes) {
final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
tz.TZDateTime scheduledDate =
tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minutes);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
void requestIOSPermissions() {
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
Future<void> _configureLocalTimeZone() async {
tz.initializeTimeZones();
final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
/* Future selectNotification(String? payload) async {
if (payload != null) {
//selectedNotificationPayload = "The best";
selectNotificationSubject.add(payload);
print('notification payload: $payload');
} else {
print("Notification Done");
}
Get.to(() => SecondScreen(selectedNotificationPayload));
} */
//Older IOS
Future onDidReceiveLocalNotification(
int id, String? title, String? body, String? payload) async {
// display a dialog with the notification details, tap ok to go to another page
/* showDialog(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
title: const Text('Title'),
content: const Text('Body'),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('Ok'),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Container(color: Colors.white),
),
);
},
)
],
),
);
*/
Get.dialog( Text(body!));
}
//I used Get package Get here to go screen notification
void _configureSelectNotificationSubject() {
selectNotificationSubject.stream.listen((String payload) async {
debugPrint('My payload is ' + payload);
await Get.to(() => NotificationScreen(payload));
});
}
}
use object from this class and call the scheduledNotificationmethod