Flutter: get message on Push notifications even if the app is closed using local notification - ios

I built an application for Android and iOS using Flutter, using local notifications, Firebase Cloud Messaging (FCM), and SharedPreferences to handle push notifications with news and calls to action (CTAs) about my product.
I have a section in the app where I display the latest received notifications, similar to a notification center. However, when the app is closed or in the background and the user receives a notification, they need to click on the notification for the app to be invoked, and the message to be saved and displayed in the notification center.
How can I overcome this limitation and make the app obtain the push content and save it in the notification center while it's in the background or closed, without the user clicking on the push notification?
I thought my method FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler) would solve this and should be called in this situation, but it's only invoked when I click on the notification.
My current code is something like on flutter:
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
.
.
.
//flutterLocalNotificationsPlugin initializationSettings
.
.
.
/// Open app receive
FirebaseMessaging.onMessage.listen((RemoteMessage? message) {
if (message != null) {
// save and processMessage
}
});
/// Background app click
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage? message) {
if (message != null) {
// save and processMessage
}
});
/// Background closed app click
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage? message) {
if (message != null) {
// save and processMessage
);
}
});
IsolateNameServer.registerPortWithName(
backgroundMessagePort.sendPort,
backgroundMessageIsolateName,
);
backgroundMessagePort.listen(backgroundMessagePortHandler);
and my firebaseMessagingBackgroundHandler method is:
static Future<void> firebaseMessagingBackgroundHandler(
RemoteMessage message) async {
.
.
.
/// some IsolateNameServer configs whit aport and isolate name and solateNameServer lookupPortByName
.
.
.
final prefs = await SharedPreferences.getInstance();
final List<String> items = prefs.getStringList('notification') ??
List.empty(growable: true);
RemoteNotification? notification = message.notification;
items.addI( //string processing logic with message information here )
prefs.setStringList('notification', items);
}
My swift code on AppDelage.swift:
import UIKit
import Flutter
import flutter_local_notifications
.
.
.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
.
.
.
How can I work around this limitation and make the app obtain the content of the push notification and save it to the notification center while in the background or closed, without the user having to click on the push, so that it is saved in the notification center and displayed when the user opens the app?

Related

iOS RemoveDeliveredNotifications(string[] identifier) will not delete the notification(s) when app in background

maybe someone can help me.
In my app I'm using push notifications to inform the users that a new message is written to the database. One user can accept the notification and work with the content or dismiss it. If the user accepts it, a silent push is sent to all other devices which received the notification earlier. Here is my code handling this silent notification:
public override void ReceivedRemoteNotification(UIApplication application, NSDictionary remoteNotification)
{
try
{
if (remoteNotification != null)
{
var alert = remoteNotification[FromObject("aps")];
if (alert != null)
{
string id = ((NSDictionary)alert)[FromObject("deleteId")].Description;
if (!String.IsNullOrEmpty(id))
{
List<string> idents = new List<string>();
UNUserNotificationCenter.Current.GetDeliveredNotifications(completionHandler: (UNNotification[] t) =>
{
foreach (UNNotification item in t)
{
UNNotificationRequest curRequest = item.Request;
var notificationId = ((NSDictionary)curRequest.Content.UserInfo[FromObject("aps")])[FromObject("notificationId")].Description;
if (id == notificationId)
{
idents.Add(curRequest.Identifier);
}
}
UNUserNotificationCenter.Current.RemoveDeliveredNotifications(idents.ToArray());
});
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
The problem is that the notification is still visible in the notification center until the app is brought to foreground. But then it gets deleted.
Is there a way to force the method to delete the notification instantly and not only when the app is (re)opened?
When you want to clear the Notifications send from this app. Set its application's badge to 0 to achieve this.
As you said you send a silent notifications to other users, Then DidReceiveRemoteNotification() will fire. In this event we can clear all notifications:
public override void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler)
{
var aps = userInfo["aps"] as NSDictionary;
if (aps["content-available"].ToString() == "1")
{
//check if this is a silent notification.
UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0;
}
completionHandler(UIBackgroundFetchResult.NewData);
}
Please notice that starting with iOS 8.0, your application needs to register for user notifications to be able to set the application icon badge number. So please add the code below in FinishedLaunching():
UIUserNotificationSettings settings = UIUserNotificationSettings.GetSettingsForTypes(UIUserNotificationType.Badge, null);
UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
Moreover silent notifications can only be received when your app is on background or foreground. If it's terminated, this will fail.
To remove a notification, you send a silent push notification to all devices with the notification ID as payload that should be removed.
On the clients you implement a UNNotificationServiceExtension which allows you to remove currently displayed notifications by their IDs: UNUserNotificationCenter.current().removeDeliveredNotifications.
This gives you the advantage that you have full control over this logic on the server side.

DidReceiveRemoteNotification never called in Xamarin.iOS

I'm trying to implement push notifications for my iOS application using Xamarin and the Xamarin.Firebase.iOS.CloudMessaging package.
I've got everything setup and working i,e. When the application is in the foreground I can receive notifications (containing and not containing the "content-available" tag) and interact with them (tapping the notification etc.)
However, when the application is in the background, I receive the notifications but, no callbacks are called. To my understanding, the "didReceiveRemoteNotification" should be called when:
The application is in the foreground regardless of if the "content-available" tag is enabled
If the application is in the background or suspended, with the "content-available" tag enabled
When the user taps on the notification
Here are the functions I've implemented in my AppDelegate.cs:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
Firebase.Core.App.Configure();
FireBaseRegistration();
return base.FinishedLaunching(app, options);
}
private void FireBaseRegistration()
{
// Register your app for remote notifications.
if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
{
// iOS 10 or later
var authOptions = UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound;
UNUserNotificationCenter.Current.RequestAuthorization(authOptions, (granted, error) => {
Console.WriteLine(granted);
});
// For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.Current.Delegate = this;
}
else
{
// iOS 9 or before
var allNotificationTypes = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound;
var settings = UIUserNotificationSettings.GetSettingsForTypes(allNotificationTypes, null);
UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
}
//Register for APNs notifications
UIApplication.SharedApplication.RegisterForRemoteNotifications();
Messaging.SharedInstance.Delegate = this;
var token = InstanceId.SharedInstance.Token;
Debug.WriteLine(token);
//////Connect to FCM (Only used for Foreground notifications)
Messaging.SharedInstance.ShouldEstablishDirectChannel = true;
// Monitor token generation
InstanceId.Notifications.ObserveTokenRefresh((sender, e) => {
// Note that this callback will be fired everytime a new token is generated, including the first
// time. So if you need to retrieve the token as soon as it is available this is where that
// should be done.
token = InstanceId.SharedInstance.Token;
Console.WriteLine(token);
});
}
//Register APNs token because method swizzling is de-activated
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
Console.WriteLine("Registered for remote notifications");
Console.WriteLine(deviceToken.GetBase64EncodedString(NSDataBase64EncodingOptions.None));
Console.WriteLine(deviceToken);
}
[Export("messaging:didReceiveMessage:")]
public void DidReceiveMessage(Messaging messaging, RemoteMessage remoteMessage)
{
// Do your magic to handle the notification data
Console.WriteLine("iOS 11 Foreground");
}
//Shows local notification and is called when user taps notification
[Export("userNotificationCenter:DidReceiveRemoteNotification:withCompletionHandler:")]
public override void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler)
{
Console.WriteLine("Received a notficaiton");
completionHandler(UIBackgroundFetchResult.NewData);
}
//To receive notifications in foreground on iOS 11 devices.
[Export("userNotificationCenter:willPresent:withCompletionHandler:")]
public void WillPresent(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
Console.WriteLine("Handling iOS 11 foreground notification");
completionHandler(UNNotificationPresentationOptions.Sound | UNNotificationPresentationOptions.Alert);
}
////Called when tapping notification
[Export("userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:")]
public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
{
Console.WriteLine("Handling push notificaiton interaction");
completionHandler();
}
//Receive data message on iOS 10 devices.
public void ApplicationReceivedRemoteMessage(RemoteMessage remoteMessage)
{
Console.WriteLine("Handling iOS 10 data message notification");
}
//// To receive notifications in foreground on iOS 10 devices.
[Export("userNotificationCenter:willPresentNotification:withCompletionHandler:")]
public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
Console.WriteLine("Handling foreground notification");
completionHandler(UNNotificationPresentationOptions.Alert);
}
I've tried these out on iOS 10.3.3 and iOS 11.2. Here are the functions called for both versions:
When the application is in the foreground
On iOS 10.3.3: WillPresentNotification() (with and without the "content-available" tag)
On iOS 11.2: WillPresentNotification() (with and without the "content-available" tag)
When the application is in the background:
On iOS 10.3.3: nothing Shoudln't it be DidReceiveRemoteNotification?
On iOS 11.2: nothing Shoudln't it be DidReceiveRemoteNotification?
I have the remote-notificaitons and background fetch background modes activated in my Info.plist file.
Packages info:
Xamarin.forms: 2.5.0.280555
Xamarin.Firebase.iOS.CloudMessage: 2.0.4.1
Xamarin.Firebase.iOS.Core: 4.0.13
Xamarin.Firebase.iOS.InsstanceID: 2.0.8
Remarks:
The DidReceiveMessage function is implemented in the Xamarin sample but, in my case, is never called.
The DidReceiveNotificationResponse is called when the user taps on the notificaiton.
The WillPresent is also never called.
I've tried with notifications via the firebase API and directly via the APNs (with the pusher application).
Set Messaging.SharedInstance.ShouldEstablishDirectChannel=false and send data-only messages.

How do I write a UI test that launches the app with a push notification payload and verifies that you are routed to the correct view?

I'm implementing push notifications in an iOS app, and as I'm doing so, I want to write a UI test that verifies that the app does the correct thing when it is launched with a certain push notification payload (i.e. the app navigates to the correct table view and highlights the correct cell).
Can this be done? I can't seem to find anyone who has done this before or has asked this question before.
Thankful for any pointers.
With Xcode 9 you can now actually test Remote Notification handling in a UITest. I implemented that using a framework called NWPusher
I wrote a long blogpost about my implementation and added a demo project to github.
Here is a short description of what I did:
Preparation
Add NWPusher to your UITest target (I used Carthage)
Download a APN Development Certificate for your app from Apple's Dev Center
Open that certificate in Keychain and export it as p12 file
Add this file to the IUTest target
Make the deviceToken available to the UITestRunner
Write the Test
The test does the following steps:
Create a reference to the app and the Springboard
Launch the app and close it by tapping the home button (dismiss the system dialog asking for permission if it pops up)
Trigger a remote notification (using NWPusher)
Query the remote notification banner from the Springboard and tap it
Test if the remote notifications has been handled correctly by your app
Close the app and test the next type of remote notification
In my demo the different types of notifications trigger differently colored modal View Controller in the app. So my test class looks like this
import XCTest
import PusherKit
class PushNotificationUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
}
func testPushNotifications() {
let app = XCUIApplication()
app.launchArguments.append("isRunningUITests")
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
app.launch()
// dismiss the system dialog if it pops up
allowPushNotificationsIfNeeded()
// get the current deviceToken from the app
let deviceToken = app.staticTexts.element(matching: .any, identifier: "tokenLabel").label
// close app
XCUIDevice.shared.press(XCUIDevice.Button.home)
sleep(1)
// trigger red Push Notification
triggerPushNotification(
withPayload: "{\"aps\":{\"alert\":\"Hello Red\"}, \"vcType\":\"red\"}",
deviceToken: deviceToken)
// tap on the notification when it is received
springboard.otherElements["PUSHNOTIFICATION, now, Hello Red"].tap()
// check if the red view controller is shown
XCTAssert(app.staticTexts["Red"].exists)
// dismiss modal view controller and close app
app.buttons["Close"].tap()
XCUIDevice.shared.press(XCUIDevice.Button.home)
sleep(1)
// trigger green Push Notification
triggerPushNotification(
withPayload: "{\"aps\":{\"alert\":\"Hello Green\"}, \"vcType\":\"green\"}",
deviceToken: deviceToken)
// tap on the notification when it is received
springboard.otherElements["PUSHNOTIFICATION, now, Hello Green"].tap()
// check if the green view controller is shown
XCTAssert(app.staticTexts["Green"].exists)
// dismiss modal view controller and close app
app.buttons["Close"].tap()
XCUIDevice.shared.press(XCUIDevice.Button.home)
sleep(1)
// trigger blue Push Notification
triggerPushNotification(
withPayload: "{\"aps\":{\"alert\":\"Hello Blue\"}, \"vcType\":\"blue\"}",
deviceToken: deviceToken)
// tap on the notification when it is received
springboard.otherElements["PUSHNOTIFICATION, now, Hello Blue"].tap()
// check if the blue view controller is shown
XCTAssert(app.staticTexts["Blue"].exists)
// dismiss modal view controller
app.buttons["Close"].tap()
}
}
extension XCTestCase {
func triggerPushNotification(withPayload payload: String, deviceToken: String) {
let uiTestBundle = Bundle(for: PushNotificationUITests.self)
guard let url = uiTestBundle.url(forResource: "pusher.p12", withExtension: nil) else { return }
do {
let data = try Data(contentsOf: url)
let pusher = try NWPusher.connect(withPKCS12Data: data, password: "pusher", environment: .auto)
try pusher.pushPayload(payload, token: deviceToken, identifier: UInt(arc4random_uniform(UInt32(999))))
} catch {
print(error)
}
}
func allowPushNotificationsIfNeeded() {
addUIInterruptionMonitor(withDescription: "“RemoteNotification” Would Like to Send You Notifications") { (alerts) -> Bool in
if(alerts.buttons["Allow"].exists){
alerts.buttons["Allow"].tap();
}
return true;
}
XCUIApplication().tap()
}
}
This only works on a physical device, because remote notifications do not work in the simulator.
Based on joern amazing article, I took a step forward and found a way to programmatically interact with the received notification, as it is identified by the XCTest framework as a XCUIElement.
As we can get a reference to the Sprinboard
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
For example, when putting the app on the background we could get a reference to the received notification (while it is being displayed in the top of the screen) like this:
let notification = springboard.otherElements["NotificationShortLookView"]
Allowing us to tap the notification:
notification.tap()
Pull it down to see its actions (if any available. Also by doing this could allow us to see the content of a Rich Notification):
notification.swipeDown()
Interact with its actions:
let action = springboard.buttons["ACTION BUTTON TITLE"]
action.tap()
And even interact with a Text Input notification action (in the example, by getting a reference to the notification textfield by its placeholder, which you can define in your code):
let notificationTextfield = springboard.textFields["Placeholder"]
notificationTextfield.typeText("this is a test message")
At last, you can also get a reference to the notification's close button in order to dismiss it:
let closeButton = springboard.buttons["Dismiss"]
closeButton.tap()
By being able to automate this interactions we could test, for example analytics, as described in this article.
You can send notification with this library. Unfortunately, you should add test-related code into your AppDelegate-class. I use custom preprocessor macros in separate application target (UITEST=1).
Somewhere in your code:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// ...
#if UITEST
// setup listening
#endif
}

