@capgo/capacitor-inappbrowser 8.6.34 → 8.7.0-beta.pr596.340.1

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.
@@ -98,7 +98,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
98
98
  case aware = "AWARE"
99
99
  case fakeVisible = "FAKE_VISIBLE"
100
100
  }
101
- private let pluginVersion: String = "8.6.34"
101
+ private let pluginVersion: String = "8.7.0"
102
102
  public let identifier = "CapgoInAppBrowserPlugin"
103
103
  public let jsName = "CapgoInAppBrowser"
104
104
  public let pluginMethods: [CAPPluginMethod] = [
@@ -109,10 +109,14 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
109
109
  CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
110
110
  CAPPluginMethod(name: "clearAllCookies", returnType: CAPPluginReturnPromise),
111
111
  CAPPluginMethod(name: "clearCache", returnType: CAPPluginReturnPromise),
112
+ CAPPluginMethod(name: "clearAllBrowsingData", returnType: CAPPluginReturnPromise),
112
113
  CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
113
114
  CAPPluginMethod(name: "setUrl", returnType: CAPPluginReturnPromise),
114
115
  CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise),
115
116
  CAPPluginMethod(name: "hide", returnType: CAPPluginReturnPromise),
117
+ CAPPluginMethod(name: "sendToBack", returnType: CAPPluginReturnPromise),
118
+ CAPPluginMethod(name: "bringToFront", returnType: CAPPluginReturnPromise),
119
+ CAPPluginMethod(name: "dispatchInputEvent", returnType: CAPPluginReturnPromise),
116
120
  CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise),
117
121
  CAPPluginMethod(name: "executeScript", returnType: CAPPluginReturnPromise),
118
122
  CAPPluginMethod(name: "postMessage", returnType: CAPPluginReturnPromise),
@@ -138,11 +142,16 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
138
142
  private var proxyBridges: [String: ProxyBridge] = [:]
139
143
  private var webViewStack: [String] = []
140
144
  private var activeWebViewId: String?
141
- private weak var presentationContainerView: UIView?
142
- private var presentationContainerWasInteractive = true
143
- private var presentationContainerPreviousAlpha: CGFloat = 1
144
145
  private var hiddenWebViewContainers: [ObjectIdentifier: UIView] = [:]
145
146
  private var closeModalTitle: String?
147
+ private var layeredWebViewIds = Set<String>()
148
+ private var transparentHostWebViewIds = Set<String>()
149
+ private var hasStoredHostWebViewAppearance = false
150
+ private var originalHostWebViewBackgroundColor: UIColor?
151
+ private var originalHostWebViewIsOpaque = true
152
+ private var originalHostSubviewBackgrounds: [(UIView, UIColor?, Bool)] = []
153
+ private var originalHostSuperviewBackgroundColor: UIColor?
154
+ private var originalHostSuperviewIsOpaque = true
146
155
  private var closeModalDescription: String?
147
156
  private var closeModalOk: String?
148
157
  private var closeModalCancel: String?
@@ -181,7 +190,20 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
181
190
  }
182
191
  }
183
192
 
193
+ private func dismissNavigationControllerIfPresented(_ navigationController: UINavigationController?, completion: (() -> Void)? = nil) {
194
+ guard let navigationController else {
195
+ completion?()
196
+ return
197
+ }
198
+ guard navigationController.presentingViewController != nil else {
199
+ completion?()
200
+ return
201
+ }
202
+ navigationController.dismiss(animated: false, completion: completion)
203
+ }
204
+
184
205
  private func unregisterWebView(id: String) {
206
+ detachLayeredWebView(id: id)
185
207
  if let webView = webViewControllers[id]?.capableWebView {
186
208
  cleanupHiddenWebViewContainer(for: webView)
187
209
  }
@@ -342,6 +364,26 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
342
364
  return stores
343
365
  }
344
366
 
