Skip to content

feat: Receive voip push notifications from backend#7045

Open
diegolmello wants to merge 5 commits intofeat.voip-lib-newfrom
feat.voip-push
Open

feat: Receive voip push notifications from backend#7045
diegolmello wants to merge 5 commits intofeat.voip-lib-newfrom
feat.voip-push

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Mar 9, 2026

Proposed changes

Issue(s)

https://rocketchat.atlassian.net/browse/VMUX-7

How to test or reproduce

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • New Features

    • VoIP token persistence and a new VoIP token accessor to improve push reliability.
    • Incoming call timeout handling (missed-call detection) and improved avatar/caller data in call notifications.
  • Bug Fixes

    • Unified call identifier handling for more reliable call routing and media session behavior.
    • Always-attempt push token registration with better retry/logging to improve notification delivery.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

Walkthrough

The PR removes the CallIdUUID native modules, migrates VoIP identifier usage from callUUID to callId across the codebase, adds persistent VoIP push-token handling (including getLastVoipToken and persistence), includes voipToken in push.token registration, and adds VoipPayload parsing and timeout handling for incoming calls.

Changes

Cohort / File(s) Summary
CallIdUUID Removal (Android)
android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDModule.kt, android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDTurboPackage.kt, android/app/src/main/java/chat/rocket/reactnative/utils/NativeCallIdUUIDSpec.kt, android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
Deleted the CallIdUUID TurboModule, spec, package, and its registration in MainApplication.
CallIdUUID Removal (iOS)
ios/Libraries/CallIdUUID.m, ios/Libraries/CallIdUUID.swift, ios/RocketChatRN.xcodeproj/project.pbxproj
Removed the Obj-C bridge and Swift UUID v5 generator and updated Xcode project references.
VoIP Push Token Persistence & API
ios/Libraries/VoipService.swift, ios/Libraries/VoipModule.mm, app/lib/services/restApi.ts, app/definitions/rest/v1/push.ts, app/lib/services/restApi.test.ts, app/lib/notifications/push.ts
Added persistent VoIP token storage and invalidate API on iOS, exposed getLastVoipToken to JS, added voipToken to push registration payloads, caching to avoid duplicates, and tests for registration behavior.
VoipPayload Model & Parsing
ios/Libraries/VoipPayload.swift, android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
Introduced a structured VoipPayload model on iOS and extended Android payload with avatarUrl/createdAt, parsing, lifetime/expiry utilities, and serialization.
FCM/Notification Handling (Android)
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java, android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt, android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt
Simplified avatar URI construction (fixed size), deferred bundle creation for non-VoIP FCM flows, improved logging, and added VoIP timeout cancellation in intent handling.
VoIP Timeout & Telecom Flow (Android)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt, android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt
Added global timeout scheduling/cancellation and timeout broadcast handling; switched to callId (not callUUID) in Telecom registration and notification lifecycle.
VoIP Module & Events
android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt, app/lib/services/voip/MediaCallEvents.ts, app/lib/services/voip/MediaSessionInstance.ts, app/lib/services/voip/MediaCallEvents.ts, app/lib/services/voip/useCallStore.ts
Replaced callUUID with callId across native and JS flows, removed deterministic UUID generation, adjusted store/actions signatures, updated event payload shapes, and replaced push-token helper usage with registerPushToken calls.
Native Voip Wrapper (JS)
app/lib/native/NativeVoip.ts, app/lib/native/NativeCallIdUUID.ts
Added getLastVoipToken to Voip spec, provided a guarded NativeVoipModule fallback, and removed the NativeCallIdUUID TS wrapper.
Removed Simulated Call & Push Token Aux
app/lib/services/voip/simulateCall.ts, app/lib/services/voip/pushTokenAux.ts
Removed the simulateCall utility and deleted module-level voipPushToken getters/setters.
Call Identifier Migration in UI/Tests
multiple story/test files and types: app/actions/deepLinking.ts, app/definitions/Voip.ts, app/stacks/types.ts, app/containers/..., app/views/CallView/...
Updated types, mock stores, stories, tests, and route param types to use callId instead of callUUID and adjusted related fixtures.
iOS PKPushRegistry Integration
ios/Libraries/AppDelegate+Voip.swift, ios/AppDelegate.swift
Moved PKPushRegistryDelegate handling into a new AppDelegate extension: credential updates, invalidation, incoming push parsing via VoipPayload, RNCallKeep reporting, and initial incoming-call preparation.
Xcode Project Updates
ios/RocketChatRN.xcodeproj/project.pbxproj
Added Voip files to project, removed CallIdUUID entries, and normalized some build script/flags.

Sequence Diagram(s)

sequenceDiagram
    participant App as App/React
    participant Native as Native (iOS/Android)
    participant Notif as VoIP Notification Handler
    participant Storage as Persistent Storage
    participant Backend as Backend API

    App->>Native: request getLastVoipToken()
    Native->>Storage: load persisted voip token
    Storage-->>Native: cached voipToken (or empty)
    Native-->>App: voipToken

    Native->>Native: Push service updates credentials
    Native-->>App: emit VoipPushTokenRegistered(token)

    App->>App: registerPushToken() (getDeviceToken + getLastVoipToken)
    App->>Backend: POST /push.token {value,type,appName,voipToken}
    Backend-->>App: success
    App->>Storage: cache lastToken / lastVoipToken
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: enabling the app to receive VoIP push notifications from the backend, which is the primary focus of this changeset.
Linked Issues check ✅ Passed The PR successfully implements all requirements from VMUX-7: Android reuses GCM token, iOS obtains separate VoIP token, and both platforms send tokens to backend for VoIP push delivery.
Out of Scope Changes check ✅ Passed All changes are directly related to receiving VoIP push notifications. The callUUID→callId refactoring and CallIdUUID module removal support the VoIP notification flow, not unrelated improvements.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link
Contributor

@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: 9

Caution

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

⚠️ Outside diff range comments (1)
app/lib/services/voip/MediaSessionInstance.ts (1)

18-18: ⚠️ Potential issue | 🔴 Critical

Remove unused import to fix pipeline failure.