iOS remote notifications do not trigger when in background mode

I do not usually post here, but this time I am stuck. I searched already on this forum, on stackoverflow and on google in general but I cannot understand what I am doing wrong.
I am developing a Xamarin.Forms app and I am having trouble to fire the methods when a remote notification is received.
Here is my code:
Just after login I execute this method to register to apple notification center:
public void RegisterForNotifications ()
{
var settings = UIUserNotificationSettings.GetSettingsForTypes(
UIUserNotificationType.Alert
| UIUserNotificationType.Badge
| UIUserNotificationType.Sound,
new NSSet());
UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
UIApplication.SharedApplication.RegisterForRemoteNotifications();
}
this call successfully trigger this method on AppDelegate
public override void RegisteredForRemoteNotifications (UIApplication application, NSData deviceToken)
{
var azureService = App.GetBackEndService ();
azureService.RegisterForNotifications (deviceToken);
}
RegisterForNotifications registers with azure notification hub:
public static async void RegisterForNotifications(this BackEndService service, NSData deviceId)
{
Microsoft.WindowsAzure.MobileServices.MobileServiceClient client = new Microsoft.WindowsAzure.MobileServices.MobileServiceClient (service.Client.ApplicationUri);
client.CurrentUser = service.Client.CurrentUser;
var push = client.GetPush ();
var tags = new List<string>() { App.GetBackEndService().Client.CurrentUser.UserId };
try{
await push.UnregisterAllAsync(deviceId);
}catch(Exception e){
var exp = e.Message;
}
await push.RegisterTemplateAsync(deviceId, NOTIFICATION_TEMPLATE, "", "NotificationTemplate", tags);
}
Now, when I send a notification and the app is on foreground the method triggered is this one (In AppDelegate):
public override void ReceivedRemoteNotification (UIApplication application, NSDictionary userInfo)
{
ShowAlerts (userInfo);
}
But when in background mode (for example when I press the Home button on the Iphone) nothing gets triggered
And nothing shows on the upper bar where the iOS notifications are usually shown.
I placed a breakpoint in ReceivedRemoteNotification, DidReceiveRemoteNotification and also on FinishedLaunching.
I properly set Remote notifications in Info.plist under Enable Background Modes
Can someone understand what I am missing?

