Handle background push notification on iOS side of Flutter's Firebase Messaging - swift

Until the moment of writing the FCM Flutter plugin did not implement a background handling for push notifications on iOS.
I'm trying to implement using native code(Swift) but I'm facing some difficulties.
This is my AppDelegate.swift:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let _platformChannel = FlutterMethodChannel(name: "br.uff.uffmobileplus/uffmobile_channel",
binaryMessenger: controller as! FlutterBinaryMessenger)
_platformChannel.setMethodCallHandler({
//omitted code
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let notificationChannel = FlutterMethodChannel(name: "br.uff.uffmobileplus/notification_channel", binaryMessenger: controller as! FlutterBinaryMessenger)
notificationChannel.invokeMethod("saveToDataBase", arguments: userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
}
(I omitted some non relevant code)
I saw some native iOS implementations and they did something near this.I'm not a Swift programmer so don't know at all if this is correct.
What is happening is that the
didReceiveRemoteNotification
is not being called when a remote data or notification message arrives.
I'm using the platform_channel to communicate between dart and swift code.
This is the json data message:
"\"data\": {"
"\"body\": \"$body\","
"\"title\": \"$title\","
"\"route\": \"$route\","
"\"sender\": \"$sender\","
"\"click_action\": \"FLUTTER_NOTIFICATION_CLICK\","
"\"mutable_content\": true,"
"\"content_available\": true"
"}, "
"\"priority\": \"high\","
"\"to\": "
"\"/topics/$group\""
Yes it's written weirdly but works because it's triggering the onMessage with correct information.
What I want is to do a background work (save on my local DB) when the data message arrives.

Related

Handling Phone Authentication using FirebaseUI and SwiftUI

I am attempting to use the FirebaseUI pre-built phone authentication screen to request a verification code. I am using SwiftUI, so a lot of the Firebase documentation is outdated, but I've gotten most of it working. To be explicit: I can open the phone Auth UI, type in my phone number, and input the verification code.
Where I'm currently having an issue is the part in which they tell you to receive the user info by using:
func authUI(_ authUI: FUIAuth, didSignInWith user: FIRUser?, error: Error?) {
// handle user and error as necessary
}
It says to put this in your FUIAuthDelegate. However I do not have an FUIAuthDelegate, and do not know if this is outdated, or something I'm lacking.
How I currently have it set up, is using some of Peter Friese's answer regarding the new SwiftUI 2.0 Lifecycle.
Where am I supposed to receive the Firebase User information in this new lifecycle? I have tried to make my AppDelegate an FUIAuthDelegate and add the "authUI" function into it, and that didn't work. I have also tried to add an "authUI" function to my LoginView struct, and that didn't work either.
Please see below for a more in-depth look at my code:
///testApp.swift
import SwiftUI
import Firebase
import FirebaseUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
print("App Starting Up...")
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("Registered for remote notifications...")
Auth.auth().setAPNSToken(deviceToken, type: .sandbox)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if Auth.auth().canHandle(url){
return true
}
print("URL Error - oops")
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping(UIBackgroundFetchResult) -> Void) {
if Auth.auth().canHandleNotification(userInfo) {
print("Handling Notification")
completionHandler(.noData)
return
}
return
}
}
#main
struct testApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
AnyView(LoginView())
}
}
}
And my LoginView:
///LoginView.swift
import SwiftUI
import Firebase
import FirebaseAuth
import FirebaseUI
struct LoginView: View {
#State var verified: Int? = nil
var body: some View {
NavigationView {
NavigationLink(destination: LoggedInView(), tag: 1, selection: $verified) {
Button(action: {
login()
}
}
}
}
func login() {
let authUI = FUIAuth.defaultAuthUI()!
let providers: [FUIAuthProvider] = [FUIPhoneAuth(authUI: FUIAuth.defaultAuthUI()!)]
authUI.providers = providers
let authViewController = authUI.authViewController()
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {return}
guard let rootViewController = windowScene.windoes.first?.rootViewController else {return}
Auth.auth().settings?.isAppVerificationDisabledForTesting = true
rootViewController.present(authViewController, animated: true, completion: nil)
}
}
I am currently testing on the xcode simulator, and using a dummy account from my firebase console.
If you have any questions, please feel free to ask and I will do my best to clarify. Any help would be much appreciated. Thanks!

Why the push notification don‘t work in iOS with Firebase from device to device?

I have created the Production Identifier and all this stuff and it has worked but sometime later suddenly it doesn't work anymore. And I have nothing changed in the Notification Area in my code.
From Firebase Cloud Messaging - Test Sending works fine. But from Device to Device not.
import UIKit
import Firebase
import FirebaseMessaging
import FirebaseInstanceID
import UserNotifications
import AVFoundation
import GoogleMobileAds
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
static let NOTIFICATION_KEY = "https://gcm-http.googleapis.com/gcm/send"
static var DEVICE_ID = String()
static let SERVER_KEY = "AAAACucgXz8:APA91bE_vEyKiqR34sruY2f5tetjj_DrwVi42v9tL015tLEOZmwBh-q7oWsNXUrpR0S9XBCrbxMJtgUu70xNAyDeouNvxn1kHKll2Dcwzxbf40lbappxs-o6IDvHVvEajzX5Dzsq8MW8"
static var EXITAPP = Bool()
static var LIKESCOUNTER = Int()
var dateForm = DateFormatter()
var timeFormatter = DateFormatter()
static var INSERTTIME = false
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible() //Damit wir die ganzen Views selber bearbeiten können.
window?.rootViewController = UINavigationController(rootViewController: ViewController())
if #available(iOS 10.0, *){
UNUserNotificationCenter.current().delegate = self
let option: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: option) { (bool, err) in
}
}
application.registerForRemoteNotifications()
UIApplication.shared.applicationIconBadgeNumber = 0
GADRewardBasedVideoAd.sharedInstance().load(GADRequest(), withAdUnitID: "ca-app-pub-3940256099942544/1712485313")
if #available(iOS 9.0, *){
application.isStatusBarHidden = true //HIDE STATUS BAR
}
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
insertOnlineActivity(online: false)
connectToFCM()
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
AppDelegate.EXITAPP = true
// if AppDelegate.INSERTTIME{
// insertLastLikedTime()
// }
insertOnlineActivity(online: false)
connectToFCM()
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
insertOnlineActivity(online: true)
connectToFCM()
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
insertOnlineActivity(online: true)
connectToFCM()
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String){
guard let newToken = InstanceID.instanceID().token() else {return}
AppDelegate.DEVICE_ID = newToken
connectToFCM()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let notification = response.notification.request.content.body
print(notification)
completionHandler()
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
guard let newToken = InstanceID.instanceID().token() else {return}
//let newToken = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
AppDelegate.DEVICE_ID = newToken
connectToFCM()
Messaging.messaging().setAPNSToken(deviceToken, type: .unknown)
}
func connectToFCM(){
Messaging.messaging().shouldEstablishDirectChannel = true
}
This is how I call the Notification
fileprivate func setUpPushNotifications(fromDeviceID: String){
let message = self.userName + " has sent you a voice message."
let title = "New Message"
let toDeviceID = fromDeviceID
var headers:HTTPHeaders = HTTPHeaders()
headers = ["Content-Type":"application/json","Authorization":"key=\(AppDelegate.SERVER_KEY)"]
let notifications = ["to" : "\(toDeviceID)" , "notification":["body":message,"title":title,"badge":1,"sound":"default"]] as [String : Any]
request(AppDelegate.NOTIFICATION_KEY as URLConvertible, method: .post as HTTPMethod, parameters: notifications, encoding: JSONEncoding.default, headers: headers).response { (response) in
print(response)
}
}

