@@ -174,7 +174,16 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
174174 . default
175175 . publisher ( for: Notification . Name ( CallNotification . callEnded) )
176176 . compactMap { $0. object as? Call }
177- . sink { [ weak self] in self ? . callEnded ( $0. cId, ringingTimedOut: false ) }
177+ . sink {
178+ [ weak self] in self ? . callEnded (
179+ $0. cId,
180+ ringingTimedOut: false ,
181+ leaveReason: StreamRejectionReasonProvider
182+ . HandledCallReason
183+ . callEndedLocally
184+ . rawValue
185+ )
186+ }
178187
179188 /// - Important:
180189 /// It used to debounce System's attempts to mute/unmute the call. It seems that the system
@@ -234,7 +243,14 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
234243 """ ,
235244 subsystems: . callKit
236245 )
237- callEnded ( cid, ringingTimedOut: false )
246+ callEnded (
247+ cid,
248+ ringingTimedOut: false ,
249+ leaveReason: StreamRejectionReasonProvider
250+ . HandledCallReason
251+ . notConfigured
252+ . rawValue
253+ )
238254 return
239255 }
240256
@@ -268,21 +284,19 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
268284 . reportCall ( )
269285
270286 let callState = try await callEntry. call. get ( )
271- if !checkIfCallWasHandled( callState: callState) {
272- callEntry. createdBy = callState. call. createdBy. toUser
273- setUpRingingTimer ( for: callState)
274- } else {
287+ if let leaveReason = checkIfCallWasHandled ( callState: callState) {
275288 log. debug (
276- """
277- Rejecting VoIP incoming call as it has been handled.
278- callUUID: \( callUUID)
279- cid: \( cid)
280- callerId: \( callerId)
281- callerName: \( localizedCallerName)
282- """ ,
289+ " Ending call with reason: \( leaveReason) { uuid: \( callUUID) , cid: \( cid) , callerId: \( callerId) , callerName: \( localizedCallerName) } " ,
283290 subsystems: . callKit
284291 )
285- callEnded ( cid, ringingTimedOut: false )
292+ callEnded (
293+ cid,
294+ ringingTimedOut: false ,
295+ leaveReason: leaveReason
296+ )
297+ } else {
298+ callEntry. createdBy = callState. call. createdBy. toUser
299+ setUpRingingTimer ( for: callState)
286300 }
287301 } catch {
288302 log. error (
@@ -295,7 +309,14 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
295309 subsystems: . callKit,
296310 error: error
297311 )
298- callEnded ( cid, ringingTimedOut: false )
312+ callEnded (
313+ cid,
314+ ringingTimedOut: false ,
315+ leaveReason: StreamRejectionReasonProvider
316+ . HandledCallReason
317+ . reportCallFailed
318+ . rawValue
319+ )
299320 }
300321 }
301322 }
@@ -376,12 +397,29 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
376397 callCache. remove ( for: newCallEntry. call. cId)
377398 }
378399
379- /// Handle call end event.
400+ /// Handles a ringing or active CallKit call ending.
401+ ///
402+ /// Calling this method requests the matching `CXEndCallAction`. If the call
403+ /// is still ringing, the eventual reject request uses `leaveReason` when
404+ /// provided; otherwise the SDK derives a rejection reason from the current
405+ /// ringing state.
406+ ///
407+ /// - Parameters:
408+ /// - cId: The call CID managed by CallKit.
409+ /// - ringingTimedOut: Whether the ringing timer elapsed before the user
410+ /// answered.
411+ /// - leaveReason: An explicit backend rejection reason to use for ringing
412+ /// calls that were already handled elsewhere.
380413 open func callEnded(
381414 _ cId: String ,
382- ringingTimedOut: Bool
415+ ringingTimedOut: Bool ,
416+ leaveReason: String ? = nil
383417 ) {
384- endCall ( cId, ringingTimedOut: ringingTimedOut)
418+ endCall (
419+ cId,
420+ ringingTimedOut: ringingTimedOut,
421+ leaveReason: leaveReason
422+ )
385423 }
386424
387425 /// Called when a participant leaves the call.
@@ -569,12 +607,16 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
569607 stackEntry. call. leave ( reason: stackEntry. leaveReason)
570608 } else {
571609 do {
572- let rejectionReason = await streamVideo?
573- . rejectionReasonProvider
574- . reason (
575- for: stackEntry. call. cId,
576- ringTimeout: stackEntry. ringingTimedOut
577- )
610+ let rejectionReason = if let leaveReason = stackEntry. leaveReason {
611+ leaveReason
612+ } else {
613+ await streamVideo?
614+ . rejectionReasonProvider
615+ . reason (
616+ for: stackEntry. call. cId,
617+ ringTimeout: stackEntry. ringingTimedOut
618+ )
619+ }
578620 log. debug (
579621 """
580622 Rejecting with reason: \( rejectionReason ?? " nil " )
@@ -639,32 +681,32 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
639681 try await callController. requestTransaction ( with: action)
640682 }
641683
642- /// Return whether this call was already accepted or rejected.
643- open func checkIfCallWasHandled( callState: GetCallResponse ) -> Bool {
684+ /// Checks whether the incoming ringing call was already handled before
685+ /// this device finished presenting CallKit.
686+ ///
687+ /// CallKit uses this gate right after fetching the latest backend state,
688+ /// before starting the local ringing timer. If a leave reason is returned,
689+ /// the service immediately ends the CallKit flow instead of continuing to
690+ /// ring locally.
691+ ///
692+ /// - Parameter callState: The latest backend state for the ringing call.
693+ /// - Returns: A backend leave reason when CallKit should stop ringing
694+ /// immediately, or `nil` when the local device should keep ringing.
695+ open func checkIfCallWasHandled( callState: GetCallResponse ) -> String ? {
644696 guard let streamVideo else {
645697 log. warning (
646698 " CallKit operation: \( #function) cannot be fulfilled because StreamVideo is nil. " ,
647699 subsystems: . callKit
648700 )
649- return false
650- }
651-
652- var allMembers = callState. members. map ( \. user. toUser)
653- let creator = callState. call. createdBy. toUser
654- let isUserInMembersArray = allMembers. filter { $0. id == creator. id } . isEmpty == false
655- if !isUserInMembersArray {
656- allMembers. append ( creator)
701+ return StreamRejectionReasonProvider
702+ . HandledCallReason
703+ . notConfigured
704+ . rawValue
657705 }
658- let allCallees = allMembers. filter { $0. id != creator. id }
659706
660- let currentUserId = streamVideo. user. id
661- let acceptedBy = callState. call. session? . acceptedBy ?? [ : ]
662- let rejectedBy = callState. call. session? . rejectedBy ?? [ : ]
663- let isAccepted = acceptedBy [ currentUserId] != nil
664- let isRejected = rejectedBy [ currentUserId] != nil
665- let isRejectedByEveryoneElse = ( allCallees. endIndex > 1 )
666- && rejectedBy. keys. filter { $0 != currentUserId } . count == ( allCallees. endIndex - 1 )
667- return isAccepted || isRejected || isRejectedByEveryoneElse
707+ return streamVideo
708+ . rejectionReasonProvider
709+ . reason ( callState: callState)
668710 }
669711
670712 /// Start the ringing timeout timer for the call.
@@ -716,7 +758,14 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
716758 guard let self else { return }
717759 switch event {
718760 case let . typeCallEndedEvent( response) :
719- callEnded ( response. callCid, ringingTimedOut: false )
761+ callEnded (
762+ response. callCid,
763+ ringingTimedOut: false ,
764+ leaveReason: StreamRejectionReasonProvider
765+ . HandledCallReason
766+ . callEventReceived
767+ . rawValue
768+ )
720769 case let . typeCallAcceptedEvent( response) :
721770 callAccepted ( response)
722771 case let . typeCallRejectedEvent( response) :
@@ -819,10 +868,13 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
819868 return
820869 }
821870
822- endCall (
871+ callEnded (
823872 activeCallEntry. call. cId,
824873 ringingTimedOut: false ,
825- leaveReason: " auto-leave "
874+ leaveReason: StreamRejectionReasonProvider
875+ . HandledCallReason
876+ . autoLeave
877+ . rawValue
826878 )
827879 }
828880
0 commit comments