Skip to content

fix(voip): reject incoming voice calls without ringing when microphone denied#7354

Open
diegolmello wants to merge 9 commits into
developfrom
fix/voip-incoming-mic-permission-gate
Open

fix(voip): reject incoming voice calls without ringing when microphone denied#7354
diegolmello wants to merge 9 commits into
developfrom
fix/voip-incoming-mic-permission-gate

Conversation

@diegolmello

@diegolmello diegolmello commented May 27, 2026

Copy link
Copy Markdown
Member

Proposed changes

When the OS microphone permission is denied, an incoming voice call used to ring anyway — but answering it produced a dead call, because the app cannot open the microphone. On a locked or backgrounded device the app cannot even show a permission dialog at answer time, so the user was offered a call they could never take. The ring was pure noise.

This PR moves the decision to the native push layer, so a call that can never carry audio is declined the moment the push arrives — before any ringing UI — as symmetrically as each platform allows:

  • Android — truly silent. VoipNotification.onMessageReceived runs a pure routing decision (decideIncomingVoipPushAction, precedence stale → no-permission → busy → show). When RECORD_AUDIO is denied it sends the REST reject and returns: no Telecom registration, no notification, no full-screen incoming screen.
  • iOS — momentary flash. PushKit hard-requires reporting the call to CallKit before the handler returns, or the OS kills the app and stops delivering VoIP pushes. The handler reports a placeholder call and ends it immediately (a sub-second CallKit flash), sends the REST reject, and skips stashing the event for JS, so opening the app cannot resurrect a dead call.

Two earlier layers are retained:

  • Pre-acquire the microphone permission proactively at session init (post-login, fire-and-forget, suppressed during an active call), so the common case stays granted and calls ring normally.
  • The JavaScript answer-time gate is demoted to a defence-in-depth backstop for the narrow window where the permission is revoked between push receipt and answer.

The granted path is unchanged on both platforms; outgoing startCall behaviour is unchanged (in-context request + denial alert).

Design and vocabulary are documented in-repo under app/lib/services/voip/docs/ (ADR 0002-reject-incoming-at-push-layer-when-mic-denied.md, CONTEXT.md, ARCHITECTURE.md, FLOWS.md §3, PLATFORMS.md).

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1139

How to test or reproduce

  1. Revoke the microphone permission for Rocket.Chat in OS Settings.
  2. Android: receive an incoming VoIP call → the device stays completely silent (no ring, no notification, no screen); the caller sees the call promptly declined.
  3. iOS: receive an incoming VoIP call → only a momentary CallKit flash appears, then it self-dismisses; the caller sees the call declined; opening the app does not present the call.
  4. Repeat 2–3 with the device locked and with the app backgrounded — same result (no answer dialog is ever needed).
  5. Grant the permission → incoming calls ring and connect normally on both platforms (regression).
  6. Fresh login with the permission undetermined → the app pre-acquires it (foreground prompt) so later locked/backgrounded calls can ring.
  7. Outgoing call without permission → unchanged: prompts in context and shows the microphone-denied alert.

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

  • Why the push layer: the ring originates in the native push handlers (Android FCM → Telecom/notification; iOS PushKit → CallKit), not in JavaScript, so the only place to stop it before it rings is natively at push receipt.
  • Platform asymmetry is intrinsic, not a shortcut: Android can decline a push with no UI; iOS PushKit mandates a CallKit report before the handler returns. The momentary iOS flash mirrors the existing expired/unparseable-payload handling and is the accepted floor.
  • Precedence (no-permission before busy) is documented in code and in the routing unit tests; in practice an active call implies the permission is granted, so the combination is near-impossible — the explicit ordering just removes ambiguity.
  • Testing: the pure Android routing decision is unit-tested (VoipIncomingCallDispatchTest: denied, denied-while-busy precedence, granted, stale). The native push wiring (Kotlin/Swift) is bound to framework singletons with no Swift unit harness, so it is verified by build plus manual QA on locked/backgrounded devices. iOS manual QA (real incoming call, mic denied, locked + backgrounded) is pending.

Summary by CodeRabbit

  • New Features

    • VoIP microphone permission is now pre-acquired at login to enable seamless incoming call handling without prompts when the device is locked or backgrounded.
    • Incoming calls are silently rejected at the native layer if microphone permission is denied, preventing unnecessary ringing.
    • Improved permission denial alerts with context-aware options to retry or access settings.
  • Bug Fixes

    • Prevented duplicate answer attempts when handling concurrent incoming calls.
  • Documentation

    • Added microphone permission architecture and decision documentation.

Request mic access before accept() so denied permissions hang up immediately
instead of stalling signaling until the 10s timeout. Adds settings redirect
for permanent denial on both platforms.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Introduce a cross-platform VoIP microphone permission result ({granted, canAskAgain, prompted}), add check-only and pre-acquire flows, denial alert UI, native push-layer rejection when mic is denied, MediaSessionInstance idempotent answer gating, and update tests, docs, saga wiring, and i18n.

Changes

VoIP Permissions & Push-gate

Layer / File(s) Summary
Permission contract, implementation, and tests
app/lib/methods/voipCallPermissions.ts, app/lib/methods/voipCallPermissions.test.ts
Add VoipCallPermissionResult with granted, canAskAgain, prompted. Implement cross-platform requestVoipCallPermissions, hasVoipCallPermission, preAcquireVoipMicPermission, and showVoipMicrophoneDeniedAlert; expand unit tests for Android/iOS outcomes, errors, and pre-acquire behavior.
MediaSessionInstance integration and tests
app/lib/services/voip/MediaSessionInstance.ts, app/lib/services/voip/MediaSessionInstance.test.ts, app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
Add answeringCallIds idempotency guard; answerCall uses non-prompt hasVoipCallPermission and silently ends/rejects when absent; startCall awaits requestVoipCallPermissions() and shows denial alert per canAskAgain. Update/extend tests and integration mock shapes.
Android push handling and tests
android/app/src/main/java/.../VoipIncomingCallDispatch.kt, android/app/src/main/java/.../VoipNotification.kt, android/app/src/test/.../VoipIncomingCallDispatchTest.kt
Add REJECT_NO_PERMISSION outcome; thread RECORD_AUDIO state into decideIncomingVoipPushAction; centralize silent REST reject helper and add rejectNoMicPermissionCall; add tests asserting precedence and stale behavior.
iOS PushKit gate and VoipService
ios/Libraries/AppDelegate+Voip.swift, ios/Libraries/VoipService.swift
Add hasMicrophonePermission() (AVFoundation), rejectNoMicPermissionCall(_), and gate incoming PushKit flows to reject with a placeholder CallKit report when mic permission is denied.
Docs, ADRs, saga and i18n updates
app/lib/services/voip/docs/*, app/sagas/login.js, app/i18n/locales/en.json
Add ADRs documenting pre-acquire and push-layer rejection; update FLOWS/PLATFORMS/ARCHITECTURE with microphone branches/notes; call preAcquireVoipMicPermission() from login saga; add Microphone_access_needed_for_voice_calls i18n key.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • OtavioStasiak
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% 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 primary change: rejecting incoming voice calls without ringing when microphone permission is denied, which is the core objective across both Android and iOS platforms.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1139: Request failed with status code 401

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.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/methods/voipCallPermissions.ts`:
- Around line 13-37: The requestVoipCallPermissions function currently calls
PermissionsAndroid.request, Audio.getPermissionsAsync, and
Audio.requestPermissionsAsync without handling rejections; wrap the entire body
of requestVoipCallPermissions in a try/catch, catch any thrown errors from
PermissionsAndroid, Audio, or permission calls, log the error (e.g. via
console.error or the app logger) including which API failed, and return a safe
denied result { granted: false, canAskAgain: false } on failure so callers get a
controlled denial UX; ensure references to PermissionsAndroid, Audio, and
PermissionStatus remain intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcdcc091-13c0-4ccd-8875-cbb71589521d

📥 Commits

Reviewing files that changed from the base of the PR and between 3961d4d and b5309c7.

📒 Files selected for processing (5)
  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode and baseUrl set to app/ for import resolution

Files:

  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
app/containers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reusable UI components should be placed in app/containers/ directory

Files:

  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

VoIP service implementation should use Zustand stores (not Redux) and include native CallKit (iOS) and Telecom (Android) integration in app/lib/services/voip/

Files:

  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
🔇 Additional comments (5)
app/lib/methods/voipCallPermissions.test.ts (1)

1-144: LGTM!

app/lib/services/voip/MediaSessionInstance.ts (1)

36-37: LGTM!

Also applies to: 176-181, 238-241

app/lib/services/voip/MediaSessionInstance.test.ts (1)

132-137: LGTM!

Also applies to: 248-249, 700-709, 877-915

app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx (1)

142-142: LGTM!

app/lib/methods/voipCallPermissions.ts (1)

3-3: ⚡ Quick win

Expo-av Audio.getPermissionsAsync() status can be compared to expo-camera’s PermissionStatus
Audio.getPermissionsAsync() (expo-av) and PermissionStatus used by expo-camera both come from Expo’s shared expo-modules-core enum, so the comparison isn’t a brittle cross-package contract (app/lib/methods/voipCallPermissions.ts, lines 3 and 29). Optionally, import PermissionStatus from expo-modules-core for clarity.

			> Likely an incorrect or invalid review comment.

Comment thread app/lib/methods/voipCallPermissions.ts
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.73.0.108986

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Code review — incoming VoIP mic-permission gate

Solid fix on the happy path with good unit coverage. The main risks are on the error / background path of the incoming-answer flow (where the new await was inserted), plus some duplication with the existing audio-recording permission code in MicOrSendButton. Inline comments below, roughly highest-severity first.

Verified non-issues: PermissionStatus from expo-camera matches expo-av's status enum (both re-export from expo-modules-core; same pattern already ships in MicOrSendButton); Android result !== NEVER_ASK_AGAIN covers all return values; all i18n keys (Ok/Cancel/Settings/…) exist; no dead imports.

Comment thread app/lib/services/voip/MediaSessionInstance.ts Outdated
Comment thread app/lib/services/voip/MediaSessionInstance.ts Outdated
Comment thread app/lib/services/voip/MediaSessionInstance.ts Outdated
Comment thread app/lib/methods/voipCallPermissions.ts Outdated
Comment thread app/lib/methods/voipCallPermissions.ts
Comment thread app/lib/methods/voipCallPermissions.ts Outdated
Comment thread app/lib/methods/voipCallPermissions.ts Outdated
Comment thread app/lib/methods/voipCallPermissions.test.ts
Comment thread app/lib/services/voip/MediaSessionInstance.test.ts
- requestVoipCallPermissions never rejects: a thrown permission check is
  treated as denied + logged, so a locked/backgrounded answer ends promptly
  instead of hanging until the 10s signaling timeout
- answerCall dedupes concurrent invocations per callId (in-flight Set,
  cleared in finally) to restore the idempotency its doc comment claims
- simplify the iOS permission branch and collapse the denied-mic alert
  variants into a single Alert.alert call
- cover the iOS live-prompt denial and permission-throw paths; tighten the
  denied-answer test to assert the reject() wire signal and native teardown
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.73.0.109006

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Request the OS microphone permission proactively at session init (login →
checkVoipPermission → preAcquireVoipMicPermission), fire-and-forget and
suppressed while a call is active; alert only on a fresh denial. The incoming
answerCall path becomes check-only (hasVoipCallPermission): it never prompts —
the device may be locked/backgrounded — and rejects the call silently when the
mic is not currently granted. Outgoing startCall still requests in-context and
alerts; a new voice-call-specific alert string keeps the shared recorder copy
untouched.

This fixes the locked/backgrounded incoming-call case where a permission dialog
is impossible at answer time, so the permission is acquired earlier instead.

Docs: add CONTEXT.md glossary + ADR 0001; update FLOWS/ARCHITECTURE for the
pre-acquire step and the check-only answer gate.
When the OS microphone permission (RECORD_AUDIO) is denied, decline an
incoming VoIP push silently — no Telecom registration, no notification,
no ringing — via the existing REST reject mechanism, rather than ringing
a call that can never carry audio.

- decideIncomingVoipPushAction gains a REJECT_NO_PERMISSION outcome and
  a hasMicPermission input (default granted, so existing callers/tests
  keep their outcomes); precedence stale -> no-permission -> busy -> show.
- onMessageReceived reads RECORD_AUDIO and feeds the decision; the
  no-permission outcome maps onto a shared silent-reject helper
  (rejectNoMicPermissionCall) with its own debug log line.
- Docs: ADR-0002 (push-layer gate + iOS PushKit limitation), CONTEXT
  glossary (Incoming-push gate, Reject-without-ringing), ARCHITECTURE
  microphone section, and FLOWS §3.

The JS answer-time gate is retained as a defence-in-depth backstop.
Mirror the Android push-layer gate on iOS. When the OS microphone
permission is denied, the PushKit incoming-push handler declines the
call without a sustained ring: it sends the REST reject and reports a
placeholder call it ends immediately (a sub-second CallKit flash —
PushKit mandates a report before the handler returns), then skips
prepareIncomingCall so nothing is stashed for JS.

- VoipService.hasMicrophonePermission() reads the grant via
  AVAudioApplication on iOS 17+ and AVAudioSession earlier; only
  .granted passes (denied/undetermined are not-granted), matching the
  Android/JS gate.
- VoipService.rejectNoMicPermissionCall wraps the private REST reject as
  a public push-handler entry point (no CallKit side effects).
- AppDelegate+Voip didReceiveIncomingPushWith gates the non-China
  branch; the granted path is unchanged.
- Docs: PLATFORMS.md iOS + Android push-handler sections reference the
  gate and ADR-0002.

A truly silent path is impossible on iOS (PushKit kills the app if the
handler returns without reporting); the momentary flash is the floor.
@diegolmello diegolmello temporarily deployed to approve_e2e_testing June 8, 2026 19:23 — with GitHub Actions Inactive
@diegolmello diegolmello changed the title fix(voip): gate incoming answer on microphone permission fix(voip): reject incoming voice calls without ringing when microphone denied Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/sagas/login.js`:
- Around line 268-273: The mediaSessionInstance.init(userId) call is currently a
floating promise allowing failures to escape checkVoipPermission's try/catch and
letting preAcquireVoipMicPermission run even if init fails; change the flow to
await mediaSessionInstance.init(userId) (or explicitly return
mediaSessionInstance.init(userId).then(...)) and only call
preAcquireVoipMicPermission() after init resolves, and funnel any errors from
both operations into the single existing catch/log path (use the same log
function used by the surrounding try/catch) so failures are observed and
pre-acquire does not run on init failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba3b64af-78bc-4d03-a206-b76ad230df1b

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed2319 and 112a46a.

📒 Files selected for processing (17)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatch.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatchTest.kt
  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
  • app/i18n/locales/en.json
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/methods/voipCallPermissions.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/docs/ARCHITECTURE.md
  • app/lib/services/voip/docs/FLOWS.md
  • app/lib/services/voip/docs/PLATFORMS.md
  • app/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.md
  • app/lib/services/voip/docs/adr/0002-reject-incoming-at-push-layer-when-mic-denied.md
  • app/sagas/login.js
  • ios/Libraries/AppDelegate+Voip.swift
  • ios/Libraries/VoipService.swift
✅ Files skipped from review due to trivial changes (3)
  • app/lib/services/voip/docs/adr/0002-reject-incoming-at-push-layer-when-mic-denied.md
  • app/i18n/locales/en.json
  • app/lib/services/voip/docs/ARCHITECTURE.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/sagas/login.js
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/methods/voipCallPermissions.ts
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/sagas/login.js
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/methods/voipCallPermissions.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/sagas/login.js
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/methods/voipCallPermissions.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/methods/voipCallPermissions.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Zustand stores (not Redux) for VoIP feature in 'app/lib/services/voip/' directory with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🧠 Learnings (2)
📚 Learning: 2026-05-07T13:19:52.152Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7304
File: app/sagas/deepLinking.js:237-243
Timestamp: 2026-05-07T13:19:52.152Z
Learning: In this codebase’s Redux-Saga usage, remember that `yield put(action)` dispatches through the Redux store synchronously, and any saga(s) that synchronously react via action listeners (and synchronous `put` chains) will run to completion before the calling saga resumes at its next `yield`. As a result, within a single saga there is no scheduler interleaving between a `yield select(...)` and a subsequent `yield take(...)` at the next `yield` point, so a check-then-take pattern like `const state = yield select(...); if (state !== TARGET) { yield take(a => a.type === TARGET); }` is safe from TOCTOU races under the synchronous `put`/take model described above.

Applied to files:

  • app/sagas/login.js
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/methods/voipCallPermissions.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/methods/voipCallPermissions.ts
🪛 LanguageTool
app/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.md

[grammar] ~5-~5: Ensure spelling is correct
Context: ... answer path check-only: it answers iff the permission is currently granted, ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (12)
app/lib/methods/voipCallPermissions.ts (1)

20-54: LGTM!

Also applies to: 63-74, 84-92, 94-107

app/lib/methods/voipCallPermissions.test.ts (1)

39-163: LGTM!

Also applies to: 165-351

app/lib/services/voip/MediaSessionInstance.ts (1)

54-55: LGTM!

Also applies to: 172-228, 256-260

app/lib/services/voip/MediaSessionInstance.test.ts (1)

132-139: LGTM!

Also applies to: 250-252, 879-1055

app/lib/services/voip/docs/FLOWS.md (1)

9-10: LGTM!

Also applies to: 40-40, 52-52, 113-114, 129-137, 142-142, 167-167

app/lib/services/voip/docs/PLATFORMS.md (1)

23-26: LGTM!

Also applies to: 66-67

app/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.md (1)

1-12: LGTM!

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

9-9: LGTM!

Also applies to: 14-22, 25-33

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

488-532: LGTM!

Also applies to: 689-709

android/app/src/test/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatchTest.kt (1)

40-74: LGTM!

ios/Libraries/AppDelegate+Voip.swift (1)

83-94: LGTM!

ios/Libraries/VoipService.swift (1)

1-1: LGTM!

Also applies to: 170-180, 577-586

Comment thread app/sagas/login.js
Comment on lines 268 to 273
mediaSessionInstance.init(userId);
// Pre-acquire the microphone now (foreground, post-login) so an incoming call while the
// device is locked/backgrounded — where a permission dialog is impossible — can still be
// answered. Fire-and-forget: never block session setup on the OS dialog.
preAcquireVoipMicPermission().catch(log);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle the init() promise explicitly before pre-acquire.

On Line 268, mediaSessionInstance.init(userId) is fired without await/terminal .catch, so async failures escape checkVoipPermission’s try/catch. It also allows Line 272 pre-acquire to run even if init fails. Chain them so pre-acquire runs only after successful init, with one error sink.

Suggested fix
 		if (!mediaSessionStore.getCurrentInstance()) {
-			mediaSessionInstance.init(userId);
-			// Pre-acquire the microphone now (foreground, post-login) so an incoming call while the
-			// device is locked/backgrounded — where a permission dialog is impossible — can still be
-			// answered. Fire-and-forget: never block session setup on the OS dialog.
-			preAcquireVoipMicPermission().catch(log);
+			const initializeVoipSession = async () => {
+				await mediaSessionInstance.init(userId);
+				// Pre-acquire the microphone now (foreground, post-login) so an incoming call while the
+				// device is locked/backgrounded — where a permission dialog is impossible — can still be
+				// answered. Fire-and-forget from saga flow, but ordered after init success.
+				await preAcquireVoipMicPermission();
+			};
+			initializeVoipSession().catch(log);
 		}

Based on learnings, this codebase requires explicitly handling floating promises instead of leaving them unobserved; as per coding guidelines, async operations should use explicit error handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/sagas/login.js` around lines 268 - 273, The
mediaSessionInstance.init(userId) call is currently a floating promise allowing
failures to escape checkVoipPermission's try/catch and letting
preAcquireVoipMicPermission run even if init fails; change the flow to await
mediaSessionInstance.init(userId) (or explicitly return
mediaSessionInstance.init(userId).then(...)) and only call
preAcquireVoipMicPermission() after init resolves, and funnel any errors from
both operations into the single existing catch/log path (use the same log
function used by the surrounding try/catch) so failures are observed and
pre-acquire does not run on init failure.

Sources: Coding guidelines, Learnings

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.74.0.109042

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