Using URLSession and background fetch together with remote notifications using firebase

I am trying to implement a basic func here which will be called when my app is backgrounded or suspended.
In reality, we aim to send about 5 a day so Apple should not throttle our utilisation.
I've put together the following which uses firebase and userNotifications, for now, it is in my app delegate.
import Firebase
import FirebaseMessaging
import UserNotifications
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundSessionCompletionHandler: (() -> Void)?
lazy var downloadsSession: Foundation.URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "bgSessionConfiguration")
configuration.timeoutIntervalForRequest = 30.0
let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
return session
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FIRApp.configure()
if #available(iOS 10.0, *) {
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: {_, _ in })
// For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.current().delegate = self
// For iOS 10 data message (sent via FCM)
FIRMessaging.messaging().remoteMessageDelegate = self
} else {
let settings: UIUserNotificationSettings =
UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
application.registerUserNotificationSettings(settings)
}
application.registerForRemoteNotifications()
let token = FIRInstanceID.instanceID().token()!
print("token is \(token) < ")
return true
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void){
print("in handleEventsForBackgroundURLSession")
_ = self.downloadsSession
self.backgroundSessionCompletionHandler = completionHandler
}
//MARK: SyncFunc
func startDownload() {
NSLog("in startDownload func")
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
// make the request
let task = downloadsSession.downloadTask(with: url)
task.resume()
NSLog(" ")
NSLog(" ")
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession){
DispatchQueue.main.async(execute: {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
})
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
NSLog("in didReceiveRemoteNotification")
NSLog("%#", userInfo)
startDownload()
DispatchQueue.main.async {
completionHandler(UIBackgroundFetchResult.newData)
}
}
}
#available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
/*
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// Print message ID.
//print("Message ID: \(userInfo["gcm.message_id"]!)")
// Print full message.
print("%#", userInfo)
startDownload()
DispatchQueue.main.async {
completionHandler(UNNotificationPresentationOptions.alert)
}
}
*/
}
extension AppDelegate : FIRMessagingDelegate {
// Receive data message on iOS 10 devices.
func applicationReceivedRemoteMessage(_ remoteMessage: FIRMessagingRemoteMessage) {
print("%#", remoteMessage.appData)
}
}
extension AppDelegate: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
NSLog("finished downloading")
}
}
The results are as follows:
When the app is in the foreground:
I get the log "in startDownload func"
I get the log "finished downloading".
When the app is in the background:
I get the log "in startDownload func"
I do not get the log "finished downloading".
The silencer isn't working i.e. when the app is backgrounded, I am still getting the notification in the tray.
I am using Postman to send the request and tried the following payload, which results in the console error 'FIRMessaging receiving notification in invalid state 2':
{
"to" : "Server_Key",
"content_available" : true,
"notification": {
"body": "Firebase Cloud Message29- BG CA1"
}
}
I have the capabilities set for background fetch and remote notifications. The app is written in swift 3 and uses the latest Firebase
EDIT: Updated AppDelegate to include funcs as per comment
A few observations:
When your app is restarted by handleEventsForBackgroundURLSessionIdentifier, you have to not only save the completion handler, but you actually have to start the session, too. You appear to be doing the former, but not the latter.
Also, you have to implement urlSessionDidFinishEvents(forBackgroundURLSession:) and call (and discard your reference to) that saved completion handler.
You appear to be doing a data task. But if you want background operation, it has to be download or upload task. [You have edited question to make it a download task.]
In userNotificationCenter(_:willPresent:completionHandler:), you don't ever call the completion handler that was passed to this method. So, when the 30 seconds (or whatever it is) expires, because you haven't called it, your app will be summarily terminated and all background requests will be canceled.
So, willPresent should call its completion handler as soon it done starting the requests. Don't confuse this completion handler (that you're done handling the notification) with the separate completion handler that is provided later to urlSessionDidFinishEvents (that you're done handling the the background URLSession events).
Your saved background session completion handler is not right. I'd suggest:
var backgroundSessionCompletionHandler: (() -> Void)?
When you save it, it is:
backgroundSessionCompletionHandler = completionHandler // note, no ()
And when you call it in urlSessionDidFinishEvents, it is:
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}

Open a ViewController from remote notification

I try to open a particular ViewController when my app catch a remote notification.
Let me show my project's architecture.
Here my storyboard :
When I receive a notification I want open a "SimplePostViewController", so this is my appDelegate :
var window: UIWindow?
var navigationVC: UINavigationController?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let notificationTypes: UIUserNotificationType = [UIUserNotificationType.Alert, UIUserNotificationType.Badge, UIUserNotificationType.Sound]
let pushNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: nil)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.navigationVC = storyboard.instantiateViewControllerWithIdentifier("LastestPostsNavigationController") as? UINavigationController
application.registerUserNotificationSettings(pushNotificationSettings)
application.registerForRemoteNotifications()
return true
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let postId = userInfo["postId"] as? String {
print(postId)
let api = EVWordPressAPI(wordpressOauth2Settings: Wordpress.wordpressOauth2Settings, site: Wordpress.siteName)
api.postById(postId) { post in
if (post != nil) {
self.navigationVC!.pushViewController(SimplePostViewController(), animated: true)
} else {
print("An error occurred")
}
}
}
}
I save my UINavigationViewController when the app is launch and simply try to push a new SimplePostViewController when I receive a notification. But nothing happen.
I placed breakpoints and seen that my pushViewController method was reached, but not the ViewWillAppear of my SimplePostViewController.
I also used the "whats new" view add perform my segue but nothing happen too.
Solution :
for child in (self.rootViewController?.childViewControllers)! {
if child.restorationIdentifier == "LastestPostsNavigationController" {
let lastestPostsTableViewController = (child.childViewControllers[0]) as! LastestPostsTableViewController
let simplePostVC = (self.storyboard?.instantiateViewControllerWithIdentifier("PostViewController"))! as! PostViewController
simplePostVC.post = post
lastestPostsTableViewController.navigationController?.pushViewController(simplePostVC, animated: true)
}
}
I use :
child.childViewControllers[0]
because I've only one child in my example.
I created a sample project with a local notification instead of a remote notification for ease of showing the functionality but it should be as simple as setting the root view controller of the window in the app delegate didreceiveremote notification.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Subscribe for notifications - assume the user chose yes for now
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil))
return true
}
func applicationDidEnterBackground(application: UIApplication) {
//Crete a local notification
let notification = UILocalNotification()
notification.alertBody = "This is a fake notification"
notification.fireDate = NSDate(timeIntervalSinceNow: 2)
UIApplication.sharedApplication().scheduleLocalNotification(notification)
}
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
let sb = UIStoryboard(name: "Main", bundle: nil)
let otherVC = sb.instantiateViewControllerWithIdentifier("otherVC") as! OtherViewController
window?.rootViewController = otherVC;
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
//Your code here
}
`
You need to worry about managing your view hierarchy and sending anything to it that you need to send from the notification user data.
In my example, I create a local notification when you close the app that fires after a view seconds. If you then launch the app from the notification, it will open the "other view controller" which would be the "SimplePostViewController" in your case.
Also, be sure that you are registering for remote notifications in the didFinishLaunchWithOptions.
Github very simple sample : https://github.com/spt131/exampleNotificationResponse

Push remote notification

I need create in my app a code that receive a remote information and push it to user when app is on background, I read on web that I need to use didReceiveRemoteNotification on appDelegate, to use remote push notication. I read something about and I need keys and certificates, I do not understand how to use didReceiveRemoteNotification
I need to learn about to push remote notification and how to use. I would like a tutorial or example how create it using swift 2.3.
I used this link and I found it the most helpful
http://www.appcoda.com/push-notification-ios/
I used this app for testing
https://itunes.apple.com/us/app/easy-apns-provider-push-notification/id989622350?mt=12
This is the code I have in my AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
registerForPushNotifications(application)
print(UIDevice.currentDevice().identifierForVendor!.UUIDString)
return true
}
func registerForPushNotifications(application: UIApplication) {
let notificationSettings = UIUserNotificationSettings(
forTypes: [.Badge, .Sound, .Alert], categories: nil)
application.registerUserNotificationSettings(notificationSettings)
}
func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
if notificationSettings.types != .None {
application.registerForRemoteNotifications()
}
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
let tokenChars = UnsafePointer<CChar>(deviceToken.bytes)
var tokenString = ""
for i in 0..<deviceToken.length {
tokenString += String(format: "%02.2hhx", arguments: [tokenChars[i]])
}
print("Device Token:", tokenString)
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
print("Failed to register:", error)
}