Skip to content

[FCE-3435] Add call signaling handshake, native timeouts, hold, call waiting and Recents redial to VoIP calls#562

Draft
Magmusacy wants to merge 47 commits into
feature/voip-callsfrom
FCE-3435
Draft

[FCE-3435] Add call signaling handshake, native timeouts, hold, call waiting and Recents redial to VoIP calls#562
Magmusacy wants to merge 47 commits into
feature/voip-callsfrom
FCE-3435

Conversation

@Magmusacy

Copy link
Copy Markdown
Collaborator

Description

Builds on the Android integration from #561 with the remaining call-lifecycle and native-call features:

  • WebSocket call signaling (useCallSignaling, server support) with an answer-to-connect handshake on iOS and Android — the native call UI only reports the call as connected once the callee has actually joined, with a connecting state in between (fulfillIncomingCallConnected / failIncomingCallConnected, reportOutgoingCallConnected).
  • Native call timeouts for incoming, outgoing, and answer-fulfillment phases, configurable via new plugin options on iOS and Android.
  • Hold / resume (setCallHeld, isCallHeld) and call waiting for a second incoming call (via a react-native-webrtc submodule bump).
  • Native mute surfaced to JS and end-call events with reasons, so the app stays in sync with CallKit/Telecom actions.
  • enableCallIntents plugin option so calls appear in the system Recents and can be redialed from there (iOS).
  • Example polish — Fishjam branding (icons, splash), server-served fish avatars, DirectoryScreenUsersScreen, speaker toggle fixes, and camera PIP improvements.

Motivation and Context

#561 made basic calls work on both platforms, but the example didn't yet behave like a real phone app: answering connected immediately even if signaling later failed, unanswered calls rang forever, there was no hold or second-call handling, and muting from the native call UI desynced the app. This PR closes those gaps so the example serves as a complete reference for production VoIP behavior on Fishjam.

Documentation impact

  • Documentation update required
  • Documentation updated in another PR
  • No documentation update required

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to
    not work as expected)

Magmusacy and others added 16 commits July 14, 2026 15:47
Recents redial still delivers INStartAudioCallIntent/INStartVideoCallIntent,
so all three intent types must be listed in NSUserActivityTypes for iOS to
hand the activity to the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The superproject's recorded submodule commit was 2 commits behind what was
actually checked out on this branch (missing the Recents and call-hold work).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear

linear Bot commented Jul 16, 2026

Copy link
Copy Markdown

FCE-3435

@Magmusacy
Magmusacy changed the base branch from main to feature/voip-calls July 16, 2026 17:42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Adds full “phone-like” VoIP call lifecycle behavior across the SDK plugin and the example app/server, including signaling, connection handshakes, native timeouts, hold/call-waiting support, mute/end-reason syncing, and Recents redial handling.

Changes:

  • Extends the public mobile-client API surface with new VoIP lifecycle/handshake/hold helpers and end reasons.
  • Adds/updates Expo config plugin behavior for Android VoIP manifest injection and iOS VoIP timeouts + call-intent wiring.
  • Upgrades the example stack (server + app) with WebSocket signaling, avatars, UI polish, and richer call state handling.

Reviewed changes

Copilot reviewed 26 out of 37 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
yarn.lock Adds expo-splash-screen dependency lock entry.
packages/mobile-client/src/index.ts Re-exports new VoIP lifecycle/handshake/hold APIs and types.
packages/mobile-client/README.md Documents native timeout configuration and bare-workflow setup details.
packages/mobile-client/plugin/src/withFishjamVoipAndroid.ts New Android VoIP manifest/metadata injection plugin.
packages/mobile-client/plugin/src/withFishjamIos.ts Adds iOS Info.plist timeout writing and call-intent AppDelegate wiring.
packages/mobile-client/plugin/src/withFishjamAndroid.ts Switches Android plugin import to new VoIP Android implementation.
packages/mobile-client/plugin/src/types.ts Adds voip plugin options (timeouts, intents, icon).
examples/mobile-client/voip-call/server/README.md Updates API docs and documents WebSocket signaling behavior.
examples/mobile-client/voip-call/server/main.ts Adds avatars, signaling WebSocket, and updates push payloads.
examples/mobile-client/voip-call/README.md Expands setup docs for handshake, timeouts, Recents/intents, hold, Android setup.
examples/mobile-client/voip-call/app/src/voip/VoipProvider.tsx Implements handshake-connected activation, hold/mute syncing, call waiting swap logic, intents.
examples/mobile-client/voip-call/app/src/voip/VoipContext.ts Extends context with end reasons, hold state, handle/outgoing info.
examples/mobile-client/voip-call/app/src/user/UserProvider.tsx Switches to user summaries with avatars; exposes avatarUrlFor.
examples/mobile-client/voip-call/app/src/user/UserContext.ts Defines UserSummary and updates user context types.
examples/mobile-client/voip-call/app/src/user/index.ts Re-exports UserSummary.
examples/mobile-client/voip-call/app/src/signaling/useCallSignaling.ts New WS signaling hook for cancel/reject propagation.
examples/mobile-client/voip-call/app/src/screens/UsersScreen.tsx Renames directory to users list and renders avatar images.
examples/mobile-client/voip-call/app/src/screens/OutgoingCallScreen.tsx Uses avatars and passes explicit end reason.
examples/mobile-client/voip-call/app/src/screens/LoginScreen.tsx Adds Fishjam branding/logo.
examples/mobile-client/voip-call/app/src/screens/InCallScreen.tsx Adds hold UI, better status bar handling, avatar usage, disables controls on hold.
examples/mobile-client/voip-call/app/src/components/VideoCallView.tsx Adds avatar support and improves PiP video rendering behavior.
examples/mobile-client/voip-call/app/src/components/InCallButton.tsx Adds disabled state and tweaks sizing.
examples/mobile-client/voip-call/app/src/components/Avatar.tsx Adds remote image avatar support with fallback-to-initials.
examples/mobile-client/voip-call/app/package.json Adds expo-splash-screen.
examples/mobile-client/voip-call/app/App.tsx Wires signaling + end logging, swaps to UsersScreen, adjusts status bar style.
examples/mobile-client/voip-call/app/app.json Adds icons/splash config and expo-splash-screen plugin config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

