@capgo/capacitor-stream-call 7.1.30 → 7.4.0

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.
@@ -12,6 +12,7 @@ import WebKit
12
12
  */
13
13
  @objc(StreamCallPlugin)
14
14
  public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
15
+ private let PLUGIN_VERSION: String = "7.4.0"
15
16
  public let identifier = "StreamCallPlugin"
16
17
  public let jsName = "StreamCall"
17
18
  public let pluginMethods: [CAPPluginMethod] = [
@@ -19,6 +20,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
19
20
  CAPPluginMethod(name: "login", returnType: CAPPluginReturnPromise),
20
21
  CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
21
22
  CAPPluginMethod(name: "call", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "joinCall", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "toggleViews", returnType: CAPPluginReturnPromise),
22
25
  CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
23
26
  CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
24
27
  CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
@@ -30,7 +33,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
30
33
  CAPPluginMethod(name: "getCallInfo", returnType: CAPPluginReturnPromise),
31
34
  CAPPluginMethod(name: "setDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
32
35
  CAPPluginMethod(name: "getDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
33
- CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise)
36
+ CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
34
38
  ]
35
39
 
36
40
  private enum State {
@@ -47,6 +51,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
47
51
  private var hostingController: UIHostingController<CallOverlayView>?
48
52
  private var tokenSubscription: AnyCancellable?
49
53
  private var activeCallSubscription: AnyCancellable?
54
+ private var speakerSubscription: AnyCancellable?
50
55
  private var lastVoIPToken: String?
51
56
  private var touchInterceptView: TouchInterceptView?
52
57
  private var needsTouchInterceptorSetup: Bool = false
@@ -59,6 +64,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
59
64
  private var currentCallId: String = ""
60
65
  private var currentCallState: String = ""
61
66
  private var hasNotifiedCallJoined: Bool = false
67
+ private var previousCallingState: CallingState?
62
68
 
63
69
  @Injected(\.callKitAdapter) var callKitAdapter
64
70
  @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
@@ -188,9 +194,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
188
194
  DispatchQueue.main.async { [weak self] in
189
195
  guard let self = self else { return }
190
196
 
191
- // Cancel existing subscription if any
197
+ // Cancel existing subscriptions if any
192
198
  self.activeCallSubscription?.cancel()
193
199
  self.activeCallSubscription = nil
200
+ self.speakerSubscription?.cancel()
201
+ self.speakerSubscription = nil
194
202
 
195
203
  // Verify callViewModel exists
196
204
  guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
@@ -208,6 +216,59 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
208
216
  // while the subscription is active
209
217
  let viewModel = callViewModel
210
218
 
219
+ let callEvents = streamVideo
220
+ .eventPublisher()
221
+ .sink { [weak self] event in
222
+ guard let self else { return }
223
+ switch event {
224
+ case let .typeCallRejectedEvent(response):
225
+ let data: [String: Any] = [
226
+ "callId": response.callCid,
227
+ "state": "rejected"
228
+ ]
229
+ notifyListeners("callEvent", data: data)
230
+ case let .typeCallAcceptedEvent(response):
231
+ if let streamUserId = self.streamVideo?.user.id,
232
+ response.user.id == streamUserId,
233
+ response.callCid != self.currentCallId {
234
+
235
+ self.updateCallStatusAndNotify(callId: response.callCid, state: "joined")
236
+ }
237
+ case let .typeCallEndedEvent(response):
238
+ let data: [String: Any] = [
239
+ "callId": response.callCid,
240
+ "state": "left"
241
+ ]
242
+ notifyListeners("callEvent", data: data)
243
+
244
+ case let .typeCallSessionParticipantCountsUpdatedEvent(response):
245
+ let activeCall = streamVideo.state.activeCall
246
+ let callDropped = self.currentCallId == response.callCid && activeCall == nil
247
+ let onlyOneParticipant = activeCall?.cId == response.callCid && activeCall?.state.participantCount == 1
248
+ if onlyOneParticipant || callDropped {
249
+ self.endCallInternal()
250
+ } else {
251
+ print("""
252
+ onlyOneParticipant check:
253
+ - activeCall?.cId: \(String(describing: activeCall?.cId))
254
+ - response.callCid: \(response.callCid)
255
+ - activeCall?.state.participantCount: \(String(describing: activeCall?.state.participantCount))
256
+ - Result (onlyOneParticipant): \(onlyOneParticipant)
257
+ """)
258
+ }
259
+ if let count = activeCall?.state.participantCount {
260
+ let data: [String: Any] = [
261
+ "callId": response.callCid,
262
+ "state": "participant_counts",
263
+ "count": count
264
+ ]
265
+ notifyListeners("callEvent", data: data)
266
+ }
267
+ default:
268
+ break
269
+ }
270
+ }
271
+
211
272
  // Subscribe to streamVideo.state.$activeCall to handle CallKit integration
212
273
  let callPublisher = streamVideo.state.$activeCall
213
274
  .receive(on: DispatchQueue.main)
@@ -220,6 +281,41 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
220
281
  // Sync callViewModel with activeCall from streamVideo state
221
282
  // This ensures CallKit integration works properly
222
283
  viewModel.setActiveCall(activeCall)
284
+ viewModel.update(participantsLayout: .grid)
285
+
286
+ // Subscribe to speaker status for this active call
287
+ self.speakerSubscription = activeCall.speaker.$status
288
+ .receive(on: DispatchQueue.main)
289
+ .sink { [weak self] speakerStatus in
290
+ guard let self = self else { return }
291
+
292
+ // Only emit if the current active call matches our current call ID
293
+ guard activeCall.cId == self.currentCallId else {
294
+ return
295
+ }
296
+
297
+ print("Speaker status update: \(speakerStatus)")
298
+
299
+ let state: String
300
+ switch speakerStatus {
301
+ case .enabled:
302
+ state = "speaker_enabled"
303
+ case .disabled:
304
+ state = "speaker_disabled"
305
+ }
306
+
307
+ let data: [String: Any] = [
308
+ "callId": self.currentCallId,
309
+ "state": state
310
+ ]
311
+
312
+ self.notifyListeners("callEvent", data: data)
313
+ }
314
+ } else {
315
+ // Clean up speaker subscription when activeCall becomes nil
316
+ print("Active call became nil, cleaning up speaker subscription")
317
+ self.speakerSubscription?.cancel()
318
+ self.speakerSubscription = nil
223
319
  }
224
320
  }
225
321
 
@@ -238,6 +334,50 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
238
334
  do {
239
335
  try self.requireInitialized()
240
336
  print("Call State Update: \(newState)")
337
+ // Check if transitioning from outgoing to idle
338
+ if newState == .idle,
339
+ let previousState = self.previousCallingState,
340
+ case .outgoing = previousState {
341
+ print("Call state changed from outgoing to idle. Ending call with ID: \(self.currentCallId)")
342
+
343
+ // End the call using the stored call ID and type
344
+ if !self.currentCallId.isEmpty {
345
+ Task {
346
+ do {
347
+ // Parse call type and id from currentCallId format "type:id"
348
+ let components = self.currentCallId.components(separatedBy: ":")
349
+ if components.count >= 2 {
350
+ let callType = components[0]
351
+ let callId = components[1]
352
+
353
+ // Try to get the call and end it using the parsed call type
354
+ if let streamVideo = self.streamVideo {
355
+ let call = streamVideo.call(callType: callType, callId: callId)
356
+ try await call.end()
357
+ print("Successfully ended outgoing call: \(callId) with type: \(callType)")
358
+
359
+ let data: [String: Any] = [
360
+ "callId": call.cId,
361
+ "state": "outgoing_call_ended"
362
+ ]
363
+
364
+ self.notifyListeners("callEvent", data: data)
365
+ }
366
+ } else {
367
+ print("Invalid call ID format: \(self.currentCallId)")
368
+ }
369
+ } catch {
370
+ print("Error ending outgoing call \(self.currentCallId): \(error)")
371
+ }
372
+
373
+ // Clean up stored call information
374
+ DispatchQueue.main.async {
375
+ self.currentCallId = ""
376
+ print("Cleaned up call information after ending outgoing call")
377
+ }
378
+ }
379
+ }
380
+ }
241
381
 
242
382
  if newState == .inCall {
243
383
  print("- In call state detected")
@@ -255,50 +395,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
255
395
  self.updateCallStatusAndNotify(callId: callId, state: "joined")
256
396
  self.hasNotifiedCallJoined = true
257
397
  }
258
- } else if case .incoming(let incomingCall) = newState {
259
- // Extract caller information
260
- Task {
261
- var caller: [String: Any]?
262
- var members: [[String: Any]]?
263
-
264
- do {
265
- // Get the call from StreamVideo to access detailed information
266
- if let streamVideo = self.streamVideo {
267
- let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
268
- let callInfo = try await call.get()
269
-
270
- // Extract caller information
271
- let createdBy = callInfo.call.createdBy
272
- var callerData: [String: Any] = [:]
273
- callerData["userId"] = createdBy.id
274
- callerData["name"] = createdBy.name
275
- callerData["imageURL"] = createdBy.image
276
- callerData["role"] = createdBy.role
277
- caller = callerData
278
-
279
- // Extract members information from current participants if available
280
- var membersArray: [[String: Any]] = []
281
- if let activeCall = streamVideo.state.activeCall {
282
- let participants = activeCall.state.participants
283
- for participant in participants {
284
- var memberData: [String: Any] = [:]
285
- memberData["userId"] = participant.userId
286
- memberData["name"] = participant.name
287
- memberData["imageURL"] = participant.profileImageURL?.absoluteString ?? ""
288
- memberData["role"] = participant.roles.first ?? ""
289
- membersArray.append(memberData)
290
- }
291
- }
292
- members = membersArray.isEmpty ? nil : membersArray
293
- }
294
- } catch {
295
- print("Failed to get call info for caller details: \(error)")
296
- }
297
398
 
298
- // Notify with caller information
299
- let fullCallId = "\(incomingCall.type):\(incomingCall.id)"
300
- self.updateCallStatusAndNotify(callId: fullCallId, state: "ringing", caller: caller, members: members)
301
- }
399
+ } else if case .incoming(let incomingCall) = newState {
400
+ self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
401
+ // }
302
402
  } else if newState == .idle {
303
403
  print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
304
404
 
@@ -313,21 +413,26 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
313
413
  // Reset notification flag when call ends
314
414
  self.hasNotifiedCallJoined = false
315
415
 
316
- // Remove the call overlay view when not in a call
416
+ // Remove the call overlay view and touch intercept view when not in a call
317
417
  self.ensureViewRemoved()
418
+
318
419
  } else {
319
420
  print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
320
421
  }
321
422
  }
