@capgo/capacitor-stream-call 0.0.19 → 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.
@@ -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,204 +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
- guard let participantsCount = await MainActor.run(body: {
283
- if call.id == streamVideo.state.activeCall?.id {
284
- return (call.state.session?.participants.count) ?? streamVideo.state.activeCall?.state.participants.count
285
- } else {
286
- return (call.state.session?.participants.count)
287
- }
288
- }) else {
289
- print("CallSessionParticipantLeftEvent no participantsCount")
290
- continue
291
- }
292
-
293
- if participantsCount - 1 <= 1 {
294
-
295
- print("We are left solo in a call. Ending. cID: \(participantLeftEvent.callCid). participantsCount: \(participantsCount)")
296
-
297
- Task {
298
- if let activeCall = streamVideo.state.activeCall {
299
- activeCall.leave()
300
- } else {
301
- print("Active call isn't the one?")
302
- }
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
303
235
  }
304
- }
305
- }
306
-
307
- if let acceptedEvent = event.rawValue as? CallAcceptedEvent {
308
- let userId = acceptedEvent.user.id
309
- let callCid = acceptedEvent.callCid
310
-
311
- // Operate on callStates on the main thread
312
- await MainActor.run {
313
- // Update the combined callStates map
314
- if var callState = self.callStates[callCid] {
315
- callState.participantResponses[userId] = "accepted"
316
-
317
- // If someone accepted, invalidate the timer as we don't need to check anymore
318
- callState.timer?.invalidate()
319
- callState.timer = nil
320
-
321
- 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)")
322
258
  }
259
+
260
+ // Remove the call overlay view when not in a call
261
+ self.ensureViewRemoved()
323
262
  }
324
-
325
- print("CallAcceptedEvent \(userId)")
326
- updateCallStatusAndNotify(callId: callCid, state: "accepted", userId: userId)
327
- continue
263
+ } catch {
264
+ log.error("Error handling call state update: \(String(describing: error))")
328
265
  }
329
-
330
- updateCallStatusAndNotify(callId: streamVideo.state.activeCall?.callId ?? "", state: event.type)
331
266
  }
267
+
268
+ // Combine both publishers
269
+ self.activeCallSubscription = AnyCancellable {
270
+ callPublisher.cancel()
271
+ statePublisher.cancel()
332
272
  }
273
+
274
+ print("Active call subscription setup completed")
275
+
276
+ // Schedule a periodic check to ensure subscription is active
277
+ self.scheduleSubscriptionCheck()
333
278
  }
334
- // Cancel existing subscription if any
335
- activeCallSubscription?.cancel()
336
- // Create new subscription
337
- 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
338
285
  guard let self = self else { return }
339
-
340
- Task { @MainActor in
341
- do {
342
- try self.requireInitialized()
343
- print("Call State Update:")
344
- print("- Call is nil: \(newState == nil)")
345
-
346
- if let state = newState?.state {
347
- print("- state: \(state)")
348
- print("- Session ID: \(state.sessionId)")
349
- print("- All participants: \(String(describing: state.participants))")
350
- print("- Remote participants: \(String(describing: state.remoteParticipants))")
351
-
352
- // Store the active call ID when a call becomes active
353
- self.currentActiveCallId = newState?.cId
354
- print("Updated current active call ID: \(String(describing: self.currentActiveCallId))")
355
-
356
- // Update overlay and make visible when there's an active call
357
- self.overlayViewModel?.updateCall(newState)
358
- self.overlayView?.isHidden = false
359
- self.webView?.isOpaque = false
360
-
361
- // Notify that a call has started
362
- self.updateCallStatusAndNotify(callId: newState?.cId ?? "", state: "joined")
363
- } else {
364
- // Get the call ID that was active before the state changed to nil
365
- let endingCallId = self.currentActiveCallId
366
- print("Call ending: \(String(describing: endingCallId))")
367
-
368
- // If newState is nil, hide overlay and clear call
369
- self.overlayViewModel?.updateCall(nil)
370
- self.overlayView?.isHidden = true
371
- self.webView?.isOpaque = true
372
-
373
- // Notify that call has ended - use the properly tracked call ID
374
- self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
375
-
376
- // Clean up any resources for this call
377
- if let callCid = endingCallId {
378
- // Invalidate and remove the timer
379
- self.callStates[callCid]?.timer?.invalidate()
380
-
381
- // Remove call from callStates
382
- self.callStates.removeValue(forKey: callCid)
383
-
384
- print("Cleaned up resources for ended call: \(callCid)")
385
- }
386
-
387
- // Clear the active call ID
388
- self.currentActiveCallId = nil
389
- }
390
- } catch {
391
- log.error("Error handling call state update: \(String(describing: error))")
392
- }
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()
393
294
  }
394
295
  }
395
296
  }
@@ -469,7 +370,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
469
370
 
470
371
  // Update UI
471
372
  await MainActor.run {
472
- self.overlayViewModel?.updateCall(nil)
373
+ // self.overlayViewModel?.updateCall(nil)
473
374
  self.overlayView?.isHidden = true
474
375
  self.webView?.isOpaque = true
475
376
  self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "timeout")
@@ -514,7 +415,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
514
415
  // Remove from callStates
515
416
  self.callStates.removeValue(forKey: callCid)
516
417
 
517
- self.overlayViewModel?.updateCall(nil)
418
+ // self.overlayViewModel?.updateCall(nil)
518
419
  self.overlayView?.isHidden = true
