I want to navigate to some page when user taps on the notification. I'm using cloud messaging and flutter_local_notifications. I've managed to the do it with foreground notifications. It was pretty straightforward. I pasted the same code to my background notification handler, didn't work. Also I've look for onTap callback for notifications but couldn't find anything related to that.
Here's my background notification handler.
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
var androidDetails = AndroidNotificationDetails(
"Channel ID",
"Shannonside",
"Shannonside Channel",
);
var iosDetails = IOSNotificationDetails();
var details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
if (message.notification != null) {
final title = message.notification.title;
final body = message.notification.body;
await NotificationService.localNotification.show(0, title, body, details);
}
if (message.data != null) {
var articleId = message.data['articleId'];
var category = message.data['category'];
if (articleId != null && category != null) {
print("ArticleID: $articleId Category $category");
//#TODO Add navigation service and move to the article detail
NavigatorService.instance.navigateTo("/articlePage", arguments: {
"articleId": articleId.toString().toLowerCase(),
"category": category.toString().toLowerCase(),
});
}
}
}
It's not working, not even my function fired up. Also they stated in the documentation that it's not possible.
Since the handler runs in its own isolate outside your applications context, it is not possible to update application state or execute any UI impacting logic. You can however perform logic such as HTTP requests, IO operations (updating local storage), communicate with other plugins etc.
I know some apps do that, they open some page when you click on notification. Like a dynamic link. I want to implement this in my app.
You can use function
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage message) {
print("FirebaseMessaging.getInitialMessage");
if (message != null) {
Navigator.of(context).pushNamed('/call');
}
});
This function only run the first time when the app open, It gets last message
Related
I'm setting up push notifications via FCM and LocalPushNotifications, I was able to set it up in foreground state of app and in the background. In terminated state of the app, I do receive the notification, but when I press on it, no action happens, even though the foreground and background state of the app is working fine and navigates the user to the notification screen, in the terminated state, the app just opens and it doesn't navigate to the notification screen, only opens the main screen of the app. Since the device is not connected I can't see the error inside the console log, but when I start the app from the emulator, this is what I get on start:
I/flutter ( 3829): Got a message whilst in the terminated state!
I/flutter ( 3829): Message data: null
This is called inside the pushNotifications() method at FirebaseMessaging.instance.getInitialMessage().then()...
Here is the code with comments inside:
Logic for handling push notifications:
Future<void> pushNotifications() async {
await Firebase.initializeApp();
RemoteMessage initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleMessage(initialMessage);
}
/// THIS IS NOT WORKING, IT OPENS THE APP BUT DOESN'T NAVIGATE TO THE DESIRED SCREEN
///gives you the message on which user taps
///and it opened the app from terminated state
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage message) {
LocalNotificationService.display(message);
print('Got a message whilst in the terminated state!');
print('Message data: ${message.data}');
if (message != null) {
print('terminated state');
}
});
///forground work
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
LocalNotificationService.display(message);
print('Got a message whilst in the foreground!');
print('Message data: ${message.data}');
});
///EVEN THOUGH IT IS SUPPOSED TO WORK LIKE THIS, I ONLY MANAGED TO MAKE IT WORK WITH BACKGROUND HANDLER, THIS METHOD NEVER TRIGGERS
///When the app is in background but opened and user taps
///on the notification
FirebaseMessaging.onMessageOpenedApp.listen((message) {
print('Got a message whilst in the background!');
print('Message data: ${message.data}');
_handleMessage(message);
LocalNotificationService.display(message);
});
}
///THIS HANDLES THE NOTIFICATIONS WHEN THE APP IS IN THE BACKGROUND
Future<void> _handleMessage(RemoteMessage message) async {
await Firebase.initializeApp();
if (message.data != null) {
print('message handler');
LocalNotificationService.display(message);/// ALL OF THESE CALLED FROM THE LocalNotificationService CLASS BELOW
}
}
///MAIN METHOD, WHERE I INITIALIZE FIREBASE AND THE METHODES ABOVE(pushNotifications()), HANDLE THE MESSAGES WITH onBackgroundMessage(_handleMessage),
void main() async {
WidgetsFlutterBinding.ensureInitialized();
pushNotifications();
var initializationSettingsAndroid =
AndroidInitializationSettings("#mipmap/ic_launcher");
var initializationSettingsIOS = IOSInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
await Firebase.initializeApp();
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
FirebaseMessaging.onBackgroundMessage(_handleMessage);
runApp(MyApp());
}
My local notifications service class:
class LocalNotificationService {
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
static void initialize(BuildContext context) {
final InitializationSettings initializationSettings =
InitializationSettings(
android: AndroidInitializationSettings("#mipmap/ic_launcher"));
_notificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String payloadData) async {
if (payloadData!= null) {
Navigator.pushNamed(context, NotificationsScreen.id);
}
});
}
static void display(RemoteMessage message) async {
try {
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
"push notifications",
"push notifications",
"push notifications",
importance: Importance.max,
priority: Priority.high,
));
await _notificationsPlugin.show(
id,
'push notifications',
'You have received a new push notification!',
notificationDetails,
payload: message.data['default'], // THIS IS NULL WHEN IN TERMINATED STATE OF APP
);
} on Exception catch (e) {
print('exception: ' + e.toString());
}
}
}
So like I said, both foreground and background state is working and corresponding to the correct screen, but the terminated app state is not corresponding at all, but it does show the notification and opens the app when tapped on it.
Am I missing something? I mostly followed the documentation and some stuff on my own, but it is still not working as desired.
Any form of help is appreciated, thanks in advance!
I haven't been able to completely crack the problem you describe, and its very frustrating.
This is what I've figured out so far, and I'll put this here as it may help you:
There are 3 types of messages, data, notification and mixed.
If you use a pure data payload
The message is essentially silent.
onMessage does respond to data notifications when app foregrounded
onMessageOpenedApp does not trigger at all for data messages in any scenario
onBackgroundMessage is used to show a message while in terminated state or background state using local notifications
It is up to you (using the local notifications package) to deal with their clicks, using the onSelectNotification
await flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: selectNotification
);
You cannot use onSelectNotification in all these scenarios (as far as I've been able to yet determine *** this is a stumbling block for me right now
If you use a pure notification or mixed payload
The FCM package will display notifications for you when the app is backgrounded (but not foreground)
onMessage does respond to data when app foregrounded
onMessageOpenedApp WILL trigger when the notification is clicked in background state (but not for terminated state)
onBackgroundMessage will trigger, but unlike above, you should not use this to display a notification, as that will force a double notification (one processed by the FCM package and the other done by you manually)
Clicks, are dealt with by the package when the app is backgrounded, but by you when foregrounded. *** possibly also terminated, not sure yet.
As I mentioned, I've laid down some of the facts as Ive figure them out so far. Its very tricky and frustrating. Whats slowing me down immensely is that when using a mixed payload (what I've been using), while terminated the notifications either don't come at all, or come at their own pace (hours after they are sent).
If you make progress on this problem, let me know I think we are in the same boat...
#GrandMagus As luck has it I just got mine to work!! (getting the onMessageOpenedApp to trigger So I made a few changes and I'm not sure which one got it going.
I cleared all my caches and deleted the app and installed from scratch.
Edited AndroidManifest.xml to make changes as suggested by the documentation, in case you did do that they are:
<intent-filter> <action android:name="FLUTTER_NOTIFICATION_CLICK" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter>
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="high_importance_channel" />
Changed my payload to be mixed data and notification. I included FLUTTER_NOTIFICATION_CLICK in my payload. I can explain that one further incase you don't know what that is. Its key to make your payload have at least the notification information. This forces the package to create the notification which will then respond to clicks. Do not handle creating the local push notification yourself in this scenario or you will get 2 notifications. If you are getting two, you will notice that the one generated by the package will in fact run the onMessageOpenedApp
I started testing my app in Release mode. Im wondering if the AndroidManifest.xml changes that allow for the heads up notifications only work if they are placed in each .xml file. In this case, I was editing the release file but testing on the debug version
So I have push notifications implemented in my App and when the app first starts up, its asks users if they would like to allow push notifications (this implementation works fine as expected).
If this user disallows the push notifications, is it possible to have a button in the app which allows the user to click on and it would ask to allow permissions again?
This is what im trying to achieve:
SettingsView
//IF PUSH NOTIFICATIONS NOT ENABLED, SHOW THIS SECTION
Section (header: Text("Push Notifications")) {
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Enable Push Notifications").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
checkPushNotifications()
}) {
Text("View").font(.system(size:12))
}
}
}
In my Push Notification Function:
class PushNotificationService: NSObject, MessagingDelegate {
static let shared = PushNotificationService()
private let SERVER_KEY = "myserverkey"
private let NOTIFICATION_URL = URL(string: "https://fcm.googleapis.com/fcm/send")!
private let PROJECT_ID = "my project name"
private override init() {
super.init()
Messaging.messaging().delegate = self
}
func askForPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted: Bool, error: Error?) in
if granted {
self.refreshFCMToken()
} else {
// Maybe tell the user to go to settings later and re-enable push notifications
}
}
}
func refreshFCMToken() {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("Error fetching remote instance ID: \(error)")
} else if let result = result {
print("Remote instance ID token: \(result.token)")
self.updateFCMToken(result.token)
}
}
}
func updateFCMToken(_ token: String) {
guard let currentUser = Auth.auth().currentUser else { return }
let firestoreUserDocumentReference = Firestore.firestore().collection("users").document(currentUser.uid)
firestoreUserDocumentReference.updateData([
"fcmToken" : token
])
}
}
What im trying to achieve is if the user HAS NOT enabled notification only then ask them the option to reenable in SettingsView.
No you cannot. However, a good UI/UX design will be careful before burning the one-time chance of asking for permissions. Instead, use a user friendly UI to explain why you need certain permissions. For example, I often found it frustrating to implement a permission display view, and handle various async permission requests in a seperate view model. So I recently made a SwiftUI package:
PermissionsSwiftUI
PermissionSwiftUI is a package to beautifully display and handle permissions.
EmptyView()
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
For a SINGLE line of code, you get a beautiful UI and the permission dialogs.
It already supports 7 OUT OF 12 iOS system permissions. More features coming 🙌
Full example
struct ContentView: View {
#State var showModal = false
var body: some View {
Button(action: {showModal=true},
label: {Text("Ask user for permissions")})
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
}
}
To use PermissionsSwiftUI, simply add the JMPermission modifier to any view.
Pass in a Binding to show the modal view, and add whatever permissions you want to show.
The short answer is no, you can't ask the user again if he once disabled the push-notifications for your app.
What you can do, is navigating the user to the settings in their phone to allow push-notifications again.
The code snippet in SwiftUI for the button would be:
Button(action: {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}, label: {
Text("Allow Push")
})
I would also refer to this question: How to ask notifications permissions if denied?
I am integrating Huawei Push Kit (https://pub.dev/packages/huawei_push) in Flutter application.
I test to schedule a notification to notify every minutes in my Huawei phone, and I able to get the notification as expected. But when I tried to cancel the notification with clearNotification() function (shown below), the notification seem like not cancelled. Wondering do I miss out anything?
scheduleLocalNotification() async {
try {
Map<String, dynamic> localNotification = {
HMSLocalNotificationAttr.TITLE: 'Notification Title',
HMSLocalNotificationAttr.MESSAGE: 'Notification Message',
HMSLocalNotificationAttr.TICKER: "OptionalTicker",
HMSLocalNotificationAttr.TAG: "push-tag",
HMSLocalNotificationAttr.BIG_TEXT: 'This is a bigText',
HMSLocalNotificationAttr.SUB_TEXT: 'This is a subText',
HMSLocalNotificationAttr.LARGE_ICON: 'ic_launcher',
HMSLocalNotificationAttr.SMALL_ICON: 'ic_notification',
HMSLocalNotificationAttr.COLOR: "white",
HMSLocalNotificationAttr.VIBRATE: true,
HMSLocalNotificationAttr.VIBRATE_DURATION: 1000.0,
HMSLocalNotificationAttr.ONGOING: false,
HMSLocalNotificationAttr.DONT_NOTIFY_IN_FOREGROUND: false,
HMSLocalNotificationAttr.AUTO_CANCEL: false,
HMSLocalNotificationAttr.INVOKE_APP: false,
HMSLocalNotificationAttr.ACTIONS: ["Yes", "No"],
HMSLocalNotificationAttr.REPEAT_TYPE: RepeatType.MINUTE,
HMSLocalNotificationAttr.FIRE_DATE:
DateTime.now().add(Duration(minutes: 1)).millisecondsSinceEpoch,
};
Map<String, dynamic> response =
await Push.localNotificationSchedule(localNotification);
print("Scheduled a local notification: " + response.toString());
} catch (e) {
print("Error: " + e.toString());
}
}
clearNotification() async {
Push.cancelNotificationsWithTag('push-tag');
}
Currently, Huawei Push Kit does not support clearNotification() function. You can use the Sending Downlink Messages API. There is an auto_clear parameter, and you can set message display duration, in milliseconds. Messages are automatically deleted after the duration expires.
Please refer to the following code:
{
"validate_only": false,
"message": {
"android": {
"notification":{"body":"Big Boom Body!","click_action":{"type":3},"title":"Big Boom Title","auto_clear": 360000}
},
"token":[
"token1",
"token2"]
}
}
For more information, see docs
Your clearNotification function which calls Push.cancelNotificationWithTag('push-tag') is async.
Possibility is Push.cancelNotificationWithTag might get called before.
Either call it directly and not thru async function or add await.
I have an app with different 'procedures' (think posts or pages), which one can like. Currently the process works: Tap like => run method "likeProcedure" => run dispatch action "likeProcedure" => update UI. It usually happens almost immediately, but sometimes there's a lag that gives this a "non-native" feel. Is there some sort of way that I could return feedback immediately, while stile holding single origin of truth on the firebase database?
Thank you!
Page Code:
<v-icon
v-if="!userProfile.likedProcedures || !userProfile.likedProcedures[procedure.id]"
color="grey lighten-1"
#click="likeProcedure({ id: procedure.id })"
>
mdi-star-outline
</v-icon>
and
computed: {
...mapState(["userProfile"]),
procedures() {
return this.$store.getters.getFilteredProcedures();
},
},
Vuex code:
async likeProcedure({ dispatch }, postId) {
const userId = fb.auth.currentUser.uid;
// update user object
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: true,
});
dispatch("fetchUserProfile", { uid: userId });
},
Side note: I'm trying to remove the dispatch("fetchUserProfile") command, but this doesn't work, because then I'm calling dispatch without using it. And I cannot remove dispatch because then the object calling it is empty. And I cannot remove the object, because then the argument ('postId') isn't working. So if anyone knows how to deal with that, that would be extremely helpful.
Thank you :)
So this is the best solution I've come up yet. It kind of destroys the idea of a single source of truth, but at least it provides an immediate UI update:
async likeProcedure({ dispatch, state }, postId) {
console.log("likeProcedure");
const userId = fb.auth.currentUser.uid;
// line below provides immediate update to state and hence to the UI
state.userProfile.likedProcedures[postId.id] = true;
// line below updates Firebase database
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: state.userProfile.likedProcedures[
postId.id
],
});
// line below then fetches the updated profile from Firebase and updates
// the profile in state. Kind of useless, but ensures that client and
// Firebase are in-sync
dispatch("fetchUserProfile", { uid: userId });
},
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
commit("setUserProfile", userProfile.data());
// change route to dashboard
if (router.currentRoute.path === "/login") {
router.push("/");
}
},
I'm using react-native-firebase in my app. The problem i'm facing is how to handle the UI updates when user tries to push data when offline.
If the user is online we can use the on() method to get realtime updates but what to do when they are offline. We know that the pushed data is stored in the cache and pushed when user is online again. Can this cached data be used to do what i aim at achieving?
Here's the code i used to receive realtime updates:
var ref333 = firebase.database().ref(`/user-posts/${uid}/`)
ref333.on('value',function (snap) {
var s = snap.val();
console.log("NEW POSTS "+JSON.stringify(s))
})
The code i use to push the data.
var postData = {
uid: uid,
body: 'body',
title: 'title',
starCount: 0
};
// Get a key for a new Post.
var newPostKey = firebase.database().ref().child('posts').push().key;
var ref222 = firebase.database().ref(`/posts/${newPostKey}`)
var ref333 = firebase.database().ref(`/user-posts/${uid}/${newPostKey}`)
ref222.push(postData, function (onComplete) {
console.log("COMPLETED")
ref333.push(postData,function (onComplete) {
console.log("NEXT COMPLETED")
}, function (error) {
console.log("ERROR IN ",error)
})
}, function (error) {
console.log("error == "+error)
})
The .on snspashot listener should be triggered even if in offline mode.
According to the docs:
https://firebase.google.com/docs/database/web/read-and-write
You can use the value event to read a static snapshot of the contents
at a given path, as they existed at the time of the event. This method
is triggered once when the listener is attached and again every time
the data, including children, changes.
This should work in offline mode as well. If you are not receiving updates - something else is wrong.
This problem was solved by adding this lines of code to your native code:
https://rnfirebase.io/docs/v5.x.x/core/default-app#Enable-Database-Persistence