423
+
424
+ // Update the previous state for next comparison
425
+ self.previousCallingState = newState
322
426
  } catch {
323
427
  log.error("Error handling call state update: \(String(describing: error))")
324
428
  }
325
429
  }
326
430
 
327
- // Combine both publishers
431
+ // Combine all publishers
328
432
  self.activeCallSubscription = AnyCancellable {
329
433
  callPublisher.cancel()
330
434
  statePublisher.cancel()
435
+ callEvents.cancel()
331
436
  }
332
437
 
333
438
  print("Active call subscription setup completed")
@@ -421,9 +526,20 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
421
526
  tokenSubscription = nil
422
527
  activeCallSubscription?.cancel()
423
528
  activeCallSubscription = nil
529
+ speakerSubscription?.cancel()
530
+ speakerSubscription = nil
424
531
  lastVoIPToken = nil
532
+ state = .notInitialized
425
533
 
426
534
  SecureUserRepository.shared.removeCurrentUser()
535
+ self.callViewModel = nil
536
+ self.overlayView = nil
537
+
538
+ // Clean up touch intercept view
539
+ if let touchInterceptView = self.touchInterceptView {
540
+ touchInterceptView.removeFromSuperview()
541
+ self.touchInterceptView = nil
542
+ }
427
543
 
