@capgo/capacitor-stream-call 0.0.2

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.
Files changed (39) hide show
  1. package/Package.swift +31 -0
  2. package/README.md +340 -0
  3. package/StreamCall.podspec +19 -0
  4. package/android/build.gradle +74 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
  7. package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
  8. package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
  9. package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
  10. package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
  11. package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
  12. package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
  13. package/android/src/main/res/.gitkeep +0 -0
  14. package/android/src/main/res/values/strings.xml +7 -0
  15. package/dist/docs.json +533 -0
  16. package/dist/esm/definitions.d.ts +169 -0
  17. package/dist/esm/definitions.js +2 -0
  18. package/dist/esm/definitions.js.map +1 -0
  19. package/dist/esm/index.d.ts +4 -0
  20. package/dist/esm/index.js +7 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/web.d.ts +32 -0
  23. package/dist/esm/web.js +323 -0
  24. package/dist/esm/web.js.map +1 -0
  25. package/dist/plugin.cjs.js +337 -0
  26. package/dist/plugin.cjs.js.map +1 -0
  27. package/dist/plugin.js +339 -0
  28. package/dist/plugin.js.map +1 -0
  29. package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
  30. package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
  31. package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
  32. package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
  33. package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
  34. package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
  35. package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
  36. package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
  37. package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
  38. package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
  39. package/package.json +96 -0