CallIdUUIDModule is imported but never used, causing the pipeline to fail with 'CallIdUUIDModule' is defined but never used. (no-unused-vars).

🐛 Proposed fix
-import CallIdUUIDModule from '../../native/NativeCallIdUUID';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/voip/MediaSessionInstance.ts` at line 18, Remove the unused
import CallIdUUIDModule from MediaSessionInstance.ts: locate the import
statement "import CallIdUUIDModule from '../../native/NativeCallIdUUID';" and
delete it so the unused variable error ('CallIdUUIDModule' is defined but never
used) is resolved; ensure no other references to CallIdUUIDModule remain in the
file (e.g., within MediaSessionInstance class or its methods) before committing.
🧹 Nitpick comments (4)
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (1)

60-76: Minor: Log message formatting when errorContext is empty.

When errorContext is an empty string (as called from getAvatarUri on line 105), the log message on line 63 produces "Cannot generate avatar URI" with a double space.

Consider adjusting the log format to handle the empty context case:

💡 Suggested improvement
     private String buildAvatarUri(String avatarPath, String errorContext) {
         String server = serverURL();
         if (server == null || server.isEmpty()) {
-            Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
+            String contextPrefix = (errorContext == null || errorContext.isEmpty()) ? "" : errorContext + " ";
+            Log.w(TAG, "Cannot generate " + contextPrefix + "avatar URI: serverURL is null");
             return null;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java`
around lines 60 - 76, The log message in buildAvatarUri produces a double space
when errorContext is empty; update buildAvatarUri (used by getAvatarUri) to
format the message safely by either using a conditional prefix/suffix (e.g., if
errorContext is empty log "Cannot generate avatar URI" else "Cannot generate
{errorContext} avatar URI") or trimming/adding the space only when errorContext
is non-empty so the Log.w(TAG, ...) never contains an extra space.
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1)

58-69: Remove unused parseEjson function.

This private function is not called anywhere in the codebase and can be safely deleted to reduce dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt`
around lines 58 - 69, The private function parseEjson in
RCFirebaseMessagingService is dead code and should be removed; delete the entire
parseEjson(ejsonStr: String?): Ejson? function (including its null/empty check,
try/catch, and imports used only by it) so the class no longer contains this
unused symbol and to avoid leaving unused gson/logging references.
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt (1)

141-147: Redundant null check on non-nullable String.

callId is declared as String (non-nullable), so callId.isNullOrEmpty() will never be null. Use callId.isEmpty() instead for clarity. Similarly, caller.isNullOrEmpty() should be caller.isEmpty().

♻️ Proposed fix
     private fun registerCallWithTelecomManager(callId: String, caller: String) {
         try {
             // Validate inputs
-            if (callId.isNullOrEmpty() || caller.isNullOrEmpty()) {
+            if (callId.isEmpty() || caller.isEmpty()) {
                 Log.e(TAG, "Cannot register call with TelecomManager: callId is null or empty")
                 return
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`
around lines 141 - 147, In registerCallWithTelecomManager, remove redundant
nullability checks on the non-nullable parameters by replacing
callId.isNullOrEmpty() with callId.isEmpty() and caller.isNullOrEmpty() with
caller.isEmpty(); update the conditional and the associated Log.e message to
reflect empty-only checks so the method logic and intent match the String
parameter types (references: registerCallWithTelecomManager, callId, caller).
ios/RocketChatRN.xcodeproj/project.pbxproj (1)

1146-1147: Consider colocating these Swift files with the app code they extend.

AppDelegate+Voip.swift and VoipPayload.swift read more like app-lifecycle/model code than native bridge code, so keeping them under Libraries makes ownership harder to scan.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/RocketChatRN.xcodeproj/project.pbxproj` around lines 1146 - 1147, The two
Swift files AppDelegate+Voip.swift and VoipPayload.swift should be moved out of
the Libraries group and colocated with the main app source files: move the
physical files into the app source folder, remove their entries from the
Libraries group in the Xcode project, and add them to the app's source group so
they live alongside other app-lifecycle/model code; while doing this keep their
target membership for the main app target intact and update the project.pbxproj
file references (file reference and group entries) so imports and build settings
continue to resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt`:
- Line 26: Replace the unsafe debug log that prints the entire FCM payload in
RCFirebaseMessagingService (the Log.d call referencing TAG and
remoteMessage.data) with a redacted or minimal-safe log: log only non-sensitive
fields such as remoteMessage.from and payload size or a whitelist of allowed
keys, or mask values before logging; do not include remoteMessage.data contents
verbatim. Update the Log.d usage to reference the sanitized information and
ensure any helper you add for redaction is used consistently where
remoteMessage.data was previously logged.

In `@app/lib/native/NativeVoip.ts`:
- Around line 46-57: The current fallback stubs around TurboModuleRegistry.get
for VoipModule (NativeVoipModule) silently hide native registration failures;
replace the use of TurboModuleRegistry.get with
TurboModuleRegistry.getEnforcing<Spec>('VoipModule') and remove the no-op
fallback object so the module throws if the native bridge isn't registered;
update any references to NativeVoipModule (and its methods like
registerVoipToken, getInitialEvents, clearInitialEvents, getLastVoipToken,
addListener, removeListeners) to assume a present native implementation or
handle the thrown error at a higher level rather than continuing with silent
stubs.