428
544
  // Update the CallOverlayView with nil StreamVideo instance
429
545
  Task { @MainActor in
@@ -457,6 +573,85 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
457
573
  }
458
574
  }
459
575
 
576
+ @objc func toggleViews(_ call: CAPPluginCall) {
577
+ Task { @MainActor in
578
+ guard let viewModel = self.callViewModel else {
579
+ call.reject("ViewModel is not initialized.")
580
+ return
581
+ }
582
+
583
+ let layouts: [ParticipantsLayout] = [.spotlight, .fullScreen, .grid]
584
+ let currentLayout = viewModel.participantsLayout
585
+
586
+ // If currentLayout is not in layouts, default to .grid index
587
+ let currentIndex = layouts.firstIndex(of: currentLayout) ?? layouts.firstIndex(of: .grid) ?? 0
588
+ let nextIndex = (currentIndex + 1) % layouts.count
589
+ let nextLayout = layouts[nextIndex]
590
+
591
+ viewModel.update(participantsLayout: nextLayout)
592
+
593
+ call.resolve([
594
+ "newLayout": "\(nextLayout)"
595
+ ])
596
+ }
597
+ }
598
+
599
+ @objc func joinCall(_ call: CAPPluginCall) {
600
+ guard let callId = call.getString("callId") else {
601
+ call.reject("Missing required parameter: callId")
602
+ return
603
+ }
604
+
605
+ guard let callType = call.getString("callType") else {
606
+ call.reject("Missing required parameter: callType")
607
+ return
608
+ }
609
+
610
+ // Initialize if needed
611
+ if state == .notInitialized {
612
+ initializeStreamVideo()
613
+ if state != .initialized {
614
+ call.reject("Failed to initialize StreamVideo")
615
+ return
616
+ }
617
+ }
618
+
619
+ do {
620
+ try requireInitialized()
621
+
622
+ Task {
623
+ do {
624
+ print("Joining call:")
625
+ print("- Call ID: \(callId)")
626
+ print("- Call Type: \(callType)")
627
+
628
+ // Create the call object
629
+ await self.callViewModel?.joinCall(
630
+ callType: callType,
631
+ callId: callId
632
+ )
633
+
634
+ // Now send the created event with complete member data
635
+ self.updateCallStatusAndNotify(callId: callId, state: "joined")
636
+
637
+ // Update UI on main thread
638
+ await MainActor.run {
639
+ // self.overlayViewModel?.updateCall(streamCall)
640
+ self.overlayView?.isHidden = false
641
+ self.webView?.isOpaque = false
642
+ }
643
+
644
+ call.resolve([
645
+ "success": true
646
+ ])
647
+
648
+ }
649
+ }
650
+ } catch {
651
+ call.reject("StreamVideo not initialized")
652
+ }
653
+ }
654
+
460
655
  @objc func call(_ call: CAPPluginCall) {
461
656
  guard let members = call.getArray("userIds", String.self) else {
462
657
  call.reject("Missing required parameter: userIds (array of user IDs)")
@@ -625,6 +820,90 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
625
820
  }
626
821
  }
627
822
 
823
+ private func endCallInternal() {
824
+ do {
825
+ try requireInitialized()
826
+
827
+ Task {
828
+ // Check both active call and callViewModel's call state to handle outgoing calls
829
+ let activeCall = streamVideo?.state.activeCall
830
+ let viewModelCall = await callViewModel?.call
831
+
832
+ // Helper function to determine if we should end or leave the call
833
+ func shouldEndCall(for streamCall: Call) async throws -> Bool {
834
+ do {
835
+ let callInfo = try await streamCall.get()
836
+ let currentUserId = streamVideo?.user.id
837
+ let createdBy = callInfo.call.createdBy.id
838
+ let isCreator = createdBy == currentUserId
839
+
840
+ // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
841
+ // let totalParticipants = await streamCall.state.participants.count
842
+ let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
843
+ let shouldEnd = isCreator || forceEnd
844
+
845
+ print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), ShouldEnd: \(shouldEnd)")
846
+
847
+ return shouldEnd
848
+ } catch {
849
+ print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
850
+ return false // Fallback to leave if we can't determine
851
+ }
852
+ }
853
+
854
+ if let activeCall = activeCall {
855
+ // There's an active call, check if we should end or leave
856
+ do {
857
+ let shouldEnd = try await shouldEndCall(for: activeCall)
858
+
859
+ if shouldEnd {
860
+ print("Ending active call \(activeCall.cId) for all participants")
861
+ try await activeCall.end()
862
+ } else {
863
+ print("Leaving active call \(activeCall.cId)")
864
+ try await activeCall.leave()
865
+ }
866
+ } catch {
867
+ print("Error ending/leaving active call: \(error)")
868
+ try await activeCall.leave() // Fallback to leave
869
+ }
870
+
871
+ await MainActor.run {
872
+ self.overlayView?.isHidden = true
873
+ self.webView?.isOpaque = true
874
+ }
875
+
876
+ } else if let viewModelCall = viewModelCall {
877
+ // There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
878
+ do {
879
+ let shouldEnd = try await shouldEndCall(for: viewModelCall)
880
+
881
+ if shouldEnd {
882
+ print("Ending viewModel call \(viewModelCall.cId) for all participants")
883
+ try await viewModelCall.end()
884
+ } else {
885
+ print("Leaving viewModel call \(viewModelCall.cId)")
886
+ try await viewModelCall.leave()
887
+ }
888
+ } catch {
889
+ print("Error ending/leaving viewModel call: \(error)")
890
+ try await viewModelCall.leave() // Fallback to leave
891
+ }
892
+
893
+ // Also hang up to reset the calling state
894
+ await callViewModel?.hangUp()
895
+
896
+ await MainActor.run {
897
+ self.overlayView?.isHidden = true
898
+ self.webView?.isOpaque = true
899
+ }
900
+ }
901
+ }
902
+ } catch {
903
+ log.error("StreamVideo not initialized")
904
+ }
905
+ }
906
+
628
907
  @objc func endCall(_ call: CAPPluginCall) {
629
908
  do {
630
909
  try requireInitialized()
@@ -641,10 +920,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
641
920
  let currentUserId = streamVideo?.user.id
642
921
  let createdBy = callInfo.call.createdBy.id
643
922
  let isCreator = createdBy == currentUserId
923
+ let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
644
924
 
645
925
  // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
646
926
  let totalParticipants = await streamCall.state.participants.count
647
- let shouldEnd = isCreator || totalParticipants <= 2
927
+ let shouldEnd = isCreator || forceEnd
648
928
 
649
929
  print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
650
930
 
@@ -735,10 +1015,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
735
1015
  Task {
736
1016
  do {
737
1017
  if let activeCall = streamVideo?.state.activeCall {
738
- if enabled {
739
- try await activeCall.microphone.enable()
740
- } else {
741
- try await activeCall.microphone.disable()
1018
+ if enabled && activeCall.microphone.status == .disabled {
1019
+ try await activeCall.microphone.toggle()
1020
+ } else if !enabled && activeCall.microphone.status == .enabled {
1021
+ try await activeCall.microphone.toggle()
742
1022
  }
743
1023
  call.resolve([
744
1024
  "success": true
@@ -768,10 +1048,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
768
1048
  Task {
769
1049
  do {
770
1050
  if let activeCall = streamVideo?.state.activeCall {
771
- if enabled {
772
- try await activeCall.camera.enable()
773
- } else {
774
- try await activeCall.camera.disable()
1051
+ if enabled && activeCall.camera.status == .disabled {
1052
+ try await activeCall.camera.toggle()
1053
+ } else if !enabled && activeCall.camera.status == .enabled {
1054
+ try await activeCall.camera.toggle()
775
1055
  }
776
1056
  call.resolve([
777
1057
  "success": true
@@ -896,7 +1176,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
896
1176
  Task { @MainActor in
897
1177
  self.callViewModel = CallViewModel(participantsLayout: .grid)
898
1178
  // self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
899
-
900
1179
  // Setup subscriptions for new StreamVideo instance
901
1180
  self.setupActiveCallSubscription()
902
1181
  }
@@ -905,7 +1184,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
905
1184
 
906
1185
  state = .initialized
907
1186
  callKitAdapter.streamVideo = self.streamVideo
908
- callKitAdapter.availabilityPolicy = .always
1187
+ callKitAdapter.availabilityPolicy = .regionBased
909
1188
 
910
1189
  setupTokenSubscription()
911
1190
 
@@ -1035,6 +1314,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1035
1314
  webView.isOpaque = false
1036
1315
  webView.backgroundColor = .clear
1037
1316
  webView.scrollView.backgroundColor = .clear
1317
+
1318
+ // Add touch intercept view when making existing overlay visible
1319
+ addTouchInterceptView(webView: webView, parent: parent, overlayView: existingOverlayView)
1038
1320
  return
1039
1321
  }
1040
1322
 
@@ -1067,8 +1349,38 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1067
1349
  self.hostingController = overlayView
1068
1350
  self.overlayView = overlayView.view
1069
1351
 
1070
- // Add touch interceptor if not already present
1071
- self.addTouchInterceptor()
1352
+ // Add touch intercept view for the new overlay
1353
+ addTouchInterceptView(webView: webView, parent: parent, overlayView: overlayView.view)
1354
+ }
1355
+
1356
+ private func addTouchInterceptView(webView: WKWebView, parent: UIView, overlayView: UIView) {
1357
+ // Create touch intercept view
1358
+ let touchInterceptView = TouchInterceptView(frame: parent.bounds)
1359
+ touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
1360
+ touchInterceptView.backgroundColor = .clear
1361
+ touchInterceptView.isOpaque = false
1362
+ touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
1363
+
1364
+ // Add to parent view
1365
+ parent.addSubview(touchInterceptView)
1366
+
1367
+ // Set up active call check function
1368
+ touchInterceptView.setActiveCallCheck { [weak self] in
1369
+ return self?.streamVideo?.state.activeCall != nil
1370
+ }
1371
+
1372
+ // Store reference and set call active
1373
+ self.touchInterceptView = touchInterceptView
1374
+
1375
+ // Setup constraints for touchInterceptView to cover the entire parent
1376
+ NSLayoutConstraint.activate([
1377
+ touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
1378
+ touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
1379
+ touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
1380
+ touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
1381
+ ])
1382
+
1383
+ print("Touch intercept view added and activated for call")
1072
1384
  }
1073
1385
 
1074
1386
  // MARK: - Dynamic API Key Management
@@ -1094,6 +1406,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1094
1406
  }
1095
1407
 
1096
1408
  func ensureViewRemoved() {
1409
+ print("Removing call views and touch intercept")
1410
+
1411
+ // Remove touch intercept view
1412
+ if let touchInterceptView = self.touchInterceptView {
1413
+ print("Removing touch intercept view")
1414
+ touchInterceptView.removeFromSuperview()
1415
+ self.touchInterceptView = nil
1416
+ }
1417
+
1097
1418
  // Check if we have an overlay view
1098
1419
  if let existingOverlayView = self.overlayView {
1099
1420
  print("Hiding call overlay view")
@@ -1120,93 +1441,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1120
1441
  return
1121
1442
  }
1122
1443
 
1123
- call.resolve([
1444
+ let result: [String: Any] = [
1124
1445
  "callId": currentCallId,
1125
1446
  "state": currentCallState
1126
- ])
1127
- }
1447
+ ]
1128
1448
 
1129
- @objc func getCallInfo(_ call: CAPPluginCall) {
1130
- guard let callId = call.getString("callId") else {
1131
- call.reject("Missing required parameter: callId")
1132
- return
1133
- }
1449
+ call.resolve(result)
1450
+ }
1134
1451
 
1452
+ func getCallInfo(callId: String, activeCall: Call) async -> [String: Any] {
1135
1453
  do {
1136
- try requireInitialized()
1454
+ let customRaw = await MainActor.run { activeCall.state.custom }
1137
1455
 
1138
- guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
1139
- call.reject("Call ID does not match active call")
1140
- return
1141
- }
1456
+ // Convert RawJSON dictionary to [String: Any] via JSONSerialization
1457
+ let custom: [String: Any]
1458
+ let jsonData = try JSONEncoder().encode(customRaw)
1459
+ custom = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] ?? [:]
1142
1460
 
1143
- Task {
1144
- do {
1145
- // Get detailed call information
1146
- let callInfo = try await activeCall.get()
1147
-
1148
- // Extract caller information
1149
- var caller: [String: Any]?
1150
- let createdBy = callInfo.call.createdBy
1151
- var callerData: [String: Any] = [:]
1152
- callerData["userId"] = createdBy.id
1153
- callerData["name"] = createdBy.name
1154
- callerData["imageURL"] = createdBy.image
1155
- callerData["role"] = createdBy.role
1156
- caller = callerData
1157
-
1158
- // Extract members information
1159
- var membersArray: [[String: Any]] = []
1160
- let participants = await activeCall.state.participants
1161
- for participant in participants {
1162
- var memberData: [String: Any] = [:]
1163
- memberData["userId"] = participant.userId
1164
- memberData["name"] = participant.name
1165
- memberData["imageURL"] = participant.profileImageURL
1166
- memberData["role"] = participant.roles.first ?? ""
1167
- membersArray.append(memberData)
1168
- }
1169
- let members = membersArray
1170
-
1171
- // Determine call state based on current calling state
1172
- let state: String
1173
- let callingState = await self.callViewModel?.callingState
1174
- switch callingState {
1175
- case .idle:
1176
- state = "idle"
1177
- case .incoming:
1178
- state = "ringing"
1179
- case .outgoing:
1180
- state = "ringing"
1181
- case .inCall:
1182
- state = "joined"
1183
- case .lobby:
1184
- state = "lobby"
1185
- case .joining:
1186
- state = "joining"
1187
- case .reconnecting:
1188
- state = "reconnecting"
1189
- case .none:
1190
- state = "unknown"
1191
- }
1461
+ let result: [String: Any] = [
1462
+ "callId": callId,
1463
+ "custom": custom
1464
+ ]
1192
1465
 
1193
- var result: [String: Any] = [:]
1194
- result["callId"] = callId
1195
- result["state"] = state
1196
-
1197
- if let caller = caller {
1198
- result["caller"] = caller
1199
- }
1466
+ print("Debug: Resolving getCallInfo with custom =", custom)
1467
+ return result
1468
+ } catch {
1469
+ print("Error resolving getCallInfo:", error)
1470
+ return [
1471
+ "callId": callId,
1472
+ "custom": [:]
1473
+ ]
1474
+ }
1475
+ }
1200
1476
 
1201
- result["members"] = members
1477
+ @objc func enableBluetooth(call: CAPPluginCall) {
1478
+ Task {
1479
+ do {
1202
1480
 
1203
- call.resolve(result)
1204
- } catch {
1205
- call.reject("Failed to get call info: \(error.localizedDescription)")
1206
- }
1481
+ let policy = DefaultAudioSessionPolicy()
1482
+ try await self.callViewModel?.call?.updateAudioSessionPolicy(policy)
1483
+ call.resolve([
1484
+ "success": true
1485
+ ])
1486
+ } catch {
1487
+ print("Failed to update policy: \(error)")
1488
+ call.reject("Unable to set bluetooth policy")
1207
1489
  }
1208
- } catch {
1209
- call.reject("StreamVideo not initialized")
1210
1490
  }
1211
1491
  }
1212
1492
 
@@ -1350,4 +1630,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1350
1630
  }
1351
1631
  }
1352
1632
 
1633
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
1634
+ call.resolve(["version": self.PLUGIN_VERSION])
1635
+ }
1636
+
1353
1637
  }