diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc9d6944..86742d890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix label color when presenting. [#1077](https://github.com/GetStream/stream-video-swift/pull/1077) - Ensure CallKit push token updates and invalidation mutate `deviceToken` on the main actor to avoid Swift concurrency/actor-isolation issues. [#1076](https://github.com/GetStream/stream-video-swift/pull/1076) - Ensure CallKit joins keep the answer action completion alive until WebRTC has configured the audio device module. [#1081](https://github.com/GetStream/stream-video-swift/pull/1081) +- Update incoming call acceptance to move `CallViewModel` into `.joining` before the call finishes entering, so the joining UI appears immediately. [#1079](https://github.com/GetStream/stream-video-swift/pull/1079) # [1.43.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.43.0) _February 27, 2026_ diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 6aca80f5b..64f0f8161 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -579,6 +579,11 @@ open class CallViewModel: ObservableObject { do { hasAcceptedCall = true try await call.accept() + + // Mirror `joinCall` so the incoming UI is dismissed before + // `enterCall` finishes the async join flow. + await MainActor.run { self.setCallingState(.joining) } + enterCall( call: call, callType: callType, diff --git a/StreamVideoSwiftUITests/CallViewModel_Tests.swift b/StreamVideoSwiftUITests/CallViewModel_Tests.swift index c2763cd32..abe40a657 100644 --- a/StreamVideoSwiftUITests/CallViewModel_Tests.swift +++ b/StreamVideoSwiftUITests/CallViewModel_Tests.swift @@ -2,6 +2,7 @@ // Copyright © 2026 Stream.io Inc. All rights reserved. // +import Combine @testable import StreamVideo @testable import StreamVideoSwiftUI import StreamWebRTC @@ -405,6 +406,34 @@ final class CallViewModel_Tests: XCTestCase, @unchecked Sendable { await assertCallingState(.inCall) } + func test_incomingCall_acceptCall_updatesCallingStateToJoiningBeforeInCall() async throws { + // Given + await prepareIncomingCallScenario() + let joiningStateExpectation = expectation( + description: "CallingState becomes joining" + ) + joiningStateExpectation.assertForOverFulfill = false + var cancellable: AnyCancellable? + + // Capture the transient state because `acceptCall` continues into the + // async `enterCall` flow immediately after the acceptance request. + cancellable = subject.$callingState + .dropFirst() + .sink { state in + if state == .joining { + joiningStateExpectation.fulfill() + } + } + defer { cancellable?.cancel() } + + // When + subject.acceptCall(callType: callType, callId: callId) + + // Then + await fulfillment(of: [joiningStateExpectation], timeout: defaultTimeout) + await assertCallingState(.inCall) + } + func test_incomingCall_acceptedFromSameUserElsewhere_callingStateChangesToIdle() async throws { // Given await prepareIncomingCallScenario()