capawesome-team / capacitor-firebase

⚡️ Firebase plugins for Capacitor. Supports Android, iOS and the Web.
https://capawesome.io/plugins/firebase/
Apache License 2.0
387 stars 100 forks source link

bug: APNS device token not set before retrieving FCM Token (Cloud Messaging) #115

Closed RRGT19 closed 2 years ago

RRGT19 commented 2 years ago

Plugin(s):

Docs: https://github.com/robingenz/capacitor-firebase/tree/main/packages/messaging

Platform(s): iOS

Current behavior: When I request the permission, I see in Xcode console:

APNS device token not set before retrieving FCM Token for Sender ID '707398199614'. Notifications to this FCM Token will not be delivered over APNS.Be sure to re-retrieve the FCM token once the APNS device token is set.

Expected behavior: No warnings and a token provided.

Steps to reproduce:

Related code: AppDelegate:

import UIKit
import Capacitor

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            // Override point for customization after application launch.
            return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
      NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
      NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        NotificationCenter.default.post(name: Notification.Name.init("didReceiveRemoteNotification"), object: completionHandler, userInfo: userInfo)
    }

    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.
    }

    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.
    }

    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.
    }

    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.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        // Called when the app was launched with a url. Feel free to add additional processing here,
        // but if you want the App API to support tracking app url opens, make sure to keep this call
        return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
    }

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        // Called when the app was launched with an activity, including Universal Links.
        // Feel free to add additional processing here, but if you want the App API to support
        // tracking app url opens, make sure to keep this call
        return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)

        let statusBarRect = UIApplication.shared.statusBarFrame
        guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }

        if statusBarRect.contains(touchPoint) {
            NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil)
        }
    }

}

My Ionic Service to handle Push Notification and Token logic:

import {Injectable, NgZone} from '@angular/core';
import {FirebaseMessaging} from '@capacitor-firebase/messaging';
import PlatformUtil from '../../shared/utilities/PlatformUtil';

@Injectable()
export class PushNotificationService {

  constructor(
    private ngZone: NgZone
  ) {
  }

  // This method is called in a specific flow of my App when I ask the user if he wants to receive push or not.
  init() {
    if (PlatformUtil.isMobile) {
      this.requestPermissions();
    }
  }

  private requestPermissions(): void {
    FirebaseMessaging.requestPermissions().then(result => {
      if (result.receive === 'granted') {
        this.addListeners();
      } else {
        // Show some error
      }
    });
  }

  private addListeners(): void {
    // Called when a new FCM token is received.
    FirebaseMessaging.addListener('tokenReceived', event => {
      console.log('Token', event.token);
      this.ngZone.run(() => {
        const token = event.token;
      })
    });

    // Called when a new push notification is received.
    FirebaseMessaging.addListener('notificationReceived', event => {
      console.log('notificationReceived', {event});
    });

    // Called when a new push notification action is performed.
    FirebaseMessaging.addListener('notificationActionPerformed', event => {
      console.log('notificationActionPerformed', {event});
    });
  }

}

Capacitor doctor:

💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 3.5.1
  @capacitor/core: 3.5.1
  @capacitor/android: 3.5.1
  @capacitor/ios: 3.5.1

Installed Dependencies:

  @capacitor/cli: 3.5.1
  @capacitor/core: 3.5.1
  @capacitor/android: 3.5.1
  @capacitor/ios: 3.5.1

[success] iOS looking great! 👌
[success] Android looking great! 👌

Any help is appreciated.

robingenz commented 2 years ago

Hi @RRGT19, please try the following: Prevent the auto initialization (see here) and call FirebaseMessaging.getToken() in the ngOnInit hook.

Does that help?

RRGT19 commented 2 years ago

Hi @robingenz,

Thanks for your response.

I did what you say and now I receive the token. A weird thing is that I receive the same warning message but also I receive the token, Xcode logs:

⚡️  To Native ->  FirebaseMessaging requestPermissions 53519772
⚡️  TO JS {"receive":"granted"}
⚡️  To Native ->  FirebaseMessaging addListener 53519773
⚡️  To Native ->  FirebaseMessaging addListener 53519774
⚡️  To Native ->  FirebaseMessaging addListener 53519775
⚡️  To Native ->  FirebaseMessaging subscribeToTopic 53519776
⚡️  To Native ->  FirebaseMessaging getToken 53519777
⚡️  To Native ->  FirebaseCrashlytics log 53519778
2022-06-16 08:29:59.607933-0400 App[89710:6301886] 9.1.0 - [FirebaseMessaging][I-FCM002022] APNS device token not set before retrieving FCM Token for Sender ID '707398199614'. Notifications to this FCM Token will not be delivered over APNS.Be sure to re-retrieve the FCM token once the APNS device token is set.
2022-06-16 08:29:59.610659-0400 App[89710:6301886] 9.1.0 - [FirebaseMessaging][I-FCM002022] APNS device token not set before retrieving FCM Token for Sender ID '707398199614'. Notifications to this FCM Token will not be delivered over APNS.Be sure to re-retrieve the FCM token once the APNS device token is set.
⚡️  TO JS undefined
⚡️  TO JS {"token":"cbodk8c-IEKaj2nDG_8xzm:APA9....................."}
⚡️  TO JS {"token":"cbodk8c-IEKaj2nDG_8xzm:APA9....................."}
⚡️  [log] - tokenReceived cbodk8c-IEKaj2nDG_8xzm:APA9..............
⚡️  TO JS {"token":"��w����,\u0015\u0007�X��u����<u-��\\��UT#�|"}
⚡️  [log] - tokenReceived ��w����,�X��u����<u-��\��UT#�|
⚡️  TO JS undefined
⚡️  [log] - Subscribed to topic example