In `@app/lib/services/restApi.ts`:
- Around line 1005-1006: The dedupe cache variables lastToken and lastVoipToken
are not cleared when tokens are unregistered, allowing a subsequent register
call with the same tokens to be incorrectly skipped; update the
unregister/remove path(s) that perform push token removal (e.g.,
removePushToken() and the // TODO: add voip token removal branch) to reset
lastToken = '' and lastVoipToken = '' immediately after a successful backend
removal (and also on any error branch where the local state is cleared) so
future registrations are not deduplicated incorrectly.
- Around line 1048-1055: The current success log prints the full push payload
(data) which leaks push and VoIP tokens; remove sensitive token values from
logs. In the block around sdk.post('push.token', data) replace
console.log('registerPushToken success', data) with a non-sensitive message like
console.log('registerPushToken success') or log a masked result, and ensure you
do not log token, voipToken, data, lastToken or lastVoipToken values anywhere;
keep assignment to lastToken/lastVoipToken but never emit them to logs or error
reports.
- Around line 1024-1046: The guard currently returns early on iOS when voipToken
is missing and can still post an empty Android payload; change the early-return
to only skip when neither token nor voipToken exist (if (isIOS && !token &&
!voipToken) return), build the data object only when at least one token exists
(set type/appName/getUniqueId/getBundleId() only when token is truthy), add
voipToken to data only when present, and ensure you never call
sdk.post('push.token', data) with the empty initializer—only post when
data.value or data.voipToken is set.
- Around line 1015-1058: The registerPushToken function currently wraps an async
executor in new Promise which can leave the outer promise unresolved on errors;
change export const registerPushToken = (): Promise<void> => new Promise(async
resolve => { ... }) to an async function export const registerPushToken = async
(): Promise<void> { ... }, remove the Promise constructor and all resolve()
calls, keep the existing try/catch around sdk.post('push.token', data) to log
errors, and ensure awaits (e.g., await getUniqueId()) and uses like
NativeVoipModule.getLastVoipToken() and getBundleId are preserved inside the new
async function body so the returned promise correctly rejects on uncaught
errors.

In `@ios/Libraries/AppDelegate`+Voip.swift:
- Line 29: The iOS AppDelegate+Voip.swift currently lowercases callId (let
callId = voipPayload.callId.lowercased()), causing inconsistent cross-platform
behavior; remove the lowercasing in AppDelegate+Voip.swift so iOS preserves the
original callId, and instead normalize call IDs in the shared/logic layers and
platform implementations for consistency: update VoipNotification.kt to apply
the same normalization (e.g., toLowerCase()) or, preferably, update
MediaCallEvents.ts (where toLowerCase() matching occurs) and other consumers
like videoConf.ts to perform case-insensitive comparisons (normalize both sides)
so all platforms treat callId consistently across call handling functions and
event matching.

In `@ios/Libraries/VoipPayload.swift`:
- Around line 78-80: The notificationId getter uses callId.hashValue which is
unstable across launches; replace it with a deterministic mapping from callId to
an Int (for example compute a stable hash like CRC32 or SHA256 and truncate to a
signed 32-bit Int, or if callId is a UUID parse its numeric components) and
return that value from the `@objc` var notificationId so notifications can be
consistently referenced across app runs; update the notificationId computed
property (and any code that assumes its type/size) to use the stable
hashing/parsing routine instead of hashValue.

In `@ios/Libraries/VoipService.swift`:
- Around line 92-102: invalidatePushToken() currently only clears local state
(lastVoipToken and storage.removeValue(forKey: voipTokenStorageKey)), leaving
server-side VoIP registrations intact; update this method to also fan out an
unregister call to the backend for every logged-in workspace so the token is
removed server-side. Locate invalidatePushToken() and after clearing local state
invoke your network/unregister API for each active workspace (e.g., iterate your
session/workspace manager’s logged-in workspaces and call the existing
unregister endpoint or a new helper like unregisterVoipToken(workspaceId:)),
handle/report errors but ensure best-effort removal for all workspaces before
returning.

---

Outside diff comments:
In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Line 18: Remove the unused import CallIdUUIDModule from
MediaSessionInstance.ts: locate the import statement "import CallIdUUIDModule
from '../../native/NativeCallIdUUID';" and delete it so the unused variable
error ('CallIdUUIDModule' is defined but never used) is resolved; ensure no
other references to CallIdUUIDModule remain in the file (e.g., within
MediaSessionInstance class or its methods) before committing.

---

Nitpick comments:
In `@android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java`:
- Around line 60-76: The log message in buildAvatarUri produces a double space
when errorContext is empty; update buildAvatarUri (used by getAvatarUri) to
format the message safely by either using a conditional prefix/suffix (e.g., if
errorContext is empty log "Cannot generate avatar URI" else "Cannot generate
{errorContext} avatar URI") or trimming/adding the space only when errorContext
is non-empty so the Log.w(TAG, ...) never contains an extra space.

In
`@android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt`:
- Around line 58-69: The private function parseEjson in
RCFirebaseMessagingService is dead code and should be removed; delete the entire
parseEjson(ejsonStr: String?): Ejson? function (including its null/empty check,
try/catch, and imports used only by it) so the class no longer contains this
unused symbol and to avoid leaving unused gson/logging references.

In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`:
- Around line 141-147: In registerCallWithTelecomManager, remove redundant
nullability checks on the non-nullable parameters by replacing
callId.isNullOrEmpty() with callId.isEmpty() and caller.isNullOrEmpty() with
caller.isEmpty(); update the conditional and the associated Log.e message to
reflect empty-only checks so the method logic and intent match the String
parameter types (references: registerCallWithTelecomManager, callId, caller).

In `@ios/RocketChatRN.xcodeproj/project.pbxproj`:
- Around line 1146-1147: The two Swift files AppDelegate+Voip.swift and
VoipPayload.swift should be moved out of the Libraries group and colocated with
the main app source files: move the physical files into the app source folder,
remove their entries from the Libraries group in the Xcode project, and add them
to the app's source group so they live alongside other app-lifecycle/model code;
while doing this keep their target membership for the main app target intact and
update the project.pbxproj file references (file reference and group entries) so
imports and build settings continue to resolve.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c605122-3ba0-4d35-a62d-e0d8c9b0a193

📥 Commits

Reviewing files that changed from the base of the PR and between 0715e89 and 066afbb.

📒 Files selected for processing (40)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt
  • android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDTurboPackage.kt
  • android/app/src/main/java/chat/rocket/reactnative/utils/NativeCallIdUUIDSpec.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • app/actions/deepLinking.ts
  • app/containers/MediaCallHeader/MediaCallHeader.stories.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.test.tsx
  • app/definitions/Voip.ts
  • app/definitions/rest/v1/push.ts
  • app/lib/native/NativeCallIdUUID.ts
  • app/lib/native/NativeVoip.ts
  • app/lib/notifications/push.ts
  • app/lib/services/restApi.test.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/pushTokenAux.ts
  • app/lib/services/voip/simulateCall.ts
  • app/lib/services/voip/useCallStore.ts
  • app/stacks/types.ts
  • app/views/CallView/CallView.stories.tsx
  • app/views/CallView/components/CallerInfo.stories.tsx
  • app/views/CallView/components/CallerInfo.test.tsx
  • app/views/CallView/components/Dialpad/Dialpad.stories.tsx
  • app/views/CallView/components/Dialpad/Dialpad.test.tsx
  • app/views/CallView/index.test.tsx
  • ios/AppDelegate.swift
  • ios/Libraries/AppDelegate+Voip.swift
  • ios/Libraries/CallIdUUID.m
  • ios/Libraries/CallIdUUID.swift
  • ios/Libraries/VoipModule.mm
  • ios/Libraries/VoipPayload.swift
  • ios/Libraries/VoipService.swift
  • ios/RocketChatRN.xcodeproj/project.pbxproj
💤 Files with no reviewable changes (13)
  • ios/Libraries/CallIdUUID.m
  • app/containers/MediaCallHeader/MediaCallHeader.test.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.stories.tsx
  • ios/AppDelegate.swift
  • android/app/src/main/java/chat/rocket/reactnative/utils/NativeCallIdUUIDSpec.kt
  • app/actions/deepLinking.ts
  • app/lib/native/NativeCallIdUUID.ts
  • app/lib/services/voip/pushTokenAux.ts
  • android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/utils/CallIdUUIDTurboPackage.kt
  • app/lib/services/voip/simulateCall.ts
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • ios/Libraries/CallIdUUID.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-05T06:06:12.277Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RCTWatchModule.mm:19-24
Timestamp: 2026-03-05T06:06:12.277Z
Learning: Do not re-activate or reset the WCSession singleton in iOS Objective-C/Swift bridge modules. Ensure WCSession is activated and its delegate is set in a single, central place (e.g., ios/RocketChat Watch App/Loaders/WatchSession.swift) and avoid duplicating activation or delegate assignment in other iOS bridge files like ios/RCTWatchModule.mm. If WCSession is already activated via the central loader, relying on WCSession.defaultSession is sufficient and maintains a single session lifecycle.

Applied to files:

  • ios/Libraries/VoipModule.mm
🪛 Biome (2.4.6)
app/lib/services/restApi.ts

[error] 1016-1058: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🪛 GitHub Actions: Format Code with Prettier
app/lib/services/voip/MediaSessionInstance.ts

[error] 18-18: 'CallIdUUIDModule' is defined but never used. (no-unused-vars)

🔇 Additional comments (29)
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (2)

105-105: LGTM!

The call is correctly updated to match the new buildAvatarUri signature.


108-125: LGTM!

The method is correctly simplified to use the refactored buildAvatarUri. The "caller" error context provides meaningful log messages, and input validation remains intact.

android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1)

43-47: LGTM!

Good refactor to defer bundle creation until after confirming it's not a VoIP payload. This avoids unnecessary allocation for VoIP notifications and keeps the code cleaner.

app/lib/notifications/push.ts (1)

185-201: The iOS first-run re-registration path is correctly implemented.

VoipPushTokenRegistered does trigger backend re-registration. In app/lib/services/voip/MediaCallEvents.ts, the listener for this event (line 27) calls registerPushToken() (line 29), ensuring the backend receives both the APNs token and the VoIP token even when they arrive at different times on a fresh install.

app/views/CallView/components/CallerInfo.stories.tsx (1)

18-23: Story fixture is aligned with the renamed call identifier.

Using callId here keeps the Storybook state in sync with the updated VoIP store shape.

app/views/CallView/components/CallerInfo.test.tsx (1)

14-24: Test setup now matches the store contract.

This keeps the fixture consistent with the callId migration and avoids stale test state shape.

app/stacks/types.ts (1)

299-299: CallView's route contract is simplified cleanly.

Making the screen parameterless is consistent with the move to store-owned call state.

app/views/CallView/index.test.tsx (1)

60-76: Shared test state is aligned with the callId rename.

Good catch updating the common helper, since it keeps the whole CallView suite consistent.

app/views/CallView/components/Dialpad/Dialpad.stories.tsx (1)

18-34: Dialpad stories now seed the current store shape.

Using callId here keeps the story helper consistent with the rest of the CallView fixtures.

android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt (1)

77-77: Payload handling is consistent with the new VoIP contract.

The callId logging updates and direct avatarUrl usage keep this activity aligned with the renamed payload fields.

Also applies to: 157-157, 232-246

app/views/CallView/CallView.stories.tsx (1)

22-56: CallView stories are using the current store field name.

Updating the shared story helper here keeps all variants aligned with the callId migration.

app/lib/services/restApi.test.ts (2)

41-87: LGTM!

The test suite covers the key scenarios for registerPushToken:

  • Waiting for both iOS tokens before registration
  • Deduplication of successful registrations
  • Retry behavior after failures

The mock setup and assertions are well-structured.


24-28: No issue found. The mock correctly defines getBundleId as a string value, matching the actual implementation in app/lib/methods/helpers/deviceInfo.ts, where it's exported as a const assigned to the result of DeviceInfo.getBundleId() (a string, not a function).

			> Likely an incorrect or invalid review comment.
android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt (2)

27-29: LGTM!

The avatarUrl property is correctly added as nullable with proper @SerializedName annotation, and is consistently propagated through toBundle() and toWritableMap().


66-145: LGTM!

The Gson-based remote payload parsing is well-structured:

  • Private helper classes (RemoteCaller, RemoteVoipPayload) keep internal details encapsulated
  • Null-safe parsing with appropriate fallbacks
  • Exception handling returns null gracefully instead of crashing
ios/Libraries/AppDelegate+Voip.swift (1)

35-49: LGTM!

The PKPushRegistryDelegate implementation correctly:

  • Reports incoming calls to CallKeep with appropriate parameters
  • Stores initial events for later processing
  • Calls the PushKit completion handler after processing
app/lib/services/voip/MediaSessionInstance.ts (3)

64-87: LGTM!

The newCall handler is correctly updated to use callId semantics:

  • Checks for existing callId instead of callUUID
  • Uses setCall(call) without a separate UUID parameter
  • RNCallKeep.endCall correctly uses call.callId

90-106: LGTM!

The answerCall method correctly:

  • Uses callId parameter for comparison with mainCall.callId
  • Sets the call active via RNCallKeep.setCurrentCallActive(callId)
  • Falls back to ending the call if not found

120-135: LGTM!

The endCall method correctly:

  • Compares mainCall.callId with the parameter
  • Handles both ringing (reject) and active (hangup) states
  • Cleans up RNCallKeep state
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt (1)

62-64: LGTM!

The callUUID to callId migration is consistent across:

  • Log messages
  • Method parameters
  • TelecomManager extras bundle

Also applies to: 119-131, 168-181

app/lib/services/voip/useCallStore.ts (3)

12-12: LGTM!

The state and action signatures are correctly updated:

  • callId: string | null replaces callUUID
  • setCallId and setCall signatures are simplified

Also applies to: 30-31, 45-45


61-69: LGTM!

setCall now correctly derives callId from call.callId rather than requiring a separate parameter. This eliminates potential inconsistencies between the stored callId and the actual call's identifier.


142-154: LGTM!

Both toggleSpeaker and endCall correctly use callId from the store state, with appropriate null checks before calling RNCallKeep methods.

Also applies to: 175-188

ios/Libraries/VoipPayload.swift (2)

3-65: LGTM!

The private helper structs (RemoteCaller, RemoteVoipPayload) are well-designed with clean null-safe parsing and conversion logic. The toVoipPayload() method correctly validates required fields before constructing the payload.


67-135: LGTM!

The public VoipPayload class:

  • Has correct ObjC interop annotations
  • Handles both direct dictionary and nested ejson payload formats
  • Uses NSNull() appropriately for nil values in dictionaries for ObjC compatibility
app/lib/services/voip/MediaCallEvents.ts (3)

27-32: LGTM!

The token registration flow is correctly updated to call registerPushToken() with proper error handling. The callback signature change to destructured { token } aligns with the native event emission pattern.


36-48: Note: callUUID from RNCallKeep events is expected.

The answerCall and endCall events from RNCallKeep provide callUUID as part of their API contract. This is passed directly to mediaSessionInstance.answerCall(callUUID) and endCall(callUUID), which is correct since the native iOS code lowercases the callId before passing it to CallKeep.


52-71: LGTM!

The callId migration is consistently applied:

  • setCallId(data.callId) for Android events
  • setCallId(initialEvents.callId) for initial events
  • Proper case-insensitive comparison between initialEvents.callId and CallKeep's callUUID

Also applies to: 104-127

ios/RocketChatRN.xcodeproj/project.pbxproj (1)

2062-2096: Configuration confirmed: VoIP background delivery is properly enabled.

The shared app plist already contains UIBackgroundModes with 'voip' (alongside 'audio' and 'fetch'). The new VoIP entry points wired into both app targets will correctly deliver VoIP pushes on device.


override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "FCM message received from: ${remoteMessage.from}")
Log.d(TAG, "FCM message received from: ${remoteMessage.from} data: ${remoteMessage.data}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging full FCM payload data.

Logging remoteMessage.data exposes potentially sensitive information (user IDs, caller details, message metadata) to device logs and crash reporting tools. Even at DEBUG level, this data can leak in production environments if ProGuard/R8 doesn't strip debug logs.

Consider logging only non-sensitive fields or redacting the payload:

🛡️ Proposed fix to avoid logging sensitive data
-        Log.d(TAG, "FCM message received from: ${remoteMessage.from} data: ${remoteMessage.data}")
+        Log.d(TAG, "FCM message received from: ${remoteMessage.from}, keys: ${remoteMessage.data.keys}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt`
at line 26, Replace the unsafe debug log that prints the entire FCM payload in
RCFirebaseMessagingService (the Log.d call referencing TAG and
remoteMessage.data) with a redacted or minimal-safe log: log only non-sensitive
fields such as remoteMessage.from and payload size or a whitelist of allowed
keys, or mask values before logging; do not include remoteMessage.data contents
verbatim. Update the Log.d usage to reference the sanitized information and
ensure any helper you add for redaction is used consistently where
remoteMessage.data was previously logged.

Comment on lines +46 to +57
const NativeVoipModule =
TurboModuleRegistry.get<Spec>('VoipModule') ??
({
registerVoipToken: () => undefined,
getInitialEvents: () => null,
clearInitialEvents: () => undefined,
getLastVoipToken: () => '',
addListener: () => undefined,
removeListeners: () => undefined
} as Spec);

export default NativeVoipModule;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file and see the actual code
cat -n app/lib/native/NativeVoip.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1899


🏁 Script executed:

# Check for git history of this file to understand the change
git log --oneline -n 20 app/lib/native/NativeVoip.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 111


🏁 Script executed:

# Search for Spec type definition
rg "interface Spec|type Spec" app/lib/native/NativeVoip.ts -A 10

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 401


🏁 Script executed:

# Check git diff to see the actual changes made
git diff HEAD~5..HEAD app/lib/native/NativeVoip.ts 2>/dev/null || echo "No recent changes in diff"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 103


🏁 Script executed:

# Search for any mention of getEnforcing in the file history
git log -p app/lib/native/NativeVoip.ts | grep -C 5 "getEnforcing" | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Check how TurboModuleRegistry is used elsewhere in codebase
rg "TurboModuleRegistry\.(get|getEnforcing)" --type ts --type tsx -B 2 -A 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 105


🏁 Script executed:

# Fix the rg command - use ts without tsx
rg "TurboModuleRegistry\.(get|getEnforcing)" --type ts -B 2 -A 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 955


🏁 Script executed:

# Also search broadly across all files
rg "TurboModuleRegistry" -B 1 -A 3

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2392


🏁 Script executed:

# Check git history more extensively for NativeVoip.ts
git log --all --oneline app/lib/native/NativeVoip.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 111


🏁 Script executed:

# Check if this is a PR or current branch state - look at the full git log
git log --all --oneline -n 50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3323


🏁 Script executed:

# Check current branch status
git status app/lib/native/NativeVoip.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 143


🏁 Script executed:

# Search for any other files that use the pattern of get() with fallback stub
rg "TurboModuleRegistry\.get" -A 3 -B 1

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 849


🏁 Script executed:

# Let's understand React Native TurboModuleRegistry semantics
# Search for any documentation or comments about get vs getEnforcing
rg "getEnforcing|get\(" app/lib/native/ -B 2 -A 1 | head -80

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 275


🏁 Script executed:

# Check if there are any usages of NativeVoipModule that might rely on the stub behavior
rg "NativeVoipModule\." --type ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 581


🏁 Script executed:

# Check the Spec interface to confirm all methods are properly stubbed
cat -n app/lib/native/NativeVoip.ts | sed -n '4,44p'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1354


🏁 Script executed:

# Look for any configuration or documentation about why get() with fallback is used
rg "VoipModule" -B 3 -A 3

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 20782


Avoid silently stubbing a critical native module.

Using get() with a no-op fallback masks integration failures. If VoipModule fails to register due to a packaging or linking error, the app will boot successfully with empty stubs: iOS never requests a PushKit token, initial call events disappear, but JS proceeds as if VoIP support exists. For this bridge, failing fast during builds is safer than silently degrading functionality. Consider getEnforcing() instead to surface native registration failures immediately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/native/NativeVoip.ts` around lines 46 - 57, The current fallback
stubs around TurboModuleRegistry.get for VoipModule (NativeVoipModule) silently
hide native registration failures; replace the use of TurboModuleRegistry.get
with TurboModuleRegistry.getEnforcing<Spec>('VoipModule') and remove the no-op
fallback object so the module throws if the native bridge isn't registered;
update any references to NativeVoipModule (and its methods like
registerVoipToken, getInitialEvents, clearInitialEvents, getLastVoipToken,
addListener, removeListeners) to assume a present native implementation or
handle the thrown error at a higher level rather than continuing with silent
stubs.

Comment on lines +1005 to +1006
let lastToken = '';
let lastVoipToken = '';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset the dedupe cache when unregistering push tokens.

lastToken / lastVoipToken survive removePushToken(), so a later registration with the same token pair can be skipped even though the backend association was removed or changed.

🧹 Proposed fix
 export const removePushToken = (): Promise<boolean | void> => {
 	const token = getDeviceToken();
+	lastToken = '';
+	lastVoipToken = '';
 	if (token) {
 		// RC 0.60.0
 		return sdk.current.del('push.token', { token });
 	}
 	return Promise.resolve();
 };

If you want, I can also sketch the follow-up change for the // TODO: add voip token removal path.

Also applies to: 1060-1067

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/restApi.ts` around lines 1005 - 1006, The dedupe cache
variables lastToken and lastVoipToken are not cleared when tokens are
unregistered, allowing a subsequent register call with the same tokens to be
incorrectly skipped; update the unregister/remove path(s) that perform push
token removal (e.g., removePushToken() and the // TODO: add voip token removal
branch) to reset lastToken = '' and lastVoipToken = '' immediately after a
successful backend removal (and also on any error branch where the local state
is cleared) so future registrations are not deduplicated incorrectly.

Comment on lines +1015 to 1058
export const registerPushToken = (): Promise<void> =>
new Promise<void>(async resolve => {
const token = getDeviceToken();
const voipToken = isIOS ? NativeVoipModule.getLastVoipToken() : '';

if (token === lastToken && voipToken === lastVoipToken) {
return resolve();
}

// TODO: server version
if (isIOS && (!token || !voipToken)) {
return resolve();
}

let data: TRegisterPushTokenData = {
id: '',
value: '',
type: '',
appName: ''
};
if (token) {
const type = isIOS ? 'apn' : 'gcm';
const data = {
data = {
id: await getUniqueId(),
value: token,
type,
appName: getBundleId
};
try {
if (isIOS) {
const voipToken = getVoipPushToken();
if (voipToken) {
// TODO: this is temp only for VoIP push token
await sdk.post('push.token', {
type: 'gcm',
value: voipToken,
appName: getBundleId
});
}
}

// RC 0.60.0
await sdk.post('push.token', data);
} catch (error) {
console.log(error);
}
}
if (isIOS && voipToken) {
data.voipToken = voipToken;
}

try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success', data);
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
}
return resolve();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether async Promise executors exist in this codepath or elsewhere.
rg -nP --type=ts 'new\s+Promise(?:<[^>]+>)?\(\s*async\b' app/lib/services/restApi.ts
rg -nP --type=ts 'new\s+Promise(?:<[^>]+>)?\(\s*async\b' app

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1934


Convert to async function instead of wrapping in Promise constructor.

Using an async Promise executor leaves the outer promise pending if any error occurs before resolve() is called, decoupling the error from the returned promise and creating unhandled rejections.

♻️ Proposed fix
-export const registerPushToken = (): Promise<void> =>
-	new Promise<void>(async resolve => {
+export const registerPushToken = async (): Promise<void> => {
 		const token = getDeviceToken();
 		const voipToken = isIOS ? NativeVoipModule.getLastVoipToken() : '';
 
 		if (token === lastToken && voipToken === lastVoipToken) {
-			return resolve();
+			return;
 		}
 
 		// TODO: server version
 		if (isIOS && (!token || !voipToken)) {
-			return resolve();
+			return;
 		}
 
 		let data: TRegisterPushTokenData = {
 			id: '',
 			value: '',
 			type: '',
 			appName: ''
 		};
 		if (token) {
 			const type = isIOS ? 'apn' : 'gcm';
 			data = {
 				id: await getUniqueId(),
 				value: token,
 				type,
 				appName: getBundleId
 			};
 		}
 		if (isIOS && voipToken) {
 			data.voipToken = voipToken;
 		}
 
 		try {
 			// RC 0.60.0
 			await sdk.post('push.token', data);
 			console.log('registerPushToken success', data);
 			lastToken = token;
 			lastVoipToken = voipToken;
 		} catch (e) {
 			log(e);
 		}
-		return resolve();
-	});
+	};
📝 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
export const registerPushToken = (): Promise<void> =>
new Promise<void>(async resolve => {
const token = getDeviceToken();
const voipToken = isIOS ? NativeVoipModule.getLastVoipToken() : '';
if (token === lastToken && voipToken === lastVoipToken) {
return resolve();
}
// TODO: server version
if (isIOS && (!token || !voipToken)) {
return resolve();
}
let data: TRegisterPushTokenData = {
id: '',
value: '',
type: '',
appName: ''
};
if (token) {
const type = isIOS ? 'apn' : 'gcm';
const data = {
data = {
id: await getUniqueId(),
value: token,
type,
appName: getBundleId
};
try {
if (isIOS) {
const voipToken = getVoipPushToken();
if (voipToken) {
// TODO: this is temp only for VoIP push token
await sdk.post('push.token', {
type: 'gcm',
value: voipToken,
appName: getBundleId
});
}
}
// RC 0.60.0
await sdk.post('push.token', data);
} catch (error) {
console.log(error);
}
}
if (isIOS && voipToken) {
data.voipToken = voipToken;
}
try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success', data);
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
}
return resolve();
});
export const registerPushToken = async (): Promise<void> => {
const token = getDeviceToken();
const voipToken = isIOS ? NativeVoipModule.getLastVoipToken() : '';
if (token === lastToken && voipToken === lastVoipToken) {
return;
}
// TODO: server version
if (isIOS && (!token || !voipToken)) {
return;
}
let data: TRegisterPushTokenData = {
id: '',
value: '',
type: '',
appName: ''
};
if (token) {
const type = isIOS ? 'apn' : 'gcm';
data = {
id: await getUniqueId(),
value: token,
type,
appName: getBundleId
};
}
if (isIOS && voipToken) {
data.voipToken = voipToken;
}
try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success', data);
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
}
};
🧰 Tools
🪛 Biome (2.4.6)

[error] 1016-1058: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/restApi.ts` around lines 1015 - 1058, The registerPushToken
function currently wraps an async executor in new Promise which can leave the
outer promise unresolved on errors; change export const registerPushToken = ():
Promise<void> => new Promise(async resolve => { ... }) to an async function
export const registerPushToken = async (): Promise<void> { ... }, remove the
Promise constructor and all resolve() calls, keep the existing try/catch around
sdk.post('push.token', data) to log errors, and ensure awaits (e.g., await
getUniqueId()) and uses like NativeVoipModule.getLastVoipToken() and getBundleId
are preserved inside the new async function body so the returned promise
correctly rejects on uncaught errors.

Comment on lines +1024 to +1046
// TODO: server version
if (isIOS && (!token || !voipToken)) {
return resolve();
}

let data: TRegisterPushTokenData = {
id: '',
value: '',
type: '',
appName: ''
};
if (token) {
const type = isIOS ? 'apn' : 'gcm';
const data = {
data = {
id: await getUniqueId(),
value: token,
type,
appName: getBundleId
};
try {
if (isIOS) {
const voipToken = getVoipPushToken();
if (voipToken) {
// TODO: this is temp only for VoIP push token
await sdk.post('push.token', {
type: 'gcm',
value: voipToken,
appName: getBundleId
});
}
}

// RC 0.60.0
await sdk.post('push.token', data);
} catch (error) {
console.log(error);
}
}
if (isIOS && voipToken) {
data.voipToken = voipToken;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The current guard can skip valid iOS registrations and still post an empty Android payload.

voipToken is optional in the request shape, so returning early on iOS when it is missing delays normal APNs registration. In the other direction, if token is falsy and the cache check misses, sdk.post('push.token', data) still runs with the empty initializer.

🛠️ Proposed fix
-		// TODO: server version
-		if (isIOS && (!token || !voipToken)) {
-			return resolve();
-		}
-
-		let data: TRegisterPushTokenData = {
-			id: '',
-			value: '',
-			type: '',
-			appName: ''
-		};
-		if (token) {
-			const type = isIOS ? 'apn' : 'gcm';
-			data = {
-				id: await getUniqueId(),
-				value: token,
-				type,
-				appName: getBundleId
-			};
-		}
+		if (!token) {
+			return;
+		}
+
+		const data: TRegisterPushTokenData = {
+			id: await getUniqueId(),
+			value: token,
+			type: isIOS ? 'apn' : 'gcm',
+			appName: getBundleId
+		};
 		if (isIOS && voipToken) {
 			data.voipToken = voipToken;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/restApi.ts` around lines 1024 - 1046, The guard currently
returns early on iOS when voipToken is missing and can still post an empty
Android payload; change the early-return to only skip when neither token nor
voipToken exist (if (isIOS && !token && !voipToken) return), build the data
object only when at least one token exists (set
type/appName/getUniqueId/getBundleId() only when token is truthy), add voipToken
to data only when present, and ensure you never call sdk.post('push.token',
data) with the empty initializer—only post when data.value or data.voipToken is
set.

Comment on lines +1048 to +1055
try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success', data);
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not log raw push tokens.

data contains both the standard push token and, on iOS, the VoIP token. Emitting that payload to logs creates an avoidable credential/identifier leak.

🔒 Proposed fix
 		try {
 			// RC 0.60.0
 			await sdk.post('push.token', data);
-			console.log('registerPushToken success', data);
+			console.log('registerPushToken success');
 			lastToken = token;
 			lastVoipToken = voipToken;
 		} catch (e) {
 			log(e);
 		}
📝 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
try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success', data);
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
try {
// RC 0.60.0
await sdk.post('push.token', data);
console.log('registerPushToken success');
lastToken = token;
lastVoipToken = voipToken;
} catch (e) {
log(e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/restApi.ts` around lines 1048 - 1055, The current success
log prints the full push payload (data) which leaks push and VoIP tokens; remove
sensitive token values from logs. In the block around sdk.post('push.token',
data) replace console.log('registerPushToken success', data) with a
non-sensitive message like console.log('registerPushToken success') or log a
masked result, and ensure you do not log token, voipToken, data, lastToken or
lastVoipToken values anywhere; keep assignment to lastToken/lastVoipToken but
never emit them to logs or error reports.

return
}

let callId = voipPayload.callId.lowercased()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how callId case is handled across platforms
rg -n 'callId.*lowercase|toLowerCase|lowercased' --type swift --type kt --type ts
rg -n 'callUUID.*lowercase|toLowerCase|lowercased' --type swift --type kt --type ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 131


🏁 Script executed:

# Search Android code (use correct file type or extension search)
rg -n 'callId' -t=java --type=kotlin 2>/dev/null | head -20
rg -n '\.callId|callId\s*=' --type=kotlin 2>/dev/null | head -20

# Search for lowercase/case handling in all files
rg -n 'toLowerCase|lowercased|uppercase|toUpperCase' -t=typescript -t=javascript | grep -i callid

# Find voipPayload definition
rg -n 'voipPayload' --type=typescript --type=javascript --type=swift | head -30

# Look for any callId comparison or matching
rg -n 'callId.*==|callId.*compare|compareCallId' --type=typescript --type=javascript --type=swift

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4529


🏁 Script executed:

# List Android-related files
fd -e 'java' -e 'kt' . | grep -i voip
fd -e 'java' -e 'kt' . | grep -i callkeep
fd -e 'ts' -e 'js' . | grep -i voip

# Search for how call identifiers are handled in Android
rg -n 'callId' -A 3 -B 3 | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3881


🏁 Script executed:

# Search TypeScript/JS production code for callId handling
rg -n 'callId' app/lib/native/NativeVoip.ts app/lib/services/voip/useCallStore.ts app/definitions/Voip.ts

# Look for any callId comparison or matching logic
rg -n 'callId.*==|callId.*===|compareCallId|callId.*match' app/ --type=ts --type=js

# Check if there's any case normalization in TypeScript
rg -n 'toLowerCase|toUpperCase' app/definitions/Voip.ts app/lib/native/NativeVoip.ts app/lib/services/voip/

# Look at the iOS AppDelegate+Voip.swift directly
cat -n ios/Libraries/AppDelegate+Voip.swift | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 5334


Align callId case handling across iOS and Android platforms.

The iOS code lowercases callId at line 29, but the Android implementation (VoipNotification.kt) does not apply any case conversion. Additionally, TypeScript code shows mixed approaches—MediaCallEvents.ts performs case-insensitive matching with toLowerCase() on both sides (line 114), while other code like videoConf.ts uses strict equality without case normalization.

This inconsistency can cause call identifier mismatches between platforms if the backend sends mixed-case IDs. Either:

  • Normalize callId consistently across all platforms (recommended: remove lowercasing from iOS and apply it uniformly if needed), or
  • Document the case-sensitivity behavior and ensure all comparisons use case-insensitive matching
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Libraries/AppDelegate`+Voip.swift at line 29, The iOS
AppDelegate+Voip.swift currently lowercases callId (let callId =
voipPayload.callId.lowercased()), causing inconsistent cross-platform behavior;
remove the lowercasing in AppDelegate+Voip.swift so iOS preserves the original
callId, and instead normalize call IDs in the shared/logic layers and platform
implementations for consistency: update VoipNotification.kt to apply the same
normalization (e.g., toLowerCase()) or, preferably, update MediaCallEvents.ts
(where toLowerCase() matching occurs) and other consumers like videoConf.ts to
perform case-insensitive comparisons (normalize both sides) so all platforms
treat callId consistently across call handling functions and event matching.

Comment on lines +78 to +80
@objc public var notificationId: Int {
return callId.hashValue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

hashValue is not stable across app launches.

Swift's hashValue can vary between different runs of the program, which could cause issues with notification management (e.g., canceling notifications by ID). Android's String.hashCode() is deterministic. Consider using a stable hashing algorithm.

🔧 Proposed fix using a stable hash
     `@objc` public var notificationId: Int {
-        return callId.hashValue
+        // Use a stable hash algorithm (djb2) for consistent notification IDs across app launches
+        var hash: Int = 5381
+        for char in callId.unicodeScalars {
+            hash = ((hash << 5) &+ hash) &+ Int(char.value)
+        }
+        return hash
     }

Alternatively, if callId is a UUID string, you could extract numeric components from it for a stable ID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Libraries/VoipPayload.swift` around lines 78 - 80, The notificationId
getter uses callId.hashValue which is unstable across launches; replace it with
a deterministic mapping from callId to an Int (for example compute a stable hash
like CRC32 or SHA256 and truncate to a signed 32-bit Int, or if callId is a UUID
parse its numeric components) and return that value from the `@objc` var
notificationId so notifications can be consistently referenced across app runs;
update the notificationId computed property (and any code that assumes its
type/size) to use the stable hashing/parsing routine instead of hashValue.

Comment on lines +92 to +102
/// Called from AppDelegate when a previously registered token is invalidated
// TODO: remove voip token from all logged in workspaces, since they share the same token
@objc
public static func invalidatePushToken() {
lastVoipToken = ""
storage.removeValue(forKey: voipTokenStorageKey)

#if DEBUG
print("[\(TAG)] Invalidated VoIP token")
#endif
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Token invalidation still leaves stale backend registrations.

invalidatePushToken() only clears the local cache. Any logged-in workspace keeps the old VoIP token registered server-side, so delivery will continue targeting an invalid token until those registrations are explicitly removed. This lifecycle gap should be closed before shipping the backend token flow.

If helpful, I can sketch the unregister fan-out for all logged-in workspaces from this callback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Libraries/VoipService.swift` around lines 92 - 102, invalidatePushToken()
currently only clears local state (lastVoipToken and storage.removeValue(forKey:
voipTokenStorageKey)), leaving server-side VoIP registrations intact; update
this method to also fan out an unregister call to the backend for every
logged-in workspace so the token is removed server-side. Locate
invalidatePushToken() and after clearing local state invoke your
network/unregister API for each active workspace (e.g., iterate your
session/workspace manager’s logged-in workspaces and call the existing
unregister endpoint or a new helper like unregisterVoipToken(workspaceId:)),
handle/report errors but ensure best-effort removal for all workspaces before
returning.

@diegolmello diegolmello requested a deployment to approve_e2e_testing March 10, 2026 14:10 — with GitHub Actions Waiting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant