capacitor-community / fcm

Enable Firebase Cloud Messaging for Capacitor apps
https://capacitor.ionicframework.com/docs/
MIT License
238 stars 83 forks source link

getToken returns some JWT instead of the instance ID #99

Closed patryk-eco closed 1 year ago

patryk-eco commented 2 years ago

Describe the bug After updating to capacitor v3 and the current version of the fcm plugin, our call to getToken() stopped working. Instead of some FCM token it returns a JSON web token.

To Reproduce Steps to reproduce the behavior:

  1. Setup an app with FCM
  2. Call requestPermissions() and register() on PushNotifications
  3. Call FCM.getToken()

Expected behavior We expected it to return an fcm token which looks like a random string with a colon. e.g. eFznuNk5RsqUaR33qoswb7:APA91... Instead we got a JWT token: jwt.io

Desktop (please complete the following information):

Smartphone (please complete the following information):

Additional context

Ionic:

   Ionic CLI                     : 6.13.1 (/home/---/.npm-packages/lib/node_modules/@ionic/cli)
   Ionic Framework               : @ionic/angular 5.8.1
   @angular-devkit/build-angular : 12.2.3
   @angular-devkit/schematics    : 12.2.3
   @angular/cli                  : 12.2.3
   @ionic/angular-toolkit        : 4.0.0

Capacitor:

   Capacitor CLI   : 3.2.4
   @capacitor/core : 3.2.4

Cordova:

   Cordova CLI       : not installed
   Cordova Platforms : not available
   Cordova Plugins   : not available

Utility:

   cordova-res                          : not installed
   native-run (update available: 1.5.0) : 1.4.1

System:

   NodeJS : v15.14.0 (/home/---/.nvm/versions/node/v15.14.0/bin/node)
   npm    : 7.19.0
   OS     : Linux 5.14
eljass commented 2 years ago

For this reason we use:

PushNotifications.addListener('registration', async ({ value }) => {
  let token = value // Push token for Android

  // Get FCM token instead the APN one returned by Capacitor
  if (Capacitor.getPlatform() === 'ios') {
    const { token: fcm_token } = await FCM.getToken()
    token = fcm_token
  }
  // Work with FCM_TOKEN
})
patryk-eco commented 2 years ago

Alright, thanks for the tip! I read about that workaround somewhere, but dismissed it :smile:

I'll try it out.

cyril-colin commented 2 years ago

Can someone explains why we have to do that ? I can't find any breaking change note in the changelog and when I browse project history, I can't find anything about that. For example, this commit https://github.com/capacitor-community/fcm/commit/b1a89f08edc7e63ed0abf172830a2e8cb266e476 is just a classic update to capacitor 3, but there is no change that explains why the spec has changed.

Please help, thanks :)

tylerclark commented 2 years ago

I have the same issue where I have to treat Android is a different way than iOS for getting the proper token. Weird.

Saqib92 commented 2 years ago

For this reason we use:

PushNotifications.addListener('registration', async ({ value }) => {
  let token = value // Push token for Android

  // Get FCM token instead the APN one returned by Capacitor
  if (Capacitor.getPlatform() === 'ios') {
    const { token: fcm_token } = await FCM.getToken()
    token = fcm_token
  }
  // Work with FCM_TOKEN
})

Above not working. Here is my WORKING Code:

getToken() {
    if (Capacitor.getPlatform() !== 'web') {
      PushNotifications.requestPermissions().then((permission) => {
        if (permission.receive == "granted") {

          PushNotifications.addListener('registration', async ({ value }) => {
            let token = value // Push token for Android

            // Get FCM token instead the APN one returned by Capacitor
            if (Capacitor.getPlatform() === 'ios') {
              const { token: fcm_token } = await FCM.getToken()
              token = fcm_token
            }
            // Work with FCM_TOKEN

            console.log(token);
          })
        } else {
          // No permission for push granted
          alert('No Permission for Notifications!')
        }
      });
    }
  }
johntang commented 2 years ago

Hi, previously when using getToken, I can get the token by using an async function returning the token. But if I changed to listener, it is not working anymore. Any solution?

 if (Capacitor.isNativePlatform()) {
      const permission = await PushNotifications.requestPermissions();
      if (permission.receive === 'granted') {
        await PushNotifications.register();
        const { token } = await FCM.getToken();
        return token;
      }
   }
kyleabens commented 2 years ago

I have gotten both types of tokens and neither seem to be working when it comes to receiving and displaying push notifications for Android 12 devices. Anyone else having this issue?

sburnicki commented 2 years ago

TL;DR: Cause of the issue is the migration of a breaking change in Firebase Android SDK 22.0 that went wrong in 18bb0a0b33bd4ed71b9b990a0153f9d06541003b via #88

Long version: It seems the problem did not arise from Capacitor3 upgrade, but from pull request #88 (precisely: commit 18bb0a0b33bd4ed71b9b990a0153f9d06541003b) where a breaking change in Firebase Android SDK 22.0 was tried to be migrated as documented. However, documentation here is a bit tricky, as migration differs depending on what you needed the token for: identifying the installation or getting an FCM token; seems like the instance id token did both jobs before. So unfortunately the wrong part of the migration guide was chosen: migration to FirebaseInstallations instead of FirebaseMessaging.

My best guess is that they still might be the same but are not guaranteed to be the same? In the end, the migration needs to be done properly to FirebaseMessaging.

Capacitor 3's push-notification plugin still uses the deprecated FirebaseInstanceId method, which is probably why the suggest to only use SDK 21.0.1 in their docs. But at least this is working, that's why the workaround mentioned by @eljass works. But I would also really like to see that code updated to use the up-to-date solution based on FirebaseMessaging

