Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0a19388
feat(voip): add MediaCallsAnswerRequest for REST accept/reject
diegolmello Apr 10, 2026
f48c448
feat(voip): migrate accept() and reject() from DDP to REST
diegolmello Apr 10, 2026
9167fa4
feat(voip): remove dead buildMediaCallAnswerParams after REST migration
diegolmello Apr 10, 2026
4b43763
fix(voip): guard against nil API in accept/reject on iOS
diegolmello Apr 10, 2026
ba637f8
fix(voip): add missing RNCallKeep.endCall on user hangup before answer
diegolmello Apr 10, 2026
d9233e7
fix(voip): add missing RNCallKeep.endCall on user hangup before answer
diegolmello Apr 10, 2026
ff477fd
Merge branch 'refactor.ddp-ios' of github.com:RocketChat/Rocket.Chat.…
diegolmello Apr 10, 2026
65b3789
fix(voip): remove duplicate MediaCallsAnswerRequest.swift and add to …
diegolmello Apr 10, 2026
4d13bbb
xcode
diegolmello Apr 10, 2026
da0b23d
merge: resolve conflicts with feat.voip-lib-new (PR 7124)
diegolmello Apr 10, 2026
8d13809
Merge branch 'feat.voip-lib-new' into refactor.ddp-ios
diegolmello Apr 14, 2026
92045ec
Fix build
diegolmello Apr 14, 2026
aa9a875
feat(voip): enhance VoIP handling with deep link normalization and RE…
diegolmello Apr 15, 2026
aec707d
feat(deepLinking): add normalizeDeepLinkingServerHost function and tests
diegolmello Apr 15, 2026
11571bf
chore(voip): address PR review slop in REST state signals refactor
diegolmello Apr 15, 2026
e2e8f8a
test(voip): mock store.getState in MediaCallEvents tests for host gate
diegolmello Apr 15, 2026
70f6239
fix(voip): drop unused async on host-gate test (require-await lint)
diegolmello Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 32 additions & 107 deletions ios/Libraries/VoipService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@ public final class VoipService: NSObject {
/// cleared when that call's DDP accept finishes or another exit path runs for that `callId`.
private static var nativeAcceptHandledCallIds = Set<String>()

private enum VoipMediaCallAnswerKind {
case accept
case reject
}

// MARK: - Static Methods (Called from VoipModule.mm and AppDelegate)

/// Registers for VoIP push notifications via PushKit
Expand Down Expand Up @@ -413,40 +408,6 @@ public final class VoipService: NSObject {
ddpRegistry.stopAllClients()
}

// MARK: - Native DDP signaling (accept / reject)

/// `contractId` must match JS `getUniqueIdSync()` from react-native-device-info (`DeviceUID` on iOS; Android uses `Settings.Secure.ANDROID_ID` in VoipNotification).
private static func buildMediaCallAnswerParams(payload: VoipPayload, kind: VoipMediaCallAnswerKind) -> [Any]? {
let credentialStorage = Storage()
guard let credentials = credentialStorage.getCredentials(server: payload.host.removeTrailingSlash()) else {
#if DEBUG
print("[\(TAG)] Missing credentials, cannot build media-call answer params for \(payload.callId)")
#endif
stopDDPClientInternal(callId: payload.callId)
return nil
}

var signal: [String: Any] = [
"callId": payload.callId,
"contractId": DeviceUID.uid(),
"type": "answer",
"answer": kind == .accept ? "accept" : "reject"
]
if kind == .accept {
signal["supportedFeatures"] = ["audio"]
}

guard
let signalData = try? JSONSerialization.data(withJSONObject: signal),
let signalString = String(data: signalData, encoding: .utf8)
else {
stopDDPClientInternal(callId: payload.callId)
return nil
}

return ["\(credentials.userId)/media-calls", signalString]
}

/// Native DDP accept when the user answers via CallKit (parity with Android `VoipNotification.handleAcceptAction`).
private static func handleNativeAccept(payload: VoipPayload) {
if nativeAcceptHandledCallIds.contains(payload.callId) {
Expand Down Expand Up @@ -506,36 +467,28 @@ public final class VoipService: NSObject {
}
}

guard let client = ddpRegistry.clientFor(callId: payload.callId) else {
guard let api = API(server: payload.host) else {
#if DEBUG
print("[\(TAG)] Native DDP client unavailable for accept \(payload.callId); relying on JS")
print("[\(TAG)] Failed to create API for host: \(payload.host)")
#endif
finishAccept(false)
return
}

guard let params = buildMediaCallAnswerParams(payload: payload, kind: .accept) else {
finishAccept(false)
return
}

if ddpRegistry.isLoggedIn(callId: payload.callId) {
client.callMethod("stream-notify-user", params: params) { success in
#if DEBUG
print("[\(TAG)] Native accept signal result for \(payload.callId): \(success)")
#endif
DispatchQueue.main.async { finishAccept(success) }
}
} else {
client.queueMethodCall("stream-notify-user", params: params) { success in
#if DEBUG
print("[\(TAG)] Queued native accept signal result for \(payload.callId): \(success)")
#endif
DispatchQueue.main.async { finishAccept(success) }
api.fetch(request: MediaCallsAnswerRequest(
callId: payload.callId,
contractId: DeviceUID.uid(),
answer: "accept",
supportedFeatures: ["audio"]
)) { result in
DispatchQueue.main.async {
switch result {
case .resource(let response) where response.success:
finishAccept(true)
default:
finishAccept(false)
}
}
#if DEBUG
print("[\(TAG)] Queued native accept signal for \(payload.callId)")
#endif
}
}

Expand All @@ -556,55 +509,30 @@ public final class VoipService: NSObject {
// End the just-reported CallKit call immediately (reason 2 = unanswered / declined).
RNCallKeep.endCall(withUUID: payload.callId, reason: 2)

// Send reject signal via native DDP if available, otherwise queue it.
if ddpRegistry.isLoggedIn(callId: payload.callId) {
sendRejectSignal(payload: payload)
} else {
queueRejectSignal(payload: payload)
}
// Send reject signal via REST
reject(payload: payload)

#if DEBUG
print("[\(TAG)] Rejected busy call \(payload.callId) — user already on a call")
#endif
}

private static func sendRejectSignal(payload: VoipPayload) {
guard let client = ddpRegistry.clientFor(callId: payload.callId) else {
private static func reject(payload: VoipPayload) {
guard let api = API(server: payload.host) else {
#if DEBUG
print("[\(TAG)] Native DDP client unavailable, cannot send reject for \(payload.callId)")
#endif
return
}

guard let params = buildMediaCallAnswerParams(payload: payload, kind: .reject) else {
return
}

client.callMethod("stream-notify-user", params: params) { success in
#if DEBUG
print("[\(TAG)] Native reject signal result for \(payload.callId): \(success)")
print("[\(TAG)] Failed to create API for reject: \(payload.host)")
#endif
stopDDPClientInternal(callId: payload.callId)
}
}

private static func queueRejectSignal(payload: VoipPayload) {
guard let client = ddpRegistry.clientFor(callId: payload.callId) else {
#if DEBUG
print("[\(TAG)] Native DDP client unavailable, cannot queue reject for \(payload.callId)")
#endif
return
}

guard let params = buildMediaCallAnswerParams(payload: payload, kind: .reject) else {
return
}

client.queueMethodCall("stream-notify-user", params: params) { success in
#if DEBUG
print("[\(TAG)] Queued native reject signal result for \(payload.callId): \(success)")
#endif
stopDDPClientInternal(callId: payload.callId)
api.fetch(request: MediaCallsAnswerRequest(
callId: payload.callId,
contractId: DeviceUID.uid(),
answer: "reject",
supportedFeatures: nil
)) { _ in
self.stopDDPClientInternal(callId: payload.callId)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -666,15 +594,12 @@ public final class VoipService: NSObject {
return
}

let endedCallId = observedCall.payload.callId
observedIncomingCalls.removeValue(forKey: call.uuid)
cancelIncomingCallTimeout(for: observedCall.payload.callId)
clearNativeAcceptDedupe(for: observedCall.payload.callId)
cancelIncomingCallTimeout(for: endedCallId)
clearNativeAcceptDedupe(for: endedCallId)

let endedCallId = observedCall.payload.callId
if ddpRegistry.isLoggedIn(callId: endedCallId) {
sendRejectSignal(payload: observedCall.payload)
} else {
queueRejectSignal(payload: observedCall.payload)
}
RNCallKeep.endCall(withUUID: endedCallId, reason: 3)
reject(payload: observedCall.payload)
}
}
Loading
Loading