@capgo/capacitor-stream-call 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,6 @@ import io.getstream.video.android.core.StreamVideo
29
29
  import io.getstream.video.android.core.StreamVideoBuilder
30
30
  import io.getstream.video.android.core.notifications.NotificationConfig
31
31
  import io.getstream.video.android.core.notifications.NotificationHandler
32
- import io.getstream.video.android.core.sounds.emptyRingingConfig
33
32
  import io.getstream.video.android.core.sounds.toSounds
34
33
  import io.getstream.video.android.model.StreamCallId
35
34
  import io.getstream.video.android.model.User
@@ -46,6 +45,7 @@ import io.getstream.android.video.generated.models.CallCreatedEvent
46
45
  import io.getstream.android.video.generated.models.CallEndedEvent
47
46
  import io.getstream.android.video.generated.models.CallMissedEvent
48
47
  import io.getstream.android.video.generated.models.CallRejectedEvent
48
+ import io.getstream.android.video.generated.models.CallRingEvent
49
49
  import io.getstream.android.video.generated.models.CallSessionEndedEvent
50
50
  import io.getstream.android.video.generated.models.CallSessionStartedEvent
51
51
  import io.getstream.android.video.generated.models.VideoEvent
@@ -185,7 +185,7 @@ public class StreamCallPlugin : Plugin() {
185
185
  declineCall(declinedCall)
186
186
  },
187
187
  onAcceptCall = { acceptedCall ->
188
- acceptCall(acceptedCall)
188
+ internalAcceptCall(acceptedCall)
189
189
  },
190
190
  onHideIncomingCall = {
191
191
  hideIncomingCall()
@@ -207,7 +207,7 @@ public class StreamCallPlugin : Plugin() {
207
207
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
208
208
  kotlinx.coroutines.GlobalScope.launch {
209
209
  call?.get()
210
- call?.let { acceptCall(it) }
210
+ call?.let { internalAcceptCall(it) }
211
211
  }
212
212
  }
213
213
  }