Saqib92 commented 2 years ago

Yes, and in newer version on this plugin is not even generating any token. But if i send notification from firebase console it wis working.

mesqueeb commented 2 years ago

@sburnicki what's a good way to work around this in the mean time? I'm hesitant to release on Android because push notifications stopped working for me.

sburnicki commented 2 years ago

A good workaorund that works also for me is what ejlass proposen in the first comment: Only use the plugin on iOS and use the token from the capacitor push plugin on android.

mesqueeb commented 2 years ago

It took a while before I came up with something usable but this is what I came up with:

import { PushNotifications } from '@capacitor/push-notifications'
import { FCM } from '@capacitor-community/fcm'

/**
 * Returns `true` if the user gave permission or false otherwise.
 */
async function askFcmPermission(): Promise<boolean> {
  const checked = await PushNotifications.checkPermissions()
  let status: 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied' = checked.receive

  if (status === 'prompt' || status === 'prompt-with-rationale') {
    const requested = await PushNotifications.requestPermissions()
    status = requested.receive
  }

  return status === 'granted'
}

/**
 * Gets the FCM token in a non-breaking way.
 *
 * - For iOS we must use the `FCM` plugin, because `PushNotifications` returns the wrong APN token.
 * - For Android we must use `PushNotifications`, because `FCM` is broken for Android.
 * @see https://github.com/capacitor-community/fcm/issues/99
 */
export function getFcmToken(): Promise<string> {
  return new Promise((resolve, reject) => {
    if (Capacitor.getPlatform() === 'web') return resolve('')

    PushNotifications.addListener('registration', ({ value }) => {
      if (Capacitor.getPlatform() === 'android') {
        resolve(value)
        return
      }

      // Get FCM token instead the APN one returned by Capacitor
      if (Capacitor.getPlatform() === 'ios') {
        FCM.getToken()
          .then(({ token }) => resolve(token))
          .catch((error) => reject(error))
        return
      }

      // will never come here
      reject(new Error('?'))
    })

    askFcmPermission()
      .then((granted) => {
        if (granted) {
          PushNotifications.register().catch((error) => reject(error))
        } else {
          reject(new Error('denied'))
        }
      })
      .catch((error) => reject(error))
  })
}
whiteboardmonk commented 2 years ago

I have gotten both types of tokens and neither seem to be working when it comes to receiving and displaying push notifications for Android 12 devices. Anyone else having this issue?

+1 - @eljass works for me with Android 10 but doesn't work for Android 12 devices.

madmacc commented 1 year ago

I spent a lot of time on this problem. How about adding it to the documentation? https://github.com/capacitor-community/fcm#usage

A lot of people will be upgrading not starting with Capacitor 3 from scratch.

madmacc commented 1 year ago

Each time the user opens my app (if it is 7 days or more since last time) I check the token to see if it has changed/expired due to a new device / install. I was using await FCM.getToken() for this.

Should I now use PushNotifications.register() every time the user opens the app? How can I get the current token against the one stored in my database?

Also I noticed that PushNotifications.addListener('registration') returns a result with the token right after I have registered the listener even when I have not called PushNotifications.register(). Has anyone else noticed this? Is this perhaps because of some native code?

jcesarmobile commented 1 year ago

should be fixed by https://github.com/capacitor-community/fcm/pull/111

FilipKrizanic commented 1 year ago

This way you don't have to add additional code to Javascript/ts/tsx. Summary:

*.tsx:

import { Device, DeviceId, DeviceInfo } from '@capacitor/device'
import { PushNotifications } from '@capacitor/push-notifications'
...
...
if (
      (await Device.getInfo()).platform !== 'web' &&
      Capacitor.isPluginAvailable('PushNotifications')
    ) {
      let permStatus = await PushNotifications.checkPermissions()
      if (permStatus.receive === 'prompt') {
        permStatus = await PushNotifications.requestPermissions()
      }

      if (permStatus.receive !== 'granted') {
        throw new Error('User denied permissions!')
      }

      await PushNotifications.addListener('registration', (token) => {
        // In this case Android and iOS get FCM PushID instead of APNs token: See: AppDelegate.switf
        log.info('PushRegistration token: ', token.value)
      })
      await PushNotifications.addListener('registrationError', (err) => {
        log.info('Registration error: ', err.error)
      })
      await PushNotifications.addListener('pushNotificationReceived', (notification) => {
        log.info('Push notification received: ', JSON.stringify(notification))
      })
      await PushNotifications.addListener('pushNotificationActionPerformed', (notification) => {
        log.info(
          'Push notification action performed',
          notification.actionId,
          notification.inputValue
        )
      })
      await PushNotifications.register()
    }
  }

AppDelegate.swift:

import Capacitor
import Firebase
import FirebaseMessaging
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        return true
    }
...

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken;
        Messaging.messaging().token { fcmToken, error in
            if let error = error {
                NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
                return
            }
            NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: fcmToken)
        }
    }

    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) {
        print("AppDelegate.swift: User Info: \(userInfo)")
    }

Pods:

pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
...
... 
pod 'Firebase/Core'
pod 'Firebase/Messaging'

Other deps: capacitor: v4.6.2 npm: @capacitor/app@4.1.1 @capacitor/ios@4.6.2 @capacitor/push-notifications@4.1.2

Info.plist: "Required background modes: Item 0: App downloads content in response to push notifications"

    <key>UIBackgroundModes</key>
    <array>
        <string>remote-notification</string>
    </array>