519
420
  self.webView?.isOpaque = true
520
421
  self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "all_rejected_or_missed")
@@ -550,7 +451,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
550
451
 
551
452
  // Update the CallOverlayView with new StreamVideo instance
552
453
  Task { @MainActor in
553
- self.overlayViewModel?.updateStreamVideo(self.streamVideo)
454
+ // self.overlayViewModel?.updateStreamVideo(self.streamVideo)
554
455
  }
555
456
 
556
457
  call.resolve([
@@ -584,8 +485,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
584
485
 
585
486
  // Update the CallOverlayView with nil StreamVideo instance
586
487
  Task { @MainActor in
587
- self.overlayViewModel?.updateCall(nil)
588
- self.overlayViewModel?.updateStreamVideo(nil)
488
+ // self.overlayViewModel?.updateCall(nil)
489
+ // self.overlayViewModel?.updateStreamVideo(nil)
589
490
  self.overlayView?.isHidden = true
590
491
  self.webView?.isOpaque = true
591
492
  }
@@ -668,7 +569,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
668
569
 
669
570
  // Update the CallOverlayView with the active call
670
571
  await MainActor.run {
671
- self.overlayViewModel?.updateCall(streamCall)
572
+ // self.overlayViewModel?.updateCall(streamCall)
672
573
  self.overlayView?.isHidden = false
673
574
  self.webView?.isOpaque = false
674
575
  }
@@ -696,7 +597,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
696
597
 
697
598
  // Update view state instead of cleaning up
698
599
  await MainActor.run {
699
- self.overlayViewModel?.updateCall(nil)
600
+ // self.overlayViewModel?.updateCall(nil)
700
601
  self.overlayView?.isHidden = true
701
602
  self.webView?.isOpaque = true
702
603
  }
@@ -795,14 +696,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
795
696
 
796
697
  // Join the call
797
698
  print("Accepting and joining call \(streamCall!.cId)...")
798
- try await streamCall?.accept()
799
- try await streamCall?.join(create: false)
800
- try await streamCall?.get()
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)
801
705
  print("Successfully joined call")
802
706
 
803
707
  // Update the CallOverlayView with the active call
804
708
  await MainActor.run {
805
- self.overlayViewModel?.updateCall(streamCall)
709
+ // self.overlayViewModel?.updateCall(streamCall)
806
710
  self.overlayView?.isHidden = false
807
711
  self.webView?.isOpaque = false
808
712
  }
@@ -821,6 +725,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
821
725
  }
822
726
 
823
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
+ }
824
743
  state = .initializing
825
744
 
826
745
  // Try to get user credentials from repository
@@ -832,11 +751,31 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
832
751
  }
833
752
  print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
834
753
 
754
+ LogConfig.level = .debug
835
755
  self.streamVideo = StreamVideo(
836
756
  apiKey: apiKey,
837
757
  user: savedCredentials.user,
838
- 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
+ }
839
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
+ }
840
779
 
841
780
  state = .initialized
842
781
  callKitAdapter.streamVideo = self.streamVideo
@@ -851,63 +790,98 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
851
790
  }
852
791
 
853
792
  private func setupViews() {
854
- guard let webView = self.webView,
855
- let parent = webView.superview else { return }
856
-
857
- // Create TouchInterceptView
858
- let touchInterceptView = TouchInterceptView(frame: parent.bounds)
859
- touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
860
- self.touchInterceptView = touchInterceptView
861
793
 
862
- // Remove webView from its parent
863
- webView.removeFromSuperview()
864
794
 
865
- // Add TouchInterceptView to the parent
866
- parent.addSubview(touchInterceptView)
867
-
868
- // 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
+
869
850
  NSLayoutConstraint.activate([
870
- touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
871
- touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
872
- touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
873
- 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)
874
855
  ])
875
-
876
- // Configure webview for transparency
877
- webView.isOpaque = true
856
+
857
+ // Set opacity for visual effect - make webView transparent to see overlay
858
+ webView.isOpaque = false
878
859
  webView.backgroundColor = .clear
879
860
  webView.scrollView.backgroundColor = .clear
880
- webView.translatesAutoresizingMaskIntoConstraints = false
881
-
882
- // Create SwiftUI view with view model
883
- let (hostingController, viewModel) = CallOverlayView.create(streamVideo: self.streamVideo)
884
- hostingController.view.backgroundColor = .clear
885
- hostingController.view.translatesAutoresizingMaskIntoConstraints = false
886
-
887
- self.hostingController = hostingController
888
- self.overlayViewModel = viewModel
889
- self.overlayView = hostingController.view
890
-
891
- if let overlayView = self.overlayView {
892
- // Setup the views in TouchInterceptView
893
- touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
894
-
895
- // Setup constraints for webView
896
- NSLayoutConstraint.activate([
897
- webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
898
- webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
899
- webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
900
- webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
901
- ])
861
+
862
+ // Store reference to the hosting controller
863
+ self.hostingController = overlayView
864
+ self.overlayView = overlayView.view
865
+ }
902
866
 
903
- // Setup constraints for overlayView
904
- let safeGuide = touchInterceptView.safeAreaLayoutGuide
905
- NSLayoutConstraint.activate([
906
- overlayView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
907
- overlayView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
908
- overlayView.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
909
- overlayView.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
910
- ])
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")
911
885
  }
912
886
  }
913
887
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.19",
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
- }