console.error('Failed to end call:', err),
);
}
}, [remotePeers.length, status, endCall, setTelecomCallActive]);
Comment on lines +67 to 85
const serviceAccount: ServiceAccount = JSON.parse(
await Deno.readTextFile("./fcm-credentials.json"),
);

const authClient = serviceAccount
? new JWT({
email: serviceAccount.client_email,
key: serviceAccount.private_key,
scopes: ["https://www.googleapis.com/auth/firebase.messaging"],
})
: null;
const authClient = new JWT({
email: serviceAccount.client_email,
key: serviceAccount.private_key,
scopes: ["https://www.googleapis.com/auth/firebase.messaging"],
});

async function getAccessToken(client: JWT): Promise<string> {
const { token } = await client.getAccessToken();
async function getAccessToken(): Promise<string> {
const { token } = await authClient.getAccessToken();
if (!token) throw new Error("Failed to get access token");
return token;
}

async function sendFcmPush(params: PushParams): Promise<void> {
if (!serviceAccount || !authClient) {
throw new Error("Android push requires ./fcm-credentials.json");
}
const accessToken = await getAccessToken(authClient);
const accessToken = await getAccessToken();

Comment on lines 120 to 123
const apnsPem = await readIfPresent("./apns.pem");
const apnsClient = apnsPem
? Deno.createHttpClient({ cert: apnsPem, key: apnsPem })
: null;
Comment on lines 125 to 128
async function sendApnsPush(params: PushParams): Promise<void> {
if (!apnsClient) {
throw new Error("iOS push requires ./apns.pem");
}
const res = await fetch(`https://${APNS_HOST}/3/device/${params.token}`, {
client: apnsClient,
method: "POST",
Comment on lines +93 to +98
if (prevStatus === 'connecting' && prevCall.isOutgoing) {
sendSignal({
type: 'call-cancelled',
to: prevCall.displayName,
roomName: prevCall.roomName,
});
Comment on lines +54 to +60
const onWaitingCallDeclined = useCallback((payload: VoipIncomingPayload) => {
sendSignalRef.current?.({
type: 'call-rejected',
to: payload.displayName,
roomName: payload.roomName,
});
}, []);
Comment on lines +196 to +200
{
"voip": {
"includeCallsInRecents": true
}
}
the app is backgrounded or killed.
4. Tap **Answer** / **End** on the system UI and confirm the `answer` / `ended`
events log in Metro.
5. With `includeCallsInRecents` enabled, complete a call and confirm that it
"incomingCallTimeout": 45,
"outgoingCallTimeout": 60,
"fulfillAnswerCallTimeout": 10,
"includeCallsInRecents": true
Comment on lines +82 to +90
<meta-data android:name="FishjamVoipIncomingCallTimeout" android:value="45" />
<meta-data android:name="FishjamVoipOutgoingCallTimeout" android:value="60" />
<meta-data android:name="FishjamVoipFulfillAnswerTimeout" android:value="10" />
</application>
```

For iOS, add the same timeout values as numeric `Info.plist` keys:
`FishjamVoipIncomingCallTimeout`, `FishjamVoipOutgoingCallTimeout`, and
`FishjamVoipFulfillAnswerTimeout`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants