Firebase Messaging Service overrides

The problem

The @stream-io/react-native-callingx package provides its own FirebaseMessagingService implementation (StreamMessagingService) and registers it in its manifest.

Android FCM only delivers each push to a single FirebaseMessagingService per app. If your merged manifest declares more than one — typically because another push SDK registers its own, or because your app ships a custom service — only the one PackageManager resolves first actually fires. Depending on which service wins the merge, you'll see either Stream ringing pushes stop showing or the other SDK stop receiving its pushes.

The manifest merger doesn't flag this as an error — declaring multiple MESSAGING_EVENT services is legal and the build succeeds. There is just no fan-out at runtime.

How to detect the conflict

In Android Studio, open AndroidManifest.xml and switch to the Merged Manifest tab. Search for MESSAGING_EVENT and look at the <service> entries that declare it.

Firebase's own default FirebaseMessagingService always appears here — that's expected and harmless, since Android's priority rules keep it from overriding your service. You have a conflict when another service (another push SDK, or your own) also declares MESSAGING_EVENT, because FCM delivers each push to only one service.

Pick the section for your workflow: React Native CLI if you write native code and manage the manifest yourself, or Expo if you use the managed workflow (expo prebuild).

React Native CLI

Two patterns resolve this — pick based on whether you can subclass StreamMessagingService or need to host your own service.

Option 1 — Subclass StreamMessagingService

Pick this when you don't need to inherit from another SDK's service. Subclassing handles Stream's call.ring push notifications automatically, keeps the React Native Firebase background-message flow intact, and gives you a hook for custom forwarding.

android/app/src/main/java/.../AppMessagingService.kt
package com.example.app

import com.google.firebase.messaging.RemoteMessage
import io.getstream.rn.callingx.StreamMessagingService

class AppMessagingService : StreamMessagingService() {

  override fun onMessageReceived(remoteMessage: RemoteMessage) {
    // handles Stream call.ring
    super.onMessageReceived(remoteMessage)

    // Optional — short-circuit other forwarders for Stream pushes.
    // `isStreamCallRing` is provided for flexibility; Stream's `call.ring`
    // has already been handled by `super.onMessageReceived(...)`.
    if (StreamMessagingHelper.isStreamCallRing(remoteMessage)) return

    // forward non-Stream pushes to other SDKs
    ExampleSDK.passRemoteMessage(applicationContext, remoteMessage)
  }
}

Option 2 — Forward via StreamMessagingHelper

Pick this when your app already hosts its own FirebaseMessagingService. Remove Stream's service from the manifest and forward call.ring payloads via StreamMessagingHelper from your own service.

android/app/src/main/java/.../AppMessagingService.kt
package com.example.app

import com.google.firebase.messaging.RemoteMessage
import io.getstream.rn.callingx.StreamMessagingHelper
import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService

class AppMessagingService : ReactNativeFirebaseMessagingService() {

  override fun onMessageReceived(remoteMessage: RemoteMessage) {
    // chain to the base service
    super.onMessageReceived(remoteMessage)

    // Optional gate — provided for finer control over the forwarding flow.
    // `handleMessage` is also safe to call unconditionally; it no-ops for
    // payloads that aren't a Stream `call.ring`.
    if (StreamMessagingHelper.isStreamCallRing(remoteMessage)) {
      StreamMessagingHelper.handleMessage(applicationContext, remoteMessage)
    } else {
      // forward to other SDKs
      ExampleSDK.passRemoteMessage(applicationContext, remoteMessage)
    }
  }
}

React Native Firebase delivers messages to JavaScript through its own broadcast receiver, so onMessage and setBackgroundMessageHandler() keep firing regardless of which service you register. Extending ReactNativeFirebaseMessagingService (as both options do) only matters for token refresh — see Forward token refresh.

The helper exposes these methods:

APIPurpose
StreamMessagingHelper.isStreamCallRing(remoteMessage)Returns true if the payload is a Stream Video call.ring. Useful when you want to short-circuit other SDK forwarders.
StreamMessagingHelper.handleMessage(context, remoteMessage)Handles a Stream call.ring payload (starts the incoming call flow). No-op for non-Stream payloads — safe to call unconditionally.
StreamMessagingHelper.forwardNewToken(token)Forwards a refreshed FCM token so Stream can re-register the device.

Forward token refresh

If your service extends ReactNativeFirebaseMessagingService (including Stream's StreamMessagingService), token refresh already works — no extra step.

If it extends a different service, forward the token so Stream can re-register the device:

android/app/src/main/java/.../AppMessagingService.kt
override fun onNewToken(token: String) {
  super.onNewToken(token)
  StreamMessagingHelper.forwardNewToken(token)
}

Register your service in the manifest

Both options need the same change in android/app/src/main/AndroidManifest.xml — remove the default StreamMessagingService and register yours in its place:

android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
  <application>
    <service
        android:name="io.getstream.rn.callingx.StreamMessagingService"
        tools:node="remove" />

    <service
        android:name=".AppMessagingService"
        android:exported="false">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
    </service>
  </application>
</manifest>

Expo

On the Expo managed workflow you don't write native Kotlin or edit AndroidManifest.xml yourself, so the React Native CLI options above don't apply. The config plugin resolves the conflict through the optional androidMessagingServiceBaseClass property:

ValueBehavior
omittedAutomatically overrides expo-notifications' service when it's installed; does nothing otherwise.
nullOpt out — no override, even when expo-notifications is installed.
a fully-qualified class nameOverride that specific FirebaseMessagingService instead, e.g. "io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService".

So by default, when expo-notifications is installed and ringing is enabled, the plugin overrides its FirebaseMessagingService automatically — you don't need to set anything. Set the property only to override a different service, or to null to opt out.

To override a specific service, set the property to that service's class name:

app.json
{
  "plugins": [
    [
      "@stream-io/video-react-native-sdk",
      {
        "ringing": true,
        "androidMessagingServiceBaseClass": "com.example.app.CustomFirebaseMessagingService"
      }
    ]
  ]
}

When an override is applied, on expo prebuild the plugin:

  • generates a StreamVideoMessagingService that extends the base class and handles Stream's call.ring pushes,
  • registers it as your app's com.google.firebase.MESSAGING_EVENT handler,
  • removes both Stream's default StreamMessagingService and the base class's own service registration, so the generated service is the one that receives pushes,
  • forwards token refreshes to Stream when the base isn't React Native Firebase, so device registration keeps working automatically — no action needed on your side.

Stream's call.ring is handled natively and not forwarded to the base class (so there's no duplicate call notification). Every other push is forwarded via super.onMessageReceived(...), so the SDK you extend (for example expo-notifications) keeps receiving its notifications.

Use the fully-qualified class name including its package — for example com.example.app.CustomFirebaseMessagingService, not just CustomFirebaseMessagingService.

When you change this property — for example set it to null to opt out, or switch to a different base — run expo prebuild --clean. A plain expo prebuild won't remove the previously generated service, manifest entries, or Gradle dependency, so the earlier override would stay in effect.

The SDK handles call.ring notifications for you — the ringing flow is fully managed natively. There's nothing you need to do with that notification type for ringing to work, so you can skip it in your own notification handling.