Skip to content

Conversation

@greenfrvr
Copy link
Contributor

@greenfrvr greenfrvr commented Dec 2, 2025

💡 Overview

📝 Implementation notes

🎫 Ticket: https://linear.app/stream/issue/RN-17/android-support-for-telecom-manager

📑 Docs: https://github.com/GetStream/docs-content/pull/881

Summary by CodeRabbit

  • New Features

    • Added comprehensive calling library with support for incoming and outgoing calls across iOS and Android.
    • Enhanced call management capabilities including mute, hold, active state control, and call display updates.
    • Improved notification handling for incoming and ongoing calls with customizable channel configuration.
  • Chores

    • Replaced calling integration library with new calling solution.
    • Simplified push notification configuration structure.

✏️ Tip: You can customize this high-level summary in your review settings.

greenfrvr and others added 30 commits December 2, 2025 20:23
…lkit-telecom-integration

# Conflicts:
#	sample-apps/react-native/dogfood/ios/Podfile.lock
#	yarn.lock
@changeset-bot
Copy link

changeset-bot bot commented Jan 5, 2026

⚠️ No Changeset found

Latest commit: 6fe17e4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Introduces a comprehensive cross-platform calling library (react-native-callingx) built on iOS CallKit and Android Telecom frameworks. Replaces react-native-callkeep dependency across the SDK and sample applications with unified calling experience, simplified configuration (from nested ringingPushNotifications to boolean ringing flag), and integrated event handling for incoming/outgoing calls.

Changes

Cohort / File(s) Summary
New React Native Calling Package
packages/react-native-callingx/ (50+ files)
Complete new cross-platform library with TypeScript/JavaScript API, iOS Swift implementation (CallKit, audio session, UUID storage), Android Kotlin implementation (Telecom framework, call service, notification management), build configs (babel, tsconfig, project.json, package.json), and comprehensive README/documentation
Android Calling Implementation
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/*
Core Android service (CallService), module bridge (CallingxModule, CallingxPackage), call state models (Call sealed class, CallAction, CallRepository with TelecomCallRepository/LegacyCallRepository), notification management (CallNotificationManager, NotificationChannelsManager, NotificationIntentFactory, NotificationReceiverActivity/Service), and utilities (HeadlessTaskManager, ResourceUtils, Utils)
iOS Calling Implementation
packages/react-native-callingx/ios/*.{swift,mm,h}
CallKit integration (Callingx.mm TurboModule, CallingxImpl with CXProviderDelegate, public header CallingxPublic.h), audio session management (AudioSessionManager), settings/configuration (Settings.swift), and UUID storage (UUIDStorage.swift)
React Native SDK Integration
packages/react-native-sdk/package.json
Dependency replacement: removed react-native-callkeep, added @stream-io/react-native-callingx with optional peer dependency; updated devDependencies and peerDependenciesMeta
SDK Push Handling Refactor
packages/react-native-sdk/src/utils/push/
New callingx integration: added setupCallingExpEvents, callingx.ts lib wrapper with extractCallingExpOptions, replaced iOS CallKeep hook (removed setupIosCallKeepEvents.ts), updated android.ts and internal/ios.ts handlers to use callingx, simplified Notifee event signatures by removing isBackground parameter
SDK Call State Synchronization
packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts, src/utils/StreamVideoRN/
New useCallingExpWithCallingStateEffect hook wiring call state transitions to callingx library; updated StreamVideoRN.setPushConfig to initialize callingx with extracted options; removed iOS-specific CallKeep hook
SDK Config & Types
packages/react-native-sdk/src/utils/StreamVideoRN/types.ts, expo-config-plugin/src/common/types.ts
Simplified push config: replaced nested ringingPushNotifications with boolean ringing flag; added iOS fields (supportsVideo, sound, imageName, etc.); renamed/restructured Android channels; added top-level flags (enableOngoingCalls, enableAutoPermissions, shouldRejectCallWhenBusy); removed deprecated navigation callbacks
Expo Config Plugin Updates
packages/react-native-sdk/expo-config-plugin/src/
Updated withAndroidManifest, withAndroidPermissions, withAppDelegate, withMainActivity, withiOSInfoPlist to use ringing boolean instead of nested ringingPushNotifications; removed RNCallKeep/RNCallKeep setup code from AppDelegate
iOS Native Module Updates
packages/react-native-sdk/ios/StreamVideoReactNative.{h,m}
Replaced call tracking dictionaries and registerIncomingCall API with new didReceiveIncomingPush and rejectIncomingCallIfNeeded methods; removed shouldRejectCallWhenBusy setter; added hasAnyActiveCall query; added busy-tone generation/playback; integrated PushKit support
Sample App Dependencies
sample-apps/react-native/{dogfood,expo-video-sample,ringing-tutorial}/
Dependency replacement: removed react-native-callkeep, added @stream-io/react-native-callingx (workspace:^) in all three sample apps; updated metro.config.js to watch new package
Sample App Config & Handlers
sample-apps/react-native/*/utils/setPushConfig.ts, sample-apps/react-native/*/app.json
Simplified Android push config by removing incomingCallChannel and incomingCallNotificationTextGetters; added enableAutoPermissions and shouldRejectCallWhenBusy flags; updated Notifee event handlers to omit isBackground parameter; simplified app.json ringing config
Sample App iOS Integration
sample-apps/react-native/dogfood/ios/AppDelegate.swift
Replaced RNCallKeep with StreamVideoReactNative.didReceiveIncomingPush; removed manual CallKit UUID setup; updated push handling to use new rejectIncomingCallIfNeeded and completion handler flow; removed audio session activation stubs
Sample App Cleanup & Updates
sample-apps/react-native/{dogfood,expo-video-sample}/
Removed react-native-callkeep from Android gradle; removed POST_NOTIFICATIONS and USE_FULL_SCREEN_INTENT from dogfood manifest; updated nav handlers in expo sample; added user data entries; simplified layout in expo sample
Misc. Expo Config & Test Updates
sample-apps/react-native/expo-video-sample/, packages/react-native-sdk/expo-config-plugin/__tests__/
Expo app.json updated with simplified ringing config; test files updated to use ringing boolean instead of ringingPushNotifications object; removed ringing-specific callkeep plugin entry

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~100 minutes

Poem

🐰 Hops with joy at the calling spree,
CallKit and Telecom now run free,
From iOS bells to Android chimes,
Ringing loud in perfect time!
No more CallKeep, just streamlined calls,
A unified voice through all our halls. 🔔

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR includes substantial out-of-scope changes: migration from CallKeep to Callingx (new library), removal of legacy CallKeep integration, sample app modifications, and Expo plugin restructuring beyond Android Telecom support. Isolate the core Telecom Manager implementation into a focused commit. Consider moving CallKeep removal, new Callingx library, and sample app updates to separate PRs for clearer scope and easier review.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive Description contains only template placeholders with links but lacks substantive implementation details, making it difficult to assess completeness. Provide detailed implementation notes describing the CallKit/Telecom integration approach, key architectural decisions, and migration strategy from CallKeep to the new solution.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title 'feat: callkit/telecom integration' clearly summarizes the main change: adding CallKit and Telecom framework support across platforms.
Linked Issues check ✅ Passed Code changes comprehensively implement Android Telecom Manager support and iOS CallKit integration as outlined in issue RN-17, including TelecomCallRepository, CallService, notification management, and platform-specific bridges.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@oliverlaz
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
sample-apps/react-native/expo-video-sample/app/index.tsx (1)

68-71: Remove dead code.

This empty useEffect serves no purpose and should be removed.

🧹 Proposed fix
-  useEffect(() => {
-    if (calls.length === 1) {
-    }
-  }, [calls]);
-
packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts (2)

362-406: ObjC incoming push handler: missing nil/type checks for stream and cid leading to potential crash.
If the push payload lacks or malforms the stream dictionary or call_cid field, cid becomes nil. Passing nil to addCompletionHandler and downstream methods causes a crash before the completion handler is invoked, violating the 30-second VoIP push completion requirement and triggering app termination by the system.

Add validation to confirm stream is a valid NSDictionary and cid is a valid non-empty NSString before proceeding. Call completion() and return early if validation fails.

Proposed hardening
 function addDidReceiveIncomingPushCallbackObjc(contents: string) {
   const onIncomingPush = `
   // process the payload and store it in the native module's cache
   NSDictionary *stream = payload.dictionaryPayload[@"stream"];
   NSString *cid = stream[@"call_cid"];
+
+  if (![stream isKindOfClass:[NSDictionary class]] ||
+      ![cid isKindOfClass:[NSString class]] ||
+      cid.length == 0) {
+      completion();
+      return;
+  }
   
   // Check if user is busy BEFORE registering the call
   BOOL canProceed = [StreamVideoReactNative rejectIncomingCallIfNeeded:completion];
   if (!canProceed) {
       return;
   }
   
   [RNVoipPushNotificationManager addCompletionHandler:cid completionHandler:completion];

307-360: Remove unnecessary created_by_display_name requirement from Swift payload parsing; align with Objective-C implementation.

The Swift guard at line 310 requires stream["created_by_display_name"] but the Objective-C variant in the same function does not. The native implementation extracts this field as optional and uses it as the caller name. Drop the requirement to match the Objective-C pattern and avoid dropping legitimate pushes if that field is absent or null.

Regarding the completion lifecycle: RNVoipPushNotificationManager.addCompletionHandler() and RNVoipPushNotificationManager.didReceiveIncomingPush() (called before StreamVideoReactNative.didReceiveIncomingPush()) handle the completion callback. Verify that these two calls properly complete the handler within the 30-second VoIP push window to prevent system termination.

sample-apps/react-native/expo-video-sample/package.json (1)

15-15: Remove unused CallKeep config plugin dependency.

The @config-plugins/react-native-callkeep dependency is not referenced in the Expo plugins array (app.json) and has no imports in the codebase. This is a leftover from the migration to @stream-io/react-native-callingx and should be removed from package.json.

sample-apps/react-native/ringing-tutorial/package.json (1)

16-16: Remove the stale @config-plugins/react-native-callkeep dependency.

The package.json still declares @config-plugins/react-native-callkeep (line 16), and it remains configured in app.json, but it is not referenced anywhere in the application code. The @stream-io/video-react-native-sdk has CallKeep integration built-in and is already configured with "ringing": true in app.json, making the separate config plugin redundant. Remove this dependency from both package.json and the plugins array in app.json.

packages/react-native-sdk/ios/StreamVideoReactNative.m (1)

395-407: Logging tag + active-call detection criteria should be revisited.

  • Log prefix is [RNCallKeep] (Line 402) but this code lives in StreamVideoReactNative.
  • hasConnected only catches connected calls; you may want “active-ish” (!hasEnded) depending on why this is used.
🤖 Fix all issues with AI agents
In
@packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt:
- Around line 54-63: The capability flags are being combined with bitwise AND
causing a zeroed flag when bits don't overlap; change the combination in the
init block from CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING and
CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING to use the bitwise OR operator so
the resulting capabilities include both flags, updating the expression used to
build `capabilities` before calling
`CallsManager(context.applicationContext).apply {
registerAppWithTelecom(capabilities) }`.

In
@packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/ResourceUtils.kt:
- Around line 36-57: The bug is that getSoundUri builds the android.resource://
URI using the original sound string while getResourceIdByName uses a normalized
name for lookup, so names like "My-Sound.mp3" can resolve to a resourceId but
produce an incorrect URI; fix getSoundUri by computing and reusing the same
normalized resource name used for getResourceIdByName (e.g., strip extension via
substringBeforeLast and any normalization used by getResourceIdByName) into a
variable (e.g., resourceName) when soundResourceId is found, and construct the
URI as "android.resource://${context.packageName}/raw/$resourceName" instead of
using the original sound.

In @packages/react-native-callingx/CODE_OF_CONDUCT.md:
- Around line 59-67: Replace the placeholder "[INSERT CONTACT METHOD]" under the
"Enforcement" section with the real enforcement contact (for example a dedicated
email like [email protected] or a link to a GitHub issue template or support
channel) so the Code of Conduct has an actionable reporting mechanism; ensure
the inserted contact appears in the same sentence after "reported to the
community leaders responsible for enforcement at" and keep the rest of the
paragraph unchanged.

In @packages/react-native-callingx/ios/AudioSessionManager.swift:
- Around line 28-30: The assignment to mode uses AVAudioSession.Mode(rawValue:
modeString) which is failable and currently being force-unwrapped into a
non-optional mode; change the logic in the AudioSessionManager to safely unwrap
and validate the result from AVAudioSession.Mode(rawValue:) (using if let or
guard let) and only assign to the non-optional mode variable when the conversion
succeeds, otherwise keep a sensible default or handle the invalid input path
(e.g., log the bad modeString via audioSessionSettings and skip assignment) so
invalid mode strings cannot cause a runtime crash.

In @packages/react-native-sdk/ios/StreamVideoReactNative.m:
- Around line 99-150: The method didReceiveIncomingPush:completionHandler: must
always invoke the provided completion block; update each early-return path (when
streamPayload is nil, when callingxClass is nil, and when callingxClass does not
respond to selector) to call the incoming completion() before returning, and
change the local void (^completionHandler)(void) to forward the incoming
completion (use the completion parameter) when setting the argument for Callingx
invocation; also ensure that after invoking the selector on callingxClass you
still call completion() if Callingx does not itself invoke the block so the
system completion is always executed exactly once.

In @packages/react-native-sdk/src/utils/push/android.ts:
- Around line 322-372: The permission check block after calling
getNotifeeLibThrowIfNotInstalledForPush() logs when settings.authorizationStatus
!== 1 but fails to exit, causing the subsequent call notification flow
(createChannel, displayNotification, pushNonRingingCallData$.next) to run
erroneously; add an early return immediately after the logger.debug that says
"Notification permission not granted, unable to post ${data.type} notifications"
to stop execution when notifications are not authorized (i.e., keep the existing
log and then return from the enclosing function before the callChannel
handling).
🟠 Major comments (25)
sample-apps/react-native/dogfood/tsconfig.json-5-5 (1)

5-5: Remove "dom" from lib in React Native app.

This React Native sample app has no react-native-web dependency and targets only iOS and Android. Including "dom" in the TypeScript lib option allows DOM-specific APIs (document, window, HTMLElement) that don't exist in the React Native runtime, degrading type safety and potentially masking errors.

Remove "dom" and keep only "esnext":

Suggested change
-    "lib": ["esnext", "dom"],
+    "lib": ["esnext"],

The base config @react-native/typescript-config provides appropriate types for React Native.

packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts-26-34 (1)

26-34: Missing teardown for didLoadWithEvents listener causes resource leak on logout.

Line 26 adds a addEventListener('didLoadWithEvents', ...) listener, but the cleanup callback at line 38 only removes the 'notification' listener (line 42). When onPushLogout() is called, the 'didLoadWithEvents' listener persists, creating a resource leak. The pattern in useIosVoipPushEventsSetupEffect.ts shows the correct approach: both listeners must be removed during cleanup.

Extract both handlers to named functions so they can be properly removed:

Proposed fix
-  voipPushNotification.addEventListener('didLoadWithEvents', (events) => {
+  const onDidLoadWithEvents = (events: any[]) => {
     //we need this for cold start scenario where the app is not running and the events are not processed when the app is launched
     for (const event of events) {
       const { name, data } = event;
       if (name === 'RNVoipPushRemoteNotificationReceivedEvent') {
         onVoipNotificationReceived(data, pushConfig);
       }
     }
-  });
-  voipPushNotification.addEventListener('notification', (notification) => {
+  };
+  const onNotification = (notification: any) => {
     onVoipNotificationReceived(notification, pushConfig);
-  });
+  };
+
+  voipPushNotification.addEventListener('didLoadWithEvents', onDidLoadWithEvents);
+  voipPushNotification.addEventListener('notification', onNotification);
   
   setPushLogoutCallback(async () => {
     videoLoggerSystem
       .getLogger('setPushLogoutCallback')
       .debug('notification event listener removed');
     voipPushNotification.removeEventListener('notification');
+    voipPushNotification.removeEventListener('didLoadWithEvents');
   });
packages/react-native-callingx/android/build.gradle-49-51 (1)

49-51: Replace deprecated lintOptions with lint block.

The lintOptions block has been deprecated since Android Gradle Plugin 7.0. Modern Gradle versions use the lint block instead.

♻️ Proposed fix to use modern lint configuration
-  lintOptions {
+  lint {
     disable "GradleCompatible"
   }
packages/react-native-callingx/android/build.gradle-53-56 (1)

53-56: Update Java compatibility to version 11 or higher.

Java 1.8 compatibility is outdated for modern React Native projects. React Native 0.73+ requires Java 11 as the minimum version. Consider updating to Java 11 or 17 for better compatibility and performance.

♻️ Proposed fix to use Java 11
   compileOptions {
-    sourceCompatibility JavaVersion.VERSION_1_8
-    targetCompatibility JavaVersion.VERSION_1_8
+    sourceCompatibility JavaVersion.VERSION_11
+    targetCompatibility JavaVersion.VERSION_11
   }
packages/react-native-callingx/android/gradle.properties-1-1 (1)

1-1: Resolve Android Gradle Plugin 8.7.2 and Kotlin 2.0.21 version mismatch.

Kotlin 2.0.21 is a valid stable release, but it is not officially compatible with AGP 8.7.2. The Kotlin compatibility matrix shows that KGP 2.0.21 supports only AGP up to 8.5. Either upgrade Kotlin to 2.1.20+ or 2.2.x (which support AGP 8.7.2), or downgrade AGP to 8.5 or lower to align with Kotlin 2.0.21.

packages/react-native-callingx/README.md-77-78 (1)

77-78: Complete the Android Setup section.

The Android Setup section is empty, which is a critical documentation gap. Based on the PR changes introducing CallService, notification channels, and intent handling, users will need detailed guidance on:

  1. Required MainActivity modifications for handling call intents
  2. Notification channel configuration
  3. Permissions handling (especially POST_NOTIFICATIONS for Android 13+ and USE_FULL_SCREEN_INTENT for Android 11+)
  4. Potential Telecom API setup requirements

Would you like me to draft the Android Setup section based on the implementation details in the PR?

packages/react-native-callingx/ios/AudioSessionManager.swift-19-19 (1)

19-19: Reconsider .allowBluetoothA2DP for VoIP calls.

The default category options include .allowBluetoothA2DP, which is typically used for high-quality music playback but introduces higher latency. For VoIP calls, the HFP (Hands-Free Profile) Bluetooth mode is generally preferred for lower latency. Consider whether A2DP should be included by default or made configurable based on use case.

Based on learnings: iOS CallKit integration requires careful audio session configuration for call scenarios.

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt-25-29 (1)

25-29: Add error handling to coroutine launches.

Both coroutine launches (lines 28 and 77) lack error handling. If an exception occurs during collection, the coroutine will terminate silently, causing:

  • Listener to stop receiving call state updates (line 28)
  • Actions to stop being processed (line 77)

This could leave the call in an inconsistent state without any indication to the caller.

🛡️ Recommended error handling
     override fun setListener(listener: Listener?) {
         this._listener = listener
         // Observe call state changes
-        scope.launch { currentCall.collect { _listener?.onCallStateChanged(it) } }
+        scope.launch {
+            try {
+                currentCall.collect { _listener?.onCallStateChanged(it) }
+            } catch (e: Exception) {
+                Log.e(TAG, "[repository] Error collecting call state", e)
+            }
+        }
     }
         // Process actions without telecom SDK
         scope.launch {
-            actionSource.consumeAsFlow().collect { action -> processActionLegacy(action) }
+            try {
+                actionSource.consumeAsFlow().collect { action -> processActionLegacy(action) }
+            } catch (e: Exception) {
+                Log.e(TAG, "[repository] Error processing call actions", e)
+            }
         }

Also applies to: 76-79

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/model/Call.kt-34-47 (1)

34-47: Potential action loss with unbuffered channel and trySend.

The actionSource channel uses the default capacity (rendezvous/0), and processAction() uses trySend(), which fails immediately if no receiver is ready. Critical actions like Answer or Disconnect could be silently dropped if the consumer is temporarily busy or the channel is closed.

Consider one of the following solutions:

  • Use a buffered channel (e.g., Channel<CallAction>(Channel.BUFFERED)) to queue actions
  • Document that callers must check the return value of processAction() and retry on failure
  • Use send() with suspension for guaranteed delivery (requires suspending caller)
🔧 Recommended fix: Use buffered channel
 data class Registered(
     val id: String,
     val callAttributes: CallAttributesCompat,
     val displayOptions: Bundle?,
     val isActive: Boolean,
     val isOnHold: Boolean,
     val isMuted: Boolean,
     val errorCode: Int?,
     val currentCallEndpoint: CallEndpointCompat?,
     val availableCallEndpoints: List<CallEndpointCompat>,
-    internal val actionSource: Channel<CallAction>,
+    internal val actionSource: Channel<CallAction> = Channel(Channel.BUFFERED),
 ) : Call() {

Alternatively, update the channel creation at the call site in repositories to specify capacity.

packages/react-native-sdk/package.json-68-68 (1)

68-68: Consider tightening the version constraint for the 0.x release.

The version constraint >=0.1.0 permits any future version including breaking changes in 0.x releases. For a newly introduced library still at 0.x, consider using a more restrictive constraint like ^0.1.0 or ~0.1.0 to prevent unexpected breaking changes.

📦 Suggested version constraint
-    "@stream-io/react-native-callingx": ">=0.1.0",
+    "@stream-io/react-native-callingx": "^0.1.0",

This allows patch and minor updates within 0.x but prevents jumping to 1.x without explicit upgrade.

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt-310-329 (1)

310-329: Unsafe cast may cause crash if state changes concurrently.

Line 313 casts _currentCall.value to Call.Registered without a null-safe check. If the state changes between the action dispatch and execution, this will throw a ClassCastException.

🔒 Proposed fix using safe cast
     private suspend fun CallControlScope.doSwitchEndpoint(action: CallAction.SwitchAudioEndpoint) {
         Log.d(TAG, "[repository] doSwitchEndpoint: Switching to endpoint: ${action.endpointId}")
-        // TODO once availableCallEndpoints is a state flow we can just get the value
-        val endpoints = (_currentCall.value as Call.Registered).availableCallEndpoints
+        val call = _currentCall.value as? Call.Registered
+        if (call == null) {
+            Log.w(TAG, "[repository] doSwitchEndpoint: No registered call, ignoring")
+            return
+        }
+        val endpoints = call.availableCallEndpoints
 
         // Switch to the given endpoint or fallback to the best possible one.
         val newEndpoint = endpoints.firstOrNull { it.identifier == action.endpointId }
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt-51-52 (1)

51-52: Race condition on isSelfAnswered and isSelfDisconnected flags.

These flags are read and written from multiple coroutines without synchronization. While the mutex protects call registration, these flags are accessed outside the mutex scope (e.g., in callbacks). Consider using AtomicBoolean or encapsulating the flag check-and-reset in a synchronized block.

🔒 Proposed fix using AtomicBoolean
+import java.util.concurrent.atomic.AtomicBoolean
+
 @RequiresApi(Build.VERSION_CODES.O)
 class TelecomCallRepository(context: Context) : CallRepository(context) {
 
     companion object {
         private const val TAG = "[Callingx] TelecomCallRepository"
     }
 
     private var observeCallStateJob: Job? = null
 
     private val callsManager: CallsManager
-    private var isSelfAnswered = false
-    private var isSelfDisconnected = false
+    private val isSelfAnswered = AtomicBoolean(false)
+    private val isSelfDisconnected = AtomicBoolean(false)

Then update usages to use getAndSet(false) for atomic check-and-reset:

val source = if (isSelfAnswered.getAndSet(false)) EventSource.APP else EventSource.SYS

Committable suggestion skipped: line range outside the PR's diff.

packages/react-native-callingx/src/EventManager.ts-15-53 (1)

15-53: Fix ref-counting: listenersCount can desync (negative or unsubscribe while listeners still exist).

removeListener() decrements even if the callback wasn’t present (or was removed twice), and you never account for duplicates added. That can incorrectly call subscription.remove() while listeners still exist.

Minimal fix: decrement only if something was removed
   removeListener<T extends EventName>(
     eventName: T,
     callback: EventListener<EventParams[T]>,
   ): void {
     const listeners = this.eventListeners.get(eventName) || [];
-    this.eventListeners.set(
-      eventName,
-      listeners.filter((c) => c !== callback),
-    );
-
-    this.listenersCount--;
+    const next = listeners.filter((c) => c !== callback);
+    this.eventListeners.set(eventName, next);
+    if (next.length !== listeners.length) {
+      this.listenersCount--;
+    }

     if (this.listenersCount === 0) {
       this.subscription?.remove();
       this.subscription = null;
     }
   }

Consider Map<EventName, Set<EventListener<...>>> and derive listenersCount from sum(set.size) to make it impossible to desync.

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt-23-40 (1)

23-40: Don't broadcast CALL_ANSWERED_ACTION with a missing EXTRA_CALL_ID.

callId can be null (line 31) and you still send the broadcast (lines 33-39). If the receiver assumes a non-null call id, this becomes a crashy cold-start path.

Proposed fix
   private fun handleIntent(intent: Intent?) {
     if (intent == null) {
       return
     }

     //we need it only for answered call event, as for cold start case we need to send broadcast event and to launch the app
     if (intent.action == CallingxModule.CALL_ANSWERED_ACTION) {
       val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID)
       val source = intent.getStringExtra(CallingxModule.EXTRA_SOURCE)
+      if (callId.isNullOrEmpty()) return
       Intent(CallingxModule.CALL_ANSWERED_ACTION)
         .apply {
           setPackage(packageName)
           putExtra(CallingxModule.EXTRA_CALL_ID, callId)
           putExtra(CallingxModule.EXTRA_SOURCE, source)
         }
         .also { sendBroadcast(it) }
     }
   }
packages/react-native-callingx/src/EventManager.ts-15-35 (1)

15-35: Gate the debug console.log behind __DEV__ and add error handling for listener invocations.

The console.log at line 27 unconditionally logs every native event, including the callId identifier. This causes production log spam and leaks sensitive user-session data. Additionally, if any listener throws an error, the forEach loop breaks and prevents remaining listeners from being notified.

Proposed fix
     if (this.subscription === null) {
       this.subscription = NativeCallingModule.onNewEvent((event: EventData) => {
-        console.log('[callingx] onNewEvent:', event);
+        if (__DEV__) {
+          // eslint-disable-next-line no-console
+          console.log('[callingx] onNewEvent:', event);
+        }
         const eventListeners =
           this.eventListeners.get(event.eventName as EventName) || [];
-        eventListeners.forEach((listener) =>
-          listener(event.params as EventParams[EventName]),
-        );
+        eventListeners.forEach((listener) => {
+          try {
+            listener(event.params as EventParams[EventName]);
+          } catch (e) {
+            if (__DEV__) {
+              // eslint-disable-next-line no-console
+              console.error('[callingx] listener error', e);
+            }
+          }
+        });
       });
     }
   }
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt-12-25 (1)

12-25: Wrap unhandled PendingIntent.send() calls in onCallAnswered() to prevent service crashes.

Both .send() calls (lines 31–36 and 38–44) can throw CanceledException if the PendingIntent is no longer valid. Unhandled exceptions here will crash the service. Catch exceptions and log them to ensure the notification action fails gracefully.

Proposed fix
  private fun onCallAnswered(intent: Intent) {
    val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID)
    val source = intent.getStringExtra(CallingxModule.EXTRA_SOURCE)
    callId?.let {
+     try {
        NotificationIntentFactory.getPendingBroadcastIntent(
                        applicationContext,
                        CallingxModule.CALL_ANSWERED_ACTION,
                        it
                ) { putExtra(CallingxModule.EXTRA_SOURCE, source) }
                .send()

        NotificationIntentFactory.getLaunchActivityIntent(
                        applicationContext,
                        CallingxModule.CALL_ANSWERED_ACTION,
                        it,
                        source
                )
                .send()
+     } catch (t: Throwable) {
+       Log.w(TAG, "Failed to send call answered intent", t)
+     }
    }
  }

Committable suggestion skipped: line range outside the PR's diff.

packages/react-native-callingx/src/utils/constants.ts-36-45 (1)

36-45: iOS end call reason mappings have semantic mismatches with CXCallEndedReason values.

The constants in iosEndCallReasonMap map to incorrect CXCallEndedReason cases based on their semantic meaning:

  • remote: 1 maps to .failed, should be 2 (.remoteEnded)
  • rejected: 4 maps to .answeredElsewhere, should be 5 (.declinedElsewhere)
  • answeredElsewhere: 3 maps to .unanswered, should be 4 (.answeredElsewhere)

Additionally, error: 0 and local: -1 don't match any case in the switch statement (hit default and return without calling reportCall), which may silently fail to report call endings for these scenarios.

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallService.kt-108-148 (1)

108-148: Don’t throw from onStartCommand; handle ACTION_STOP_SERVICE.

Throwing on unknown action (Line 141-144) can crash the app from an external/malformed intent. Also ACTION_STOP_SERVICE is defined (Line 58) but not handled.

Proposed safer action handling
 when (intent.action) {
@@
   ACTION_UPDATE_CALL -> {
     updateCall(intent)
   }
+  ACTION_STOP_SERVICE -> {
+    Log.d(TAG, "[service] onStartCommand: Stopping service")
+    stopSelf()
+  }
   else -> {
-    Log.e(TAG, "[service] onStartCommand: Unknown action: ${intent.action}")
-    throw IllegalArgumentException("Unknown action")
+    Log.e(TAG, "[service] onStartCommand: Unknown action: ${intent.action}; stopping")
+    stopSelf()
+    return START_NOT_STICKY
   }
 }

Also applies to: 53-59

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt-57-65 (1)

57-65: updateCall(...) ignores most of its parameters; likely breaks “update call UI” flows.

updateCall receives callId, displayName, address, isVideo, but only applies displayOptions (Line 64). At minimum, validate callId matches the current registered call and update relevant fields/attributes (or rename the API if this is intentionally “displayOptions-only”).

Sketch: validate callId + apply more fields
 open fun updateCall(
   callId: String,
   displayName: String,
   address: Uri,
   isVideo: Boolean,
   displayOptions: Bundle?,
 ) {
-  updateCurrentCall { copy(displayOptions = displayOptions) }
+  updateCurrentCall {
+    if (id != callId) return@updateCurrentCall this
+    copy(
+      callAttributes = createCallAttributes(displayName, address, isIncoming(), isVideo),
+      displayOptions = displayOptions,
+    )
+  }
 }

Also applies to: 71-94

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt-28-43 (1)

28-43: Avoid FLAG_MUTABLE unless strictly required (security + Play policy expectations).

All these PendingIntents set action/extras up front, so they can typically be FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT. Using FLAG_MUTABLE (Line 40, 62-63, 80) widens attack surface (intent tampering) without clear need.

Proposed tightening
 return PendingIntent.getService(
   context,
   REQUEST_CODE_SERVICE,
   intent,
-  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
 )
@@
 return PendingIntent.getActivities(
   context,
   REQUEST_CODE_RECEIVER_ACTIVITY,
   arrayOf(launchActivityIntent, receiverIntent),
-  PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+  PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
 )
@@
 return PendingIntent.getActivity(
   context,
   REQUEST_CODE_LAUNCH_ACTIVITY,
   callIntent,
-  PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+  PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
 )

Also applies to: 44-64, 66-82

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallService.kt-332-345 (1)

332-345: Untrusted intent extras are force-unwrapped (!!) and can crash the process.

extractIntentParams() uses !! for EXTRA_CALL_ID, EXTRA_NAME, and EXTRA_URI (Line 333-340). Since intents can originate from notifications, broadcasts, or OEM flows, treat these as untrusted and fail gracefully.

Safer parsing sketch
 private fun extractIntentParams(intent: Intent): CallInfo {
-  val callId = intent.getStringExtra(EXTRA_CALL_ID)!!
-  val name = intent.getStringExtra(EXTRA_NAME)!!
+  val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: error("Missing EXTRA_CALL_ID")
+  val name = intent.getStringExtra(EXTRA_NAME) ?: ""
@@
-  val uri =
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-      intent.getParcelableExtra(EXTRA_URI, Uri::class.java)!!
-    } else {
-      @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_URI)!!
-    }
+  val uri: Uri =
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+      intent.getParcelableExtra(EXTRA_URI, Uri::class.java)
+    } else {
+      @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_URI)
+    } ?: Uri.EMPTY

Also applies to: 321-330, 284-293

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt-52-63 (1)

52-63: Handle getLaunchIntentForPackage(...) == null to avoid broken notification taps.

getLaunchIntentForPackage can return null (e.g., unusual manifests / disabled launcher activity). Today you construct Intent(launchIntent) anyway (Line 54, 69), which can degrade into an empty intent and make the PendingIntent effectively no-op.

Also applies to: 67-75

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/HeadlessTaskManager.kt-18-25 (1)

18-25: Make class open or fix the unsafe casting to prevent crashes.

The class isn't declared open (Line 18), so subclasses can't override the protected open val reactNativeHost and protected open val reactHost properties (Lines 116–125). However, the KDoc at Lines 107–113 explicitly invites subclasses to override these properties, creating a contradiction.

Additionally, the casts (context.applicationContext as ReactApplication) on Lines 117 and 125 are unsafe and will crash with ClassCastException if the app's Application doesn't implement ReactApplication.

Either make the class open to allow the documented override pattern, or replace the unsafe casts with null-checks or a factory pattern to handle apps that don't implement ReactApplication.

packages/react-native-callingx/package.json-2-14 (1)

2-14: Add missing CommonJS build target per repo's established pattern.

This package only builds ESM and TypeScript definitions, but all other React Native packages in the repo (react-native-sdk, video-filters-react-native, noise-cancellation-react-native) use three targets: ["commonjs", "module", "typescript"]. Update the bob configuration to include the CommonJS target.

Additionally:

  • Replace placeholder description "test" with a descriptive summary.
  • Update exports to use explicit "import" and "require" conditions matching the build targets.
  • Consider adding "react-native": "./src/index.tsx" entry point to aid Metro resolution.
Suggested fix
{
  "name": "@stream-io/react-native-callingx",
  "version": "0.1.0",
-  "description": "test",
+  "description": "Cross-platform calling (iOS CallKit + Android Telecom) for Stream Video React Native",
   "main": "./dist/module/index.js",
+  "react-native": "./src/index.tsx",
   "types": "./dist/typescript/src/index.d.ts",
   "exports": {
     ".": {
       "source": "./src/index.tsx",
       "types": "./dist/typescript/src/index.d.ts",
+      "import": "./dist/module/index.js",
+      "require": "./dist/commonjs/index.js",
       "default": "./dist/module/index.js"
     },
     "./package.json": "./package.json"
   },
   ...
   "react-native-builder-bob": {
     "source": "src",
     "output": "dist",
     "targets": [
+      "commonjs",
       "module",
       "typescript"
     ]
   },
packages/react-native-callingx/ios/CallingxImpl.swift-294-313 (1)

294-313: Duplicate event emission in onAudioRouteChange.

The method emits the event twice: once directly via eventEmitter?.emitEvent(dictionary) on line 311, and again via sendEvent() on line 312. This will cause listeners to receive duplicate didChangeAudioRoute events.

Proposed fix - remove the duplicate emission
     @objc private func onAudioRouteChange(_ notification: Notification) {
         guard let info = notification.userInfo,
               let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
               let output = CallingxImpl.getAudioOutput() else {
             return
         }
         
         let params: [String: Any] = [
             "output": output,
             "reason": reasonValue
         ]
         
-        let dictionary: [String: Any] = [
-            "eventName": CallingxEvents.didChangeAudioRoute,
-            "params": params
-        ]
-        
-        eventEmitter?.emitEvent(dictionary)
         sendEvent(CallingxEvents.didChangeAudioRoute, body: params)
     }
🟡 Minor comments (26)
sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts-28-32 (1)

28-32: Add await for consistency and error handling.

The foreground handler should await onAndroidNotifeeEvent just like the background handler does on line 19. Since onAndroidNotifeeEvent is async, not awaiting it may result in unhandled Promise rejections if errors occur.

⚡ Proposed fix
-  notifee.onForegroundEvent((event) => {
+  notifee.onForegroundEvent(async (event) => {
     if (isNotifeeStreamVideoEvent(event)) {
-      onAndroidNotifeeEvent({ event });
+      await onAndroidNotifeeEvent({ event });
     }
   });
sample-apps/react-native/expo-video-sample/app/index.tsx-9-9 (1)

9-9: Remove unused import.

The View import is not used anywhere in this component.

🧹 Proposed fix
-  View,
packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts-171-173 (1)

171-173: Incorrect variable name in debug log.

The log message references isOutcomingCall but the actual variable is isIncomingCall. This is misleading for debugging.

🐛 Proposed fix
     logger.debug(
-      `useEffect: ${activeCallCid} isCallRegistered: ${isCallRegistered} isOutcomingCall: ${isIncomingCall} prevState: ${prevState.current}, currentState: ${callingState} isOngoingCallsEnabled: ${callingx.isOngoingCallsEnabled}`,
+      `useEffect: ${activeCallCid} isCallRegistered: ${isCallRegistered} isIncomingCall: ${isIncomingCall} prevState: ${prevState.current}, currentState: ${callingState} isOngoingCallsEnabled: ${callingx.isOngoingCallsEnabled}`,
     );
packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts-258-273 (1)

258-273: Missing error handling for updateDisplay.

Other callingx method calls (like endCallWithReason, startCall, answerIncomingCall) include .catch() error handling, but updateDisplay does not. This inconsistency could lead to silent failures.

🐛 Proposed fix
-    callingx.updateDisplay(activeCallCid, activeCallCid, callDisplayName);
+    callingx
+      .updateDisplay(activeCallCid, activeCallCid, callDisplayName)
+      .catch((error: unknown) => {
+        logger.error(
+          `Error updating display in calling exp: ${activeCallCid}`,
+          error,
+        );
+      });
packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts-275-290 (1)

275-290: Missing error handling for setMutedCall.

Similar to updateDisplay, this async call lacks error handling which is inconsistent with the pattern used elsewhere in this hook.

🐛 Proposed fix
-    callingx.setMutedCall(activeCallCid, isMute);
+    callingx.setMutedCall(activeCallCid, isMute).catch((error: unknown) => {
+      logger.error(
+        `Error setting mute state in calling exp: ${activeCallCid}`,
+        error,
+      );
+    });
packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts-90-107 (1)

90-107: Async error in onEndCall is silently lost.

The onEndCall handler is async and calls processCallFromPushInBackground, but any rejection is unhandled since the event listener doesn't await the result. Per coding guidelines, native module calls should be wrapped in try-catch.

Suggested error handling
 const onEndCall =
   (pushConfig: PushConfig) =>
   async ({ callId: call_cid, source }: EventParams['endCall']) => {
     videoLoggerSystem
       .getLogger('callingExpRejectCall')
       .debug(
         `callingExpRejectCall event callId: ${call_cid} source: ${source}`,
       );

     if (source === 'app' || !call_cid) {
       //we only need to process the call if the call was rejected from the system
       return;
     }

     clearPushWSEventSubscriptions(call_cid);

-    await processCallFromPushInBackground(pushConfig, call_cid, 'decline');
+    try {
+      await processCallFromPushInBackground(pushConfig, call_cid, 'decline');
+    } catch (e) {
+      videoLoggerSystem
+        .getLogger('callingExpRejectCall')
+        .warn(`Failed to process decline for callId: ${call_cid}`, e);
+    }
   };
packages/react-native-sdk/src/utils/push/internal/ios.ts-66-80 (1)

66-80: Native module calls lack error handling.

Per coding guidelines, native module calls should be wrapped in try-catch to handle promise rejection from native code. The calls to callingx.endCallWithReason (line 73) and voipPushNotification.onVoipNotificationCompleted (line 76) could fail.

Additionally, client.onRingingCall (line 66) is not wrapped in error handling.

Suggested error handling
-  const callFromPush = await client.onRingingCall(call_cid);
+  let callFromPush;
+  try {
+    callFromPush = await client.onRingingCall(call_cid);
+  } catch (e) {
+    logger.error(`Failed to get ringing call for call_cid: ${call_cid}`, e);
+    return;
+  }

   function closeCallIfNecessary() {
     const mustEndCall = shouldCallBeClosed(callFromPush, notification?.stream);
     if (mustEndCall) {
       logger.debug(`callkeep.reportEndCallWithUUID for call_cid: ${call_cid}`);
-      //TODO: think about sending appropriate reason for end call
-      callingx.endCallWithReason(call_cid, 'local');
-
-      const voipPushNotification = getVoipPushNotificationLib();
-      voipPushNotification.onVoipNotificationCompleted(call_cid);
+      try {
+        //TODO: think about sending appropriate reason for end call
+        callingx.endCallWithReason(call_cid, 'local');
+        const voipPushNotification = getVoipPushNotificationLib();
+        voipPushNotification.onVoipNotificationCompleted(call_cid);
+      } catch (e) {
+        logger.warn(`Failed to end call for call_cid: ${call_cid}`, e);
+      }
       return true;
     }
     return false;
   }
packages/react-native-sdk/src/utils/StreamVideoRN/index.ts-126-137 (1)

126-137: Overly broad exception handling masks setup errors and sensitive data logging.

The catch block catches any error and assumes it means the callingx library is missing. However, extractCallingExpOptions() or callingx.setup() could fail for other reasons (invalid config, runtime error), and the user would still see the misleading "library not installed" message. Additionally, logging the entire pushConfig object exposes callbacks and internal configuration that should not be logged.

Suggested fix
   static setPushConfig(pushConfig: NonNullable<StreamVideoConfig['push']>) {
     try {
       const callingx = getCallingxLib();
       videoLoggerSystem
         .getLogger('StreamVideoRN.setPushConfig')
-        .info(JSON.stringify({ pushConfig }));
+        .info('Setting push config');
       const options = extractCallingExpOptions(pushConfig);
       callingx.setup(options);
-    } catch (_) {
-      throw new Error(
-        'react-native-callingx library is not installed. Please check the installation instructions: https://getstream.io/video/docs/react-native/incoming-calls/ringing-setup/react-native/.',
-      );
+    } catch (e) {
+      videoLoggerSystem
+        .getLogger('StreamVideoRN.setPushConfig')
+        .error('Failed to setup callingx', e);
+      throw e;
     }
packages/react-native-callingx/android/build.gradle-12-12 (1)

12-12: Update Android Gradle Plugin to the latest stable version.

AGP 8.7.2 is stable but outdated. The latest stable version is 8.13. Update to benefit from bug fixes, performance improvements, and compatibility enhancements with newer Android tooling.

sample-apps/react-native/dogfood/ios/AppDelegate.swift-72-77 (1)

72-77: Remove unused created_by_display_name extraction or document why validation is required.

The created_by_display_name is extracted but immediately discarded with _. This enforces that the field must exist in the payload, but the value isn't used. If this field is no longer needed, remove it from the guard:

 guard let stream = payload.dictionaryPayload["stream"] as? [String: Any],
-      let _ = stream["created_by_display_name"] as? String,
       let cid = stream["call_cid"] as? String else {

If the presence check is intentionally required for validation purposes, add a comment explaining why.

sample-apps/react-native/dogfood/ios/AppDelegate.swift-84-90 (1)

84-90: Clarify which component invokes the VoIP completion handler.

The completion handler is passed to both RNVoipPushNotificationManager.addCompletionHandler() and StreamVideoReactNative.didReceiveIncomingPush() in AppDelegate. However, the JS-side handler (onVoipNotificationReceived) does not invoke the completion directly—only voipPushNotification.onVoipNotificationCompleted() is called in specific scenarios. Callingx (via StreamVideoReactNative) ultimately handles completion invocation through reportNewIncomingCall:...withCompletionHandler:.

The current pattern works but is unclear. The expo-config-plugin template comments suggest "the JS SDK will handle the rest and call the completionHandler," which is misleading since it doesn't. Document which component is responsible for calling the completion handler to prevent confusion and potential regressions.

packages/react-native-callingx/.nvmrc-1-1 (1)

1-1: Align Node.js version with root .nvmrc (v24).

v22.20.0 is a valid Node.js release, but it diverges from the root .nvmrc which specifies v24. Ensure consistency across the project's Node version specifications.

sample-apps/react-native/expo-video-sample/components/NavigationHeader.tsx-2-10 (1)

2-10: Remove unused Pressable import.

Pressable is imported on line 5 but is not used anywhere in the component. The component now uses TouchableOpacity instead.

Suggested fix
 import {
   Alert,
   Image,
-  Pressable,
   StyleSheet,
   Text,
   TouchableOpacity,
   View,
 } from 'react-native';
packages/react-native-callingx/README.md-380-380 (1)

380-380: Document the handleCallingIntent method.

The troubleshooting section references handleCallingIntent but this method is not documented in the API Reference section (lines 294-316). Users need to know:

  • The method signature
  • When and where to call it (onCreate and onNewIntent in MainActivity)
  • What parameters it accepts
  • Example implementation
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt-394-404 (1)

394-404: TOCTOU race: _currentCall.value read twice.

Lines 398-399 read _currentCall.value twice in a non-atomic manner. Between the is check and the cast, the value could change, causing a potential ClassCastException or incorrect callId.

🐛 Proposed fix
-        var callId: String? = null
-        if (_currentCall.value is Call.Registered) {
-            callId = (_currentCall.value as Call.Registered).id
-        }
+        val callId = (_currentCall.value as? Call.Registered)?.id
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt-11-21 (1)

11-21: Call setIntent(intent) in onNewIntent, or remove reliance on Activity.intent.

Right now you pass intent directly, so it works, but calling setIntent(intent) is the standard contract and avoids surprises if future code reads this.intent.

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/CallingxModule.kt-367-400 (1)

367-400: Potential bug: paramsMap is created but not used in the emit call.

At line 383, params is emitted instead of the newly created paramsMap. The paramsMap variable is built by iterating over params but is only used in the map passed to emitOnNewEvent. This appears inconsistent.

If the intent was to emit a normalized map, consider:

Suggested fix
             reactApplicationContext
                     .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
-                    .emit(eventName, params)
+                    .emit(eventName, paramsMap)
packages/react-native-callingx/src/CallingxModule.ts-144-146 (1)

144-146: Fix void return type mismatch.

The function has a void return type but includes a return statement. Per the static analysis hint, this should be fixed.

Suggested fix
   clearInitialEvents(): void {
-    return NativeCallingModule.clearInitialEvents();
+    NativeCallingModule.clearInitialEvents();
   }
packages/react-native-callingx/ios/UUIDStorage.swift-15-33 (1)

15-33: Potential data corruption: fallback to new UUID on parse failure.

At line 21, if UUID(uuidString: existingUUID) fails (returns nil), a new random UUID() is returned. This creates a mismatch between the stored string and the returned UUID, potentially corrupting the bidirectional mapping.

Since the stored value was created from a valid UUID (line 26), parse failure should be impossible unless storage is corrupted. Consider asserting or logging an error instead.

Suggested fix
             if let existingUUID = uuidDict[cid] {
                 #if DEBUG
                 print("[UUIDStorage] getUUIDForCid: found existing UUID \(existingUUID) for cid \(cid)")
                 #endif
-                return UUID(uuidString: existingUUID) ?? UUID()
+                guard let uuid = UUID(uuidString: existingUUID) else {
+                    assertionFailure("[UUIDStorage] Failed to parse stored UUID: \(existingUUID)")
+                    // Fallback: recreate and update storage
+                    let newUUID = UUID()
+                    let uuidString = newUUID.uuidString.lowercased()
+                    uuidDict[cid] = uuidString
+                    cidDict[uuidString] = cid
+                    return newUUID
+                }
+                return uuid
             }
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/HeadlessTaskManager.kt-44-53 (1)

44-53: Clear activeTaskId on stop to avoid “stuck active” if finish callback doesn’t fire.

stopHeadlessTask() finishes the task (Line 44-53, 78-83) but doesn’t clear activeTaskId unless onHeadlessJsTaskFinish runs (Line 102-105). Clearing it when you explicitly stop is safer.

Also applies to: 78-87, 102-105

packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt-84-104 (1)

84-104: Use a requestCode that’s unique per (action, callId), not just action hashCode.

action.hashCode() (Line 100) can collide and will also intentionally reuse the same PendingIntent across different calls (if that ever happens), leading to stale extras even with FLAG_UPDATE_CURRENT.

Safer requestCode
 return PendingIntent.getBroadcast(
   context,
-  action.hashCode(),
+  (31 * action.hashCode()) + callId.hashCode(),
   intent,
   PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
 )
packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt-46-84 (1)

46-84: Full-screen intent is set for all call states; usually should be incoming-only.

setFullScreenIntent(contentIntent, true) (Line 62) on an ongoing call notification can cause unexpected UI popping depending on OEM behavior. Consider limiting it to “incoming + not active”.

Suggested adjustment
 val builder =
   NotificationCompat.Builder(context, channelId)
     .setContentIntent(contentIntent)
-    .setFullScreenIntent(contentIntent, true)
     .setStyle(callStyle)
@@
+if (call.isIncoming() && !call.isActive) {
+  builder.setFullScreenIntent(contentIntent, true)
+}

Committable suggestion skipped: line range outside the PR's diff.

packages/react-native-callingx/package.json-58-71 (1)

58-71: Narrow peerDependencies to match minimum supported React Native version.

peerDependencies with "*" (lines 68-71) allows any React Native version, including versions before 0.73.0. However, the README specifies "React Native 0.73+" as the minimum requirement for New Architecture/Turbo Modules support. Additionally, the Android code uses com.facebook.react.ReactHost, which was introduced in React Native 0.76+. Update peerDependencies to enforce the minimum supported version: "react-native": ">=0.76.0".

packages/react-native-callingx/src/spec/NativeCallingx.ts-36-45 (1)

36-45: Inconsistent event params between getInitialEvents and onNewEvent.

getInitialEvents returns params with a source field (Line 43), but onNewEvent params definition (Lines 104-109) doesn't include source. This inconsistency could cause runtime issues when processing initial events vs. live events.

Proposed fix - add `source` to onNewEvent params
   readonly onNewEvent: EventEmitter<{
     eventName: string;
     params: {
       callId: string;
       cause?: string;
       muted?: boolean;
       hold?: boolean;
+      source?: string;
     };
   }>;

Also applies to: 102-110

packages/react-native-callingx/src/spec/NativeCallingx.ts-62-63 (1)

62-63: Fix typo in comment.

"withing" should be "within".

Proposed fix
-  //use when need to answer an incoming call withing app UI
+  //use when need to answer an incoming call within app UI
packages/react-native-callingx/src/types.ts-10-18 (1)

10-18: Malformed JSDoc comment syntax.

There's a duplicate /** opening on lines 10-11 which creates invalid JSDoc. The inner /** should just be *.

Proposed fix
   /**
-   /**
-    * Setup the module. This method must be called before any other method.
-    * For iOS, the module will setup CallKit parameters.
-    *    See: {@link InternalIOSOptions}
-    * For Android, the module will create notification channels.
-    *    See: {@link InternalAndroidOptions}
-    * @param options - The options to setup the callingx module. See {@link CallingExpOptions}
-    */
+   * Setup the module. This method must be called before any other method.
+   * For iOS, the module will setup CallKit parameters.
+   *    See: {@link InternalIOSOptions}
+   * For Android, the module will create notification channels.
+   *    See: {@link InternalAndroidOptions}
+   * @param options - The options to setup the callingx module. See {@link CallingExpOptions}
+   */

Comment on lines +59 to +67
## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fill in the enforcement contact method before release.

Line 63 contains a placeholder [INSERT CONTACT METHOD] that must be replaced with an actual contact mechanism (e.g., email address, GitHub issue form link, or support channel) for the Code of Conduct enforcement process to be functional.

📋 Replace the placeholder with the intended contact method
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
 reported to the community leaders responsible for enforcement at
-[INSERT CONTACT METHOD].
+[email protected]  <!-- or appropriate contact method -->
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected] <!-- or appropriate contact method -->
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
🤖 Prompt for AI Agents
In @packages/react-native-callingx/CODE_OF_CONDUCT.md around lines 59 - 67,
Replace the placeholder "[INSERT CONTACT METHOD]" under the "Enforcement"
section with the real enforcement contact (for example a dedicated email like
[email protected] or a link to a GitHub issue template or support channel) so
the Code of Conduct has an actionable reporting mechanism; ensure the inserted
contact appears in the same sentence after "reported to the community leaders
responsible for enforcement at" and keep the rest of the paragraph unchanged.

greenfrvr and others added 12 commits January 9, 2026 15:40
… notifee (#2082)

### 💡 Overview

Previously we used Notifee for the foreground service to keep the call
alive when app goes to background. Now this foreground service is added
to our SDK.

<img width="640" height="400" alt="image"
src="https://github.com/user-attachments/assets/83750dd9-0602-47d5-835e-952d6d64277d"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added an SDK-managed keep-alive service for Android calls, enabling
better background call persistence with system notifications.
* Added support for Android 13+ notification permissions, ensuring
proper permission requests before displaying call notifications.

* **Improvements**
* Enhanced Android notification channel configuration for better call
state management.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Artsiom Grintsevich <[email protected]>
Co-authored-by: GitHub Actions Bot <>
Co-authored-by: Oliver Lazoroski <[email protected]>
Co-authored-by: jdimovska <[email protected]>
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.

4 participants