Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Emanuel-111 opened this issue Sep 23, 2024 · 6 comments

Comments

@Emanuel-111
Copy link

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
Replace this line with the contents of your Package.resolved.

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
Replace this line with the contents of your Podfile.lock!
@google-oss-bot
Copy link

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

@rizafran
Copy link
Contributor

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
Copy link
Author

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
Copy link
Author

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
Copy link
Member

ncooke3 commented Sep 25, 2024

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

#10679 (comment) may be relevant here, especially if you recently updated from a Firebase version before the change mentioned went into effect

@Emanuel-111
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants