Skip to content

feat(android): emit tokenReceived event via FCMMessagingService subclass - #191

Closed
eljass wants to merge 2 commits into
capacitor-community:masterfrom
eljass:upstream/feat-token-received-android
Closed

eljass wants to merge 2 commits into
capacitor-community:masterfrom
eljass:upstream/feat-token-received-android

Conversation

@eljass

@eljass eljass commented May 7, 2026

Copy link
Copy Markdown

What

Adds the Android side of the tokenReceived event. Pairs with #190 (iOS) — together they close #177.

Implementation:

  • FCMMessagingService extends com.capacitorjs.plugins.pushnotifications.MessagingService. Calls super.onNewToken() first (preserving PushNotifications' existing registration handling) then forwards to FCMPlugin.onNewTokenReceived(token), which calls notifyListeners("tokenReceived", { token }).
  • AndroidManifest.xml registers FCMMessagingService and uses tools:node="remove" to drop @capacitor/push-notifications' default service entry. Android FCM only routes MESSAGING_EVENT to a single FirebaseMessagingService, so we cannot simply add ours alongside.
  • FCMPlugin holds a static instance + single-slot pendingToken buffer so cold-start tokens (those emitted before the plugin's load() runs) are not lost. Dispatch is synchronized and dedupes against lastNotifiedToken.

JS API (same as iOS, registered in definitions.ts):

FCM.addListener('tokenReceived', ({ token }) => {
  await saveFcmToken(token);
});

Why

Issue #177 has been open since May 2025 asking for a way to monitor token refresh without polling getToken(). On Android specifically, this event is the only signal for asynchronous token rotations after initial registration.

Dependency: @capacitor/push-notifications as peer

Added as a peerDependency (not a hard dependency) at >=8.0.0. Rationale:

  • The two plugins already coexist in most consuming apps.
  • Android can only route FCM messages to one FirebaseMessagingService per APK; subclassing PushNotifications' service keeps both plugins functional simultaneously.
  • Apps that don't use @capacitor/push-notifications today must add it. Open to discussing alternative architectures (e.g. extend FirebaseMessagingService directly and lose PushNotifications interop) if the maintainers prefer.

Maintenance contract

MessagingServiceContractTest (in android/src/test/) asserts that @capacitor/push-notifications' MessagingService still exposes onNewToken(String) and still extends FirebaseMessagingService. If either changes in a future PushNotifications release, ./gradlew test fails — preventing the silent regression that the manifest comment warns about.

Test plan

  • Unit: cd android && ./gradlew test — MessagingServiceContractTest passes against @capacitor/push-notifications 8.x.
  • Integration in our app: listener fires on first FCM token issuance, on refreshToken(), and is properly deduped on subsequent re-emissions from FCM.
  • Manifest merge verified locally via ./gradlew :app:processDebugManifest — only FCMMessagingService is registered post-merge.

Notes

  • getToken(), refreshToken(), subscribeTo, unsubscribeFrom, etc. are unchanged. Purely additive.
  • No dependencies change — only peerDependencies.

Stacking

This PR includes the iOS commit from #190 since both share definitions.ts (the addListener('tokenReceived', ...) overload and TokenReceivedEvent type). When #190 merges, this PR's diff narrows to Android-only. If you'd prefer I rebase off master and duplicate the TS changes here, happy to do that — let me know.

eljass added 2 commits May 7, 2026 19:58
Adds a `tokenReceived` event JS clients can subscribe to for live FCM
registration token updates on iOS. Addresses the long-standing request
in capacitor-community#177.

Implementation:
- Plugin.swift `messaging(_:didReceiveRegistrationToken:)` now calls
  `notifyListeners("tokenReceived", ["token": token])`
- Gated on `Messaging.messaging().apnsToken != nil` so the pre-APNs
  registration token Firebase mints on first launch (which gets
  immediately replaced) is not emitted to consumers
- Deduped against `lastNotifiedToken` so foreground/background
  transitions do not re-emit the same token

JS API:

    FCM.addListener('tokenReceived', ({ token }) => { ... });

`getToken()` behaviour is unchanged; this PR is purely additive. No new
dependencies. The Android side will arrive in a follow-up PR.
Adds the Android side of the `tokenReceived` event introduced for iOS in
the previous commit. Closes capacitor-community#177.

Implementation:
- FCMMessagingService extends @capacitor/push-notifications'
  MessagingService, calls super first to preserve PushNotifications'
  registration handling, then forwards onNewToken to FCMPlugin.
- AndroidManifest.xml registers FCMMessagingService and uses
  tools:node="remove" to drop the PushNotifications default service so
  Android FCM routes MESSAGING_EVENT to ours (FCM only routes to a
  single FirebaseMessagingService).
- FCMPlugin static instance + single-slot pending token buffer + dedup
  in synchronized dispatchTokenReceived(). Tokens minted before the
  plugin loads are buffered and dispatched once load() runs.

@capacitor/push-notifications is added as a peerDependency. The
plugin already coexists with PushNotifications in most apps, and Android
cannot route FCM messages to two services independently. Subclassing
keeps both working.

Maintenance contract:
MessagingServiceContractTest asserts that
@capacitor/push-notifications' MessagingService still exposes
onNewToken(String) and still extends FirebaseMessagingService. CI fails
if either changes, preventing silent breakage on push-notifications
version bumps.
@stewones

Copy link
Copy Markdown
Member

Thanks for the thorough write-up and the contract test. We're closing this one for three reasons:

  1. It breaks the build for apps without @capacitor/push-notifications. implementation project(':capacitor-push-notifications') fails with "project not found" when that plugin isn't installed, and the new peer dependency makes it required. Some apps use this plugin with a different push setup.

  2. It replaces another plugin's service. tools:node="remove" on push-notifications' MessagingService plus a subclass ties us to that plugin's internals. The contract test catches a rename, but not behavior changes inside the parent class.

  3. Android already has this. PushNotificationsPlugin.onNewToken fires the registration event, and on Android that value is the FCM token, including after a refresh. So you can get the same thing with:

    PushNotifications.addListener('registration', ({ value }) => saveFcmToken(value));

#190 (iOS) is merged, since on iOS registration gives the APNs token and there was no FCM token event before. We'll document the Android route there.

If you have a case registration doesn't cover, open an issue and we can talk about a design that doesn't depend on push-notifications.

@stewones stewones closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to Monitor token refresh on android/ios using this plugin

2 participants