package io.getstream.rn.callingx import android.content.Context import android.os.Bundle import com.google.firebase.messaging.RemoteMessage import io.getstream.rn.callingx.utils.LifecycleListener import io.getstream.rn.callingx.utils.SettingsStore import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingSerializer /** * Public entry point for consumer apps that host their own [com.google.firebase.messaging.FirebaseMessagingService]. * * To opt out of the default [StreamMessagingService] registration, add to the app manifest: * ``` * * ``` * then invoke [handleMessage] from the app's own messaging service: * ``` * class AppMessagingService : ReactNativeFirebaseMessagingService() { * override fun onMessageReceived(remoteMessage: RemoteMessage) { * // pass remote message to React Native Firebase JS background handler * 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 push SDKs * } * } * } * ``` * * If your service does NOT extend [ReactNativeFirebaseMessagingService] (e.g. it extends * another SDK's service), also forward token refreshes so Stream can re-register the device: * ``` * override fun onNewToken(token: String) { * super.onNewToken(token) * StreamMessagingHelper.forwardNewToken(token) * } * ``` */ object StreamMessagingHelper { private const val TAG = "[Callingx] StreamMessagingHelper" /** * Returns `true` if [remoteMessage] is a Stream Video incoming `call.ring` push. * Useful when you want to short-circuit other SDK forwarders for Stream pushes. */ @JvmStatic fun isStreamCallRing(remoteMessage: RemoteMessage): Boolean { val data = remoteMessage.data return data["sender"] == "stream.video" && data["type"] == "call.ring" } /** * Handles a Stream Video `call.ring` payload by starting the incoming call flow. * No-op for any other payload, so it is safe to call unconditionally from a host * messaging service. */ @JvmStatic fun handleMessage(context: Context, remoteMessage: RemoteMessage) { val data = remoteMessage.data if (!isStreamCallRing(remoteMessage)) { debugLog(TAG, "sender or type is not supported, skipping CallService start") return } val callCid = data["call_cid"] if (callCid.isNullOrEmpty()) { debugLog(TAG, "missing call_cid for call.ring, skipping CallService start") return } val extras = Bundle().apply { data.forEach { (key, value) -> putString(key, value) } } CallEventBus.publish(CallEvent(CallingxModuleImpl.CALL_RING_PUSH_ACTION, extras)) if ( SettingsStore.shouldSkipIncomingPushInForeground(context) && LifecycleListener.isInForeground ) { debugLog( TAG, "app is in foreground and skipIncomingPushInForeground=true, letting JS handle call.ring — skipping CallService start", ) return } CallService.startIncomingCallFromPush(context.applicationContext, data) } /** * Re-emits React Native Firebase's `onTokenRefresh` JS event so Stream can re-register the * rotated device token. Call from your service's `onNewToken` when the base class is NOT * `ReactNativeFirebaseMessagingService` (otherwise the base already emits it). No-op if React * Native Firebase is unavailable. */ @JvmStatic fun forwardNewToken(token: String) { try { ReactNativeFirebaseEventEmitter.getSharedInstance() .sendEvent(ReactNativeFirebaseMessagingSerializer.newTokenToTokenEvent(token)) } catch (t: Throwable) { debugLog(TAG, "failed to forward new FCM token to JS: ${t.message}") } } }