I have a few questions though:

  1. getToken() has a required parameter called options, I needed to pass null.
FirebaseMessaging.getToken(null).then(res => {
      console.log('getToken', res.token);
      const token = res.token;
    })

I think the parameter should be optional, as the doc says, the parameters are meant to be used on Web. Example:

getToken(options?: GetTokenOptions): Promise<GetTokenResult>; (Notice the question mark)

  1. Do I need to prevent the auto initialization on Android too?

If I need to do this on Android too:

  1. After testing your advice, I have received the same token in different places, inside of getToken() and inside of addListener('tokenReceived'.....). Which one should I keep? Maybe the implementation should be to call getToken without .then()? Example:
await FirebaseMessaging.getToken(null);

// Here I get the token for iOS and Android.
FirebaseMessaging.addListener('tokenReceived', event => {
      console.log('tokenReceived', event.token);
});
  1. I see that to fix my issue I need to call getToken only for iOS, that means that I need to handle this scenario depending on the platform? check my complete implementation:
import {Injectable, NgZone} from '@angular/core';
import {FirebaseMessaging} from '@capacitor-firebase/messaging';
import PlatformUtil from '../../shared/utilities/PlatformUtil';

@Injectable()
export class PushNotificationService {

  constructor(
    private ngZone: NgZone
  ) {
  }

  init() {
    if (PlatformUtil.isMobile) {
      this.requestPermissions();
    }
  }

  private requestPermissions(): void {
    FirebaseMessaging.requestPermissions().then(result => {
      if (result.receive === 'granted') {
        this.addListeners();

        // Here, only for iOS
        if (PlatformUtil.isIos) {
          this.getToken();
        }
      } else {
        // Show some error
      }
    });
  }

  private getToken(): void {
    FirebaseMessaging.getToken(null).then(res => {
      console.log('getToken', res.token);
      const token = res.token;
    })
  }

  private addListeners(): void {
    // Called when a new FCM token is received.
    FirebaseMessaging.addListener('tokenReceived', event => {
      console.log('tokenReceived', event.token);
    });

    // Called when a new push notification is received.
    FirebaseMessaging.addListener('notificationReceived', event => {
      console.log('notificationReceived', {event});
    });

    // Called when a new push notification action is performed.
    FirebaseMessaging.addListener('notificationActionPerformed', event => {
      console.log('notificationActionPerformed', {event});
    });
  }

}

This is correct?

  1. The listeners are okay there or do I need to register them inside the constructor to be called every time the App is started? If the App gets killed, I think the listeners are cleared. I'm not sure if the listeners should be set again when the App starts, for example, if checkPermissions() return granted.

Thanks again for your help, just trying to understand the correct way to do this.

robingenz commented 2 years ago

getToken() has a required parameter called options, I needed to pass null.

You need to pass a empty object {}. null is not a valid value (see typescript defintions). But yes, this param could be optional.

Do I need to prevent the auto initialization on Android too?

Prevent the auto initialization is optional. You can use it (for privacy reasons for example) but you don't have to.

What happens on iOS if you don't use it but call getToken anyway? The warning should still appear, but everything should work.

But for now you can use my workaround.

How can we enable again the analytics collection? maybe it's enabled automatically after a token is generated? I think this point is not too clear to me.

Call setEnabled.

The Google docs says that we need to re-enable FCM auto-init, see here. I don't see a method within the library to do this. iOS also has this requirement, see here.

Just call getToken to re-enable it.

After testing your advice, I have received the same token in different places, inside of getToken() and inside of addListener('tokenReceived'.....). Which one should I keep?

It should be the same token, so it doesn't matter where you take it from.

I see that to fix my issue I need to call getToken only for iOS, that means that I need to handle this scenario depending on the platform? check my complete implementation:

Just call getToken on every platform as soon as everything is ready to receive push notification. You should call this on every startup anyway to check if the FCM token has changed. Your backend should always know the latest token of a user.

The listeners are okay there or do I need to register them inside the constructor to be called every time the App is started? If the App gets killed, I think the listeners are cleared. I'm not sure if the listeners should be set again when the App starts, for example, if checkPermissions() return granted.

You have to register the listeners at every app start.

Thanks again for your help, just trying to understand the correct way to do this.

No problem, you're welcome!

RRGT19 commented 2 years ago

@robingenz awesome explanation thank you!.

Just a few questions:

  1. About the setEnabled, I didn't know it was in another library. Maybe a little mention on the doc of the Cloud Messaging library should be okay?

  2. Before calling getToken and listeners on every startup, the best practice is to check if the user has granted permission using checkPermissions() ? otherwise, I think I will be initializing listeners and subscribing to topics without knowing if the user has given his permission before.

All your advice helped me and now I'm more confident that my implementation is more correct. I think after this, this thread can be closed. :innocent:

robingenz commented 2 years ago

About the setEnabled, I didn't know it was in another library. Maybe a little mention on the doc of the Cloud Messaging library should be okay?

I'm going to rewrite and unify the setEnabled methods anyway, so that should be more obvious soon (see #13).

Before calling getToken and listeners on every startup, the best practice is to check if the user has granted permission using checkPermissions() ?

Right. First request permissions, then retrieve FCM token.

Thanks again for your issue. This helps me to improve this library. 👍

I'm leaving the issue open yet as I want to debug the ios issue before closing it.