367
+ private func allBrowsingDataStores() -> [WKWebsiteDataStore] {
368
+ var seen = Set<ObjectIdentifier>()
369
+ var stores: [WKWebsiteDataStore] = []
370
+
371
+ func append(_ store: WKWebsiteDataStore) {
372
+ let identifier = ObjectIdentifier(store)
373
+ if seen.insert(identifier).inserted {
374
+ stores.append(store)
375
+ }
376
+ }
377
+
378
+ append(WKWebsiteDataStore.default())
379
+ for controller in webViewControllers.values {
380
+ if let store = controller.websiteDataStore() {
381
+ append(store)
382
+ }
383
+ }
384
+ return stores
385
+ }
386
+
345
387
  private func parseProxyRules(_ rawRules: [Any]) throws -> [NativeProxyRule] {
346
388
  try rawRules.enumerated().map { index, item in
347
389
  guard let dictionary = item as? [String: Any] else {
@@ -355,19 +397,154 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
355
397
  }
356
398
  }
357
399
 
358
- func presentView(webViewId: String? = nil, isAnimated: Bool = true) {
400
+ @discardableResult
401
+ func presentView(webViewId: String? = nil, isAnimated: Bool = true) -> Bool {
359
402
  let resolvedId = webViewId ?? activeWebViewId
360
403
  let navigationController = resolvedId.flatMap { navigationControllers[$0] } ?? self.navigationWebViewController
361
404
  guard let navigationController else {
362
405
  self.currentPluginCall?.reject("Navigation controller is not initialized")
363
- return
406
+ return false
364
407
  }
365
408
 
409
+ if let resolvedId,
410
+ let webViewController = webViewControllers[resolvedId],
411
+ webViewController.isLayeredBehind {
412
+ if !sendNavigationControllerToBack(id: resolvedId, transparentBackground: webViewController.transparentHostBackground) {
413
+ self.currentPluginCall?.reject("Failed to send webview to back")
414
+ return false
415
+ }
416
+ return true
417
+ }
418
+
419
+ if let resolvedId {
420
+ detachLayeredWebView(id: resolvedId)
421
+ }
366
422
  dismissActiveKeyboard()
367
423
  let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
368
424
  presenter?.present(navigationController, animated: isAnimated, completion: {
369
425
  self.currentPluginCall?.resolve()
370
426
  })
427
+ return true
428
+ }
429
+
430
+ private func targetFrame(for webViewController: WKWebViewController, in hostWebView: UIView) -> CGRect {
431
+ return CustomWebViewFrameSupport.resolvedFrame(
432
+ width: webViewController.customWidth,
433
+ height: webViewController.customHeight,
434
+ x: webViewController.customX,
435
+ y: webViewController.customY,
436
+ fallbackSize: hostWebView.bounds.size
437
+ ) ?? hostWebView.bounds
438
+ }
439
+
440
+ @discardableResult
441
+ private func sendNavigationControllerToBack(id: String, transparentBackground: Bool) -> Bool {
442
+ guard let navigationController = navigationControllers[id],
443
+ let webViewController = webViewControllers[id],
444
+ let hostWebView = self.bridge?.webView else {
445
+ return false
446
+ }
447
+
448
+ if let webView = webViewController.capableWebView, webView.superview !== webViewController.view {
449
+ cleanupHiddenWebViewContainer(for: webView)
450
+ attachWebViewToController(webViewController, webView: webView)
451
+ }
452
+
453
+ dismissActiveKeyboard()
454
+ navigationController.view.removeFromSuperview()
455
+ if transparentBackground {
456
+ makeHostWebViewTransparent(for: id)
457
+ } else {
458
+ restoreHostWebViewBackgroundIfNeeded(for: id)
459
+ }
460
+ navigationController.view.frame = targetFrame(for: webViewController, in: hostWebView)
461
+ navigationController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
462
+ navigationController.view.isUserInteractionEnabled = false
463
+ hostWebView.addSubview(navigationController.view)
464
+ hostWebView.sendSubviewToBack(navigationController.view)
465
+ navigationController.view.setNeedsLayout()
466
+ navigationController.view.layoutIfNeeded()
467
+
468
+ layeredWebViewIds.insert(id)
469
+ webViewController.isLayeredBehind = true
470
+ webViewController.transparentHostBackground = transparentBackground
471
+ return true
472
+ }
473
+
474
+ private func detachLayeredWebView(id: String) {
475
+ if let navigationController = navigationControllers[id] {
476
+ if navigationController.view.superview === self.bridge?.webView {
477
+ navigationController.view.removeFromSuperview()
478
+ }
479
+ navigationController.view.isUserInteractionEnabled = true
480
+ }
481
+ layeredWebViewIds.remove(id)
482
+ restoreHostWebViewBackgroundIfNeeded(for: id)
483
+ }
484
+
485
+ private func storeSubviewBackgrounds(from view: UIView) {
486
+ for subview in view.subviews {
487
+ originalHostSubviewBackgrounds.append((subview, subview.backgroundColor, subview.isOpaque))
488
+ storeSubviewBackgrounds(from: subview)
489
+ }
490
+ }
491
+
492
+ private func clearBackgrounds(in view: UIView) {
493
+ view.isOpaque = false
494
+ view.backgroundColor = .clear
495
+ view.subviews.forEach { clearBackgrounds(in: $0) }
496
+ }
497
+
498
+ private func makeHostWebViewTransparent(for id: String) {
499
+ guard let hostWebView = self.bridge?.webView else {
500
+ return
501
+ }
502
+
503
+ transparentHostWebViewIds.insert(id)
504
+ if !hasStoredHostWebViewAppearance {
505
+ hasStoredHostWebViewAppearance = true
506
+ originalHostWebViewBackgroundColor = hostWebView.backgroundColor
507
+ originalHostWebViewIsOpaque = hostWebView.isOpaque
508
+ originalHostSuperviewBackgroundColor = hostWebView.superview?.backgroundColor
509
+ originalHostSuperviewIsOpaque = hostWebView.superview?.isOpaque ?? true
510
+ originalHostSubviewBackgrounds.removeAll()
511
+ storeSubviewBackgrounds(from: hostWebView)
512
+ }
513
+
514
+ clearBackgrounds(in: hostWebView)
515
+ hostWebView.superview?.isOpaque = false
516
+ hostWebView.superview?.backgroundColor = .clear
517
+ hostWebView.setNeedsLayout()
518
+ hostWebView.layoutIfNeeded()
519
+ }
520
+
521
+ private func restoreHostWebViewBackgroundIfNeeded(for id: String? = nil) {
522
+ if let id {
523
+ transparentHostWebViewIds.remove(id)
524
+ } else {
525
+ transparentHostWebViewIds.removeAll()
526
+ }
527
+ guard transparentHostWebViewIds.isEmpty, hasStoredHostWebViewAppearance else {
528
+ return
529
+ }
530
+
531
+ if let hostWebView = self.bridge?.webView {
532
+ hostWebView.backgroundColor = originalHostWebViewBackgroundColor
533
+ hostWebView.isOpaque = originalHostWebViewIsOpaque
534
+ hostWebView.superview?.backgroundColor = originalHostSuperviewBackgroundColor
535
+ hostWebView.superview?.isOpaque = originalHostSuperviewIsOpaque
536
+ for (view, backgroundColor, isOpaque) in originalHostSubviewBackgrounds {
537
+ view.backgroundColor = backgroundColor
538
+ view.isOpaque = isOpaque
539
+ }
540
+ hostWebView.setNeedsLayout()
541
+ hostWebView.layoutIfNeeded()
542
+ }
543
+
544
+ hasStoredHostWebViewAppearance = false
545
+ originalHostWebViewBackgroundColor = nil
546
+ originalHostSuperviewBackgroundColor = nil
547
+ originalHostSubviewBackgrounds.removeAll()
371
548
  }
372
549
 
373
550
  private func dismissActiveKeyboard() {
@@ -568,6 +745,24 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
568
745
  }
569
746
  }
570
747
 
748
+ @objc func clearAllBrowsingData(_ call: CAPPluginCall) {
749
+ DispatchQueue.main.async {
750
+ let dataStores = self.allBrowsingDataStores()
751
+ let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
752
+ let group = DispatchGroup()
753
+ for dataStore in dataStores {
754
+ group.enter()
755
+ dataStore.removeData(ofTypes: dataTypes,
756
+ modifiedSince: Date(timeIntervalSince1970: 0)) {
757
+ group.leave()
758
+ }
759
+ }
760
+ group.notify(queue: .main) {
761
+ call.resolve()
762
+ }
763
+ }
764
+ }
765
+
571
766
  @objc func clearCookies(_ call: CAPPluginCall) {
572
767
  guard let url = call.getString("url"),
573
768
  let host = URL(string: url)?.host else {
@@ -767,12 +962,15 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
767
962
  let enabledSafeBottomMargin = call.getBool("enabledSafeBottomMargin", false)
768
963
  let enabledSafeTopMargin = call.getBool("enabledSafeTopMargin", true)
769
964
  let hidden = call.getBool("hidden", false)
965
+ let toBack = call.getBool("toBack", false)
966
+ let transparentBackground = call.getBool("transparentBackground", true)
770
967
  self.isHidden = hidden
771
968
  let hiddenPopupWindow = call.getBool("hiddenPopupWindow", false)
772
969
  let allowWebViewJsVisibilityControl = self.getConfig().getBoolean("allowWebViewJsVisibilityControl", false)
773
970
  let allowScreenshotsFromWebPage = call.getBool("allowScreenshotsFromWebPage", false)
774
971
  let captureConsoleLogs = call.getBool("captureConsoleLogs", false)
775
972
  let handleDownloads = call.getBool("handleDownloads", false)
973
+ let persistWebViewData = call.getBool("persistWebViewData", true)
776
974
  let invisibilityModeRaw = call.getString("invisibilityMode", "AWARE")
777
975
  self.invisibilityMode = InvisibilityMode(rawValue: invisibilityModeRaw.uppercased()) ?? .aware
778
976
 
@@ -914,38 +1112,36 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
914
1112
  self.proxyBridges[webViewId] = proxyBridge
915
1113
  }
916
1114
 
917
- self.webViewController = WKWebViewController.init(
918
- url: url,
919
- headers: headers,
920
- isInspectable: isInspectable,
921
- credentials: credentials,
922
- preventDeeplink: preventDeeplink,
923
- blankNavigationTab: toolbarType == "blank",
924
- enabledSafeBottomMargin: enabledSafeBottomMargin,
925
- enabledSafeTopMargin: enabledSafeTopMargin,
926
- blockedHosts: blockedHosts,
1115
+ let webViewController = WKWebViewController()
1116
+ webViewController.blankNavigationTab = toolbarType == "blank"
1117
+ webViewController.enabledSafeBottomMargin = enabledSafeBottomMargin
1118
+ webViewController.enabledSafeTopMargin = enabledSafeTopMargin
1119
+ webViewController.source = .remote(url)
1120
+ webViewController.setCredentials(credentials: credentials)
1121
+ webViewController.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
1122
+ webViewController.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
1123
+ webViewController.captureConsoleLogs = captureConsoleLogs
1124
+ webViewController.proxyRequests = legacyProxyRequests.isEnabled
1125
+ webViewController.proxySchemeHandler = proxyHandler
1126
+ webViewController.proxyBridge = proxyBridge
1127
+ webViewController.proxyBridgeAccessToken = proxyBridgeAccessToken
1128
+ webViewController.legacyProxyRequestURLRegexPattern = legacyProxyRequests.urlRegex?.pattern
1129
+ webViewController.openBlankTargetInWebView = openBlankTargetInWebView
1130
+ webViewController.persistWebViewData = persistWebViewData
1131
+ webViewController.setHeaders(headers: headers)
1132
+ webViewController.setPreventDeeplink(preventDeeplink: preventDeeplink)
1133
+ webViewController.setBlockedHosts(blockedHosts: blockedHosts)
1134
+ webViewController.setAuthorizedAppLinks(authorizedAppLinks: authorizedAppLinks)
1135
+ webViewController.documentStartUserScripts = self.documentStartUserScripts(
927
1136
  authorizedAppLinks: authorizedAppLinks,
928
- allowWebViewJsVisibilityControl: allowWebViewJsVisibilityControl,
929
- allowScreenshotsFromWebPage: allowScreenshotsFromWebPage,
930
- captureConsoleLogs: captureConsoleLogs,
931
- proxyRequests: legacyProxyRequests.isEnabled,
932
- proxySchemeHandler: proxyHandler,
933
- proxyBridge: proxyBridge,
934
- proxyBridgeAccessToken: proxyBridgeAccessToken,
935
- legacyProxyRequestURLRegexPattern: legacyProxyRequests.urlRegex?.pattern,
936
- documentStartUserScripts: self.documentStartUserScripts(
937
- authorizedAppLinks: authorizedAppLinks,
938
- openBlankTargetInWebView: openBlankTargetInWebView
939
- ),
940
1137
  openBlankTargetInWebView: openBlankTargetInWebView
941
1138
  )
942
-
943
- guard let webViewController = self.webViewController else {
944
- call.reject("Failed to initialize WebViewController")
945
- return
946
- }
1139
+ webViewController.initWebview(isInspectable: isInspectable)
1140
+ self.webViewController = webViewController
947
1141
 
948
1142
  webViewController.instanceId = webViewId
1143
+ webViewController.isLayeredBehind = toBack
1144
+ webViewController.transparentHostBackground = transparentBackground
949
1145
 
950
1146
  // Set HTTP method and body if provided
951
1147
  if let method = httpMethod {
@@ -1178,7 +1374,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
1178
1374
  }
1179
1375
 
1180
1376
  // Configure modal presentation for touch passthrough if custom dimensions are set
1181
- if width != nil || height != nil {
1377
+ if (width != nil || height != nil) && !toBack {
1182
1378
  self.navigationWebViewController?.modalPresentationStyle = .overFullScreen
1183
1379
 
1184
1380
  // Create a pass-through container
@@ -1268,7 +1464,9 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
1268
1464
  return
1269
1465
  }
1270
1466
  } else if !self.isPresentAfterPageLoad {
1271
- self.presentView(webViewId: webViewId, isAnimated: isAnimated)
1467
+ guard self.presentView(webViewId: webViewId, isAnimated: isAnimated) else {
1468
+ return
1469
+ }
1272
1470
  }
1273
1471
  call.resolve(["id": webViewId])
1274
1472
  }
@@ -1334,42 +1532,35 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
1334
1532
  self.isHidden = hidden
1335
1533
 
1336
1534
  if hidden {
1337
- if let navController = navigationController, navController.presentingViewController != nil {
1338
- navController.view.isHidden = true
1339
- navController.view.isUserInteractionEnabled = false
1340
- if let containerView = navController.view.superview {
1341
- if self.presentationContainerView == nil || self.presentationContainerView !== containerView {
1342
- self.presentationContainerView = containerView
1343
- self.presentationContainerWasInteractive = containerView.isUserInteractionEnabled
1344
- self.presentationContainerPreviousAlpha = containerView.alpha
1345
- }
1346
- containerView.isUserInteractionEnabled = false
1347
- containerView.alpha = 0
1348
- }
1535
+ if let resolvedId {
1536
+ self.detachLayeredWebView(id: resolvedId)
1349
1537
  }
1350
-
1351
1538
  if !self.attachWebViewToWindow(webView) {
1352
1539
  call?.reject("Failed to get active window for hidden webview")
1353
1540
  return
1354
1541
  }
1542
+ self.dismissNavigationControllerIfPresented(navigationController)
1355
1543
  } else {
1356
1544
  if webView.superview !== webViewController.view {
1357
1545
  self.attachWebViewToController(webViewController, webView: webView)
1358
1546
  }
1359
1547
 
1548
+ if webViewController.isLayeredBehind, let resolvedId {
1549
+ if self.sendNavigationControllerToBack(id: resolvedId, transparentBackground: webViewController.transparentHostBackground) {
1550
+ if let navController = navigationController {
1551
+ self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navController)
1552
+ }
1553
+ call?.resolve()
1554
+ } else {
1555
+ call?.reject("Failed to send webview to back")
1556
+ }
1557
+ return
1558
+ }
1559
+
1360
1560
  if let navController = navigationController {
1361
1561
  if let resolvedId {
1362
1562
  self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navController)
1363
1563
  }
1364
- navController.view.isHidden = false
1365
- navController.view.isUserInteractionEnabled = true
1366
- if let containerView = self.presentationContainerView {
1367
- containerView.isUserInteractionEnabled = self.presentationContainerWasInteractive
1368
- containerView.alpha = self.presentationContainerPreviousAlpha
1369
- self.presentationContainerView = nil
1370
- self.presentationContainerWasInteractive = true
1371
- self.presentationContainerPreviousAlpha = 1
1372
- }
1373
1564
 
1374
1565
  if navController.presentingViewController == nil {
1375
1566
  let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
@@ -1404,6 +1595,200 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
1404
1595
  self.setHiddenState(false, targetId: call.getString("id"), call: call)
1405
1596
  }
1406
1597
 
1598
+ @objc func sendToBack(_ call: CAPPluginCall) {
1599
+ DispatchQueue.main.async {
1600
+ let targetId = call.getString("id") ?? self.activeWebViewId
1601
+ guard let resolvedId = targetId,
1602
+ let webViewController = self.resolveWebViewController(for: resolvedId),
1603
+ let navigationController = self.resolveNavigationController(for: resolvedId) else {
1604
+ call.reject("WebView is not initialized")
1605
+ return
1606
+ }
1607
+
1608
+ let transparentBackground = call.getBool("transparentBackground", true)
1609
+ self.dismissNavigationControllerIfPresented(navigationController) {
1610
+ webViewController.isLayeredBehind = true
1611
+ webViewController.transparentHostBackground = transparentBackground
1612
+ guard self.sendNavigationControllerToBack(id: resolvedId, transparentBackground: transparentBackground) else {
1613
+ call.reject("Failed to send webview to back")
1614
+ return
1615
+ }
1616
+ self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navigationController)
1617
+ call.resolve()
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ @objc func bringToFront(_ call: CAPPluginCall) {
1623
+ DispatchQueue.main.async {
1624
+ let targetId = call.getString("id") ?? self.activeWebViewId
1625
+ guard let resolvedId = targetId,
1626
+ let webViewController = self.resolveWebViewController(for: resolvedId),
1627
+ let navigationController = self.resolveNavigationController(for: resolvedId) else {
1628
+ call.reject("WebView is not initialized")
1629
+ return
1630
+ }
1631
+
1632
+ webViewController.isLayeredBehind = false
1633
+ webViewController.transparentHostBackground = true
1634
+ self.detachLayeredWebView(id: resolvedId)
1635
+ if let webView = webViewController.capableWebView, webView.superview !== webViewController.view {
1636
+ self.attachWebViewToController(webViewController, webView: webView)
1637
+ }
1638
+ self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navigationController)
1639
+ if navigationController.presentingViewController == nil {
1640
+ let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
1641
+ presenter?.present(navigationController, animated: true, completion: {
1642
+ call.resolve()
1643
+ })
1644
+ return
1645
+ }
1646
+ call.resolve()
1647
+ }
1648
+ }
1649
+
1650
+ private func jsNumber(_ value: Float?, fallback: String = "0") -> String {
1651
+ guard let value, value.isFinite else {
1652
+ return fallback
1653
+ }
1654
+ return String(Double(value))
1655
+ }
1656
+
1657
+ private func dispatchInputScript(type: String, x: Float?, y: Float?, deltaX: Float?, deltaY: Float?) -> String? {
1658
+ if type == "scroll" {
1659
+ guard let x, let y, let deltaX, let deltaY,
1660
+ x.isFinite, y.isFinite, deltaX.isFinite, deltaY.isFinite else {
1661
+ return nil
1662
+ }
1663
+ let xLiteral = jsNumber(x)
1664
+ let yLiteral = jsNumber(y)
1665
+ let deltaXLiteral = jsNumber(deltaX)
1666
+ let deltaYLiteral = jsNumber(deltaY)
1667
+ return """
1668
+ (function() {
1669
+ const x = \(xLiteral);
1670
+ const y = \(yLiteral);
1671
+ const dx = \(deltaXLiteral);
1672
+ const dy = \(deltaYLiteral);
1673
+ let target = Number.isFinite(x) && Number.isFinite(y) ? document.elementFromPoint(x, y) : null;
1674
+ while (target && target !== document.body && target !== document.documentElement) {
1675
+ const style = window.getComputedStyle(target);
1676
+ const canScroll = /(auto|scroll)/.test(style.overflow + style.overflowX + style.overflowY) &&
1677
+ (target.scrollHeight > target.clientHeight || target.scrollWidth > target.clientWidth);
1678
+ if (canScroll && typeof target.scrollBy === 'function') {
1679
+ target.scrollBy(dx, dy);
1680
+ return true;
1681
+ }
1682
+ target = target.parentElement;
1683
+ }
1684
+ window.scrollBy(dx, dy);
1685
+ return true;
1686
+ })();
1687
+ """
1688
+ }
1689
+
1690
+ let pointerEvents: [String]
1691
+ let mouseEvents: [String]
1692
+ let touchEvents: [String]
1693
+ switch type {
1694
+ case "click":
1695
+ pointerEvents = ["pointerdown", "pointerup"]
1696
+ mouseEvents = ["mousedown", "mouseup", "click"]
1697
+ touchEvents = ["touchstart", "touchend"]
1698
+ case "touchstart":
1699
+ pointerEvents = ["pointerdown"]
1700
+ mouseEvents = ["mousedown"]
1701
+ touchEvents = ["touchstart"]
1702
+ case "touchmove":
1703
+ pointerEvents = ["pointermove"]
1704
+ mouseEvents = ["mousemove"]
1705
+ touchEvents = ["touchmove"]
1706
+ case "touchend":
1707
+ pointerEvents = ["pointerup"]
1708
+ mouseEvents = ["mouseup"]
1709
+ touchEvents = ["touchend"]
1710
+ case "touchcancel":
1711
+ pointerEvents = ["pointercancel"]
1712
+ mouseEvents = []
1713
+ touchEvents = ["touchcancel"]
1714
+ default:
1715
+ return nil
1716
+ }
1717
+
1718
+ guard let x, let y, x.isFinite, y.isFinite else {
1719
+ return nil
1720
+ }
1721
+
1722
+ let pointerEventsData = try? JSONSerialization.data(withJSONObject: pointerEvents)
1723
+ let mouseEventsData = try? JSONSerialization.data(withJSONObject: mouseEvents)
1724
+ let touchEventsData = try? JSONSerialization.data(withJSONObject: touchEvents)
1725
+ let pointerEventsJSON = pointerEventsData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1726
+ let mouseEventsJSON = mouseEventsData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1727
+ let touchEventsJSON = touchEventsData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1728
+
1729
+ return """
1730
+ (function() {
1731
+ const x = \(jsNumber(x));
1732
+ const y = \(jsNumber(y));
1733
+ const target = document.elementFromPoint(x, y);
1734
+ if (!target) return false;
1735
+ const base = { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, screenX: x, screenY: y };
1736
+ for (const name of \(pointerEventsJSON)) {
1737
+ if (typeof PointerEvent === 'function') {
1738
+ target.dispatchEvent(new PointerEvent(name, Object.assign({}, base, { pointerId: 1, pointerType: 'touch', isPrimary: true })));
1739
+ }
1740
+ }
1741
+ for (const name of \(mouseEventsJSON)) {
1742
+ target.dispatchEvent(new MouseEvent(name, base));
1743
+ }
1744
+ if (typeof TouchEvent === 'function' && typeof Touch === 'function') {
1745
+ for (const name of \(touchEventsJSON)) {
1746
+ try {
1747
+ const touch = new Touch({ identifier: 1, target: target, clientX: x, clientY: y, screenX: x, screenY: y });
1748
+ const activeTouches = name === 'touchend' || name === 'touchcancel' ? [] : [touch];
1749
+ target.dispatchEvent(new TouchEvent(name, {
1750
+ bubbles: true,
1751
+ cancelable: true,
1752
+ composed: true,
1753
+ touches: activeTouches,
1754
+ targetTouches: activeTouches,
1755
+ changedTouches: [touch]
1756
+ }));
1757
+ } catch (_) {}
1758
+ }
1759
+ }
1760
+ return true;
1761
+ })();
1762
+ """
1763
+ }
1764
+
1765
+ @objc func dispatchInputEvent(_ call: CAPPluginCall) {
1766
+ guard let type = call.getString("type") else {
1767
+ call.reject("Input event type is required")
1768
+ return
1769
+ }
1770
+ guard let script = dispatchInputScript(
1771
+ type: type,
1772
+ x: call.getFloat("x"),
1773
+ y: call.getFloat("y"),
1774
+ deltaX: call.getFloat("deltaX"),
1775
+ deltaY: call.getFloat("deltaY")
1776
+ ) else {
1777
+ call.reject("Unsupported input event type or invalid input payload: \(type)")
1778
+ return
1779
+ }
1780
+
1781
+ DispatchQueue.main.async {
1782
+ let targetId = call.getString("id") ?? self.activeWebViewId
1783
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1784
+ call.reject("WebView is not initialized")
1785
+ return
1786
+ }
1787
+ webViewController.executeScript(script: script)
1788
+ call.resolve()
1789
+ }
1790
+ }
1791
+
1407
1792
  @objc func executeScript(_ call: CAPPluginCall) {
1408
1793
  guard let script = call.getString("code") else {
1409
1794
  call.reject("Cannot get script to execute")
@@ -1169,7 +1169,6 @@ public class ProxySchemeHandler: NSObject, WKURLSchemeHandler, URLSessionTaskDel
1169
1169
  }
1170
1170
  }
1171
1171
 
1172
-
1173
1172
  private func makeInitialRequestContext(from urlSchemeTask: WKURLSchemeTask, requestURL: URL) -> NativeRequestContext? {
1174
1173
  let requestURLString = requestURL.absoluteString
1175
1174