@capgo/capacitor-stream-call 7.1.24 → 7.1.25

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.
@@ -66,7 +66,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
66
66
 
67
67
  // Declare as optional and initialize in load() method
68
68
  private var callViewModel: CallViewModel?
69
-
69
+
70
70
  // Constants for UserDefaults keys
71
71
  private let dynamicApiKeyKey = "stream.video.dynamic.apikey"
72
72
 
@@ -90,11 +90,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
90
90
  if let reason = reason {
91
91
  data["reason"] = reason
92
92
  }
93
-
93
+
94
94
  if let caller = caller {
95
95
  data["caller"] = caller
96
96
  }
97
-
97
+
98
98
  if let members = members {
99
99
  data["members"] = members
100
100
  }
@@ -105,7 +105,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
105
105
 
106
106
  override public func load() {
107
107
  print("StreamCallPlugin: load() called")
108
-
108
+
109
109
  // Read API key from Info.plist
110
110
  if let apiKey = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_STREAM_VIDEO_APIKEY") as? String {
111
111
  self.apiKey = apiKey
@@ -114,7 +114,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
114
114
  if self.apiKey == nil {
115
115
  fatalError("Cannot get apikey")
116
116
  }
117
-
117
+
118
118
  // Check if we have a logged in user for handling incoming calls
119
119
  if let credentials = SecureUserRepository.shared.loadCurrentUser() {
120
120
  print("StreamCallPlugin: Found stored credentials during load() for user: \(credentials.user.name)")
@@ -187,11 +187,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
187
187
  // Ensure this method is called on the main thread and properly establishes the subscription
188
188
  DispatchQueue.main.async { [weak self] in
189
189
  guard let self = self else { return }
190
-
190
+
191
191
  // Cancel existing subscription if any
192
192
  self.activeCallSubscription?.cancel()
193
193
  self.activeCallSubscription = nil
194
-
194
+
195
195
  // Verify callViewModel exists
196
196
  guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
197
197
  print("Warning: setupActiveCallSubscription called but callViewModel or streamVideo is nil")
@@ -201,31 +201,31 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
201
201
  }
202
202
  return
203
203
  }
204
-
204
+
205
205
  print("Setting up active call subscription")
206
-
206
+
207
207
  // Create a strong reference to callViewModel to ensure it's not deallocated
208
208
  // while the subscription is active
209
209
  let viewModel = callViewModel
210
-
210
+
211
211
  // Subscribe to streamVideo.state.$activeCall to handle CallKit integration
212
212
  let callPublisher = streamVideo.state.$activeCall
213
213
  .receive(on: DispatchQueue.main)
214
214
  .sink { [weak self, weak viewModel] activeCall in
215
215
  guard let self = self, let viewModel = viewModel else { return }
216
-
216
+
217
217
  print("Active call update from streamVideo: \(String(describing: activeCall?.cId))")
218
-
218
+
219
219
  if let activeCall = activeCall {
220
220
  // Sync callViewModel with activeCall from streamVideo state
221
221
  // This ensures CallKit integration works properly
222
222
  viewModel.setActiveCall(activeCall)
223
223
  }
224
224
  }
225
-
225
+
226
226
  // Store the subscription for activeCall updates
227
227
  self.activeCallSubscription = callPublisher
228
-
228
+
229
229
  // Additionally, subscribe to callingState for other call state changes
230
230
  let statePublisher = viewModel.$callingState
231
231
  .receive(on: DispatchQueue.main)
@@ -234,21 +234,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
234
234
  print("Warning: Call state update received but self or viewModel is nil")
235
235
  return
236
236
  }