How to get notification additionaldata(payloadData) that was at AppDelegate' didFinishLunchingWithOptions if the user didn't open the notification

I am currently using OneSignal for notification service to my app.I really need a help with accessing notification additionaldata(payload data) from AppDelegate inside didFinishLunchingWithOption where OneSignal API can give me like this.
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var data : [NSObject : AnyObject]!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let oneSignal = OneSignal(launchOptions: launchOptions, appId: "48755d3d-abc0-4bac-8f71-095729bb3a65") { (message, additionalData, isActive) in
NSLog("OneSignal Notification opened:\nMessage: %#", message)
if additionalData != nil {
NSLog("additionalData: %#", additionalData)
self.data = additionalData
print(self.data)
}
}
oneSignal.enableInAppAlertNotification(true)
return true
}
}
but,I can only get the data if user click notification when appear or open it from notification center.So,if user neglect that notification without tapping when appear or without swiping or tapping from notification center,how do i get the additional data?
Actually,I want to store all payload data every time it comes into my device at realm database and fetch the data from my server according to that payload data.
You should use application(_:didReceiveRemoteNotification:fetchCompletionHandler:).
If you have enabled remote notifications background mode most of your notifications will be delivered even if the app is not running in the foreground. The only caveat being that the app must have been launched (since notifications are being pushed the user has done this) and it mustn't have been force-quit by the user.
More info in Apple's docs about that specific method.
Or in the "Local and Remote Notification Programming Guide's" notification handling chapter
You can extract all the payload in did finishLaunching by following method..
Let data = launchOptions.objectForKey(UIApplicationLaunchOptionsUIApplicationLaunchOptionsRemoteNotificationUIApplicationLaunchOptionsUIApplicationLaunchOptionsRemoteNotificationKey)

Resources