fix(voip): reject incoming voice calls without ringing when microphone denied#7354
fix(voip): reject incoming voice calls without ringing when microphone denied#7354diegolmello wants to merge 9 commits into
Conversation
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>
WalkthroughIntroduce 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. ChangesVoIP Permissions & Push-gate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
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. Comment |
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.test.tsapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/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.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/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 numbersUse TypeScript with strict mode and baseUrl set to app/ for import resolution
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/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-configbase with React, React Native, TypeScript, Jest plugins
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/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.tsapp/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.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/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 winExpo-av
Audio.getPermissionsAsync()status can be compared toexpo-camera’sPermissionStatus
Audio.getPermissionsAsync()(expo-av) andPermissionStatusused byexpo-cameraboth come from Expo’s sharedexpo-modules-coreenum, so the comparison isn’t a brittle cross-package contract (app/lib/methods/voipCallPermissions.ts, lines 3 and 29). Optionally, importPermissionStatusfromexpo-modules-corefor clarity.> Likely an incorrect or invalid review comment.
|
Android Build Available Rocket.Chat 4.73.0.108985 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNS__32oABVGneMrdnlkqLu9q2iqsMyqMSuQ0KZ3r7zS-sBD-tSngWZdakKZ-QKi9t3Ydday7lCTV9-jH_S6 |
|
iOS Build Available Rocket.Chat 4.73.0.108986 |
diegolmello
left a comment
There was a problem hiding this comment.
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.
- 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
|
iOS Build Available Rocket.Chat 4.73.0.109006 |
|
Android Build Available Rocket.Chat 4.73.0.109005 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNR8XgVARz7XAWRwuK-OKKoa5YUPW95EyR1tcy2r1yNPkpXkXKJXSDjxVVcwH2h-VZajvcqCVesLuThozELV |
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (17)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatch.ktandroid/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.ktandroid/app/src/test/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatchTest.ktapp/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/i18n/locales/en.jsonapp/lib/methods/voipCallPermissions.test.tsapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/docs/ARCHITECTURE.mdapp/lib/services/voip/docs/FLOWS.mdapp/lib/services/voip/docs/PLATFORMS.mdapp/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.mdapp/lib/services/voip/docs/adr/0002-reject-incoming-at-push-layer-when-mic-denied.mdapp/sagas/login.jsios/Libraries/AppDelegate+Voip.swiftios/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.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/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.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Enforce ESLint rules from
@rocket.chat/eslint-configwith React, React Native, TypeScript, and Jest plugins
Files:
app/sagas/login.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/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 numbersUse TypeScript with strict mode enabled
Files:
app/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/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.tsapp/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.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/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
| 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); | ||
| } |
There was a problem hiding this comment.
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
|
iOS Build Available Rocket.Chat 4.74.0.109042 |
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:
VoipNotification.onMessageReceivedruns a pure routing decision (decideIncomingVoipPushAction, precedencestale → no-permission → busy → show). WhenRECORD_AUDIOis denied it sends the RESTrejectand returns: no Telecom registration, no notification, no full-screen incoming screen.reject, and skips stashing the event for JS, so opening the app cannot resurrect a dead call.Two earlier layers are retained:
The granted path is unchanged on both platforms; outgoing
startCallbehaviour is unchanged (in-context request + denial alert).Design and vocabulary are documented in-repo under
app/lib/services/voip/docs/(ADR0002-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
Screenshots
Types of changes
Checklist
Further comments
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
Bug Fixes
Documentation