@@ -565,6 +565,9 @@ public class StreamCallPlugin : Plugin() {
565
565
  client.subscribe { event: VideoEvent ->
566
566
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
567
567
  when (event) {
568
+ is CallRingEvent -> {
569
+ updateCallStatusAndNotify(event.callCid, "ringing")
570
+ }
568
571
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
569
572
  is CallCreatedEvent -> {
570
573
  val callCid = event.callCid
@@ -744,8 +747,42 @@ public class StreamCallPlugin : Plugin() {
744
747
  })
745
748
  }
746
749
 
750
+ @PluginMethod
751
+ public fun acceptCall(call: PluginCall) {
752
+ try {
753
+ val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
754
+ if (streamVideoCall == null) {
755
+ call.reject("Ringing call is null")
756
+ return
757
+ }
758
+ kotlinx.coroutines.GlobalScope.launch {
759
+ internalAcceptCall(streamVideoCall)
760
+ }
761
+ } catch (t: Throwable) {
762
+ android.util.Log.d("StreamCallPlugin", "JS -> acceptCall fail", t);
763
+ call.reject("Cannot acceptCall")
764
+ }
765
+ }
766
+
767
+ @PluginMethod
768
+ public fun rejectCall(call: PluginCall) {
769
+ try {
770
+ val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
771
+ if (streamVideoCall == null) {
772
+ call.reject("Ringing call is null")
773
+ return
774
+ }
775
+ kotlinx.coroutines.GlobalScope.launch {
776
+ declineCall(streamVideoCall)
777
+ }
778
+ } catch (t: Throwable) {
779
+ android.util.Log.d("StreamCallPlugin", "JS -> rejectCall fail", t);
780
+ call.reject("Cannot rejectCall")
781
+ }
782
+ }
783
+
747
784
  @OptIn(DelicateCoroutinesApi::class)
748
- private fun acceptCall(call: Call) {
785
+ private fun internalAcceptCall(call: Call) {
749
786
  kotlinx.coroutines.GlobalScope.launch {
750
787
  try {
751
788
  // Stop ringtone
@@ -3,53 +3,6 @@ import StreamVideo
3
3
  import StreamVideoSwiftUI
4
4
  import Combine
5
5
 
6
- class CallOverlayViewModel: ObservableObject {
7
- @Published var streamVideo: StreamVideo?
8
- @Published var call: Call?
9
- @Published var callState: CallState?
10
- @Published var viewModel: CallViewModel?
11
- @Published var participants: [CallParticipant] = []
12
-
13
- private var participantsSubscription: AnyCancellable?
14
-
15
- init(streamVideo: StreamVideo?) {
16
- self.streamVideo = streamVideo
17
- }
18
-
19
- @MainActor
20
- func updateCall(_ call: Call?) {
21
- self.call = call
22
- // Clean up previous subscription if any
23
- participantsSubscription?.cancel()
24
-
25
- if let call = call {
26
- participantsSubscription = call.state.$participants.sink { [weak self] participants in
27
- print("Participants update \(participants.map { $0.name })")
28
- self?.participants = participants
29
- }
30
- self.callState = call.state
31
- participantsSubscription = call.state.$callSettings.sink { [weak self] callSettings in
32
- print("Call settings update")
33
- self?.viewModel = CallViewModel(callSettings: callSettings)
34
- self?.viewModel?.setActiveCall(call)
35
- }
36
- } else {
37
- // Clear participants when call ends
38
- self.participants = []
39
- self.callState = nil
40
- }
41
- }
42
-
43
- @MainActor
44
- func updateStreamVideo(_ streamVideo: StreamVideo?) {
45
- self.streamVideo = streamVideo
46
- if streamVideo == nil {
47
- self.call = nil
48
- self.callState = nil
49
- }
50
- }
51
- }
52
-
53
6
  class CallOverlayViewFactory: ViewFactory {
54
7
  // ... existing ViewFactory methods ...
55
8
  func makeVideoParticipantView(
@@ -73,26 +26,22 @@ class CallOverlayViewFactory: ViewFactory {
73
26
  }
74
27
 
75
28
  struct CallOverlayView: View {
76
- @ObservedObject var viewModel: CallOverlayViewModel
29
+ @ObservedObject var viewModel: CallViewModel
77
30
  @State private var safeAreaInsets: EdgeInsets = .init()
78
31
  private let viewFactory: CallOverlayViewFactory
79
32
 
80
- init(viewModel: CallOverlayViewModel) {
33
+ init(viewModel: CallViewModel) {
81
34
  self.viewModel = viewModel
82
35
  self.viewFactory = CallOverlayViewFactory()
83
36
  }
84
37
 
85
38
  var body: some View {
86
39
  VStack(spacing: 0) {
87
- if let viewModelStandard = viewModel.viewModel {
88
- ZStack {
89
- CustomCallView(viewFactory: viewFactory, viewModel: viewModelStandard)
90
- }
91
- .padding(.top, safeAreaInsets.top)
92
- .padding(.bottom, safeAreaInsets.bottom)
93
- } else {
94
- Color.white
40
+ ZStack {
41
+ CustomCallView(viewFactory: viewFactory, viewModel: viewModel)
95
42
  }
43
+ .padding(.top, safeAreaInsets.top)
44
+ .padding(.bottom, safeAreaInsets.bottom)
96
45
  }
97
46
  .edgesIgnoringSafeArea(.all)
98
47
  .overlay(
@@ -108,7 +57,7 @@ struct CallOverlayView: View {
108
57
  }
109
58
 
110
59
  private func changeTrackVisibility(_ participant: CallParticipant?, isVisible: Bool) {
111
- print("changeTrackVisibility for \(participant?.userId), visible: \(isVisible)")
60
+ print("changeTrackVisibility for \(String(describing: participant?.userId)), visible: \(isVisible)")
112
61
  guard let participant = participant,
113
62
  let call = viewModel.call else { return }
114
63
  Task {
@@ -118,26 +67,17 @@ struct CallOverlayView: View {
118
67
  }
119
68
 
120
69
  extension CallOverlayView {
121
- static func create(streamVideo: StreamVideo?) -> (UIHostingController<CallOverlayView>, CallOverlayViewModel) {
122
- let viewModel = CallOverlayViewModel(streamVideo: streamVideo)
123
- let view = CallOverlayView(viewModel: viewModel)
70
+ static func create(callViewModel: CallViewModel) -> UIHostingController<CallOverlayView> {
71
+ let view = CallOverlayView(viewModel: callViewModel)
124
72
  let hostingController = UIHostingController(rootView: view)
125
73
  hostingController.view.backgroundColor = .clear
126
74
 
127
75
  // Make sure we respect safe areas
128
76
  hostingController.view.insetsLayoutMarginsFromSafeArea = true
129
77
 
130
- return (hostingController, viewModel)
131
- }
132
- }
133
-
134
- #if DEBUG
135
- struct CallOverlayView_Previews: PreviewProvider {
136
- static var previews: some View {
137
- CallOverlayView(viewModel: CallOverlayViewModel(streamVideo: nil))
78
+ return (hostingController)
138
79
  }
139
80
  }
140
- #endif
141
81
 
142
82
  struct SafeAreaInsetsKey: PreferenceKey {
143
83
  static var defaultValue: EdgeInsets = .init()
@@ -50,18 +50,6 @@ struct ParticipantsView: View {
50
50
  var localParticipant: CallParticipant
51
51
  @State private var labeledFrames: [ViewFramePreferenceData] = []
52
52
 
53
- private func findTouchInterceptView() -> TouchInterceptView? {
54
- // Find the TouchInterceptView by traversing up the view hierarchy
55
- var currentView = UIApplication.shared.windows.first?.rootViewController?.view
56
- while let view = currentView {
57
- if let touchInterceptView = view as? TouchInterceptView {
58
- return touchInterceptView
59
- }
60
- currentView = view.superview
61
- }
62
- return nil
63
- }
64
-
65
53
  var body: some View {
66
54
  GeometryReader { proxy in
67
55
  if !participants.isEmpty {
@@ -170,20 +158,6 @@ struct ParticipantsView: View {
170
158
  }
171
159
  }
172
160
  }
173
- .onPreferenceChange(ViewFramePreferenceKey.self) { frames in
174
- print("ParticipantsView - Received frame updates:")
175
- print("Number of frames: \(frames.count)")
176
- frames.forEach { frame in
177
- print("Label: \(frame.label), Frame: \(frame.frame)")
178
- }
179
- self.labeledFrames = frames
180
- if let touchInterceptView = findTouchInterceptView() {
181
- print("ParticipantsView - Found TouchInterceptView, updating frames")
182
- touchInterceptView.updateLabeledFrames(frames)
183
- } else {
184
- print("ParticipantsView - Failed to find TouchInterceptView!")
185
- }
186
- }
187
161
  } else {
188
162
  Color.gray
189
163
  }
@@ -41,20 +41,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
41
41
 
42
42
  private var overlayView: UIView?
43
43
  private var hostingController: UIHostingController<CallOverlayView>?
44
- private var overlayViewModel: CallOverlayViewModel?
45
44
  private var tokenSubscription: AnyCancellable?
46
45
  private var activeCallSubscription: AnyCancellable?
47
46
  private var lastVoIPToken: String?
48
- private var touchInterceptView: TouchInterceptView?
49
47
 
50
48
  private var streamVideo: StreamVideo?
51
49
 
52
- // Track the current active call ID
53
- private var currentActiveCallId: String?
54
-
55
50
  // Store current call info for getCallStatus
56
51
  private var currentCallId: String = ""
57
52
  private var currentCallState: String = ""
53
+ private var hasNotifiedCallJoined: Bool = false
58
54
 
59
55
  @Injected(\.callKitAdapter) var callKitAdapter
60
56
  @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
@@ -63,6 +59,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
63
59
  // Add class property to store call states
64
60
  private var callStates: [String: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)] = [:]
65
61
 
62
+ // Declare as optional and initialize in load() method
63
+ private var callViewModel: CallViewModel?
64
+
66
65
  // Helper method to update call status and notify listeners
67
66
  private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil) {
68
67
  // Update stored call info
@@ -95,7 +94,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
95
94
  if self.apiKey == nil {
96
95
  fatalError("Cannot get apikey")
97
96
  }
98
-
97
+
99
98
  // Check if we have a logged in user for handling incoming calls
100
99
  if let credentials = SecureUserRepository.shared.loadCurrentUser() {
101
100
  print("Loading user for StreamCallPlugin: \(credentials.user.name)")
@@ -118,32 +117,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
118
117
  self.webView?.navigationDelegate = self.webviewDelegate
119
118
  }
120
119
 
121
- // private func cleanupStreamVideo() {
122
- // // Cancel subscriptions
123
- // tokenSubscription?.cancel()
124
- // tokenSubscription = nil
125
- // activeCallSubscription?.cancel()
126
- // activeCallSubscription = nil
127
- // lastVoIPToken = nil
128
- //
129
- // // Cleanup UI
130
- // Task { @MainActor in
131
- // self.overlayViewModel?.updateCall(nil)
132
- // self.overlayViewModel?.updateStreamVideo(nil)
133
- // self.overlayView?.removeFromSuperview()
134
- // self.overlayView = nil
135
- // self.hostingController = nil
136
- // self.overlayViewModel = nil
137
- //
138
- // // Reset webview
139
- // self.webView?.isOpaque = true
140
- // self.webView?.backgroundColor = .white
141
- // self.webView?.scrollView.backgroundColor = .white
142
- // }
143
- //
144
- // state = .notInitialized
145
- // }
146
-
147
120
  private func requireInitialized() throws {
148
121
  guard state == .initialized else {
149
122
  throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "StreamVideo not initialized"])
@@ -192,191 +165,132 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
192
165
  }
193
166
 
194
167
  private func setupActiveCallSubscription() {
195
- if let streamVideo = streamVideo {
196
- Task {
197
- for await event in streamVideo.subscribe() {
198
- // print("Event", event)
199
- if let ringingEvent = event.rawValue as? CallRingEvent {
200
- updateCallStatusAndNotify(callId: ringingEvent.callCid, state: "ringing")
201
- continue
202
- }
203
-
204
- if let callCreatedEvent = event.rawValue as? CallCreatedEvent {
205
- print("CallCreatedEvent \(String(describing: userId))")
206
-
207
- let callCid = callCreatedEvent.callCid
208
- let members = callCreatedEvent.members
209
-
210
- // Create timer on main thread
211
- await MainActor.run {
212
- // Store in the combined callStates map
213
- self.callStates[callCid] = (
214
- members: members,
215
- participantResponses: [:],
216
- createdAt: Date(),
217
- timer: nil
218
- )
219
-
220
- // Start timer to check for timeout every second
221
- // Use @objc method as timer target to avoid sendable closure issues
222
- let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.checkCallTimeoutTimer(_:)), userInfo: callCid, repeats: true)
223
-
224
- // Update timer in callStates
225
- self.callStates[callCid]?.timer = timer
226
- }
227
-
228
- updateCallStatusAndNotify(callId: callCid, state: "created")
229
- }
230
-
231
- if let rejectedEvent = event.rawValue as? CallRejectedEvent {
232
- let userId = rejectedEvent.user.id
233
- let callCid = rejectedEvent.callCid
234
-
235
- // Operate on callStates on the main thread
236
- await MainActor.run {
237
- // Update the combined callStates map
238
- if var callState = self.callStates[callCid] {
239
- callState.participantResponses[userId] = "rejected"
240
- self.callStates[callCid] = callState
241
- }
242
- }
243
-
244
- print("CallRejectedEvent \(userId)")
245
- updateCallStatusAndNotify(callId: callCid, state: "rejected", userId: userId)
246
-
247
- await checkAllParticipantsResponded(callCid: callCid)
248
- continue
168
+ // Ensure this method is called on the main thread and properly establishes the subscription
169
+ DispatchQueue.main.async { [weak self] in
170
+ guard let self = self else { return }
171
+
172
+ // Cancel existing subscription if any
173
+ self.activeCallSubscription?.cancel()
174
+ self.activeCallSubscription = nil
175
+
176
+ // Verify callViewModel exists
177
+ guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
178
+ print("Warning: setupActiveCallSubscription called but callViewModel or streamVideo is nil")
179
+ // Schedule a retry after a short delay if callViewModel is nil
180
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
181
+ self?.setupActiveCallSubscription()
182
+ }
183
+ return
184
+ }
185
+
186
+ print("Setting up active call subscription")
187
+
188
+ // Create a strong reference to callViewModel to ensure it's not deallocated
189
+ // while the subscription is active
190
+ let viewModel = callViewModel
191
+
192
+ // Subscribe to streamVideo.state.$activeCall to handle CallKit integration
193
+ let callPublisher = streamVideo.state.$activeCall
194
+ .receive(on: DispatchQueue.main)
195
+ .sink { [weak self, weak viewModel] activeCall in
196
+ guard let self = self, let viewModel = viewModel else { return }
197
+
198
+ print("Active call update from streamVideo: \(String(describing: activeCall?.cId))")
199
+
200
+ if let activeCall = activeCall {
201
+ // Sync callViewModel with activeCall from streamVideo state
202
+ // This ensures CallKit integration works properly
203
+ viewModel.setActiveCall(activeCall)
249
204
  }
250
-
251
- if let missedEvent = event.rawValue as? CallMissedEvent {
252
- let userId = missedEvent.user.id
253
- let callCid = missedEvent.callCid
254
-
255
- // Operate on callStates on the main thread
256
- await MainActor.run {
257
- // Update the combined callStates map
258
- if var callState = self.callStates[callCid] {
259
- callState.participantResponses[userId] = "missed"
260
- self.callStates[callCid] = callState
261
- }
262
- }
263
-
264
- print("CallMissedEvent \(userId)")
265
- updateCallStatusAndNotify(callId: callCid, state: "missed", userId: userId)
266
-
267
- await checkAllParticipantsResponded(callCid: callCid)
268
- continue
205
+ }
206
+
207
+ // Store the subscription for activeCall updates
208
+ self.activeCallSubscription = callPublisher
209
+
210
+ // Additionally, subscribe to callingState for other call state changes
211
+ let statePublisher = viewModel.$callingState
212
+ .receive(on: DispatchQueue.main)
213
+ .sink { [weak self, weak viewModel] newState in
214
+ guard let self = self, let viewModel = viewModel else {
215
+ print("Warning: Call state update received but self or viewModel is nil")
216
+ return
269
217
  }
270
-
271
- if let participantLeftEvent = event.rawValue as? CallSessionParticipantLeftEvent {
272
- let callIdSplit = participantLeftEvent.callCid.split(separator: ":")
273
- if callIdSplit.count != 2 {
274
- print("CallSessionParticipantLeftEvent invalid cID \(participantLeftEvent.callCid)")
275
- continue
276
- }
277
-
278
- let callType = callIdSplit[0]
279
- let callId = callIdSplit[1]
280
-
281
- let call = streamVideo.call(callType: String(callType), callId: String(callId))
282
- if await MainActor.run(body: { (call.state.session?.participants.count ?? 1) - 1 <= 1 }) {
283
-
284
- print("We are left solo in a call. Ending. cID: \(participantLeftEvent.callCid)")
285
-
286
- Task {
287
- if let activeCall = streamVideo.state.activeCall {
288
- activeCall.leave()
289
- }
218
+
219
+ do {
220
+ try self.requireInitialized()
221
+ print("Call State Update: \(newState)")
222
+
223
+ if newState == .inCall {
224
+ print("- In call state detected")
225
+ print("- All participants: \(String(describing: viewModel.participants))")
226
+
227
+ // Create/update overlay and make visible when there's an active call
228
+ self.createCallOverlayView()
229
+
230
+ // Notify that a call has started - but only if we haven't notified for this call yet
231
+ if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
232
+ print("Notifying call joined: \(callId)")
233
+ self.updateCallStatusAndNotify(callId: callId, state: "joined")
234
+ self.hasNotifiedCallJoined = true
290
235
  }
291
- }
292
- }
293
-
294
- if let acceptedEvent = event.rawValue as? CallAcceptedEvent {
295
- let userId = acceptedEvent.user.id
296
- let callCid = acceptedEvent.callCid
297
-
298
- // Operate on callStates on the main thread
299
- await MainActor.run {
300
- // Update the combined callStates map
301
- if var callState = self.callStates[callCid] {
302
- callState.participantResponses[userId] = "accepted"
303
-
304
- // If someone accepted, invalidate the timer as we don't need to check anymore
305
- callState.timer?.invalidate()
306
- callState.timer = nil
307
-
308
- self.callStates[callCid] = callState
236
+ } else if case .incoming(let incomingCall) = newState {
237
+ self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
238
+ } else if newState == .idle && self.streamVideo?.state.activeCall == nil {
239
+ // Get the call ID that was active before the state changed
240
+ let endingCallId = viewModel.call?.cId
241
+ print("Call ending: \(String(describing: endingCallId))")
242
+
243
+ // Notify that call has ended - use the properly tracked call ID
244
+ self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
245
+
246
+ // Reset notification flag when call ends
247
+ self.hasNotifiedCallJoined = false
248
+
249
+ // Clean up any resources for this call
250
+ if let callCid = endingCallId {
251
+ // Invalidate and remove the timer
252
+ self.callStates[callCid]?.timer?.invalidate()
253
+
254
+ // Remove call from callStates
255
+ self.callStates.removeValue(forKey: callCid)
256
+
257
+ print("Cleaned up resources for ended call: \(callCid)")
309
258
  }
259
+
260
+ // Remove the call overlay view when not in a call
261
+ self.ensureViewRemoved()
310
262
  }
311
-
312
- print("CallAcceptedEvent \(userId)")
313
- updateCallStatusAndNotify(callId: callCid, state: "accepted", userId: userId)
314
- continue
263
+ } catch {
264
+ log.error("Error handling call state update: \(String(describing: error))")
315
265
  }
316
-
317
- updateCallStatusAndNotify(callId: streamVideo.state.activeCall?.callId ?? "", state: event.type)
318
266
  }
267
+
268
+ // Combine both publishers
269
+ self.activeCallSubscription = AnyCancellable {
270
+ callPublisher.cancel()
271
+ statePublisher.cancel()
319
272
  }
273
+
274
+ print("Active call subscription setup completed")
275
+
276
+ // Schedule a periodic check to ensure subscription is active
277
+ self.scheduleSubscriptionCheck()
320
278
  }
321
- // Cancel existing subscription if any
322
- activeCallSubscription?.cancel()
323
- // Create new subscription
324
- activeCallSubscription = streamVideo?.state.$activeCall.sink { [weak self] newState in
279
+ }
280
+
281
+ // Add a new method to periodically check and restore the subscription if needed
282
+ private func scheduleSubscriptionCheck() {
283
+ // Create a timer that checks the subscription every 5 seconds
284
+ DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
325
285
  guard let self = self else { return }
326
-
327
- Task { @MainActor in
328
- do {
329
- try self.requireInitialized()
330
- print("Call State Update:")
331
- print("- Call is nil: \(newState == nil)")
332
-
333
- if let state = newState?.state {
334
- print("- state: \(state)")
335
- print("- Session ID: \(state.sessionId)")
336
- print("- All participants: \(String(describing: state.participants))")
337
- print("- Remote participants: \(String(describing: state.remoteParticipants))")
338
-
339
- // Store the active call ID when a call becomes active
340
- self.currentActiveCallId = newState?.cId
341
- print("Updated current active call ID: \(String(describing: self.currentActiveCallId))")
342
-
343
- // Update overlay and make visible when there's an active call
344
- self.overlayViewModel?.updateCall(newState)
345
- self.overlayView?.isHidden = false
346
- self.webView?.isOpaque = false
347
-
348
- // Notify that a call has started
349
- self.updateCallStatusAndNotify(callId: newState?.cId ?? "", state: "joined")
350
- } else {
351
- // Get the call ID that was active before the state changed to nil
352
- let endingCallId = self.currentActiveCallId
353
- print("Call ending: \(String(describing: endingCallId))")
354
-
355
- // If newState is nil, hide overlay and clear call
356
- self.overlayViewModel?.updateCall(nil)
357
- self.overlayView?.isHidden = true
358
- self.webView?.isOpaque = true
359
-
360
- // Notify that call has ended - use the properly tracked call ID
361
- self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
362
-
363
- // Clean up any resources for this call
364
- if let callCid = endingCallId {
365
- // Invalidate and remove the timer
366
- self.callStates[callCid]?.timer?.invalidate()
367
-
368
- // Remove call from callStates
369
- self.callStates.removeValue(forKey: callCid)
370
-
371
- print("Cleaned up resources for ended call: \(callCid)")
372
- }
373
-
374
- // Clear the active call ID
375
- self.currentActiveCallId = nil
376
- }
377
- } catch {
378
- log.error("Error handling call state update: \(String(describing: error))")
379
- }
286
+
287
+ // Check if we're in a state where we need the subscription but it's not active
288
+ if self.state == .initialized && self.activeCallSubscription == nil && self.callViewModel != nil {
289
+ print("Subscription check: Restoring lost activeCallSubscription")
290
+ self.setupActiveCallSubscription()
291
+ } else {
292
+ // Schedule the next check
293
+ self.scheduleSubscriptionCheck()
380
294
  }
381
295
  }
382
296
  }
@@ -456,7 +370,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
456
370
 
457
371
  // Update UI
458
372
  await MainActor.run {
459
- self.overlayViewModel?.updateCall(nil)
373
+ // self.overlayViewModel?.updateCall(nil)
460
374
  self.overlayView?.isHidden = true
461
375
  self.webView?.isOpaque = true
462
376
  self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "timeout")
@@ -501,7 +415,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
501
415
  // Remove from callStates
502
416
  self.callStates.removeValue(forKey: callCid)
503
417
 
504
- self.overlayViewModel?.updateCall(nil)
418
+ // self.overlayViewModel?.updateCall(nil)
505
419
  self.overlayView?.isHidden = true
506
420
  self.webView?.isOpaque = true
507
421
  self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "all_rejected_or_missed")
@@ -537,7 +451,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
537
451
 
538
452
  // Update the CallOverlayView with new StreamVideo instance
539
453
  Task { @MainActor in
540
- self.overlayViewModel?.updateStreamVideo(self.streamVideo)
454
+ // self.overlayViewModel?.updateStreamVideo(self.streamVideo)
541
455
  }
542
456
 
543
457
  call.resolve([
@@ -571,8 +485,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
571
485
 
572
486
  // Update the CallOverlayView with nil StreamVideo instance
573
487
  Task { @MainActor in
574
- self.overlayViewModel?.updateCall(nil)
575
- self.overlayViewModel?.updateStreamVideo(nil)
488
+ // self.overlayViewModel?.updateCall(nil)
489
+ // self.overlayViewModel?.updateStreamVideo(nil)
576
490
  self.overlayView?.isHidden = true
577
491
  self.webView?.isOpaque = true
578
492
  }
@@ -655,7 +569,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
655
569
 
656
570
  // Update the CallOverlayView with the active call
657
571
  await MainActor.run {
658
- self.overlayViewModel?.updateCall(streamCall)
572
+ // self.overlayViewModel?.updateCall(streamCall)
659
573
  self.overlayView?.isHidden = false
660
574
  self.webView?.isOpaque = false
661
575
  }
@@ -683,7 +597,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
683
597
 
684
598
  // Update view state instead of cleaning up
685
599
  await MainActor.run {
686
- self.overlayViewModel?.updateCall(nil)
600
+ // self.overlayViewModel?.updateCall(nil)
687
601
  self.overlayView?.isHidden = true
688
602
  self.webView?.isOpaque = true
689
603
  }
@@ -782,13 +696,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
782
696
 
783
697
  // Join the call
784
698
  print("Accepting and joining call \(streamCall!.cId)...")
785
- try await streamCall?.accept()
786
- try await streamCall?.join(create: false)
699
+ guard case .incoming(let incomingCall) = await self.callViewModel?.callingState else {
700
+ call.reject("Failed to accept call as there is no call ID")
701
+ return
702
+ }
703
+
704
+ await self.callViewModel?.acceptCall(callType: incomingCall.type, callId: incomingCall.id)
787
705
  print("Successfully joined call")
788
706
 
789
707
  // Update the CallOverlayView with the active call
790
708
  await MainActor.run {
791
- self.overlayViewModel?.updateCall(streamCall)
709
+ // self.overlayViewModel?.updateCall(streamCall)
792
710
  self.overlayView?.isHidden = false
793
711
  self.webView?.isOpaque = false
794
712
  }
@@ -807,6 +725,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
807
725
  }
808
726
 
809
727
  private func initializeStreamVideo() {
728
+ if (state == .initialized) {
729
+ print("initializeStreamVideo already initialized")
730
+ // Try to get user credentials from repository
731
+ guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
732
+ print("Save credentials not found, skipping initialization")
733
+ return
734
+ }
735
+ if (savedCredentials.user.id == streamVideo?.user.id) {
736
+ print("Skipping initializeStreamVideo as user is already logged in")
737
+ return
738
+ }
739
+ } else if (state == .initializing) {
740
+ print("initializeStreamVideo rejected - already initializing")
741
+ return
742
+ }
810
743
  state = .initializing
811
744
 
812
745
  // Try to get user credentials from repository
@@ -818,11 +751,31 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
818
751
  }
819
752
  print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
820
753
 
754
+ LogConfig.level = .debug
821
755
  self.streamVideo = StreamVideo(
822
756
  apiKey: apiKey,
823
757
  user: savedCredentials.user,
824
- token: UserToken(stringLiteral: savedCredentials.tokenValue)
758
+ token: UserToken(stringLiteral: savedCredentials.tokenValue),
759
+ tokenProvider: {completion in
760
+ guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
761
+ print("No saved credentials or API key found, cannot refresh token")
762
+
763
+ completion(.failure(NSError(domain: "No saved credentials or API key found, cannot refresh token", code: 0, userInfo: nil)))
764
+ return
765
+ }
766
+ completion(.success(UserToken(stringLiteral: savedCredentials.tokenValue)))
767
+ }
825
768
  )
769
+
770
+ if (self.callViewModel == nil) {
771
+ // Initialize on main thread with proper MainActor isolation
772
+ DispatchQueue.main.async {
773
+ Task { @MainActor in
774
+ self.callViewModel = CallViewModel(participantsLayout: .grid)
775
+ self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
776
+ }
777
+ }
778
+ }
826
779
 
827
780
  state = .initialized
828
781
  callKitAdapter.streamVideo = self.streamVideo
@@ -837,63 +790,98 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
837
790
  }
838
791
 
839
792
  private func setupViews() {
840
- guard let webView = self.webView,
841
- let parent = webView.superview else { return }
842
793
 
843
- // Create TouchInterceptView
844
- let touchInterceptView = TouchInterceptView(frame: parent.bounds)
845
- touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
846
- self.touchInterceptView = touchInterceptView
847
794
 
848
- // Remove webView from its parent
849
- webView.removeFromSuperview()
850
-
851
- // Add TouchInterceptView to the parent
852
- parent.addSubview(touchInterceptView)
853
-
854
- // Setup TouchInterceptView constraints
795
+ // // Create SwiftUI view with view model
796
+ // let (hostingController, viewModel) = CallOverlayView.create(streamVideo: self.streamVideo)
797
+ // hostingController.view.backgroundColor = .clear
798
+ // hostingController.view.translatesAutoresizingMaskIntoConstraints = false
799
+ //
800
+ // self.hostingController = hostingController
801
+ // self.overlayViewModel = viewModel
802
+ // self.overlayView = hostingController.view
803
+ //
804
+ // if let overlayView = self.overlayView {
805
+ // // Setup the views in TouchInterceptView
806
+ // touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
807
+ //
808
+ // // Setup constraints for webView
809
+ // NSLayoutConstraint.activate([
810
+ // webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
811
+ // webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
812
+ // webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
813
+ // webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
814
+ // ])
815
+ //
816
+ // // Setup constraints for overlayView
817
+ // let safeGuide = touchInterceptView.safeAreaLayoutGuide
818
+ // NSLayoutConstraint.activate([
819
+ // overlayView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
820
+ // overlayView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
821
+ // overlayView.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
822
+ // overlayView.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
823
+ // ])
824
+ // }
825
+ }
826
+
827
+ private func createCallOverlayView() {
828
+ guard let webView = self.webView,
829
+ let parent = webView.superview,
830
+ let callOverlayView = self.callViewModel else { return }
831
+
832
+ // Check if we already have an overlay view - do nothing if it exists
833
+ if let existingOverlayView = self.overlayView, existingOverlayView.superview != nil {
834
+ print("Call overlay view already exists, doing nothing")
835
+ return
836
+ }
837
+
838
+ print("Creating new call overlay view")
839
+
840
+ // First, create the overlay view
841
+ let overlayView = CallOverlayView.create(callViewModel: callOverlayView)
842
+ overlayView.view.translatesAutoresizingMaskIntoConstraints = false
843
+
844
+ // Important: Insert the overlay view BELOW the webView in the view hierarchy
845
+ parent.insertSubview(overlayView.view, belowSubview: webView)
846
+
847
+ // Set constraints to fill the parent's safe area
848
+ let safeGuide = parent.safeAreaLayoutGuide
849
+
855
850
  NSLayoutConstraint.activate([
856
- touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
857
- touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
858
- touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
859
- touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
851
+ overlayView.view.topAnchor.constraint(equalTo: safeGuide.topAnchor),
852
+ overlayView.view.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
853
+ overlayView.view.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
854
+ overlayView.view.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
860
855
  ])
861
-
862
- // Configure webview for transparency
863
- webView.isOpaque = true
856
+
857
+ // Set opacity for visual effect - make webView transparent to see overlay
858
+ webView.isOpaque = false
864
859
  webView.backgroundColor = .clear
865
860
  webView.scrollView.backgroundColor = .clear
866
- webView.translatesAutoresizingMaskIntoConstraints = false
867
-
868
- // Create SwiftUI view with view model
869
- let (hostingController, viewModel) = CallOverlayView.create(streamVideo: self.streamVideo)
870
- hostingController.view.backgroundColor = .clear
871
- hostingController.view.translatesAutoresizingMaskIntoConstraints = false
872
-
873
- self.hostingController = hostingController
874
- self.overlayViewModel = viewModel
875
- self.overlayView = hostingController.view
876
-
877
- if let overlayView = self.overlayView {
878
- // Setup the views in TouchInterceptView
879
- touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
880
-
881
- // Setup constraints for webView
882
- NSLayoutConstraint.activate([
883
- webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
884
- webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
885
- webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
886
- webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
887
- ])
861
+
862
+ // Store reference to the hosting controller
863
+ self.hostingController = overlayView
864
+ self.overlayView = overlayView.view
865
+ }
888
866
 
889
- // Setup constraints for overlayView
890
- let safeGuide = touchInterceptView.safeAreaLayoutGuide
891
- NSLayoutConstraint.activate([
892
- overlayView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
893
- overlayView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
894
- overlayView.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
895
- overlayView.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
896
- ])
867
+ private func ensureViewRemoved() {
868
+ // Check if we have an overlay view
869
+ if let existingOverlayView = self.overlayView {
870
+ print("Removing call overlay view")
871
+
872
+ // Remove the view from its superview
873
+ existingOverlayView.removeFromSuperview()
874
+
875
+ // Reset opacity for webView
876
+ self.webView?.isOpaque = true
877
+ self.webView?.backgroundColor = nil
878
+ self.webView?.scrollView.backgroundColor = nil
879
+
880
+ // Clear references
881
+ self.overlayView = nil
882
+ self.hostingController = nil
883
+ } else {
884
+ print("No call overlay view to remove")
897
885
  }
898
886
  }
899
887
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,177 +0,0 @@
1
- import UIKit
2
-
3
- class TouchInterceptView: UIView {
4
- private var webView: UIView?
5
- private var overlayView: UIView?
6
- private var labeledFrames: [ViewFramePreferenceData] = []
7
-
8
- override init(frame: CGRect) {
9
- super.init(frame: frame)
10
- isUserInteractionEnabled = true
11
- }
12
-
13
- required init?(coder: NSCoder) {
14
- super.init(coder: coder)
15
- isUserInteractionEnabled = true
16
- }
17
-
18
- func setupWithWebView(_ webView: UIView, overlayView: UIView) {
19
- self.webView = webView
20
- self.overlayView = overlayView
21
-
22
- // Add both views as subviews
23
- addSubview(overlayView)
24
- addSubview(webView)
25
-
26
- // Ensure both views can receive touches
27
- webView.isUserInteractionEnabled = true
28
- overlayView.isUserInteractionEnabled = true
29
- }
30
-
31
- override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
32
- guard let webView = webView,
33
- let overlayView = overlayView else {
34
- super.touchesBegan(touches, with: event)
35
- return
36
- }
37
-
38
- // Convert touch locations and check labeled frames
39
- if let touch = touches.first {
40
- let point = touch.location(in: self)
41
- let globalPoint = convert(point, to: nil)
42
-
43
- // If touch is in a labeled frame, only send to overlay
44
- if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
45
- overlayView.touchesBegan(touches, with: event)
46
- } else {
47
- // Otherwise broadcast to both views
48
- webView.touchesBegan(touches, with: event)
49
- overlayView.touchesBegan(touches, with: event)
50
- }
51
- }
52
-
53
- super.touchesBegan(touches, with: event)
54
- }
55
-
56
- override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
57
- guard let webView = webView,
58
- let overlayView = overlayView else {
59
- super.touchesMoved(touches, with: event)
60
- return
61
- }
62
-
63
- if let touch = touches.first {
64
- let point = touch.location(in: self)
65
- let globalPoint = convert(point, to: nil)
66
-
67
- if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
68
- overlayView.touchesMoved(touches, with: event)
69
- } else {
70
- webView.touchesMoved(touches, with: event)
71
- overlayView.touchesMoved(touches, with: event)
72
- }
73
- }
74
-
75
- super.touchesMoved(touches, with: event)
76
- }
77
-
78
- override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
79
- guard let webView = webView,
80
- let overlayView = overlayView else {
81
- super.touchesEnded(touches, with: event)
82
- return
83
- }
84
-
85
- if let touch = touches.first {
86
- let point = touch.location(in: self)
87
- let globalPoint = convert(point, to: nil)
88
-
89
- if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
90
- overlayView.touchesEnded(touches, with: event)
91
- } else {
92
- webView.touchesEnded(touches, with: event)
93
- overlayView.touchesEnded(touches, with: event)
94
- }
95
- }
96
-
97
- super.touchesEnded(touches, with: event)
98
- }
99
-
100
- override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
101
- guard let webView = webView,
102
- let overlayView = overlayView else {
103
- super.touchesCancelled(touches, with: event)
104
- return
105
- }
106
-
107
- if let touch = touches.first {
108
- let point = touch.location(in: self)
109
- let globalPoint = convert(point, to: nil)
110
-
111
- if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
112
- overlayView.touchesCancelled(touches, with: event)
113
- } else {
114
- webView.touchesCancelled(touches, with: event)
115
- overlayView.touchesCancelled(touches, with: event)
116
- }
117
- }
118
-
119
- super.touchesCancelled(touches, with: event)
120
- }
121
-
122
- override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
123
- return true
124
- }
125
-
126
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
127
- guard let webView = webView,
128
- let overlayView = overlayView else {
129
- return super.hitTest(point, with: event)
130
- }
131
-
132
- // Convert point to global coordinates for labeled frame checking
133
- let globalPoint = convert(point, to: nil)
134
-
135
- // print("TouchInterceptView - Hit test at global point: \(globalPoint)")
136
- // print("Current labeled frames: \(labeledFrames.map { "\($0.label): \($0.frame)" }.joined(separator: ", "))")
137
-
138
- // Convert point for both views
139
- let webViewPoint = convert(point, to: webView)
140
- let overlayPoint = convert(point, to: overlayView)
141
-
142
- // First check if the point is inside any labeled frame
143
- for labeledFrame in labeledFrames {
144
- if labeledFrame.frame.contains(globalPoint) {
145
- // print("Hit labeled frame: \(labeledFrame.label)")
146
- // If it's in a labeled frame, let the overlay handle it
147
- if overlayView.point(inside: overlayPoint, with: event),
148
- let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
149
- return overlayHitView
150
- }
151
- }
152
- }
153
-
154
- // If not in a labeled frame, let webview try first
155
- if webView.point(inside: webViewPoint, with: event),
156
- let webViewHitView = webView.hitTest(webViewPoint, with: event) {
157
- return webViewHitView
158
- }
159
-
160
- // Finally check if overlay wants to handle the touch
161
- if overlayView.point(inside: overlayPoint, with: event),
162
- let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
163
- return overlayHitView
164
- }
165
-
166
- return super.hitTest(point, with: event)
167
- }
168
-
169
- func updateLabeledFrames(_ frames: [ViewFramePreferenceData]) {
170
- print("TouchInterceptView - Updating labeled frames:")
171
- print("Number of frames: \(frames.count)")
172
- frames.forEach { frame in
173
- print("Label: \(frame.label), Frame: \(frame.frame)")
174
- }
175
- self.labeledFrames = frames
176
- }
177
- }