@capgo/capacitor-stream-call 0.0.2 → 0.0.4

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.
@@ -22,9 +22,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
22
22
  CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
23
23
  CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
24
24
  CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
25
- CAPPluginMethod(name: "acceptCall", returnType: CAPPluginReturnPromise)
25
+ CAPPluginMethod(name: "acceptCall", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "isCameraEnabled", returnType: CAPPluginReturnPromise)
26
27
  ]
27
-
28
+
28
29
  private enum State {
29
30
  case notInitialized
30
31
  case initializing
@@ -36,7 +37,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
36
37
  private static let tokenRefreshSemaphore = DispatchSemaphore(value: 1)
37
38
  private var currentToken: String?
38
39
  private var tokenWaitSemaphore: DispatchSemaphore?
39
-
40
+
40
41
  private var overlayView: UIView?
41
42
  private var hostingController: UIHostingController<CallOverlayView>?
42
43
  private var overlayViewModel: CallOverlayViewModel?
@@ -44,17 +45,19 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
44
45
  private var activeCallSubscription: AnyCancellable?
45
46
  private var lastVoIPToken: String?
46
47
  private var touchInterceptView: TouchInterceptView?
47
-
48
+
48
49
  private var streamVideo: StreamVideo?
49
-
50
+
51
+ // Track the current active call ID
52
+ private var currentActiveCallId: String?
53
+
50
54
  @Injected(\.callKitAdapter) var callKitAdapter
51
55
  @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
52
-
53
- private var refreshTokenURL: String?
54
- private var refreshTokenHeaders: [String: String]?
55
-
56
56
  private var webviewDelegate: WebviewNavigationDelegate?
57
-
57
+
58
+ // Add class property to store call states
59
+ private var callStates: [String: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)] = [:]
60
+
58
61
  override public func load() {
59
62
  // Read API key from Info.plist
60
63
  if let apiKey = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_STREAM_VIDEO_APIKEY") as? String {
@@ -63,7 +66,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
63
66
  if self.apiKey == nil {
64
67
  fatalError("Cannot get apikey")
65
68
  }
66
-
69
+
67
70
  // Check if we have a logged in user for handling incoming calls
68
71
  if let credentials = SecureUserRepository.shared.loadCurrentUser() {
69
72
  print("Loading user for StreamCallPlugin: \(credentials.user.name)")
@@ -71,21 +74,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
71
74
  self.initializeStreamVideo()
72
75
  }
73
76
  }
74
-
77
+
75
78
  // Create and set the navigation delegate
76
79
  self.webviewDelegate = WebviewNavigationDelegate(
77
80
  wrappedDelegate: self.webView?.navigationDelegate,
78
81
  onSetupOverlay: { [weak self] in
79
82
  guard let self = self else { return }
80
83
  print("Attempting to setup call view")
81
-
84
+
82
85
  self.setupViews()
83
86
  }
84
87
  )
85
-
88
+
86
89
  self.webView?.navigationDelegate = self.webviewDelegate
87
90
  }
88
-
91
+
89
92
  // private func cleanupStreamVideo() {
90
93
  // // Cancel subscriptions
91
94
  // tokenSubscription?.cancel()
@@ -111,125 +114,28 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
111
114
  //
112
115
  // state = .notInitialized
113
116
  // }
114
-
117
+
115
118
  private func requireInitialized() throws {
116
119
  guard state == .initialized else {
117
120
  throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "StreamVideo not initialized"])
118
121
  }
119
122
  }
120
-
121
- private func refreshToken() throws -> UserToken {
122
- // Acquire the semaphore in a thread-safe way
123
- StreamCallPlugin.tokenRefreshQueue.sync {
124
- StreamCallPlugin.tokenRefreshSemaphore.wait()
125
- }
126
-
127
- defer {
128
- // Always release the semaphore when we're done, even if we throw an error
129
- StreamCallPlugin.tokenRefreshSemaphore.signal()
130
- }
131
-
132
- // Clear current token
133
- currentToken = nil
134
-
135
- // Create a local semaphore for waiting on the token
136
- let localSemaphore = DispatchSemaphore(value: 0)
137
- tokenWaitSemaphore = localSemaphore
138
-
139
- // Capture webView before async context
140
- guard let webView = self.webView else {
141
- print("WebView not available")
142
- tokenWaitSemaphore = nil
143
- throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "WebView not available"])
144
- }
145
-
146
- guard let refreshURL = self.refreshTokenURL else {
147
- print("Refresh URL not configured")
148
- tokenWaitSemaphore = nil
149
- throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Refresh URL not configured"])
150
- }
151
-
152
- let headersJSON = (self.refreshTokenHeaders ?? [:]).reduce(into: "") { result, pair in
153
- result += "\n'\(pair.key)': '\(pair.value)',"
154
- }
155
-
156
- let script = """
157
- (function() {
158
- console.log('Starting token refresh...');
159
- fetch('\(refreshURL)', {
160
- headers: {
161
- \(headersJSON)
162
- }
163
- })
164
- .then(response => {
165
- console.log('Got response:', response.status);
166
- return response.json();
167
- })
168
- .then(data => {
169
- console.log('Got data, token length:', data.token?.length);
170
- const tokenA = data.token;
171
- window.Capacitor.Plugins.StreamCall.loginMagicToken({
172
- token: tokenA
173
- });
174
- })
175
- .catch(error => {
176
- console.error('Token refresh error:', error);
177
- });
178
- })();
179
- """
180
-
181
- if Thread.isMainThread {
182
- print("Executing script on main thread")
183
- webView.evaluateJavaScript(script, completionHandler: nil)
184
- } else {
185
- print("Executing script from background thread")
186
- DispatchQueue.main.sync {
187
- webView.evaluateJavaScript(script, completionHandler: nil)
188
- }
189
- }
190
-
191
- // Set up a timeout
192
- let timeoutQueue = DispatchQueue.global()
193
- let timeoutWork = DispatchWorkItem {
194
- print("Token refresh timed out")
195
- self.tokenWaitSemaphore?.signal()
196
- self.tokenWaitSemaphore = nil
197
- }
198
- timeoutQueue.asyncAfter(deadline: .now() + 10.0, execute: timeoutWork) // 10 second timeout
199
-
200
- // Wait for token to be set via loginMagicToken or timeout
201
- localSemaphore.wait()
202
- timeoutWork.cancel()
203
- tokenWaitSemaphore = nil
204
-
205
- guard let token = currentToken else {
206
- print("Failed to get token")
207
- throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get token or timeout occurred"])
208
- }
209
-
210
- // Save the token
211
- SecureUserRepository.shared.save(token: token)
212
-
213
- print("Got the token!!!")
214
- return UserToken(stringLiteral: token)
215
- }
216
-
217
123
  @objc func loginMagicToken(_ call: CAPPluginCall) {
218
124
  guard let token = call.getString("token") else {
219
125
  call.reject("Missing token parameter")
220
126
  return
221
127
  }
222
-
128
+
223
129
  print("loginMagicToken received token")
224
130
  currentToken = token
225
131
  tokenWaitSemaphore?.signal()
226
132
  call.resolve()
227
133
  }
228
-
134
+
229
135
  private func setupTokenSubscription() {
230
136
  // Cancel existing subscription if any
231
137
  tokenSubscription?.cancel()
232
-
138
+
233
139
  // Create new subscription
234
140
  tokenSubscription = callKitPushNotificationAdapter.$deviceToken.sink { [weak self] (updatedDeviceToken: String) in
235
141
  guard let self = self else { return }
@@ -255,19 +161,144 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
255
161
  }
256
162
  }
257
163
  }
258
-
164
+
259
165
  private func setupActiveCallSubscription() {
260
166
  if let streamVideo = streamVideo {
261
167
  Task {
262
168
  for await event in streamVideo.subscribe() {
263
- print("Event", event)
169
+ // print("Event", event)
264
170
  if let ringingEvent = event.rawValue as? CallRingEvent {
265
171
  notifyListeners("callEvent", data: [
266
172
  "callId": ringingEvent.callCid,
267
173
  "state": "ringing"
268
174
  ])
269
- return
175
+ continue
176
+ }
177
+
178
+ if let callCreatedEvent = event.rawValue as? CallCreatedEvent {
179
+ print("CallCreatedEvent \(String(describing: userId))")
180
+
181
+ let callCid = callCreatedEvent.callCid
182
+ let members = callCreatedEvent.members
183
+
184
+ // Create timer on main thread
185
+ await MainActor.run {
186
+ // Store in the combined callStates map
187
+ self.callStates[callCid] = (
188
+ members: members,
189
+ participantResponses: [:],
190
+ createdAt: Date(),
191
+ timer: nil
192
+ )
193
+
194
+ // Start timer to check for timeout every second
195
+ // Use @objc method as timer target to avoid sendable closure issues
196
+ let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.checkCallTimeoutTimer(_:)), userInfo: callCid, repeats: true)
197
+
198
+ // Update timer in callStates
199
+ self.callStates[callCid]?.timer = timer
200
+ }
201
+ }
202
+
203
+ if let rejectedEvent = event.rawValue as? CallRejectedEvent {
204
+ let userId = rejectedEvent.user.id
205
+ let callCid = rejectedEvent.callCid
206
+
207
+ // Operate on callStates on the main thread
208
+ await MainActor.run {
209
+ // Update the combined callStates map
210
+ if var callState = self.callStates[callCid] {
211
+ callState.participantResponses[userId] = "rejected"
212
+ self.callStates[callCid] = callState
213
+ }
214
+ }
215
+
216
+ print("CallRejectedEvent \(userId)")
217
+ notifyListeners("callEvent", data: [
218
+ "callId": callCid,
219
+ "state": "rejected",
220
+ "userId": userId
221
+ ])
222
+
223
+ await checkAllParticipantsResponded(callCid: callCid)
224
+ continue
225
+ }
226
+
227
+ if let missedEvent = event.rawValue as? CallMissedEvent {
228
+ let userId = missedEvent.user.id
229
+ let callCid = missedEvent.callCid
230
+
231
+ // Operate on callStates on the main thread
232
+ await MainActor.run {
233
+ // Update the combined callStates map
234
+ if var callState = self.callStates[callCid] {
235
+ callState.participantResponses[userId] = "missed"
236
+ self.callStates[callCid] = callState
237
+ }
238
+ }
239
+
240
+ print("CallMissedEvent \(userId)")
241
+ notifyListeners("callEvent", data: [
242
+ "callId": callCid,
243
+ "state": "missed",
244
+ "userId": userId
245
+ ])
246
+
247
+ await checkAllParticipantsResponded(callCid: callCid)
248
+ continue
249
+ }
250
+
251
+ if let participantLeftEvent = event.rawValue as? CallSessionParticipantLeftEvent {
252
+ let callIdSplit = participantLeftEvent.callCid.split(separator: ":")
253
+ if (callIdSplit.count != 2) {
254
+ print("CallSessionParticipantLeftEvent invalid cID \(participantLeftEvent.callCid)")
255
+ continue
256
+ }
257
+
258
+ let callType = callIdSplit[0]
259
+ let callId = callIdSplit[1]
260
+
261
+ let call = streamVideo.call(callType: String(callType), callId: String(callId))
262
+ if await MainActor.run(body: { (call.state.session?.participants.count ?? 1) - 1 <= 1 }) {
263
+
264
+
265
+ print("We are left solo in a call. Ending. cID: \(participantLeftEvent.callCid)")
266
+
267
+ Task {
268
+ if let activeCall = streamVideo.state.activeCall {
269
+ activeCall.leave()
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ if let acceptedEvent = event.rawValue as? CallAcceptedEvent {
276
+ let userId = acceptedEvent.user.id
277
+ let callCid = acceptedEvent.callCid
278
+
279
+ // Operate on callStates on the main thread
280
+ await MainActor.run {
281
+ // Update the combined callStates map
282
+ if var callState = self.callStates[callCid] {
283
+ callState.participantResponses[userId] = "accepted"
284
+
285
+ // If someone accepted, invalidate the timer as we don't need to check anymore
286
+ callState.timer?.invalidate()
287
+ callState.timer = nil
288
+
289
+ self.callStates[callCid] = callState
290
+ }
291
+ }
292
+
293
+ print("CallAcceptedEvent \(userId)")
294
+ notifyListeners("callEvent", data: [
295
+ "callId": callCid,
296
+ "state": "accepted",
297
+ "userId": userId
298
+ ])
299
+ continue
270
300
  }
301
+
271
302
  notifyListeners("callEvent", data: [
272
303
  "callId": streamVideo.state.activeCall?.callId ?? "",
273
304
  "state": event.type
@@ -280,38 +311,62 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
280
311
  // Create new subscription
281
312
  activeCallSubscription = streamVideo?.state.$activeCall.sink { [weak self] newState in
282
313
  guard let self = self else { return }
314
+
283
315
  Task { @MainActor in
284
316
  do {
285
317
  try self.requireInitialized()
286
318
  print("Call State Update:")
287
319
  print("- Call is nil: \(newState == nil)")
320
+
288
321
  if let state = newState?.state {
289
322
  print("- state: \(state)")
290
323
  print("- Session ID: \(state.sessionId)")
291
324
  print("- All participants: \(String(describing: state.participants))")
292
325
  print("- Remote participants: \(String(describing: state.remoteParticipants))")
293
-
326
+
327
+ // Store the active call ID when a call becomes active
328
+ self.currentActiveCallId = newState?.cId
329
+ print("Updated current active call ID: \(String(describing: self.currentActiveCallId))")
330
+
294
331
  // Update overlay and make visible when there's an active call
295
332
  self.overlayViewModel?.updateCall(newState)
296
333
  self.overlayView?.isHidden = false
297
334
  self.webView?.isOpaque = false
298
-
335
+
299
336
  // Notify that a call has started
300
337
  self.notifyListeners("callEvent", data: [
301
338
  "callId": newState?.cId ?? "",
302
339
  "state": "joined"
303
340
  ])
304
341
  } else {
342
+ // Get the call ID that was active before the state changed to nil
343
+ let endingCallId = self.currentActiveCallId
344
+ print("Call ending: \(String(describing: endingCallId))")
345
+
305
346
  // If newState is nil, hide overlay and clear call
306
347
  self.overlayViewModel?.updateCall(nil)
307
348
  self.overlayView?.isHidden = true
308
349
  self.webView?.isOpaque = true
309
-
310
- // Notify that call has ended
350
+
351
+ // Notify that call has ended - use the properly tracked call ID
311
352
  self.notifyListeners("callEvent", data: [
312
- "callId": newState?.cId ?? "",
353
+ "callId": endingCallId ?? "",
313
354
  "state": "left"
314
355
  ])
356
+
357
+ // Clean up any resources for this call
358
+ if let callCid = endingCallId {
359
+ // Invalidate and remove the timer
360
+ self.callStates[callCid]?.timer?.invalidate()
361
+
362
+ // Remove call from callStates
363
+ self.callStates.removeValue(forKey: callCid)
364
+
365
+ print("Cleaned up resources for ended call: \(callCid)")
366
+ }
367
+
368
+ // Clear the active call ID
369
+ self.currentActiveCallId = nil
315
370
  }
316
371
  } catch {
317
372
  log.error("Error handling call state update: \(String(describing: error))")
@@ -319,57 +374,187 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
319
374
  }
320
375
  }
321
376
  }
322
-
377
+
378
+ @objc private func checkCallTimeoutTimer(_ timer: Timer) {
379
+ guard let callCid = timer.userInfo as? String else { return }
380
+
381
+ Task { [weak self] in
382
+ guard let self = self else { return }
383
+ await self.checkCallTimeout(callCid: callCid)
384
+ }
385
+ }
386
+
387
+ private func checkCallTimeout(callCid: String) async {
388
+ // Get a local copy of the call state from the main thread
389
+ let callState: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)? = await MainActor.run {
390
+ return self.callStates[callCid]
391
+ }
392
+
393
+ guard let callState = callState else { return }
394
+
395
+ // Calculate time elapsed since call creation
396
+ let now = Date()
397
+ let elapsedSeconds = now.timeIntervalSince(callState.createdAt)
398
+
399
+ // Check if 30 seconds have passed
400
+ if elapsedSeconds >= 30.0 {
401
+
402
+ // Check if anyone has accepted
403
+ let hasAccepted = callState.participantResponses.values.contains { $0 == "accepted" }
404
+
405
+ if !hasAccepted {
406
+ print("Call \(callCid) has timed out after \(elapsedSeconds) seconds")
407
+ print("No one accepted call \(callCid), marking all non-responders as missed")
408
+
409
+ // Mark all members who haven't responded as "missed"
410
+ for member in callState.members {
411
+ let memberId = member.userId
412
+ let needsToBeMarkedAsMissed = await MainActor.run {
413
+ return self.callStates[callCid]?.participantResponses[memberId] == nil
414
+ }
415
+
416
+ if needsToBeMarkedAsMissed {
417
+ // Update callStates map on main thread
418
+ await MainActor.run {
419
+ var updatedCallState = self.callStates[callCid]
420
+ updatedCallState?.participantResponses[memberId] = "missed"
421
+ if let updatedCallState = updatedCallState {
422
+ self.callStates[callCid] = updatedCallState
423
+ }
424
+ }
425
+
426
+ // Notify listeners
427
+ await MainActor.run {
428
+ self.notifyListeners("callEvent", data: [
429
+ "callId": callCid,
430
+ "state": "missed",
431
+ "userId": memberId
432
+ ])
433
+ }
434
+ }
435
+ }
436
+
437
+ // End the call
438
+ if let call = streamVideo?.state.activeCall, call.cId == callCid {
439
+ call.leave()
440
+ }
441
+
442
+ // Clean up timer on main thread
443
+ await MainActor.run {
444
+ self.callStates[callCid]?.timer?.invalidate()
445
+ var updatedCallState = self.callStates[callCid]
446
+ updatedCallState?.timer = nil
447
+ if let updatedCallState = updatedCallState {
448
+ self.callStates[callCid] = updatedCallState
449
+ }
450
+
451
+ // Remove from callStates
452
+ self.callStates.removeValue(forKey: callCid)
453
+ }
454
+
455
+ // Update UI
456
+ await MainActor.run {
457
+ self.overlayViewModel?.updateCall(nil)
458
+ self.overlayView?.isHidden = true
459
+ self.webView?.isOpaque = true
460
+ self.notifyListeners("callEvent", data: [
461
+ "callId": callCid,
462
+ "state": "ended",
463
+ "reason": "timeout"
464
+ ])
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ private func checkAllParticipantsResponded(callCid: String) async {
471
+ // Get a local copy of the call state from the main thread
472
+ let callState: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)? = await MainActor.run {
473
+ return self.callStates[callCid]
474
+ }
475
+
476
+ guard let callState = callState else {
477
+ print("Call state not found for cId: \(callCid)")
478
+ return
479
+ }
480
+
481
+ let totalParticipants = callState.members.count
482
+ let responseCount = callState.participantResponses.count
483
+
484
+ print("Total participants: \(totalParticipants), Responses: \(responseCount)")
485
+
486
+ let allResponded = responseCount >= totalParticipants
487
+ let allRejectedOrMissed = allResponded &&
488
+ callState.participantResponses.values.allSatisfy { $0 == "rejected" || $0 == "missed" }
489
+
490
+ if allResponded && allRejectedOrMissed {
491
+ print("All participants have rejected or missed the call")
492
+
493
+ // End the call
494
+ if let call = streamVideo?.state.activeCall, call.cId == callCid {
495
+ call.leave()
496
+ }
497
+
498
+ // Clean up timer and remove from callStates on main thread
499
+ await MainActor.run {
500
+ // Clean up timer
501
+ self.callStates[callCid]?.timer?.invalidate()
502
+
503
+ // Remove from callStates
504
+ self.callStates.removeValue(forKey: callCid)
505
+
506
+ self.overlayViewModel?.updateCall(nil)
507
+ self.overlayView?.isHidden = true
508
+ self.webView?.isOpaque = true
509
+ self.notifyListeners("callEvent", data: [
510
+ "callId": callCid,
511
+ "state": "ended",
512
+ "reason": "all_rejected_or_missed"
513
+ ])
514
+ }
515
+ }
516
+ }
517
+
323
518
  @objc func login(_ call: CAPPluginCall) {
324
519
  guard let token = call.getString("token"),
325
520
  let userId = call.getString("userId"),
326
- let name = call.getString("name"),
327
- let apiKey = call.getString("apiKey") else {
521
+ let name = call.getString("name") else {
328
522
  call.reject("Missing required parameters")
329
523
  return
330
524
  }
331
-
525
+
332
526
  let imageURL = call.getString("imageURL")
333
- let refreshTokenConfig = call.getObject("refreshToken")
334
- let refreshTokenURL = refreshTokenConfig?["url"] as? String
335
- let refreshTokenHeaders = refreshTokenConfig?["headers"] as? [String: String]
336
-
337
527
  let user = User(
338
528
  id: userId,
339
529
  name: name,
340
530
  imageURL: imageURL.flatMap { URL(string: $0) },
341
531
  customData: [:]
342
532
  )
343
-
533
+
344
534
  let credentials = UserCredentials(user: user, tokenValue: token)
345
535
  SecureUserRepository.shared.save(user: credentials)
346
-
347
- // Store API key and refresh config for later use
348
- self.refreshTokenURL = refreshTokenURL
349
- self.refreshTokenHeaders = refreshTokenHeaders
350
-
351
536
  // Initialize Stream Video with new credentials
352
537
  initializeStreamVideo()
353
-
538
+
354
539
  if state != .initialized {
355
540
  call.reject("Failed to initialize StreamVideo")
356
541
  return
357
542
  }
358
-
543
+
359
544
  // Update the CallOverlayView with new StreamVideo instance
360
545
  Task { @MainActor in
361
546
  self.overlayViewModel?.updateStreamVideo(self.streamVideo)
362
547
  }
363
-
548
+
364
549
  call.resolve([
365
550
  "success": true
366
551
  ])
367
552
  }
368
-
553
+
369
554
  @objc func logout(_ call: CAPPluginCall) {
370
555
  // Remove VOIP token from repository
371
556
  SecureUserRepository.shared.save(voipPushToken: nil)
372
-
557
+
373
558
  // Try to delete the device from Stream if we have the last token
374
559
  if let lastVoIPToken = lastVoIPToken {
375
560
  Task {
@@ -380,16 +565,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
380
565
  }
381
566
  }
382
567
  }
383
-
568
+
384
569
  // Cancel subscriptions
385
570
  tokenSubscription?.cancel()
386
571
  tokenSubscription = nil
387
572
  activeCallSubscription?.cancel()
388
573
  activeCallSubscription = nil
389
574
  lastVoIPToken = nil
390
-
575
+
391
576
  SecureUserRepository.shared.removeCurrentUser()
392
-
577
+
393
578
  // Update the CallOverlayView with nil StreamVideo instance
394
579
  Task { @MainActor in
395
580
  self.overlayViewModel?.updateCall(nil)
@@ -397,24 +582,41 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
397
582
  self.overlayView?.isHidden = true
398
583
  self.webView?.isOpaque = true
399
584
  }
400
-
585
+
401
586
  call.resolve([
402
587
  "success": true
403
588
  ])
404
589
  }
590
+
591
+ @objc func isCameraEnabled(_ call: CAPPluginCall) {
592
+ do {
593
+ try requireInitialized()
594
+ } catch {
595
+ call.reject("SDK not initialized")
596
+ }
597
+
598
+ if let activeCall = streamVideo?.state.activeCall {
599
+ call.resolve([
600
+ "enabled": activeCall.camera.status == .enabled
601
+ ])
602
+ } else {
603
+ call.reject("No active call")
604
+ }
605
+ }
405
606
 
406
607
  @objc func call(_ call: CAPPluginCall) {
407
- guard let userId = call.getString("userId") else {
408
- call.reject("Missing required parameter: userId")
608
+ guard let members = call.getArray("userIds", String.self) else {
609
+ call.reject("Missing required parameter: userIds (array of user IDs)")
610
+ return
611
+ }
612
+
613
+ if members.isEmpty {
614
+ call.reject("userIds array cannot be empty")
409
615
  return
410
616
  }
411
617
 
412
618
  // Initialize if needed
413
619
  if state == .notInitialized {
414
- guard let credentials = SecureUserRepository.shared.loadCurrentUser() else {
415
- call.reject("No user credentials found")
416
- return
417
- }
418
620
  initializeStreamVideo()
419
621
  if state != .initialized {
420
622
  call.reject("Failed to initialize StreamVideo")
@@ -436,16 +638,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
436
638
  print("Creating call:")
437
639
  print("- Call ID: \(callId)")
438
640
  print("- Call Type: \(callType)")
439
- print("- User ID: \(userId)")
641
+ print("- Users: \(members)")
440
642
  print("- Should Ring: \(shouldRing)")
441
643
 
442
644
  // Create the call object
443
645
  let streamCall = streamVideo?.call(callType: callType, callId: callId)
444
646
 
445
- // Start the call with the member
446
- print("Creating call with member...")
647
+ // Start the call with the members
648
+ print("Creating call with members...")
447
649
  try await streamCall?.create(
448
- memberIds: [userId],
650
+ memberIds: members,
449
651
  custom: [:],
450
652
  ring: shouldRing
451
653
  )
@@ -480,26 +682,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
480
682
  try requireInitialized()
481
683
 
482
684
  Task {
483
- do {
484
- if let activeCall = streamVideo?.state.activeCall {
485
- try await activeCall.leave()
486
-
487
- // Update view state instead of cleaning up
488
- await MainActor.run {
489
- self.overlayViewModel?.updateCall(nil)
490
- self.overlayView?.isHidden = true
491
- self.webView?.isOpaque = true
492
- }
685
+ if let activeCall = streamVideo?.state.activeCall {
686
+ activeCall.leave()
493
687
 
494
- call.resolve([
495
- "success": true
496
- ])
497
- } else {
498
- call.reject("No active call to end")
688
+ // Update view state instead of cleaning up
689
+ await MainActor.run {
690
+ self.overlayViewModel?.updateCall(nil)
691
+ self.overlayView?.isHidden = true
692
+ self.webView?.isOpaque = true
499
693
  }
500
- } catch {
501
- log.error("Error ending call: \(String(describing: error))")
502
- call.reject("Failed to end call: \(error.localizedDescription)")
694
+
695
+ call.resolve([
696
+ "success": true
697
+ ])
698
+ } else {
699
+ call.reject("No active call to end")
503
700
  }
504
701
  }
505
702
  } catch {
@@ -625,26 +822,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
625
822
  }
626
823
  print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
627
824
 
628
- // Create a local reference to refreshToken to avoid capturing self
629
- let refreshTokenFn = self.refreshToken
630
-
631
825
  self.streamVideo = StreamVideo(
632
826
  apiKey: apiKey,
633
827
  user: savedCredentials.user,
634
- token: UserToken(stringLiteral: savedCredentials.tokenValue),
635
- tokenProvider: { result in
636
- print("attempt to refresh")
637
- DispatchQueue.global().async {
638
- do {
639
- let newToken = try refreshTokenFn()
640
- print("Refresh successful")
641
- result(.success(newToken))
642
- } catch {
643
- print("Refresh fail")
644
- result(.failure(error))
645
- }
646
- }
647
- }
828
+ token: UserToken(stringLiteral: savedCredentials.tokenValue)
648
829
  )
649
830
 
650
831
  state = .initialized