@@ -0,0 +1,722 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import StreamVideo
4
+ import StreamVideoSwiftUI
5
+ import SwiftUI
6
+ import Combine
7
+ import WebKit
8
+
9
+ /**
10
+ * Please read the Capacitor iOS Plugin Development Guide
11
+ * here: https://capacitorjs.com/docs/plugins/ios
12
+ */
13
+ @objc(StreamCallPlugin)
14
+ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
15
+ public let identifier = "StreamCallPlugin"
16
+ public let jsName = "StreamCall"
17
+ public let pluginMethods: [CAPPluginMethod] = [
18
+ CAPPluginMethod(name: "loginMagicToken", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "login", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "call", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "acceptCall", returnType: CAPPluginReturnPromise)
26
+ ]
27
+
28
+ private enum State {
29
+ case notInitialized
30
+ case initializing
31
+ case initialized
32
+ }
33
+ private var apiKey: String?
34
+ private var state: State = .notInitialized
35
+ private static let tokenRefreshQueue = DispatchQueue(label: "stream.call.token.refresh")
36
+ private static let tokenRefreshSemaphore = DispatchSemaphore(value: 1)
37
+ private var currentToken: String?
38
+ private var tokenWaitSemaphore: DispatchSemaphore?
39
+
40
+ private var overlayView: UIView?
41
+ private var hostingController: UIHostingController<CallOverlayView>?
42
+ private var overlayViewModel: CallOverlayViewModel?
43
+ private var tokenSubscription: AnyCancellable?
44
+ private var activeCallSubscription: AnyCancellable?
45
+ private var lastVoIPToken: String?
46
+ private var touchInterceptView: TouchInterceptView?
47
+
48
+ private var streamVideo: StreamVideo?
49
+
50
+ @Injected(\.callKitAdapter) var callKitAdapter
51
+ @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
52
+
53
+ private var refreshTokenURL: String?
54
+ private var refreshTokenHeaders: [String: String]?
55
+
56
+ private var webviewDelegate: WebviewNavigationDelegate?
57
+
58
+ override public func load() {
59
+ // Read API key from Info.plist
60
+ if let apiKey = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_STREAM_VIDEO_APIKEY") as? String {
61
+ self.apiKey = apiKey
62
+ }
63
+ if self.apiKey == nil {
64
+ fatalError("Cannot get apikey")
65
+ }
66
+
67
+ // Check if we have a logged in user for handling incoming calls
68
+ if let credentials = SecureUserRepository.shared.loadCurrentUser() {
69
+ print("Loading user for StreamCallPlugin: \(credentials.user.name)")
70
+ DispatchQueue.global(qos: .userInitiated).async {
71
+ self.initializeStreamVideo()
72
+ }
73
+ }
74
+
75
+ // Create and set the navigation delegate
76
+ self.webviewDelegate = WebviewNavigationDelegate(
77
+ wrappedDelegate: self.webView?.navigationDelegate,
78
+ onSetupOverlay: { [weak self] in
79
+ guard let self = self else { return }
80
+ print("Attempting to setup call view")
81
+
82
+ self.setupViews()
83
+ }
84
+ )
85
+
86
+ self.webView?.navigationDelegate = self.webviewDelegate
87
+ }
88
+
89
+ // private func cleanupStreamVideo() {
90
+ // // Cancel subscriptions
91
+ // tokenSubscription?.cancel()
92
+ // tokenSubscription = nil
93
+ // activeCallSubscription?.cancel()
94
+ // activeCallSubscription = nil
95
+ // lastVoIPToken = nil
96
+ //
97
+ // // Cleanup UI
98
+ // Task { @MainActor in
99
+ // self.overlayViewModel?.updateCall(nil)
100
+ // self.overlayViewModel?.updateStreamVideo(nil)
101
+ // self.overlayView?.removeFromSuperview()
102
+ // self.overlayView = nil
103
+ // self.hostingController = nil
104
+ // self.overlayViewModel = nil
105
+ //
106
+ // // Reset webview
107
+ // self.webView?.isOpaque = true
108
+ // self.webView?.backgroundColor = .white
109
+ // self.webView?.scrollView.backgroundColor = .white
110
+ // }
111
+ //
112
+ // state = .notInitialized
113
+ // }
114
+
115
+ private func requireInitialized() throws {
116
+ guard state == .initialized else {
117
+ throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "StreamVideo not initialized"])
118
+ }
119
+ }
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
+ @objc func loginMagicToken(_ call: CAPPluginCall) {
218
+ guard let token = call.getString("token") else {
219
+ call.reject("Missing token parameter")
220
+ return
221
+ }
222
+
223
+ print("loginMagicToken received token")
224
+ currentToken = token
225
+ tokenWaitSemaphore?.signal()
226
+ call.resolve()
227
+ }
228
+
229
+ private func setupTokenSubscription() {
230
+ // Cancel existing subscription if any
231
+ tokenSubscription?.cancel()
232
+
233
+ // Create new subscription
234
+ tokenSubscription = callKitPushNotificationAdapter.$deviceToken.sink { [weak self] (updatedDeviceToken: String) in
235
+ guard let self = self else { return }
236
+ Task {
237
+ do {
238
+ print("Setting up token subscription")
239
+ try self.requireInitialized()
240
+ if let lastVoIPToken = self.lastVoIPToken, !lastVoIPToken.isEmpty {
241
+ print("Deleting device: \(lastVoIPToken)")
242
+ try await self.streamVideo?.deleteDevice(id: lastVoIPToken)
243
+ }
244
+ if !updatedDeviceToken.isEmpty {
245
+ print("Setting voip device: \(updatedDeviceToken)")
246
+ try await self.streamVideo?.setVoipDevice(id: updatedDeviceToken)
247
+ // Save the token to our secure storage
248
+ print("Saving voip token: \(updatedDeviceToken)")
249
+ SecureUserRepository.shared.save(voipPushToken: updatedDeviceToken)
250
+ }
251
+ self.lastVoIPToken = updatedDeviceToken
252
+ } catch {
253
+ log.error("Error updating VOIP token: \(String(describing: error))")
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ private func setupActiveCallSubscription() {
260
+ if let streamVideo = streamVideo {
261
+ Task {
262
+ for await event in streamVideo.subscribe() {
263
+ print("Event", event)
264
+ if let ringingEvent = event.rawValue as? CallRingEvent {
265
+ notifyListeners("callEvent", data: [
266
+ "callId": ringingEvent.callCid,
267
+ "state": "ringing"
268
+ ])
269
+ return
270
+ }
271
+ notifyListeners("callEvent", data: [
272
+ "callId": streamVideo.state.activeCall?.callId ?? "",
273
+ "state": event.type
274
+ ])
275
+ }
276
+ }
277
+ }
278
+ // Cancel existing subscription if any
279
+ activeCallSubscription?.cancel()
280
+ // Create new subscription
281
+ activeCallSubscription = streamVideo?.state.$activeCall.sink { [weak self] newState in
282
+ guard let self = self else { return }
283
+ Task { @MainActor in
284
+ do {
285
+ try self.requireInitialized()
286
+ print("Call State Update:")
287
+ print("- Call is nil: \(newState == nil)")
288
+ if let state = newState?.state {
289
+ print("- state: \(state)")
290
+ print("- Session ID: \(state.sessionId)")
291
+ print("- All participants: \(String(describing: state.participants))")
292
+ print("- Remote participants: \(String(describing: state.remoteParticipants))")
293
+
294
+ // Update overlay and make visible when there's an active call
295
+ self.overlayViewModel?.updateCall(newState)
296
+ self.overlayView?.isHidden = false
297
+ self.webView?.isOpaque = false
298
+
299
+ // Notify that a call has started
300
+ self.notifyListeners("callEvent", data: [
301
+ "callId": newState?.cId ?? "",
302
+ "state": "joined"
303
+ ])
304
+ } else {
305
+ // If newState is nil, hide overlay and clear call
306
+ self.overlayViewModel?.updateCall(nil)
307
+ self.overlayView?.isHidden = true
308
+ self.webView?.isOpaque = true
309
+
310
+ // Notify that call has ended
311
+ self.notifyListeners("callEvent", data: [
312
+ "callId": newState?.cId ?? "",
313
+ "state": "left"
314
+ ])
315
+ }
316
+ } catch {
317
+ log.error("Error handling call state update: \(String(describing: error))")
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ @objc func login(_ call: CAPPluginCall) {
324
+ guard let token = call.getString("token"),
325
+ let userId = call.getString("userId"),
326
+ let name = call.getString("name"),
327
+ let apiKey = call.getString("apiKey") else {
328
+ call.reject("Missing required parameters")
329
+ return
330
+ }
331
+
332
+ 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
+ let user = User(
338
+ id: userId,
339
+ name: name,
340
+ imageURL: imageURL.flatMap { URL(string: $0) },
341
+ customData: [:]
342
+ )
343
+
344
+ let credentials = UserCredentials(user: user, tokenValue: token)
345
+ 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
+ // Initialize Stream Video with new credentials
352
+ initializeStreamVideo()
353
+
354
+ if state != .initialized {
355
+ call.reject("Failed to initialize StreamVideo")
356
+ return
357
+ }
358
+
359
+ // Update the CallOverlayView with new StreamVideo instance
360
+ Task { @MainActor in
361
+ self.overlayViewModel?.updateStreamVideo(self.streamVideo)
362
+ }
363
+
364
+ call.resolve([
365
+ "success": true
366
+ ])
367
+ }
368
+
369
+ @objc func logout(_ call: CAPPluginCall) {
370
+ // Remove VOIP token from repository
371
+ SecureUserRepository.shared.save(voipPushToken: nil)
372
+
373
+ // Try to delete the device from Stream if we have the last token
374
+ if let lastVoIPToken = lastVoIPToken {
375
+ Task {
376
+ do {
377
+ try await streamVideo?.deleteDevice(id: lastVoIPToken)
378
+ } catch {
379
+ log.error("Error deleting device during logout: \(String(describing: error))")
380
+ }
381
+ }
382
+ }
383
+
384
+ // Cancel subscriptions
385
+ tokenSubscription?.cancel()
386
+ tokenSubscription = nil
387
+ activeCallSubscription?.cancel()
388
+ activeCallSubscription = nil
389
+ lastVoIPToken = nil
390
+
391
+ SecureUserRepository.shared.removeCurrentUser()
392
+
393
+ // Update the CallOverlayView with nil StreamVideo instance
394
+ Task { @MainActor in
395
+ self.overlayViewModel?.updateCall(nil)
396
+ self.overlayViewModel?.updateStreamVideo(nil)
397
+ self.overlayView?.isHidden = true
398
+ self.webView?.isOpaque = true
399
+ }
400
+
401
+ call.resolve([
402
+ "success": true
403
+ ])
404
+ }
405
+
406
+ @objc func call(_ call: CAPPluginCall) {
407
+ guard let userId = call.getString("userId") else {
408
+ call.reject("Missing required parameter: userId")
409
+ return
410
+ }
411
+
412
+ // Initialize if needed
413
+ if state == .notInitialized {
414
+ guard let credentials = SecureUserRepository.shared.loadCurrentUser() else {
415
+ call.reject("No user credentials found")
416
+ return
417
+ }
418
+ initializeStreamVideo()
419
+ if state != .initialized {
420
+ call.reject("Failed to initialize StreamVideo")
421
+ return
422
+ }
423
+ }
424
+
425
+ do {
426
+ try requireInitialized()
427
+
428
+ let callType = call.getString("type") ?? "default"
429
+ let shouldRing = call.getBool("ring") ?? true
430
+
431
+ // Generate a unique call ID
432
+ let callId = UUID().uuidString
433
+
434
+ Task {
435
+ do {
436
+ print("Creating call:")
437
+ print("- Call ID: \(callId)")
438
+ print("- Call Type: \(callType)")
439
+ print("- User ID: \(userId)")
440
+ print("- Should Ring: \(shouldRing)")
441
+
442
+ // Create the call object
443
+ let streamCall = streamVideo?.call(callType: callType, callId: callId)
444
+
445
+ // Start the call with the member
446
+ print("Creating call with member...")
447
+ try await streamCall?.create(
448
+ memberIds: [userId],
449
+ custom: [:],
450
+ ring: shouldRing
451
+ )
452
+
453
+ // Join the call
454
+ print("Joining call...")
455
+ try await streamCall?.join(create: false)
456
+ print("Successfully joined call")
457
+
458
+ // Update the CallOverlayView with the active call
459
+ await MainActor.run {
460
+ self.overlayViewModel?.updateCall(streamCall)
461
+ self.overlayView?.isHidden = false
462
+ self.webView?.isOpaque = false
463
+ }
464
+
465
+ call.resolve([
466
+ "success": true
467
+ ])
468
+ } catch {
469
+ log.error("Error making call: \(String(describing: error))")
470
+ call.reject("Failed to make call: \(error.localizedDescription)")
471
+ }
472
+ }
473
+ } catch {
474
+ call.reject("StreamVideo not initialized")
475
+ }
476
+ }
477
+
478
+ @objc func endCall(_ call: CAPPluginCall) {
479
+ do {
480
+ try requireInitialized()
481
+
482
+ 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
+ }
493
+
494
+ call.resolve([
495
+ "success": true
496
+ ])
497
+ } else {
498
+ call.reject("No active call to end")
499
+ }
500
+ } catch {
501
+ log.error("Error ending call: \(String(describing: error))")
502
+ call.reject("Failed to end call: \(error.localizedDescription)")
503
+ }
504
+ }
505
+ } catch {
506
+ call.reject("StreamVideo not initialized")
507
+ }
508
+ }
509
+
510
+ @objc func setMicrophoneEnabled(_ call: CAPPluginCall) {
511
+ guard let enabled = call.getBool("enabled") else {
512
+ call.reject("Missing required parameter: enabled")
513
+ return
514
+ }
515
+
516
+ do {
517
+ try requireInitialized()
518
+
519
+ Task {
520
+ do {
521
+ if let activeCall = streamVideo?.state.activeCall {
522
+ if enabled {
523
+ try await activeCall.microphone.enable()
524
+ } else {
525
+ try await activeCall.microphone.disable()
526
+ }
527
+ call.resolve([
528
+ "success": true
529
+ ])
530
+ } else {
531
+ call.reject("No active call")
532
+ }
533
+ } catch {
534
+ log.error("Error setting microphone: \(String(describing: error))")
535
+ call.reject("Failed to set microphone: \(error.localizedDescription)")
536
+ }
537
+ }
538
+ } catch {
539
+ call.reject("StreamVideo not initialized")
540
+ }
541
+ }
542
+
543
+ @objc func setCameraEnabled(_ call: CAPPluginCall) {
544
+ guard let enabled = call.getBool("enabled") else {
545
+ call.reject("Missing required parameter: enabled")
546
+ return
547
+ }
548
+
549
+ do {
550
+ try requireInitialized()
551
+
552
+ Task {
553
+ do {
554
+ if let activeCall = streamVideo?.state.activeCall {
555
+ if enabled {
556
+ try await activeCall.camera.enable()
557
+ } else {
558
+ try await activeCall.camera.disable()
559
+ }
560
+ call.resolve([
561
+ "success": true
562
+ ])
563
+ } else {
564
+ call.reject("No active call")
565
+ }
566
+ } catch {
567
+ log.error("Error setting camera: \(String(describing: error))")
568
+ call.reject("Failed to set camera: \(error.localizedDescription)")
569
+ }
570
+ }
571
+ } catch {
572
+ call.reject("StreamVideo not initialized")
573
+ }
574
+ }
575
+
576
+ @objc func acceptCall(_ call: CAPPluginCall) {
577
+ do {
578
+ try requireInitialized()
579
+
580
+ Task {
581
+ do {
582
+
583
+ // Get the call object for the given ID
584
+ let streamCall = streamVideo?.state.ringingCall
585
+ if streamCall == nil {
586
+ call.reject("Failed to accept call as there is no ringing call")
587
+ return
588
+ }
589
+
590
+ // Join the call
591
+ print("Accepting and joining call \(streamCall!.cId)...")
592
+ try await streamCall?.accept()
593
+ try await streamCall?.join(create: false)
594
+ print("Successfully joined call")
595
+
596
+ // Update the CallOverlayView with the active call
597
+ await MainActor.run {
598
+ self.overlayViewModel?.updateCall(streamCall)
599
+ self.overlayView?.isHidden = false
600
+ self.webView?.isOpaque = false
601
+ }
602
+
603
+ call.resolve([
604
+ "success": true
605
+ ])
606
+ } catch {
607
+ log.error("Error accepting call: \(String(describing: error))")
608
+ call.reject("Failed to accept call: \(error.localizedDescription)")
609
+ }
610
+ }
611
+ } catch {
612
+ call.reject("StreamVideo not initialized")
613
+ }
614
+ }
615
+
616
+ private func initializeStreamVideo() {
617
+ state = .initializing
618
+
619
+ // Try to get user credentials from repository
620
+ guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser(),
621
+ let apiKey = self.apiKey else {
622
+ print("No saved credentials or API key found, skipping initialization")
623
+ state = .notInitialized
624
+ return
625
+ }
626
+ print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
627
+
628
+ // Create a local reference to refreshToken to avoid capturing self
629
+ let refreshTokenFn = self.refreshToken
630
+
631
+ self.streamVideo = StreamVideo(
632
+ apiKey: apiKey,
633
+ 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
+ }
648
+ )
649
+
650
+ state = .initialized
651
+ callKitAdapter.streamVideo = self.streamVideo
652
+ callKitAdapter.availabilityPolicy = .always
653
+
654
+ // Setup subscriptions for new StreamVideo instance
655
+ setupActiveCallSubscription()
656
+ setupTokenSubscription()
657
+
658
+ // Register for incoming calls
659
+ callKitAdapter.registerForIncomingCalls()
660
+ }
661
+
662
+ private func setupViews() {
663
+ guard let webView = self.webView,
664
+ let parent = webView.superview else { return }
665
+
666
+ // Create TouchInterceptView
667
+ let touchInterceptView = TouchInterceptView(frame: parent.bounds)
668
+ touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
669
+ self.touchInterceptView = touchInterceptView
670
+
671
+ // Remove webView from its parent
672
+ webView.removeFromSuperview()
673
+
674
+ // Add TouchInterceptView to the parent
675
+ parent.addSubview(touchInterceptView)
676
+
677
+ // Setup TouchInterceptView constraints
678
+ NSLayoutConstraint.activate([
679
+ touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
680
+ touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
681
+ touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
682
+ touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
683
+ ])
684
+
685
+ // Configure webview for transparency
686
+ webView.isOpaque = true
687
+ webView.backgroundColor = .clear
688
+ webView.scrollView.backgroundColor = .clear
689
+ webView.translatesAutoresizingMaskIntoConstraints = false
690
+
691
+ // Create SwiftUI view with view model
692
+ let (hostingController, viewModel) = CallOverlayView.create(streamVideo: self.streamVideo)
693
+ hostingController.view.backgroundColor = .clear
694
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
695
+
696
+ self.hostingController = hostingController
697
+ self.overlayViewModel = viewModel
698
+ self.overlayView = hostingController.view
699
+
700
+ if let overlayView = self.overlayView {
701
+ // Setup the views in TouchInterceptView
702
+ touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
703
+
704
+ // Setup constraints for webView
705
+ NSLayoutConstraint.activate([
706
+ webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
707
+ webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
708
+ webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
709
+ webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
710
+ ])
711
+
712
+ // Setup constraints for overlayView
713
+ let safeGuide = touchInterceptView.safeAreaLayoutGuide
714
+ NSLayoutConstraint.activate([
715
+ overlayView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
716
+ overlayView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
717
+ overlayView.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
718
+ overlayView.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
719
+ ])
720
+ }
721
+ }
722
+ }