Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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)
}
}

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