[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
Draft
[FCE-3435] Add call signaling handshake, native timeouts, hold, call waiting and Recents redial to VoIP calls#562Magmusacy wants to merge 47 commits into
Magmusacy wants to merge 47 commits into
Conversation
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Builds on the Android integration from #561 with the remaining call-lifecycle and native-call features:
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 aconnectingstate in between (fulfillIncomingCallConnected/failIncomingCallConnected,reportOutgoingCallConnected).setCallHeld,isCallHeld) and call waiting for a second incoming call (via areact-native-webrtcsubmodule bump).enableCallIntentsplugin option so calls appear in the system Recents and can be redialed from there (iOS).DirectoryScreen→UsersScreen, 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
Types of changes
not work as expected)