firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.67k stars 1.49k forks source link

Unexpected ‘No APNS Token Specified’ Error in FirebaseMessaging After APNS Token Assignment (iOS) #13701

Open Emanuel-111 opened 1 month ago

Emanuel-111 commented 1 month ago

Description

A few days ago, Firebase was working like a charm, I had no issues with connecting to the server that I had connected to an iOS app. Then one day, I kept coming across this error

10.29.0 - [FirebaseMessaging][I-FCM002022] APNS device token not set before retrieving FCM Token for Sender ID 'XXXXXXXXX'.Be sure to re-retrieve the FCM token once the APNS device token is set. It was cryptic to a whole new level. So I began searching for an answer as to why this was happening. After doing some debugging, I came across something odd.

For context:

The Firebase server is running Entitlement files are set to production (APS Environment and App Attest) FirebaseAppDelegateProxyEnabled is disabled but has been enabled at times I have AppCheck running well I have added the APN Auth Key to the appropriate spot I put the correct name in the Key ID and Team ID Same for the Bundle ID and Team ID for the app GoogleService.plist is in my app I had no prior issue with the server until September 21 at around 12pm No code in the AppDelegate was changed prior to September 21 Here's the portion of code it would run

//1. Called when the app is launched func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let providerFactory = DriverAppCheckProviderFactory()
    AppCheck.setAppCheckProviderFactory(providerFactory)

    print("1st - didFinishLaunchingWithOptions")

    // Firebase configuration
    FirebaseApp.configure()

    print("2nd - Firebase Configured")

    print("3rd - UNUserNotificationCenter.current.delegate")
    if #available(iOS 10.0, *) {
    // For iOS 10 display notification (sent via APNS)
    UNUserNotificationCenter.current().delegate = self

        print("4th - authOptions")
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions,
    completionHandler: { _, _ in }
    )} else {
        let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
        application.registerUserNotificationSettings(settings)
    }

    print("5th - application.registerForRemoteNotifications")
    application.registerForRemoteNotifications()

    print("6th - Messaging Delegate")
    Messaging.messaging().delegate = self

    print ("7th - True is returned")
    return true
}

Then it runs some other method that is not in the AppDelegate and these print statements appeared

10.29.0 - [FirebaseInAppMessaging][I-IAM280002] Firebase In App Messaging was not configured with FirebaseAnalytics. 10.29.0 - [FirebaseMessaging][I-FCM002022] APNS device token not set before retrieving FCM Token for Sender ID '762004236193'.Be sure to re-retrieve the FCM token once the APNS device token is set. 10.29.0 - [FirebaseMessaging][I-FCM002022] Declining request for FCM Token since no APNS Token specified 10.29.0 - [FirebaseMessaging][I-FCM002010] The subscription operation failed due to an error getting the FCM token: Error Domain=com.google.fcm Code=505 "No APNS token specified before fetching FCM Token" UserInfo={NSLocalizedFailureReason=No APNS token specified before fetching FCM Token}. Afterwards (with swizzling on), it runs these methods

// 2. Called when the app successfully registers with APNS and receives a device token
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

    print("8th - Setting APNs Token")
    Messaging.messaging().setAPNSToken(deviceToken, type: MessagingAPNSTokenType.unknown)

    print("9th - Assigning APNs Token to deviceToken")
    // Set the APNS token for Firebase Messaging
    Messaging.messaging().apnsToken = deviceToken

    print("Device Token received: \(deviceToken)")

    // Fetch the FCM token now that APNS token is available
    Messaging.messaging().token { token, error in
        if let error = error {
            print("Error fetching FCM token: \(error)")
        } else if let token = token {
            print("FCM token: \(token)")
            // You can also subscribe to topics here now that FCM token is available
            Messaging.messaging().subscribe(toTopic: "newPickupRequest") { error in
                if let error = error {
                    print("Error subscribing to topic: \(error)")
                } else {
                    print("Subscribed to newPickupRequest topic")
                }
            }
        }
    }
}

Here's the print statements

Error subscribing to topic: The operation couldn’t be completed. No APNS token specified before fetching FCM Token 8th - Setting APNs Token 9th - Assigning APNs Token to deviceToken Device Token received: 32 bytes FCM token received: [The FCM token] FCM token: [The FCM token] Subscribed to newPickupRequest topic Based on the bebugging I made, it seems like some other method is being called in between both methods.

What could be the issue here? Is it a bug or glitch I'm not aware of?

In case needed, here the full class and the [NameofApp]App

import UIKit import UserNotifications import Firebase import FirebaseCore import FirebaseMessaging import TabularData

class DriverAppCheckProviderFactory: NSObject, AppCheckProviderFactory { func createProvider(with app: FirebaseApp) -> AppCheckProvider? { return AppAttestProvider(app: app) } }

class AppDelegate: NSObject, UIApplicationDelegate, MessagingDelegate {

let gcmMessageIDKey = "gcm.message_id"

// 1. Called when the app is launched
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let providerFactory = DriverAppCheckProviderFactory()
    AppCheck.setAppCheckProviderFactory(providerFactory)

    print("1st - didFinishLaunchingWithOptions")

    // Firebase configuration
    FirebaseApp.configure()

    print("2nd - Firebase Configured")

    print("3rd - UNUserNotificationCenter.current.delegate")
    if #available(iOS 10.0, *) {
    // For iOS 10 display notification (sent via APNS)
    UNUserNotificationCenter.current().delegate = self

        print("4th - authOptions")
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions,
    completionHandler: { _, _ in }
    )} else {
        let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
        application.registerUserNotificationSettings(settings)
    }

    print("5th - application.registerForRemoteNotifications")
    application.registerForRemoteNotifications()

    print("6th - Messaging Delegate")
    Messaging.messaging().delegate = self

    print ("7th - True is returned")
    return true
}

// 2. Called when the app successfully registers with APNS and receives a device token
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

    print("8th - Setting APNs Token")
    Messaging.messaging().setAPNSToken(deviceToken, type: MessagingAPNSTokenType.unknown)

    print("9th - Assigning APNs Token to deviceToken")
    // Set the APNS token for Firebase Messaging
    Messaging.messaging().apnsToken = deviceToken

    print("Device Token received: \(deviceToken)")

    // Fetch the FCM token now that APNS token is available
    Messaging.messaging().token { token, error in
        if let error = error {
            print("Error fetching FCM token: \(error)")
        } else if let token = token {
            print("FCM token: \(token)")
            // You can also subscribe to topics here now that FCM token is available
            Messaging.messaging().subscribe(toTopic: "newPickupRequest") { error in
                if let error = error {
                    print("Error subscribing to topic: \(error)")
                } else {
                    print("Subscribed to newPickupRequest topic")
                }
            }
        }
    }
}

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
  // If you are receiving a notification message while your app is in the background,
  // this callback will not be fired till the user taps on the notification launching the application.
  // TODO: Handle data of notification

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // Messaging.messaging().appDidReceiveMessage(userInfo)

  // Print message ID.
  if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
  }

  // Print full message.
  print(userInfo)
}

// 3. Called when the app fails to register for remote notifications
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register for remote notifications: \(error)")
}

// 4. Called when a new FCM token is generated
internal func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
    print("FCM token received: \(fcmToken ?? "No FCM token")")
    // Optionally, handle additional operations like topic subscription here if needed
}

func scheduleDailyReport() {
    print("Scheduling the daily report...")

    // Schedule the generation of the report
    var dateComponents = DateComponents()
    dateComponents.hour = 16  // Adjust to your preferred time
    dateComponents.minute = 10

    let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
    let request = UNNotificationRequest(identifier: "DailyReport", content: UNMutableNotificationContent(), trigger: trigger)

    UNUserNotificationCenter.current().add(request) { error in
        if let error = error {
            print("Error scheduling daily report: \(error.localizedDescription)")
        } else {
            print("Daily report scheduled successfully.")
        }
    }

    // Automatically generate and display the report when the scheduled time is reached
    generateDailyReport()
}

private func generateDailyReport() {
    let startOfDay = Calendar.current.startOfDay(for: Date())
    let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
    let historyRef = Database.database().reference().child("history")
    let query = historyRef.queryOrdered(byChild: "timestamp").queryStarting(atValue: startOfDay.timeIntervalSince1970).queryEnding(atValue: endOfDay.timeIntervalSince1970)
    query.observeSingleEvent(of: .value) { snapshot in
        var services: [[String: Any]] = []
        for child in snapshot.children {
            if let childSnapshot = child as? DataSnapshot, let serviceData = childSnapshot.value as? [String: Any] {
                services.append(serviceData)
            }
        }
        let csvURL = self.createCSVReport(from: services)
        self.displayCSVFile(at: csvURL)
        historyRef.removeValue { error, _ in
            if let error = error {
                print("Error deleting history: \(error.localizedDescription)")
            } else {
                print("History deleted successfully")
            }
        }
    }
}

private func createCSVReport(from services: [[String: Any]]) -> URL {
    var dataFrame = DataFrame()

    // Create columns
    let customerNames = Column(name: "Customer Name", contents: services.map { $0["customerName"] as? String ?? "N/A" })
    let addresses = Column(name: "Address", contents: services.map { $0["address"] as? String ?? "N/A" })
    let phoneNumbers = Column(name: "Phone Number", contents: services.map { $0["phoneNumber"] as? String ?? "N/A" })
    let driverNames = Column(name: "Driver Name", contents: services.map { $0["driverName"] as? String ?? "N/A" })
    let statuses = Column(name: "Status", contents: services.map { $0["status"] as? String ?? "N/A" })

    let timestamps = Column(name: "Timestamp", contents: services.map { service in
        let timestamp = service["timestamp"] as? TimeInterval ?? 0
        let date = Date(timeIntervalSince1970: timestamp)
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return dateFormatter.string(from: date)
    })

    // Add columns to the DataFrame
    dataFrame.append(column: customerNames)
    dataFrame.append(column: addresses)
    dataFrame.append(column: phoneNumbers)
    dataFrame.append(column: driverNames)
    dataFrame.append(column: statuses)
    dataFrame.append(column: timestamps)

    // Export DataFrame to CSV format
    let csvData = try! dataFrame.csvRepresentation()

    // Save to directory
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let fileURL = documentsDirectory.appendingPathComponent("Service_Report_\(Date().toFormattedString()).csv")

    try! csvData.write(to: fileURL)
    print("CSV file created at: \(fileURL.path)")
    return fileURL
}

private func displayCSVFile(at url: URL) {
    let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)

    if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
        windowScene.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)
    }
}

private func handleDailyReportTrigger() {
    let viewModel = AdminDashboardViewModel() // Replace with your actual way of accessing the viewModel
    viewModel.generateDailyReport()
}

}

extension AppDelegate: UNUserNotificationCenterDelegate { // Receive displayed notifications for iOS 10 devices. func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { let userInfo = notification.request.content.userInfo

// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)

// ...

// Print full message.
print(userInfo)

// Change this to your preferred presentation option
return [[.alert, .sound]]

}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { let userInfo = response.notification.request.content.userInfo

// ...

// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)

// Print full message.
print(userInfo)

}

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async
  -> UIBackgroundFetchResult {
  // If you are receiving a notification message while your app is in the background,
  // this callback will not be fired till the user taps on the notification launching the application.
  // TODO: Handle data of notification

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // Messaging.messaging().appDidReceiveMessage(userInfo)

  // Print message ID.
  if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
  }

  // Print full message.
  print(userInfo)

  return UIBackgroundFetchResult.newData
}

}

@MainActor class NotificationManager: ObservableObject{ @Published private(set) var hasPermission = false static let shared = NotificationManager()

init() {
    Task{
        await getAuthStatus()
    }
}

func request() async{
    do {
        try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
         await getAuthStatus()
    } catch{
        print(error)
    }
}

func getAuthStatus() async {
    let status = await UNUserNotificationCenter.current().notificationSettings()
    switch status.authorizationStatus {
    case .authorized, .ephemeral, .provisional:
        hasPermission = true
    default:
        hasPermission = false
    }
}

func postNewPickupRequestNotification() {
    NotificationCenter.default.post(name: .newPickupRequest, object: nil)
   }

@MainActor
func observeNewPickupRequestNotification(observer: Any, selector: Selector) {
       NotificationCenter.default.addObserver(observer, selector: selector, name: .newPickupRequest, object: nil)
   }

}

