Skip to content

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

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

feat(android): emit tokenReceived event via FCMMessagingService subclass#191
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 testMessagingServiceContractTest 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.
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

1 participant