237
-
237
+
238
238
  do {
239
239
  try self.requireInitialized()
240
240
  print("Call State Update: \(newState)")
241
-
241
+
242
242
  if newState == .inCall {
243
243
  print("- In call state detected")
244
244
  print("- All participants: \(String(describing: viewModel.participants))")
245
-
245
+
246
246
  // Ensure views are set up first (important when accepting call from notification)
247
247
  self.setupViews()
248
-
248
+
249
249
  // Create/update overlay and make visible when there's an active call
250
250
  self.createCallOverlayView()
251
-
251
+
252
252
  // Notify that a call has started - but only if we haven't notified for this call yet
253
253
  if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
254
254
  print("Notifying call joined: \(callId)")
@@ -258,15 +258,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
258
258
  } else if case .incoming(let incomingCall) = newState {
259
259
  // Extract caller information
260
260
  Task {
261
- var caller: [String: Any]? = nil
262
- var members: [[String: Any]]? = nil
263
-
261
+ var caller: [String: Any]?
262
+ var members: [[String: Any]]?
263
+
264
264
  do {
265
265
  // Get the call from StreamVideo to access detailed information
266
266
  if let streamVideo = self.streamVideo {
267
267
  let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
268
268
  let callInfo = try await call.get()
269
-
269
+
270
270
  // Extract caller information
271
271
  let createdBy = callInfo.call.createdBy
272
272
  var callerData: [String: Any] = [:]
@@ -275,7 +275,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
275
275
  callerData["imageURL"] = createdBy.image
276
276
  callerData["role"] = createdBy.role
277
277
  caller = callerData
278
-
278
+
279
279
  // Extract members information from current participants if available
280
280
  var membersArray: [[String: Any]] = []
281
281
  if let activeCall = streamVideo.state.activeCall {
@@ -294,25 +294,25 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
294
294
  } catch {
295
295
  print("Failed to get call info for caller details: \(error)")
296
296
  }
297
-
298
- // Notify with caller information
297
+
298
+ // Notify with caller information
299
299
  let fullCallId = "\(incomingCall.type):\(incomingCall.id)"
300
300
  self.updateCallStatusAndNotify(callId: fullCallId, state: "ringing", caller: caller, members: members)
301
301
  }
302
302
  } else if newState == .idle {
303
303
  print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
304
-
304
+
305
305
  // Only notify about call ending if we have a valid stored call ID and there's truly no active call
306
306
  // This prevents false "left" events during normal state transitions
307
307
  if !self.currentCallId.isEmpty && self.streamVideo?.state.activeCall == nil {
308
308
  print("Call actually ending: \(self.currentCallId)")
309
-
309
+
310
310
  // Notify that call has ended - use the stored call ID
311
311
  self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
312
-
312
+
313
313
  // Reset notification flag when call ends
314
314
  self.hasNotifiedCallJoined = false
315
-
315
+
316
316
  // Remove the call overlay view when not in a call
317
317
  self.ensureViewRemoved()
318
318
  } else {
@@ -323,26 +323,26 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
323
323
  log.error("Error handling call state update: \(String(describing: error))")
324
324
  }
325
325
  }
326
-
326
+
327
327
  // Combine both publishers
328
328
  self.activeCallSubscription = AnyCancellable {
329
329
  callPublisher.cancel()
330
330
  statePublisher.cancel()
331
331
  }
332
-
332
+
333
333
  print("Active call subscription setup completed")
334
-
334
+
335
335
  // Schedule a periodic check to ensure subscription is active
336
336
  self.scheduleSubscriptionCheck()
337
337
  }
338
338
  }
339
-
339
+
340
340
  // Add a new method to periodically check and restore the subscription if needed
341
341
  private func scheduleSubscriptionCheck() {
342
342
  // Create a timer that checks the subscription every 5 seconds
343
343
  DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
344
344
  guard let self = self else { return }
345
-
345
+
346
346
  // Check if we're in a state where we need the subscription but it's not active
347
347
  if self.state == .initialized && self.activeCallSubscription == nil && self.callViewModel != nil {
348
348
  print("Subscription check: Restoring lost activeCallSubscription")
@@ -374,7 +374,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
374
374
  if let pushConfig = call.getObject("pushNotificationsConfig") {
375
375
  let pushProviderName = pushConfig["pushProviderName"] as? String ?? "ios-apn"
376
376
  let voipProviderName = pushConfig["voipProviderName"] as? String ?? "ios-voip"
377
-
377
+
378
378
  self.pushNotificationsConfig = PushNotificationsConfig(
379
379
  pushProviderInfo: PushProviderInfo(name: pushProviderName, pushProvider: .apn),
380
380
  voipPushProviderInfo: PushProviderInfo(name: voipProviderName, pushProvider: .apn)
@@ -431,7 +431,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
431
431
  // self.overlayViewModel?.updateStreamVideo(nil)
432
432
  self.overlayView?.isHidden = true
433
433
  self.webView?.isOpaque = true
434
-
434
+
435
435
  // Remove touch interceptor if it exists
436
436
  self.removeTouchInterceptor()
437
437
  }
@@ -524,7 +524,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
524
524
  case let date as Date:
525
525
  let formatter = ISO8601DateFormatter()
526
526
  return .string(formatter.string(from: date))
527
- case let array as Array<JSValue>:
527
+ case let array as [JSValue]:
528
528
  let mappedArray = array.compactMap { item -> RawJSON? in
529
529
  // Recursive conversion for array elements
530
530
  switch item {
@@ -538,7 +538,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
538
538
  }
539
539
  }
540
540
  return .array(mappedArray)
541
- case let dict as Dictionary<String, JSValue>:
541
+ case let dict as [String: JSValue]:
542
542
  let mappedDict = dict.compactMapValues { value -> RawJSON? in
543
543
  // Recursive conversion for dictionary values
544
544
  switch value {
@@ -558,15 +558,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
558
558
  },
559
559
  video: video
560
560
  )
561
-
561
+
562
562
  // Wait for call state to be populated by WebSocket events
563
563
  let callStream = streamVideo!.call(callType: callType, callId: callId)
564
-
564
+
565
565
  // Wait until we have member data - with timeout to prevent infinite loop
566
566
  var allMembers: [[String: Any]] = []
567
567
  var attempts = 0
568
568
  let maxAttempts = 50 // 5 seconds max
569
-
569
+
570
570
  while allMembers.isEmpty && attempts < maxAttempts {
571
571
  let membersList = await callStream.state.members
572
572
  if !membersList.isEmpty {
@@ -584,7 +584,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
584
584
  try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
585
585
  }
586
586
  }
587
-
587
+
588
588
  // If we still don't have members after timeout, use basic data
589
589
  if allMembers.isEmpty {
590
590
  allMembers = members.map { userId in
@@ -596,16 +596,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
596
596
  ]
597
597
  }
598
598
  }
599
-
599
+
600
600
  // Now send the created event with complete member data
601
601
  let fullCallId = "\(callType):\(callId)"
602
602
  self.updateCallStatusAndNotify(callId: fullCallId, state: "created", members: allMembers)
603
-
603
+
604
604
  // Update UI on main thread
605
605
  await MainActor.run {
606
606
  // Add touch interceptor for the call
607
607
  self.addTouchInterceptor()
608
-
608
+
609
609
  // self.overlayViewModel?.updateCall(streamCall)
610
610
  self.overlayView?.isHidden = false
611
611
  self.webView?.isOpaque = false
@@ -633,7 +633,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
633
633
  // Check both active call and callViewModel's call state to handle outgoing calls
634
634
  let activeCall = streamVideo?.state.activeCall
635
635
  let viewModelCall = await callViewModel?.call
636
-
636
+
637
637
  // Helper function to determine if we should end or leave the call
638
638
  func shouldEndCall(for streamCall: Call) async throws -> Bool {
639
639
  do {
@@ -641,25 +641,25 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
641
641
  let currentUserId = streamVideo?.user.id
642
642
  let createdBy = callInfo.call.createdBy.id
643
643
  let isCreator = createdBy == currentUserId
644
-
644
+
645
645
  // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
646
646
  let totalParticipants = await streamCall.state.participants.count
647
647
  let shouldEnd = isCreator || totalParticipants <= 2
648
-
648
+
649
649
  print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
650
-
650
+
651
651
  return shouldEnd
652
652
  } catch {
653
653
  print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
654
654
  return false // Fallback to leave if we can't determine
655
655
  }
656
656
  }
657
-
657
+
658
658
  if let activeCall = activeCall {
659
659
  // There's an active call, check if we should end or leave
660
660
  do {
661
661
  let shouldEnd = try await shouldEndCall(for: activeCall)
662
-
662
+
663
663
  if shouldEnd {
664
664
  print("Ending active call \(activeCall.cId) for all participants")
665
665
  try await activeCall.end()
@@ -671,15 +671,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
671
671
  print("Error ending/leaving active call: \(error)")
672
672
  try await activeCall.leave() // Fallback to leave
673
673
  }
674
-
674
+
675
675
  await MainActor.run {
676
676
  self.overlayView?.isHidden = true
677
677
  self.webView?.isOpaque = true
678
-
678
+
679
679
  // Remove touch interceptor
680
680
  self.removeTouchInterceptor()
681
681
  }
682
-
682
+
683
683
  call.resolve([
684
684
  "success": true
685
685
  ])
@@ -687,7 +687,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
687
687
  // There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
688
688
  do {
689
689
  let shouldEnd = try await shouldEndCall(for: viewModelCall)
690
-
690
+
691
691
  if shouldEnd {
692
692
  print("Ending viewModel call \(viewModelCall.cId) for all participants")
693
693
  try await viewModelCall.end()
@@ -699,18 +699,18 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
699
699
  print("Error ending/leaving viewModel call: \(error)")
700
700
  try await viewModelCall.leave() // Fallback to leave
701
701
  }
702
-
702
+
703
703
  // Also hang up to reset the calling state
704
704
  await callViewModel?.hangUp()
705
-
705
+
706
706
  await MainActor.run {
707
707
  self.overlayView?.isHidden = true
708
708
  self.webView?.isOpaque = true
709
-
709
+
710
710
  // Remove touch interceptor
711
711
  self.removeTouchInterceptor()
712
712
  }
713
-
713
+
714
714
  call.resolve([
715
715
  "success": true
716
716
  ])
@@ -816,17 +816,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
816
816
  // Update the CallOverlayView with the active call
817
817
  await MainActor.run {
818
818
  print("acceptCall: Setting up UI for accepted call")
819
-
819
+
820
820
  // Ensure views are set up first
821
821
  self.setupViews()
822
-
822
+
823
823
  // Add touch interceptor for the call
824
824
  self.addTouchInterceptor()
825
-
825
+
826
826
  // self.overlayViewModel?.updateCall(streamCall)
827
827
  self.overlayView?.isHidden = false
828
828
  self.webView?.isOpaque = false
829
-
829
+
830
830
  print("acceptCall: UI setup complete - overlay visible: \(!self.overlayView!.isHidden), touch interceptor: \(self.touchInterceptView != nil)")
831
831
  }
832
832
 
@@ -844,18 +844,18 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
844
844
  }
845
845
 
846
846
  private func initializeStreamVideo() {
847
- if (state == .initialized) {
847
+ if state == .initialized {
848
848
  print("initializeStreamVideo already initialized")
849
849
  // Try to get user credentials from repository
850
850
  guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
851
851
  print("Save credentials not found, skipping initialization")
852
852
  return
853
853
  }
854
- if (savedCredentials.user.id == streamVideo?.user.id) {
854
+ if savedCredentials.user.id == streamVideo?.user.id {
855
855
  print("Skipping initializeStreamVideo as user is already logged in")
856
856
  return
857
857
  }
858
- } else if (state == .initializing) {
858
+ } else if state == .initializing {
859
859
  print("initializeStreamVideo rejected - already initializing")
860
860
  return
861
861
  }
@@ -881,7 +881,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
881
881
  print("StreamCallPlugin: Token provider called for token refresh")
882
882
  guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
883
883
  print("StreamCallPlugin: Token provider - No saved credentials found, failing token refresh")
884
-
884
+
885
885
  completion(.failure(NSError(domain: "No saved credentials or API key found, cannot refresh token", code: 0, userInfo: nil)))
886
886
  return
887
887
  }
@@ -889,14 +889,14 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
889
889
  completion(.success(UserToken(stringLiteral: savedCredentials.tokenValue)))
890
890
  }
891
891
  )
892
-
893
- if (self.callViewModel == nil) {
892
+
893
+ if self.callViewModel == nil {
894
894
  // Initialize on main thread with proper MainActor isolation
895
895
  DispatchQueue.main.async {
896
896
  Task { @MainActor in
897
897
  self.callViewModel = CallViewModel(participantsLayout: .grid)
898
898
  // self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
899
-
899
+
900
900
  // Setup subscriptions for new StreamVideo instance
901
901
  self.setupActiveCallSubscription()
902
902
  }
@@ -906,7 +906,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
906
906
  state = .initialized
907
907
  callKitAdapter.streamVideo = self.streamVideo
908
908
  callKitAdapter.availabilityPolicy = .always
909
-
909
+
910
910
  setupTokenSubscription()
911
911
 
912
912
  // Register for incoming calls
@@ -915,21 +915,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
915
915
 
916
916
  private func setupViews() {
917
917
  guard let webView = self.webView, let parent = webView.superview else { return }
918
-
918
+
919
919
  // Create SwiftUI view with view model if not already created
920
920
  if self.overlayView == nil, let callViewModel = self.callViewModel {
921
921
  let hostingController = UIHostingController(rootView: CallOverlayView(viewModel: callViewModel))
922
922
  hostingController.view.backgroundColor = .clear
923
923
  hostingController.view.translatesAutoresizingMaskIntoConstraints = false
924
924
  hostingController.view.isHidden = true // Initially hidden until a call is active
925
-
925
+
926
926
  self.hostingController = hostingController
927
927
  self.overlayView = hostingController.view
928
-
928
+
929
929
  if let overlayView = self.overlayView {
930
930
  // Insert overlay view below webview
931
931
  parent.insertSubview(overlayView, belowSubview: webView)
932
-
932
+
933
933
  // Setup constraints for overlayView
934
934
  let safeGuide = parent.safeAreaLayoutGuide
935
935
  NSLayoutConstraint.activate([
@@ -940,7 +940,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
940
940
  ])
941
941
  }
942
942
  }
943
-
943
+
944
944
  // Check if we have an active call and need to add touch interceptor
945
945
  if let activeCall = self.streamVideo?.state.activeCall {
946
946
  print("Active call detected during setupViews, ensuring touch interceptor is added")
@@ -959,26 +959,26 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
959
959
  }
960
960
  }
961
961
  }
962
-
962
+
963
963
  private func addTouchInterceptor() {
964
964
  guard let webView = self.webView, let parent = webView.superview else {
965
965
  print("Cannot add touch interceptor - webView or parent not ready, marking for deferred setup")
966
966
  self.needsTouchInterceptorSetup = true
967
967
  return
968
968
  }
969
-
969
+
970
970
  // Check if touch interceptor already exists
971
971
  if self.touchInterceptView != nil {
972
972
  print("Touch interceptor already exists, skipping creation")
973
973
  return
974
974
  }
975
-
975
+
976
976
  // Create the touch intercept view as an overlay for touch passthrough
977
977
  let touchInterceptView = TouchInterceptView(frame: parent.bounds)
978
978
  touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
979
979
  touchInterceptView.backgroundColor = .clear
980
980
  touchInterceptView.isOpaque = false
981
-
981
+
982
982
  // Setup touch intercept view with references to webview and overlay
983
983
  if let overlayView = self.overlayView {
984
984
  touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
@@ -991,15 +991,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
991
991
  parent.addSubview(touchInterceptView)
992
992
  parent.bringSubviewToFront(touchInterceptView)
993
993
  }
994
-
994
+
995
995
  // Set up active call check function
996
996
  touchInterceptView.setActiveCallCheck { [weak self] in
997
997
  return self?.streamVideo?.state.activeCall != nil
998
998
  }
999
-
999
+
1000
1000
  // Store reference to touch intercept view
1001
1001
  self.touchInterceptView = touchInterceptView
1002
-
1002
+
1003
1003
  // Setup constraints for touchInterceptView to cover the entire parent
1004
1004
  NSLayoutConstraint.activate([
1005
1005
  touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
@@ -1007,26 +1007,26 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1007
1007
  touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
1008
1008
  touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
1009
1009
  ])
1010
-
1010
+
1011
1011
  print("Touch interceptor added for active call - view hierarchy: \(parent.subviews.map { type(of: $0) })")
1012
1012
  }
1013
-
1013
+
1014
1014
  private func removeTouchInterceptor() {
1015
1015
  guard let touchInterceptView = self.touchInterceptView else { return }
1016
-
1016
+
1017
1017
  // Remove touch interceptor from view hierarchy
1018
1018
  touchInterceptView.removeFromSuperview()
1019
1019
  self.touchInterceptView = nil
1020
1020
  self.needsTouchInterceptorSetup = false
1021
-
1021
+
1022
1022
  print("Touch interceptor removed after call ended")
1023
1023
  }
1024
-
1024
+
1025
1025
  private func createCallOverlayView() {
1026
1026
  guard let webView = self.webView,
1027
1027
  let parent = webView.superview,
1028
1028
  let callOverlayView = self.callViewModel else { return }
1029
-
1029
+
1030
1030
  // Check if we already have an overlay view - do nothing if it exists
1031
1031
  if let existingOverlayView = self.overlayView, existingOverlayView.superview != nil {
1032
1032
  print("Call overlay view already exists, making it visible")
@@ -1037,50 +1037,50 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1037
1037
  webView.scrollView.backgroundColor = .clear
1038
1038
  return
1039
1039
  }
1040
-
1040
+
1041
1041
  print("Creating new call overlay view")
1042
-
1042
+
1043
1043
  // First, create the overlay view
1044
1044
  let overlayView = UIHostingController(rootView: CallOverlayView(viewModel: callOverlayView))
1045
1045
  overlayView.view.translatesAutoresizingMaskIntoConstraints = false
1046
1046
  overlayView.view.isHidden = false // Make visible during a call
1047
-
1047
+
1048
1048
  // Insert the overlay view below the webView in the view hierarchy
1049
1049
  parent.insertSubview(overlayView.view, belowSubview: webView)
1050
-
1050
+
1051
1051
  // Set constraints to fill the parent's safe area
1052
1052
  let safeGuide = parent.safeAreaLayoutGuide
1053
-
1053
+
1054
1054
  NSLayoutConstraint.activate([
1055
1055
  overlayView.view.topAnchor.constraint(equalTo: safeGuide.topAnchor),
1056
1056
  overlayView.view.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
1057
1057
  overlayView.view.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
1058
1058
  overlayView.view.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
1059
1059
  ])
1060
-
1060
+
1061
1061
  // Make webview transparent to see StreamCall UI underneath
1062
1062
  webView.isOpaque = false
1063
1063
  webView.backgroundColor = .clear
1064
1064
  webView.scrollView.backgroundColor = .clear
1065
-
1065
+
1066
1066
  // Store reference to the hosting controller
1067
1067
  self.hostingController = overlayView
1068
1068
  self.overlayView = overlayView.view
1069
-
1069
+
1070
1070
  // Add touch interceptor if not already present
1071
1071
  self.addTouchInterceptor()
1072
1072
  }
1073
-
1073
+
1074
1074
  // MARK: - Dynamic API Key Management
1075
-
1075
+
1076
1076
  func saveDynamicApiKey(_ apiKey: String) {
1077
1077
  UserDefaults.standard.set(apiKey, forKey: dynamicApiKeyKey)
1078
1078
  }
1079
-
1079
+
1080
1080
  func getDynamicApiKey() -> String? {
1081
1081
  return UserDefaults.standard.string(forKey: dynamicApiKeyKey)
1082
1082
  }
1083
-
1083
+
1084
1084
  func getEffectiveApiKey() -> String? {
1085
1085
  // A) Check if the key exists in UserDefaults
1086
1086
  if let dynamicApiKey = getDynamicApiKey(), !dynamicApiKey.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty {
@@ -1092,15 +1092,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1092
1092
  return self.apiKey
1093
1093
  }
1094
1094
  }
1095
-
1095
+
1096
1096
  func ensureViewRemoved() {
1097
1097
  // Check if we have an overlay view
1098
1098
  if let existingOverlayView = self.overlayView {
1099
1099
  print("Hiding call overlay view")
1100
-
1100
+
1101
1101
  // Hide the view instead of removing it
1102
1102
  existingOverlayView.isHidden = true
1103
-
1103
+
1104
1104
  // Reset opacity for webView
1105
1105
  self.webView?.isOpaque = true
1106
1106
  self.webView?.backgroundColor = nil
@@ -1108,7 +1108,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1108
1108
  } else {
1109
1109
  print("No call overlay view to hide")
1110
1110
  }
1111
-
1111
+
1112
1112
  // Remove touch interceptor
1113
1113
  self.removeTouchInterceptor()
1114
1114
  }
@@ -1125,28 +1125,28 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1125
1125
  "state": currentCallState
1126
1126
  ])
1127
1127
  }
1128
-
1128
+
1129
1129
  @objc func getCallInfo(_ call: CAPPluginCall) {
1130
1130
  guard let callId = call.getString("callId") else {
1131
1131
  call.reject("Missing required parameter: callId")
1132
1132
  return
1133
1133
  }
1134
-
1134
+
1135
1135
  do {
1136
1136
  try requireInitialized()
1137
-
1137
+
1138
1138
  guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
1139
1139
  call.reject("Call ID does not match active call")
1140
1140
  return
1141
1141
  }
1142
-
1142
+
1143
1143
  Task {
1144
1144
  do {
1145
1145
  // Get detailed call information
1146
1146
  let callInfo = try await activeCall.get()
1147
-
1147
+
1148
1148
  // Extract caller information
1149
- var caller: [String: Any]? = nil
1149
+ var caller: [String: Any]?
1150
1150
  let createdBy = callInfo.call.createdBy
1151
1151
  var callerData: [String: Any] = [:]
1152
1152
  callerData["userId"] = createdBy.id
@@ -1154,7 +1154,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1154
1154
  callerData["imageURL"] = createdBy.image
1155
1155
  callerData["role"] = createdBy.role
1156
1156
  caller = callerData
1157
-
1157
+
1158
1158
  // Extract members information
1159
1159
  var membersArray: [[String: Any]] = []
1160
1160
  let participants = await activeCall.state.participants
@@ -1167,7 +1167,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1167
1167
  membersArray.append(memberData)
1168
1168
  }
1169
1169
  let members = membersArray
1170
-
1170
+
1171
1171
  // Determine call state based on current calling state
1172
1172
  let state: String
1173
1173
  let callingState = await self.callViewModel?.callingState
@@ -1189,17 +1189,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1189
1189
  case .none:
1190
1190
  state = "unknown"
1191
1191
  }
1192
-
1192
+
1193
1193
  var result: [String: Any] = [:]
1194
1194
  result["callId"] = callId
1195
1195
  result["state"] = state
1196
-
1196
+
1197
1197
  if let caller = caller {
1198
1198
  result["caller"] = caller
1199
1199
  }
1200
-
1200
+
1201
1201
  result["members"] = members
1202
-
1202
+
1203
1203
  call.resolve(result)
1204
1204
  } catch {
1205
1205
  call.reject("Failed to get call info: \(error.localizedDescription)")
@@ -1240,7 +1240,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1240
1240
  call.reject("StreamVideo not initialized")
1241
1241
  }
1242
1242
  }
1243
-
1243
+
1244
1244
  @objc func switchCamera(_ call: CAPPluginCall) {
1245
1245
  guard let camera = call.getString("camera") else {
1246
1246
  call.reject("Missing required parameter: camera")
@@ -1254,7 +1254,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1254
1254
  do {
1255
1255
  if let activeCall = streamVideo?.state.activeCall {
1256
1256
  if (camera == "front" && activeCall.camera.direction != .front) ||
1257
- (camera == "back" && activeCall.camera.direction != .back) {
1257
+ (camera == "back" && activeCall.camera.direction != .back) {
1258
1258
  try await activeCall.camera.flip()
1259
1259
  }
1260
1260
  }
@@ -1270,13 +1270,13 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1270
1270
  call.reject("StreamVideo not initialized")
1271
1271
  }
1272
1272
  }
1273
-
1273
+
1274
1274
  @objc func setDynamicStreamVideoApikey(_ call: CAPPluginCall) {
1275
1275
  guard let apiKey = call.getString("apiKey") else {
1276
1276
  call.reject("Missing required parameter: apiKey")
1277
1277
  return
1278
1278
  }
1279
-
1279
+
1280
1280
  do {
1281
1281
  saveDynamicApiKey(apiKey)
1282
1282
  print("Dynamic API key saved successfully")
@@ -1288,7 +1288,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1288
1288
  call.reject("Failed to save API key: \(error.localizedDescription)")
1289
1289
  }
1290
1290
  }
1291
-
1291
+
1292
1292
  @objc func getDynamicStreamVideoApikey(_ call: CAPPluginCall) {
1293
1293
  do {
1294
1294
  let apiKey = getDynamicApiKey()
@@ -1301,22 +1301,22 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1301
1301
  call.reject("Failed to get API key: \(error.localizedDescription)")
1302
1302
  }
1303
1303
  }
1304
-
1304
+
1305
1305
  @objc func getCurrentUser(_ call: CAPPluginCall) {
1306
1306
  print("StreamCallPlugin: getCurrentUser called")
1307
1307
  print("StreamCallPlugin: getCurrentUser: Current state: \(state), StreamVideo initialized: \(streamVideo != nil)")
1308
-
1308
+
1309
1309
  do {
1310
1310
  if let savedCredentials = SecureUserRepository.shared.loadCurrentUser() {
1311
1311
  print("StreamCallPlugin: getCurrentUser: Found saved credentials for user: \(savedCredentials.user.id)")
1312
-
1312
+
1313
1313
  // Check if StreamVideo session matches the stored credentials
1314
1314
  let isStreamVideoActive = streamVideo != nil && state == .initialized
1315
1315
  let streamVideoUserId = streamVideo?.user.id
1316
1316
  let credentialsMatch = streamVideoUserId == savedCredentials.user.id
1317
-
1317
+
1318
1318
  print("StreamCallPlugin: getCurrentUser: StreamVideo active: \(isStreamVideoActive), StreamVideo user: \(streamVideoUserId ?? "nil"), Credentials match: \(credentialsMatch)")
1319
-
1319
+
1320
1320
  // If credentials exist but StreamVideo session is not active, try to reinitialize
1321
1321
  if !isStreamVideoActive || !credentialsMatch {
1322
1322
  print("StreamCallPlugin: getCurrentUser: StreamVideo session not active or user mismatch, attempting to reinitialize...")
@@ -1324,7 +1324,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1324
1324
  self.initializeStreamVideo()
1325
1325
  }
1326
1326
  }
1327
-
1327
+
1328
1328
  let result: [String: Any] = [
1329
1329
  "userId": savedCredentials.user.id,
1330
1330
  "name": savedCredentials.user.name,
@@ -1349,5 +1349,5 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1349
1349
  call.reject("Failed to get current user: \(error.localizedDescription)")
1350
1350
  }
1351
1351
  }
1352
-
1352
+
1353
1353
  }