extension Notification.Name { static let newPickupRequest = Notification.Name("newPickupRequest") }

@MainActor class NotificationHandler: NSObject, ObservableObject { @Published var newRequestAlert = false

override init() {
    super.init()
    Task { @MainActor in
        NotificationManager.shared.observeNewPickupRequestNotification(observer: self, selector: #selector(handleNewPickupRequest))
    }
}

@objc private func handleNewPickupRequest() {
    newRequestAlert = true
    scheduleLocalNotification(title: "New Pickup Request", body: "A new pickup request has been added.", timeInterval: 1)

    // Dismiss alert after some time (e.g., 3 seconds)
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        self.newRequestAlert = false
    }
}

private func scheduleLocalNotification(title: String, body: String, timeInterval: TimeInterval) {
    let content = UNMutableNotificationContent()
    content.title = title
    content.body = body
    content.sound = .default

    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

    UNUserNotificationCenter.current().add(request)
}

}

class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    let userInfo = response.notification.request.content.userInfo
    print(userInfo)

    completionHandler()

}

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.banner, .sound, .badge])
}

}

Here's the DriverLocationApp

import SwiftUI import FirebaseCore

@main struct DriverLocationApp: App {

// register app delegate for Firebase setup @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate private var Notedelegate: NotificationDelegate = NotificationDelegate()

init() {
    let center = UNUserNotificationCenter.current()
    center.delegate = appDelegate
    center.requestAuthorization(options: [.alert, .sound, .badge]) { result, error in
        if let error = error {
            print("Hello There, I'm from the DriverLocationApp")
            print(error)
        }
    }
}

var body: some Scene { WindowGroup { ContentView() } } }

Reproducing the issue

No response

Firebase SDK Version

11.2

Xcode Version

16.1

Installation Method

Swift Package Manager

Firebase Product(s)

App Check, Database, In-App Messaging, Messaging

Targeted Platforms

iOS

Relevant Log Output

No response

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
```json Replace this line with the contents of your Package.resolved. ```

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
```yml Replace this line with the contents of your Podfile.lock! ```
google-oss-bot commented 1 month ago

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

rizafran commented 1 month ago

Thanks for reporting, @Emanuel-111. I tried to reproduce the issue, but I can't get the same error you encountered. May I know if you're still able to reproduce the issue? You may also try our sample app as your baseline for troubleshooting.

Emanuel-111 commented 1 month ago

I did change the code a tad but yes, I can still reproduce the issue.

Give me some time and I can try to come up with a sample app

If you're still trying the code at the top, I left some relevant photos of what the firebase console, info and Google plists, and a blurred APN Auth Key to see if maybe I'm missing something

Screenshot 2024-09-24 at 1 05 50 PM Bundle and Team ID APN Auth Key Google Info plist Info plist
Emanuel-111 commented 1 month ago

I was able to figure out the problem, its actually due to another method being called in another file.

private func subscribeToNotifications() { Messaging.messaging().subscribe(toTopic: "drivers") { error in if let error = error { print("Error subscribing to topic: \(error)") } else { print("Subscribed to topic: drivers") } } print("It skipped the subscribeToNotifications") }

When I subscribe to Notifications, the FCM error appears. Why would that be?
ncooke3 commented 1 month ago

When I subscribe to Notifications, the FCM error appears. Why would that be?

https://github.com/firebase/firebase-ios-sdk/issues/10679#issuecomment-1402776795 may be relevant here, especially if you recently updated from a Firebase version before the change mentioned went into effect

Emanuel-111 commented 1 month ago

It just might be, though, I notice when I call my reference.observe(.value) function it seems to skip over the function entirely, I'm not sure if that might be the issue