Skip to content

fix(voip): reconnect SDK on lock-screen CallKit native accept#7299

Closed
diegolmello wants to merge 3 commits into
fix.foreground-reconnectfrom
fix.voip-reconnect-on-native-accept
Closed

fix(voip): reconnect SDK on lock-screen CallKit native accept#7299
diegolmello wants to merge 3 commits into
fix.foreground-reconnectfrom
fix.voip-reconnect-on-native-accept

Conversation

@diegolmello

@diegolmello diegolmello commented May 4, 2026

Copy link
Copy Markdown
Member

Proposed changes

Stacks on #7297. When CallKit accepts a VoIP call from the lock screen, the app stays backgrounded — APP_STATE.FOREGROUND never transitions, so the foreground-reconnect saga added in #7297 never fires. The DDP socket can be a zombie (kept-alive readyState=OPEN, but TCP dead after a long iOS suspend), and any outgoing SDP/ICE sends triggered by applyRestStateSignals() are silently swallowed.

This adds a parallel reconnect trigger inside handleVoipAcceptSucceededFromNative: when the accepted call's host matches the active workspace, call checkAndReopen() (fire-and-forget) before replaying REST state signals. Same primitive as the saga, just driven from the VOIP accept path instead of the app-state listener.

The behavior depends on #7297's unconditional teardown — without that patch checkAndReopen no-ops against the zombie socket (the pre-#7297 implementation only opens when !this.connected).

Issue(s)

Follow-up to #7297 — covers the lock-screen accept scenario that PR did not.

How to test or reproduce

  1. Open the app, make sure you are on the same workspace as the caller.
  2. Lock the device and wait long enough for iOS to suspend the JS runtime (a couple of minutes is usually enough).
  3. Have another user place a VOIP call to your account.
  4. Accept the call from the iOS lock-screen CallKit UI without unlocking.
  5. The call should connect — audio flows in both directions, signaling completes. Without the fix, the call stays in a connecting state because SDP/ICE messages cannot be sent over the dead socket.

Screenshots

N/A — logging only on the JS side.

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

Cross-platform (MediaCallEvents.test.ts) covers four cases: same-workspace fires checkAndReopen once, cross-workspace does not fire, duplicate callId deliveries dedupe, and checkAndReopen is invoked before applyRestStateSignals. The iOS test file (MediaCallEvents.ios.test.ts) gets the matching '../connect' mock so the new import does not break its module graph.

Considered (and rejected) alternatives:

  • Synthesizing an APP_STATE.FOREGROUND redux dispatch from the accept path — would re-run localAuthenticate (biometric prompt) and setUserPresenceOnline, both wrong for a backgrounded lock-screen accept.
  • Triggering the reopen on the native side (Swift) — checkAndReopen lives in the JS SDK, so the trigger has to cross the bridge anyway; doing it from JS keeps the concern in one place.

No native code changed.

Summary by CodeRabbit

  • Tests

    • Added tests that mock reconnection behavior and verify reconnection is attempted only for calls from the active workspace, only once for duplicate accept events, and occurs before restoring media session state.
  • Bug Fixes

    • Ensure the client attempts reconnection for accepted calls from your workspace, invokes it before restoring media session state, and logs errors without altering existing accept/failure flows.

The foreground-reconnect path (PR #7297) doesn't fire when CallKit
accepts a call from the lock screen, since the app stays backgrounded
and APP_STATE.FOREGROUND never transitions. Trigger checkAndReopen
inline from VoipAcceptSucceeded so the zombie DDP socket is torn down
before SDP/ICE replays kick off.
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19cceb54-6472-49e6-877f-953a419c45f2

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdbbc0 and d27047f.

📒 Files selected for processing (1)
  • app/lib/services/voip/MediaCallEvents.ts
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/MediaCallEvents.ts
📜 Recent 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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format

Walkthrough

Invoke SDK reconnection when an incoming VoIP accept matches the active workspace: call checkAndReopen() (errors logged) before mediaSessionInstance.applyRestStateSignals(). Tests added/mocked to verify workspace matching, deduplication, and execution order; iOS test mock added for isolation.

Changes

VoIP SDK Reconnection on Call Accept

Layer / File(s) Summary
Core Behavior
app/lib/services/voip/MediaCallEvents.ts
Imports checkAndReopen and calls it when data.host matches the active workspace inside handleVoipAcceptSucceededFromNative, catching and logging errors, and invoking it before applyRestStateSignals().
Unit Tests (behavior)
app/lib/services/voip/MediaCallEvents.test.ts
Adds a Jest mock for ../connect (stubbing checkAndReopen) and new VoipAcceptSucceeded tests: reconnection on same-workspace host, no reconnection for cross-workspace host, deduplication for duplicate callId, and that checkAndReopen runs before applyRestStateSignals.
Unit Tests (iOS isolation)
app/lib/services/voip/MediaCallEvents.ios.test.ts
Adds Jest mock for ../connect to stub checkAndReopen during iOS-specific tests.

Sequence Diagram

sequenceDiagram
    participant Native as Native layer (event)
    participant Handler as MediaCallEvents handler
    participant Connect as SDK (checkAndReopen)
    participant Media as mediaSessionInstance

    Native->>Handler: VoipAcceptSucceeded(host, callId,...)
    Handler->>Handler: verify host matches active workspace
    Handler->>Connect: call checkAndReopen()
    Connect-->>Handler: resolves (or rejects, logged)
    Handler->>Media: applyRestStateSignals(...)
    Handler-->>Native: complete handling
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix(voip): reconnect SDK on lock-screen CallKit native accept' directly and clearly summarizes the main change: fixing VoIP reconnection when accepting a call from the iOS lock screen via CallKit.
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.


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.

@diegolmello diegolmello temporarily deployed to experimental_ios_build May 4, 2026 22:17 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to official_android_build May 4, 2026 22:17 — with GitHub Actions Error
@diegolmello diegolmello temporarily deployed to experimental_android_build May 4, 2026 22:17 — with GitHub Actions Inactive

@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

🧹 Nitpick comments (1)
app/lib/services/voip/MediaCallEvents.test.ts (1)

166-225: 💤 Low value

New reconnect tests are comprehensive and well-structured

All four assertions — triggering on host match, skipping on cross-workspace, deduplication, and invocation order — correctly exercise the added code path via the real DeviceEventEmitter emitter rather than unit-testing the handler in isolation. The ordering test (lines 205-225) correctly validates synchronous invocation ordering using mockImplementationOnce, which matches the current fire-and-forget design.

One note: the dedupe test at line 191-203 uses mockServerSelector.mockReturnValue(...) (persistent) rather than mockReturnValueOnce. Jest's clearAllMocks() clears usage data (calls, instances, results) but not implementation. Since the value set is identical to the declared default ('https://workspace-a.example.com'), there is no observable cross-test leakage — but prefer mockReturnValueOnce for consistency with the other tests in the suite.

♻️ Consistency fix for the dedupe test mock
-           mockServerSelector.mockReturnValue('https://workspace-a.example.com');
+           mockServerSelector.mockReturnValueOnce('https://workspace-a.example.com');
+           mockServerSelector.mockReturnValueOnce('https://workspace-a.example.com');
🤖 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/lib/services/voip/MediaCallEvents.test.ts` around lines 166 - 225, The
dedupe test uses mockServerSelector.mockReturnValue(...) which sets a persistent
implementation; change it to
mockServerSelector.mockReturnValueOnce('https://workspace-a.example.com') in the
'triggers SDK reconnect only once for duplicate VoipAcceptSucceeded with the
same callId' test (the one that builds payload with callId 'reopen-dedupe' and
emits via DeviceEventEmitter) so the mock matches the pattern used by other
tests and avoids persistent test state while still ensuring checkAndReopen is
called only once.
🤖 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/services/voip/MediaCallEvents.ts`:
- Around line 99-102: checkAndReopen() returns a Promise and currently lacks
error handling and sequencing with applyRestStateSignals; update the block
around mediaSessionInstance so you await checkAndReopen() (or at minimum append
.catch(...) like you do for applyRestStateSignals) and only call
mediaSessionInstance.applyRestStateSignals() after checkAndReopen completes,
handling errors for both with mediaCallLogger.error and TAG; target the
checkAndReopen() call and the mediaSessionInstance.applyRestStateSignals()
invocation to implement the .catch handlers (or make the function async and use
await) so there are no unhandled rejections and the reconnect completes before
signals are replayed.

---

Nitpick comments:
In `@app/lib/services/voip/MediaCallEvents.test.ts`:
- Around line 166-225: The dedupe test uses
mockServerSelector.mockReturnValue(...) which sets a persistent implementation;
change it to
mockServerSelector.mockReturnValueOnce('https://workspace-a.example.com') in the
'triggers SDK reconnect only once for duplicate VoipAcceptSucceeded with the
same callId' test (the one that builds payload with callId 'reopen-dedupe' and
emits via DeviceEventEmitter) so the mock matches the pattern used by other
tests and avoids persistent test state while still ensuring checkAndReopen is
called only once.
🪄 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: 2d666c73-1874-4d94-8983-dfe2839ab3a7

📥 Commits

Reviewing files that changed from the base of the PR and between cdab63a and 23aeda0.

📒 Files selected for processing (3)
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
  • app/lib/services/voip/MediaCallEvents.ts
📜 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
📓 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/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.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

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character width, no trailing commas, avoid arrow parens, and bracket same line

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP WebRTC peer-to-peer audio calls with Zustand stores (not Redux) in 'app/lib/services/voip/' with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.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/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
🔇 Additional comments (2)
app/lib/services/voip/MediaCallEvents.ios.test.ts (1)

91-93: LGTM — minimal stub correctly prevents module-graph breakage

The checkAndReopen: jest.fn() stub is all that's needed here; the reconnect-path assertions belong in MediaCallEvents.test.ts (where they live).

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

72-74: LGTM — correct mirror of the iOS test file's connect stub

Comment thread app/lib/services/voip/MediaCallEvents.ts Outdated
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108761

@diegolmello diegolmello had a problem deploying to upload_experimental_android May 5, 2026 00:00 — with GitHub Actions Error
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108760

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTgQysT27xV5Qg-Jxf4z__YC67cy2QkaPXFijbiX688QlPj_kIPo9kuHXozpkmbeUO7yG8z2btF5xsNZUMW

@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/services/voip/MediaCallEvents.ts`:
- Around line 99-101: The catch handlers for checkAndReopen() use an implicitly
typed parameter; change the arrow parameter to an explicitly typed catch
parameter (error: unknown) in the MediaCallEvents.ts handlers (the
checkAndReopen().catch(...) callback and the other catch on the adjacent line),
and before passing it to mediaCallLogger.error ensure it's narrowed/serialized
(e.g., inspect or String(error) / JSON.stringify when safe) so the logger
receives a well-typed value rather than an implicit any.
🪄 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: 7e8e5156-d10d-417e-9e08-e19298302c4e

📥 Commits

Reviewing files that changed from the base of the PR and between 23aeda0 and 9bdbbc0.

📒 Files selected for processing (3)
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
  • app/lib/services/voip/MediaCallEvents.ts
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/lib/services/voip/MediaCallEvents.test.ts
📜 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: format
🧰 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/lib/services/voip/MediaCallEvents.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

Files:

  • app/lib/services/voip/MediaCallEvents.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character width, no trailing commas, avoid arrow parens, and bracket same line

Files:

  • app/lib/services/voip/MediaCallEvents.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/MediaCallEvents.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP WebRTC peer-to-peer audio calls with Zustand stores (not Redux) in 'app/lib/services/voip/' with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/MediaCallEvents.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/lib/services/voip/MediaCallEvents.ts
🪛 GitHub Check: ESLint and Test / run-eslint-and-test
app/lib/services/voip/MediaCallEvents.ts

[failure] 99-99:
Parameter 'error' implicitly has an 'any' type.

Comment thread app/lib/services/voip/MediaCallEvents.ts Outdated
@diegolmello diegolmello had a problem deploying to approve_e2e_testing May 5, 2026 02:38 — with GitHub Actions Failure
@diegolmello diegolmello temporarily deployed to experimental_ios_build May 5, 2026 02:42 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_android_build May 5, 2026 02:42 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to official_ios_build May 5, 2026 02:42 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_android_build May 5, 2026 02:42 — with GitHub Actions Failure
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108763

@diegolmello diegolmello had a problem deploying to upload_experimental_android May 5, 2026 04:11 — with GitHub Actions Failure
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108762

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNQRS8i_wVfhUJwYwfZN99TzgG4gSBUcwVzW_e5rkEZ1CQHfsNGsQgrzplP-f6imInCfqKgFOXnioNTkb16P

@diegolmello diegolmello closed this May 5, 2026
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