@capgo/capacitor-inappbrowser 8.6.15

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 (59) hide show
  1. package/CapgoCapacitorInappbrowser.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +1610 -0
  5. package/android/build.gradle +64 -0
  6. package/android/src/main/AndroidManifest.xml +22 -0
  7. package/android/src/main/assets/proxy-bridge.js +586 -0
  8. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ActiveWebViewSupport.java +10 -0
  9. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CustomSchemeInterceptSupport.java +51 -0
  10. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +1820 -0
  11. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/MobileAppBridgeScript.java +122 -0
  12. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/NativeProxyRule.java +128 -0
  13. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +725 -0
  14. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyBridge.java +93 -0
  15. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyRequestSupport.java +869 -0
  16. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyResponseRouting.java +35 -0
  17. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/SafeAreaInsetsSupport.java +28 -0
  18. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +42 -0
  19. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +6022 -0
  20. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewPermissionSupport.java +58 -0
  21. package/android/src/main/res/.gitkeep +0 -0
  22. package/android/src/main/res/drawable/arrow_back_disabled.xml +9 -0
  23. package/android/src/main/res/drawable/arrow_back_enabled.xml +9 -0
  24. package/android/src/main/res/drawable/arrow_forward_disabled.xml +9 -0
  25. package/android/src/main/res/drawable/arrow_forward_enabled.xml +9 -0
  26. package/android/src/main/res/drawable/ic_clear_24px.xml +9 -0
  27. package/android/src/main/res/drawable/ic_refresh.xml +9 -0
  28. package/android/src/main/res/drawable/ic_share.xml +10 -0
  29. package/android/src/main/res/layout/activity_browser.xml +32 -0
  30. package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
  31. package/android/src/main/res/layout/content_browser.xml +17 -0
  32. package/android/src/main/res/layout/tool_bar.xml +87 -0
  33. package/android/src/main/res/values/browser_theme.xml +3 -0
  34. package/android/src/main/res/values/colors.xml +5 -0
  35. package/android/src/main/res/values/dimens.xml +3 -0
  36. package/android/src/main/res/values/strings.xml +13 -0
  37. package/android/src/main/res/values/styles.xml +4 -0
  38. package/android/src/main/res/values/themes.xml +27 -0
  39. package/android/src/main/res/xml/file_paths.xml +14 -0
  40. package/dist/docs.json +3200 -0
  41. package/dist/esm/definitions.d.ts +1507 -0
  42. package/dist/esm/definitions.js +40 -0
  43. package/dist/esm/definitions.js.map +1 -0
  44. package/dist/esm/index.d.ts +6 -0
  45. package/dist/esm/index.js +102 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/web.d.ts +47 -0
  48. package/dist/esm/web.js +120 -0
  49. package/dist/esm/web.js.map +1 -0
  50. package/dist/plugin.cjs.js +270 -0
  51. package/dist/plugin.cjs.js.map +1 -0
  52. package/dist/plugin.js +273 -0
  53. package/dist/plugin.js.map +1 -0
  54. package/ios/Sources/InAppBrowserPlugin/Enums.swift +65 -0
  55. package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +1932 -0
  56. package/ios/Sources/InAppBrowserPlugin/ProxySchemeHandler.swift +1185 -0
  57. package/ios/Sources/InAppBrowserPlugin/WKWebView+SchemeHandling.swift +43 -0
  58. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +3032 -0
  59. package/package.json +139 -0
@@ -0,0 +1,1932 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import WebKit
4
+ import AuthenticationServices
5
+
6
+ enum ActiveWebViewSupport {
7
+ static func shouldActivateNewWebView(isHidden: Bool, hasActiveWebView: Bool) -> Bool {
8
+ !isHidden || !hasActiveWebView
9
+ }
10
+
11
+ static func resolveVisibilityTarget(originatingWebViewId: String?, activeWebViewId: String?) -> String? {
12
+ originatingWebViewId ?? activeWebViewId
13
+ }
14
+ }
15
+
16
+ protocol ProxyRequestLocating {
17
+ func hasPendingProxyRequest(_ requestId: String) -> Bool
18
+ }
19
+
20
+ enum ProxyResponseRoutingSupport {
21
+ enum Resolution<T> {
22
+ case matched(T)
23
+ case ambiguous
24
+ case missing
25
+ }
26
+
27
+ static func resolveTargetHandler<T: ProxyRequestLocating>(
28
+ webviewId: String?,
29
+ requestId: String,
30
+ handlers: [String: T]
31
+ ) -> Resolution<T> {
32
+ if let webviewId, !webviewId.isEmpty {
33
+ guard let handler = handlers[webviewId] else {
34
+ return .missing
35
+ }
36
+ return .matched(handler)
37
+ }
38
+
39
+ var matchedHandler: T?
40
+ for handler in handlers.values where handler.hasPendingProxyRequest(requestId) {
41
+ if matchedHandler != nil {
42
+ return .ambiguous
43
+ }
44
+ matchedHandler = handler
45
+ }
46
+
47
+ guard let matchedHandler else {
48
+ return .missing
49
+ }
50
+ return .matched(matchedHandler)
51
+ }
52
+ }
53
+
54
+ extension UIColor {
55
+
56
+ convenience init(hexString: String) {
57
+ let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
58
+ var int = UInt64()
59
+ Scanner(string: hex).scanHexInt64(&int)
60
+ let components = (
61
+ R: CGFloat((int >> 16) & 0xff) / 255,
62
+ G: CGFloat((int >> 08) & 0xff) / 255,
63
+ B: CGFloat((int >> 00) & 0xff) / 255
64
+ )
65
+ self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
66
+ }
67
+
68
+ }
69
+
70
+ /**
71
+ * Please read the Capacitor iOS Plugin Development Guide
72
+ * here: https://capacitorjs.com/docs/plugins/ios
73
+ */
74
+ @objc(InAppBrowserPlugin)
75
+ public class InAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
76
+ enum InvisibilityMode: String {
77
+ case aware = "AWARE"
78
+ case fakeVisible = "FAKE_VISIBLE"
79
+ }
80
+ private let pluginVersion: String = "8.6.15"
81
+ public let identifier = "InAppBrowserPlugin"
82
+ public let jsName = "InAppBrowser"
83
+ public let pluginMethods: [CAPPluginMethod] = [
84
+ CAPPluginMethod(name: "goBack", returnType: CAPPluginReturnPromise),
85
+ CAPPluginMethod(name: "open", returnType: CAPPluginReturnPromise),
86
+ CAPPluginMethod(name: "openWebView", returnType: CAPPluginReturnPromise),
87
+ CAPPluginMethod(name: "clearCookies", returnType: CAPPluginReturnPromise),
88
+ CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
89
+ CAPPluginMethod(name: "clearAllCookies", returnType: CAPPluginReturnPromise),
90
+ CAPPluginMethod(name: "clearCache", returnType: CAPPluginReturnPromise),
91
+ CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
92
+ CAPPluginMethod(name: "setUrl", returnType: CAPPluginReturnPromise),
93
+ CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise),
94
+ CAPPluginMethod(name: "hide", returnType: CAPPluginReturnPromise),
95
+ CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise),
96
+ CAPPluginMethod(name: "executeScript", returnType: CAPPluginReturnPromise),
97
+ CAPPluginMethod(name: "postMessage", returnType: CAPPluginReturnPromise),
98
+ CAPPluginMethod(name: "takeScreenshot", returnType: CAPPluginReturnPromise),
99
+ CAPPluginMethod(name: "updateDimensions", returnType: CAPPluginReturnPromise),
100
+ CAPPluginMethod(name: "setEnabledSafeTopMargin", returnType: CAPPluginReturnPromise),
101
+ CAPPluginMethod(name: "setEnabledSafeBottomMargin", returnType: CAPPluginReturnPromise),
102
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
103
+ CAPPluginMethod(name: "openSecureWindow", returnType: CAPPluginReturnPromise),
104
+ CAPPluginMethod(name: "handleProxyRequest", returnType: CAPPluginReturnPromise)
105
+ ]
106
+ var navigationWebViewController: UINavigationController?
107
+ private var navigationControllers: [String: UINavigationController] = [:]
108
+ private var privacyScreen: UIImageView?
109
+ private var isSetupDone = false
110
+ var currentPluginCall: CAPPluginCall?
111
+ var isPresentAfterPageLoad = false
112
+ var isHidden = false
113
+ var invisibilityMode: InvisibilityMode = .aware
114
+ var webViewController: WKWebViewController?
115
+ private var webViewControllers: [String: WKWebViewController] = [:]
116
+ private var proxySchemeHandlers: [String: ProxySchemeHandler] = [:]
117
+ private var webViewStack: [String] = []
118
+ private var activeWebViewId: String?
119
+ private weak var presentationContainerView: UIView?
120
+ private var presentationContainerWasInteractive = true
121
+ private var presentationContainerPreviousAlpha: CGFloat = 1
122
+ private var hiddenWebViewContainers: [ObjectIdentifier: UIView] = [:]
123
+ private var closeModalTitle: String?
124
+ private var closeModalDescription: String?
125
+ private var closeModalOk: String?
126
+ private var closeModalCancel: String?
127
+ private var openSecureWindowCall: CAPPluginCall?
128
+
129
+ private func setup() {
130
+ self.isSetupDone = true
131
+
132
+ #if swift(>=4.2)
133
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
134
+ NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil)
135
+ #else
136
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: nil)
137
+ NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: .UIApplicationWillResignActive, object: nil)
138
+ #endif
139
+ }
140
+
141
+ private func setActiveWebView(id: String, webView: WKWebViewController, navigationController: UINavigationController) {
142
+ activeWebViewId = id
143
+ self.webViewController = webView
144
+ self.navigationWebViewController = navigationController
145
+ }
146
+
147
+ private func registerWebView(
148
+ id: String,
149
+ webView: WKWebViewController,
150
+ navigationController: UINavigationController,
151
+ makeActive: Bool = true
152
+ ) {
153
+ webViewControllers[id] = webView
154
+ navigationControllers[id] = navigationController
155
+ webViewStack.removeAll { $0 == id }
156
+ webViewStack.append(id)
157
+ if makeActive || activeWebViewId == nil || self.webViewController == nil || self.navigationWebViewController == nil {
158
+ setActiveWebView(id: id, webView: webView, navigationController: navigationController)
159
+ }
160
+ }
161
+
162
+ private func unregisterWebView(id: String) {
163
+ if let webView = webViewControllers[id]?.capableWebView {
164
+ cleanupHiddenWebViewContainer(for: webView)
165
+ }
166
+ proxySchemeHandlers[id]?.cancelAllPendingTasks()
167
+ proxySchemeHandlers[id] = nil
168
+ webViewControllers[id] = nil
169
+ navigationControllers[id] = nil
170
+ webViewStack.removeAll { $0 == id }
171
+ activeWebViewId = webViewStack.last
172
+ if let activeId = activeWebViewId {
173
+ self.webViewController = webViewControllers[activeId]
174
+ self.navigationWebViewController = navigationControllers[activeId]
175
+ } else {
176
+ self.webViewController = nil
177
+ self.navigationWebViewController = nil
178
+ }
179
+ }
180
+
181
+ private func resolveNavigationController(for id: String?) -> UINavigationController? {
182
+ if let id {
183
+ return navigationControllers[id]
184
+ }
185
+ return navigationWebViewController
186
+ }
187
+
188
+ private func notifyPopupWindowOpened(id: String, parentId: String?, url: String?, visible: Bool) {
189
+ var event: [String: Any] = [
190
+ "id": id,
191
+ "visible": visible
192
+ ]
193
+ if let parentId, !parentId.isEmpty {
194
+ event["parentId"] = parentId
195
+ }
196
+ if let url, !url.isEmpty {
197
+ event["url"] = url
198
+ }
199
+ notifyListeners("popupWindowOpened", data: event)
200
+ }
201
+
202
+ private func cloneNavigationAppearance(from source: UINavigationController?, to target: UINavigationController) {
203
+ guard let source else { return }
204
+ target.navigationBar.isTranslucent = source.navigationBar.isTranslucent
205
+ target.toolbar.isTranslucent = source.toolbar.isTranslucent
206
+ target.navigationBar.tintColor = source.navigationBar.tintColor
207
+ target.navigationBar.barTintColor = source.navigationBar.barTintColor
208
+ target.navigationBar.titleTextAttributes = source.navigationBar.titleTextAttributes
209
+ target.toolbar.tintColor = source.toolbar.tintColor
210
+
211
+ if #available(iOS 13.0, *) {
212
+ target.navigationBar.standardAppearance = source.navigationBar.standardAppearance
213
+ target.navigationBar.scrollEdgeAppearance = source.navigationBar.scrollEdgeAppearance
214
+ target.navigationBar.compactAppearance = source.navigationBar.compactAppearance
215
+ target.navigationBar.compactScrollEdgeAppearance = source.navigationBar.compactScrollEdgeAppearance
216
+ target.toolbar.standardAppearance = source.toolbar.standardAppearance
217
+ target.toolbar.compactAppearance = source.toolbar.compactAppearance
218
+ target.toolbar.scrollEdgeAppearance = source.toolbar.scrollEdgeAppearance
219
+ } else {
220
+ target.navigationBar.setBackgroundImage(source.navigationBar.backgroundImage(for: .default), for: .default)
221
+ target.navigationBar.shadowImage = source.navigationBar.shadowImage
222
+ }
223
+ }
224
+
225
+ func createManagedPopupWebView(
226
+ from parentController: WKWebViewController,
227
+ configuration: WKWebViewConfiguration,
228
+ navigationAction: WKNavigationAction
229
+ ) -> WKWebView? {
230
+ let popupId = UUID().uuidString
231
+ let shouldHidePopup = parentController.hiddenPopupWindow
232
+ if let parentWebView = parentController.capableWebView {
233
+ configuration.processPool = parentWebView.configuration.processPool
234
+ }
235
+ if let dataStore = parentController.websiteDataStore() {
236
+ configuration.websiteDataStore = dataStore
237
+ }
238
+
239
+ let proxyHandler = parentController.proxySchemeHandler?.duplicate(for: popupId)
240
+ if let proxyHandler {
241
+ proxySchemeHandlers[popupId] = proxyHandler
242
+ }
243
+
244
+ let popupController = WKWebViewController()
245
+ guard let popupWebView = popupController.inheritPopupPresentation(
246
+ from: parentController,
247
+ request: navigationAction.request,
248
+ configuration: configuration,
249
+ instanceId: popupId,
250
+ proxySchemeHandler: proxyHandler
251
+ ) else {
252
+ proxySchemeHandlers[popupId] = nil
253
+ return nil
254
+ }
255
+
256
+ let navigationController = UINavigationController(rootViewController: popupController)
257
+ cloneNavigationAppearance(from: parentController.navigationController, to: navigationController)
258
+ let shouldActivatePopup = ActiveWebViewSupport.shouldActivateNewWebView(
259
+ isHidden: shouldHidePopup,
260
+ hasActiveWebView: activeWebViewId != nil
261
+ )
262
+ registerWebView(
263
+ id: popupId,
264
+ webView: popupController,
265
+ navigationController: navigationController,
266
+ makeActive: shouldActivatePopup
267
+ )
268
+ if shouldHidePopup {
269
+ guard attachWebViewToWindow(popupWebView) else {
270
+ unregisterWebView(id: popupId)
271
+ return nil
272
+ }
273
+ } else {
274
+ let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
275
+ presenter?.present(navigationController, animated: true, completion: nil)
276
+ }
277
+ notifyPopupWindowOpened(
278
+ id: popupId,
279
+ parentId: parentController.instanceId,
280
+ url: navigationAction.request.url?.absoluteString,
281
+ visible: !shouldHidePopup
282
+ )
283
+ return popupWebView
284
+ }
285
+
286
+ private func resolveWebViewController(for id: String?) -> WKWebViewController? {
287
+ if let id {
288
+ return webViewControllers[id]
289
+ }
290
+ return webViewController
291
+ }
292
+
293
+ func cookieStore(for id: String) -> WKHTTPCookieStore? {
294
+ resolveWebViewController(for: id)?.websiteDataStore()?.httpCookieStore
295
+ }
296
+
297
+ private func dataStores(for targetId: String?) -> [WKWebsiteDataStore] {
298
+ if let targetId {
299
+ guard let controller = webViewControllers[targetId],
300
+ let store = controller.websiteDataStore() else {
301
+ return []
302
+ }
303
+ return [store]
304
+ }
305
+
306
+ let controllers = Array(webViewControllers.values)
307
+ if controllers.isEmpty {
308
+ return [WKWebsiteDataStore.default()]
309
+ }
310
+
311
+ var seen = Set<ObjectIdentifier>()
312
+ var stores: [WKWebsiteDataStore] = []
313
+ for controller in controllers {
314
+ if let store = controller.websiteDataStore() {
315
+ let identifier = ObjectIdentifier(store)
316
+ if seen.insert(identifier).inserted {
317
+ stores.append(store)
318
+ }
319
+ }
320
+ }
321
+ return stores
322
+ }
323
+
324
+ private func parseProxyRules(_ rawRules: [Any]) throws -> [NativeProxyRule] {
325
+ try rawRules.enumerated().map { index, item in
326
+ guard let dictionary = item as? [String: Any] else {
327
+ throw NSError(
328
+ domain: "InAppBrowserPlugin",
329
+ code: -1,
330
+ userInfo: [NSLocalizedDescriptionKey: "Proxy rule at index \(index) must be an object"]
331
+ )
332
+ }
333
+ return try NativeProxyRule.from(dictionary: dictionary)
334
+ }
335
+ }
336
+
337
+ func presentView(webViewId: String? = nil, isAnimated: Bool = true) {
338
+ let resolvedId = webViewId ?? activeWebViewId
339
+ let navigationController = resolvedId.flatMap { navigationControllers[$0] } ?? self.navigationWebViewController
340
+ guard let navigationController else {
341
+ self.currentPluginCall?.reject("Navigation controller is not initialized")
342
+ return
343
+ }
344
+
345
+ let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
346
+ presenter?.present(navigationController, animated: isAnimated, completion: {
347
+ self.currentPluginCall?.resolve()
348
+ })
349
+ }
350
+
351
+ private func activeWindow() -> UIWindow? {
352
+ return UIApplication.shared.connectedScenes
353
+ .compactMap { $0 as? UIWindowScene }
354
+ .first { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }?
355
+ .windows
356
+ .first { $0.isKeyWindow }
357
+ }
358
+
359
+ private func hiddenContainerFrame(in window: UIWindow) -> CGRect {
360
+ switch self.invisibilityMode {
361
+ case .aware:
362
+ return CGRect(
363
+ x: window.bounds.maxX + 2048,
364
+ y: window.bounds.maxY + 2048,
365
+ width: 1,
366
+ height: 1
367
+ )
368
+ case .fakeVisible:
369
+ return CGRect(
370
+ x: window.bounds.maxX + 2048,
371
+ y: 0,
372
+ width: max(window.bounds.width, 1),
373
+ height: max(window.bounds.height, 1)
374
+ )
375
+ }
376
+ }
377
+
378
+ private func ensureHiddenWebViewContainer(for webView: WKWebView, in window: UIWindow) -> UIView {
379
+ let key = ObjectIdentifier(webView)
380
+ let frame = hiddenContainerFrame(in: window)
381
+
382
+ if let existingContainer = hiddenWebViewContainers[key] {
383
+ existingContainer.frame = frame
384
+ if existingContainer.superview !== window {
385
+ existingContainer.removeFromSuperview()
386
+ window.addSubview(existingContainer)
387
+ }
388
+ return existingContainer
389
+ }
390
+
391
+ let container = UIView(frame: frame)
392
+ container.backgroundColor = .clear
393
+ container.isOpaque = false
394
+ container.isUserInteractionEnabled = false
395
+ container.clipsToBounds = true
396
+ container.accessibilityElementsHidden = true
397
+ window.addSubview(container)
398
+ hiddenWebViewContainers[key] = container
399
+ return container
400
+ }
401
+
402
+ private func cleanupHiddenWebViewContainer(for webView: WKWebView?) {
403
+ guard let webView else {
404
+ return
405
+ }
406
+
407
+ let key = ObjectIdentifier(webView)
408
+ if let container = hiddenWebViewContainers.removeValue(forKey: key) {
409
+ container.removeFromSuperview()
410
+ }
411
+ }
412
+
413
+ private func attachWebViewToWindow(_ webView: WKWebView) -> Bool {
414
+ guard let window = activeWindow() else {
415
+ return false
416
+ }
417
+
418
+ let container = ensureHiddenWebViewContainer(for: webView, in: window)
419
+ webView.removeFromSuperview()
420
+ webView.translatesAutoresizingMaskIntoConstraints = true
421
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
422
+
423
+ switch self.invisibilityMode {
424
+ case .aware:
425
+ webView.frame = container.bounds
426
+ webView.alpha = 1
427
+ webView.isOpaque = false
428
+ webView.backgroundColor = .clear
429
+ webView.scrollView.backgroundColor = .clear
430
+ case .fakeVisible:
431
+ webView.frame = container.bounds
432
+ webView.alpha = 1
433
+ webView.isOpaque = false
434
+ webView.backgroundColor = .clear
435
+ webView.scrollView.backgroundColor = .clear
436
+ }
437
+
438
+ webView.isUserInteractionEnabled = false
439
+ container.addSubview(webView)
440
+ return true
441
+ }
442
+
443
+ private func attachWebViewToController(_ webViewController: WKWebViewController, webView: WKWebView) {
444
+ cleanupHiddenWebViewContainer(for: webView)
445
+ webView.removeFromSuperview()
446
+ webView.translatesAutoresizingMaskIntoConstraints = false
447
+ webViewController.view.addSubview(webView)
448
+
449
+ let bottomAnchor = webViewController.enabledSafeBottomMargin
450
+ ? webViewController.view.safeAreaLayoutGuide.bottomAnchor
451
+ : webViewController.view.bottomAnchor
452
+
453
+ let topAnchor = webViewController.enabledSafeTopMargin
454
+ ? webViewController.view.safeAreaLayoutGuide.topAnchor
455
+ : webViewController.view.topAnchor
456
+
457
+ NSLayoutConstraint.activate([
458
+ webView.topAnchor.constraint(equalTo: topAnchor),
459
+ webView.leadingAnchor.constraint(equalTo: webViewController.view.leadingAnchor),
460
+ webView.trailingAnchor.constraint(equalTo: webViewController.view.trailingAnchor),
461
+ webView.bottomAnchor.constraint(equalTo: bottomAnchor)
462
+ ])
463
+
464
+ if let backgroundColor = webViewController.view.backgroundColor {
465
+ webView.backgroundColor = backgroundColor
466
+ webView.scrollView.backgroundColor = backgroundColor
467
+ }
468
+ webView.alpha = 1
469
+ webView.isOpaque = true
470
+ webView.isUserInteractionEnabled = true
471
+ }
472
+
473
+ func handleWebViewDidClose(id: String, url: String) {
474
+ if !id.isEmpty, webViewControllers[id] != nil {
475
+ self.notifyListeners("closeEvent", data: ["id": id, "url": url])
476
+ unregisterWebView(id: id)
477
+ return
478
+ }
479
+
480
+ cleanupHiddenWebViewContainer(for: self.webViewController?.capableWebView)
481
+ self.notifyListeners("closeEvent", data: ["url": url])
482
+ self.webViewController = nil
483
+ self.navigationWebViewController = nil
484
+ }
485
+
486
+ @objc func clearAllCookies(_ call: CAPPluginCall) {
487
+ DispatchQueue.main.async {
488
+ let targetId = call.getString("id")
489
+ if let targetId, self.webViewControllers[targetId] == nil {
490
+ call.reject("WebView is not initialized")
491
+ return
492
+ }
493
+
494
+ let dataStores = self.dataStores(for: targetId)
495
+ if dataStores.isEmpty {
496
+ call.reject("WebView is not initialized")
497
+ return
498
+ }
499
+
500
+ let dataTypes = Set([WKWebsiteDataTypeCookies])
501
+ let group = DispatchGroup()
502
+ for dataStore in dataStores {
503
+ group.enter()
504
+ dataStore.removeData(ofTypes: dataTypes,
505
+ modifiedSince: Date(timeIntervalSince1970: 0)) {
506
+ group.leave()
507
+ }
508
+ }
509
+ group.notify(queue: .main) {
510
+ call.resolve()
511
+ }
512
+ }
513
+ }
514
+
515
+ @objc func clearCache(_ call: CAPPluginCall) {
516
+ DispatchQueue.main.async {
517
+ let targetId = call.getString("id")
518
+ if let targetId, self.webViewControllers[targetId] == nil {
519
+ call.reject("WebView is not initialized")
520
+ return
521
+ }
522
+
523
+ let dataStores = self.dataStores(for: targetId)
524
+ if dataStores.isEmpty {
525
+ call.reject("WebView is not initialized")
526
+ return
527
+ }
528
+
529
+ let dataTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
530
+ let group = DispatchGroup()
531
+ for dataStore in dataStores {
532
+ group.enter()
533
+ dataStore.removeData(ofTypes: dataTypes,
534
+ modifiedSince: Date(timeIntervalSince1970: 0)) {
535
+ group.leave()
536
+ }
537
+ }
538
+ group.notify(queue: .main) {
539
+ call.resolve()
540
+ }
541
+ }
542
+ }
543
+
544
+ @objc func clearCookies(_ call: CAPPluginCall) {
545
+ guard let url = call.getString("url"),
546
+ let host = URL(string: url)?.host else {
547
+ call.reject("Invalid URL")
548
+ return
549
+ }
550
+
551
+ DispatchQueue.main.async {
552
+ let targetId = call.getString("id")
553
+ if let targetId, self.webViewControllers[targetId] == nil {
554
+ call.reject("WebView is not initialized")
555
+ return
556
+ }
557
+
558
+ let dataStores = self.dataStores(for: targetId)
559
+ if dataStores.isEmpty {
560
+ call.reject("WebView is not initialized")
561
+ return
562
+ }
563
+
564
+ let outerGroup = DispatchGroup()
565
+ for dataStore in dataStores {
566
+ outerGroup.enter()
567
+ dataStore.httpCookieStore.getAllCookies { cookies in
568
+ let innerGroup = DispatchGroup()
569
+ for cookie in cookies {
570
+ if cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain) {
571
+ innerGroup.enter()
572
+ dataStore.httpCookieStore.delete(cookie) {
573
+ innerGroup.leave()
574
+ }
575
+ }
576
+ }
577
+ innerGroup.notify(queue: .main) {
578
+ outerGroup.leave()
579
+ }
580
+ }
581
+ }
582
+
583
+ outerGroup.notify(queue: .main) {
584
+ call.resolve()
585
+ }
586
+ }
587
+ }
588
+
589
+ @objc func getCookies(_ call: CAPPluginCall) {
590
+ let urlString = call.getString("url") ?? ""
591
+ let includeHttpOnly = call.getBool("includeHttpOnly") ?? true
592
+
593
+ guard let url = URL(string: urlString), let host = url.host else {
594
+ call.reject("Invalid URL")
595
+ return
596
+ }
597
+
598
+ DispatchQueue.main.async {
599
+ WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
600
+ var cookieDict = [String: String]()
601
+ for cookie in cookies {
602
+
603
+ if (includeHttpOnly || !cookie.isHTTPOnly) && (cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain)) {
604
+ cookieDict[cookie.name] = cookie.value
605
+ }
606
+ }
607
+ call.resolve(cookieDict)
608
+ }
609
+ }
610
+
611
+ }
612
+
613
+ @objc func openWebView(_ call: CAPPluginCall) {
614
+ if !self.isSetupDone {
615
+ self.setup()
616
+ }
617
+ self.currentPluginCall = call
618
+
619
+ guard let urlString = call.getString("url") else {
620
+ call.reject("Must provide a URL to open")
621
+ return
622
+ }
623
+
624
+ if urlString.isEmpty {
625
+ call.reject("URL must not be empty")
626
+ return
627
+ }
628
+
629
+ let webViewId = UUID().uuidString
630
+ let showScreenshotButton = call.getBool("showScreenshotButton", false)
631
+ let buttonNearDoneSettings = call.getObject("buttonNearDone")
632
+
633
+ var buttonNearDoneIcon: UIImage?
634
+ if let buttonNearDoneSettings {
635
+ guard let iosSettingsRaw = buttonNearDoneSettings["ios"] else {
636
+ call.reject("IOS settings not found")
637
+ return
638
+ }
639
+ guard let iosSettings = iosSettingsRaw as? JSObject else {
640
+ call.reject("IOS settings are not an object")
641
+ return
642
+ }
643
+
644
+ guard let iconType = iosSettings["iconType"] as? String else {
645
+ call.reject("buttonNearDone.iconType is empty")
646
+ return
647
+ }
648
+ if iconType != "sf-symbol" && iconType != "asset" {
649
+ call.reject("IconType is neither 'sf-symbol' nor 'asset'")
650
+ return
651
+ }
652
+ guard let icon = iosSettings["icon"] as? String else {
653
+ call.reject("buttonNearDone.icon is empty")
654
+ return
655
+ }
656
+
657
+ if iconType == "sf-symbol" {
658
+ buttonNearDoneIcon = UIImage(systemName: icon)?.withRenderingMode(.alwaysTemplate)
659
+ print("[DEBUG] Set buttonNearDone SF Symbol icon: \(icon)")
660
+ } else {
661
+ // Look in app's web assets/public directory
662
+ guard let webDir = Bundle.main.resourceURL?.appendingPathComponent("public") else {
663
+ call.reject("Failed to locate bundled web assets")
664
+ return
665
+ }
666
+
667
+ // Try several path combinations to find the asset
668
+ let paths = [
669
+ icon, // Just the icon name
670
+ "public/\(icon)", // With public/ prefix
671
+ icon.replacingOccurrences(of: "public/", with: "") // Without public/ prefix
672
+ ]
673
+
674
+ var foundImage = false
675
+
676
+ for path in paths {
677
+ // Try as a direct path from web assets dir
678
+ let assetPath = path.replacingOccurrences(of: "public/", with: "")
679
+ let fileURL = webDir.appendingPathComponent(assetPath)
680
+
681
+ print("[DEBUG] Trying to load from: \(fileURL.path)")
682
+
683
+ if FileManager.default.fileExists(atPath: fileURL.path),
684
+ let data = try? Data(contentsOf: fileURL),
685
+ let img = UIImage(data: data) {
686
+ buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
687
+ print("[DEBUG] Successfully loaded buttonNearDone from web assets: \(fileURL.path)")
688
+ foundImage = true
689
+ break
690
+ }
691
+
692
+ // Try with www directory as an alternative
693
+ if let wwwDir = Bundle.main.resourceURL?.appendingPathComponent("www") {
694
+ let wwwFileURL = wwwDir.appendingPathComponent(assetPath)
695
+
696
+ print("[DEBUG] Trying to load from www dir: \(wwwFileURL.path)")
697
+
698
+ if FileManager.default.fileExists(atPath: wwwFileURL.path),
699
+ let data = try? Data(contentsOf: wwwFileURL),
700
+ let img = UIImage(data: data) {
701
+ buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
702
+ print("[DEBUG] Successfully loaded buttonNearDone from www dir: \(wwwFileURL.path)")
703
+ foundImage = true
704
+ break
705
+ }
706
+ }
707
+
708
+ // Try looking in app bundle assets
709
+ if let iconImage = UIImage(named: path) {
710
+ buttonNearDoneIcon = iconImage.withRenderingMode(.alwaysTemplate)
711
+ print("[DEBUG] Successfully loaded buttonNearDone from app bundle: \(path)")
712
+ foundImage = true
713
+ break
714
+ }
715
+ }
716
+
717
+ if !foundImage {
718
+ call.reject("Failed to load buttonNearDone icon: \(icon)")
719
+ return
720
+ }
721
+ }
722
+ }
723
+
724
+ if showScreenshotButton, buttonNearDoneSettings != nil {
725
+ call.reject("showScreenshotButton is not compatible with buttonNearDone")
726
+ return
727
+ }
728
+
729
+ let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
730
+ let closeModal = call.getBool("closeModal", false)
731
+ let closeModalTitle = call.getString("closeModalTitle", "Close")
732
+ let closeModalDescription = call.getString("closeModalDescription", "Are you sure you want to close this window?")
733
+ let closeModalOk = call.getString("closeModalOk", "OK")
734
+ let closeModalCancel = call.getString("closeModalCancel", "Cancel")
735
+ let closeModalURLPattern = call.getString("closeModalURLPattern")
736
+ let isInspectable = call.getBool("isInspectable", false)
737
+ let preventDeeplink = call.getBool("preventDeeplink", false)
738
+ let openBlankTargetInWebView = call.getBool("openBlankTargetInWebView", false)
739
+ let isAnimated = call.getBool("isAnimated", true)
740
+ let enabledSafeBottomMargin = call.getBool("enabledSafeBottomMargin", false)
741
+ let enabledSafeTopMargin = call.getBool("enabledSafeTopMargin", true)
742
+ let hidden = call.getBool("hidden", false)
743
+ self.isHidden = hidden
744
+ let hiddenPopupWindow = call.getBool("hiddenPopupWindow", false)
745
+ let allowWebViewJsVisibilityControl = self.getConfig().getBoolean("allowWebViewJsVisibilityControl", false)
746
+ let allowScreenshotsFromWebPage = call.getBool("allowScreenshotsFromWebPage", false)
747
+ let captureConsoleLogs = call.getBool("captureConsoleLogs", false)
748
+ let handleDownloads = call.getBool("handleDownloads", false)
749
+ let invisibilityModeRaw = call.getString("invisibilityMode", "AWARE")
750
+ self.invisibilityMode = InvisibilityMode(rawValue: invisibilityModeRaw.uppercased()) ?? .aware
751
+
752
+ // Validate preShowScript requires isPresentAfterPageLoad
753
+ if call.getString("preShowScript") != nil && !call.getBool("isPresentAfterPageLoad", false) {
754
+ call.reject("preShowScript requires isPresentAfterPageLoad to be true")
755
+ return
756
+ }
757
+
758
+ // Validate closeModal options
759
+ if closeModal {
760
+ if call.getString("closeModalTitle") != nil ||
761
+ call.getString("closeModalDescription") != nil ||
762
+ call.getString("closeModalOk") != nil ||
763
+ call.getString("closeModalCancel") != nil {
764
+ // Store the values to be set after proper initialization
765
+ self.closeModalTitle = closeModalTitle
766
+ self.closeModalDescription = closeModalDescription
767
+ self.closeModalOk = closeModalOk
768
+ self.closeModalCancel = closeModalCancel
769
+ }
770
+ if let pattern = closeModalURLPattern {
771
+ if (try? NSRegularExpression(pattern: pattern)) == nil {
772
+ call.reject("Invalid closeModalURLPattern regex")
773
+ return
774
+ }
775
+ }
776
+ } else {
777
+ // Reject if closeModal is false but closeModal options are provided
778
+ if call.getString("closeModalTitle") != nil ||
779
+ call.getString("closeModalDescription") != nil ||
780
+ call.getString("closeModalOk") != nil ||
781
+ call.getString("closeModalCancel") != nil ||
782
+ call.getString("closeModalURLPattern") != nil {
783
+ call.reject("closeModal options require closeModal to be true")
784
+ return
785
+ }
786
+ }
787
+
788
+ // Validate shareDisclaimer requires shareSubject
789
+ if call.getString("shareSubject") == nil && call.getObject("shareDisclaimer") != nil {
790
+ call.reject("shareDisclaimer requires shareSubject to be provided")
791
+ return
792
+ }
793
+
794
+ // Validate buttonNearDone compatibility with toolbar type
795
+ if buttonNearDoneSettings != nil || showScreenshotButton {
796
+ let toolbarType = call.getString("toolbarType", "")
797
+ if toolbarType == "activity" || toolbarType == "navigation" || toolbarType == "blank" {
798
+ let optionName = showScreenshotButton ? "showScreenshotButton" : "buttonNearDone"
799
+ call.reject(optionName + " is not compatible with toolbarType: " + toolbarType)
800
+ return
801
+ }
802
+ }
803
+
804
+ var disclaimerContent: JSObject?
805
+ if let shareDisclaimerRaw = call.getObject("shareDisclaimer"), !shareDisclaimerRaw.isEmpty {
806
+ disclaimerContent = shareDisclaimerRaw
807
+ }
808
+
809
+ let toolbarType = call.getString("toolbarType", "")
810
+ let backgroundColor = call.getString("backgroundColor", "black") == "white" ? UIColor.white : UIColor.black
811
+
812
+ // Don't null out shareDisclaimer regardless of toolbarType
813
+ // if toolbarType != "activity" {
814
+ // disclaimerContent = nil
815
+ // }
816
+
817
+ let ignoreUntrustedSSLError = call.getBool("ignoreUntrustedSSLError", false)
818
+ let enableGooglePaySupport = call.getBool("enableGooglePaySupport", false)
819
+ let activeNativeNavigationForWebview = call.getBool("activeNativeNavigationForWebview", true)
820
+
821
+ self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
822
+ let showReloadButton = call.getBool("showReloadButton", false)
823
+
824
+ let blockedHostsRaw = call.getArray("blockedHosts", [])
825
+ let blockedHosts = blockedHostsRaw.compactMap { $0 as? String }
826
+
827
+ let authorizedAppLinksRaw = call.getArray("authorizedAppLinks", [])
828
+ let authorizedAppLinks = authorizedAppLinksRaw.compactMap { $0 as? String }
829
+
830
+ let credentials = self.readCredentials(call)
831
+
832
+ // Read HTTP method and body for custom requests
833
+ let httpMethod = call.getString("method")
834
+ let httpBody = call.getString("body")
835
+
836
+ // Read dimension options
837
+ let width = call.getFloat("width")
838
+ let height = call.getFloat("height")
839
+ let xPos = call.getFloat("x")
840
+ let yPos = call.getFloat("y")
841
+
842
+ // Read disableOverscroll option (iOS only - controls WebView bounce effect)
843
+ let disableOverscroll = call.getBool("disableOverscroll", false)
844
+
845
+ let legacyProxyRequests = ProxySchemeRequestSupport.legacyProxyRequestsConfiguration(from: call.options["proxyRequests"])
846
+ let outboundProxyRulesRaw = call.getArray("outboundProxyRules", [])
847
+ let inboundProxyRulesRaw = call.getArray("inboundProxyRules", [])
848
+ let outboundProxyRules: [NativeProxyRule]
849
+ let inboundProxyRules: [NativeProxyRule]
850
+ do {
851
+ outboundProxyRules = try parseProxyRules(outboundProxyRulesRaw)
852
+ inboundProxyRules = try parseProxyRules(inboundProxyRulesRaw)
853
+ } catch {
854
+ call.reject("Invalid proxy rules: \(error.localizedDescription)")
855
+ return
856
+ }
857
+
858
+ // Validate dimension parameters
859
+ if width != nil && height == nil {
860
+ call.reject("Height must be specified when width is provided")
861
+ return
862
+ }
863
+
864
+ DispatchQueue.main.async {
865
+ guard let url = URL(string: urlString) else {
866
+ call.reject("Invalid URL format")
867
+ return
868
+ }
869
+
870
+ var proxyHandler: ProxySchemeHandler?
871
+ if legacyProxyRequests.isEnabled || !outboundProxyRules.isEmpty || !inboundProxyRules.isEmpty {
872
+ proxyHandler = ProxySchemeHandler(
873
+ plugin: self,
874
+ webviewId: webViewId,
875
+ legacyProxyRequests: legacyProxyRequests.isEnabled,
876
+ legacyProxyRequestURLRegex: legacyProxyRequests.urlRegex,
877
+ outboundRules: outboundProxyRules,
878
+ inboundRules: inboundProxyRules
879
+ )
880
+ self.proxySchemeHandlers[webViewId] = proxyHandler
881
+ }
882
+
883
+ self.webViewController = WKWebViewController.init(
884
+ url: url,
885
+ headers: headers,
886
+ isInspectable: isInspectable,
887
+ credentials: credentials,
888
+ preventDeeplink: preventDeeplink,
889
+ blankNavigationTab: toolbarType == "blank",
890
+ enabledSafeBottomMargin: enabledSafeBottomMargin,
891
+ enabledSafeTopMargin: enabledSafeTopMargin,
892
+ blockedHosts: blockedHosts,
893
+ authorizedAppLinks: authorizedAppLinks,
894
+ allowWebViewJsVisibilityControl: allowWebViewJsVisibilityControl,
895
+ allowScreenshotsFromWebPage: allowScreenshotsFromWebPage,
896
+ captureConsoleLogs: captureConsoleLogs,
897
+ proxyRequests: legacyProxyRequests.isEnabled,
898
+ proxySchemeHandler: proxyHandler,
899
+ documentStartUserScripts: self.documentStartUserScripts(
900
+ authorizedAppLinks: authorizedAppLinks,
901
+ openBlankTargetInWebView: openBlankTargetInWebView
902
+ ),
903
+ openBlankTargetInWebView: openBlankTargetInWebView
904
+ )
905
+
906
+ guard let webViewController = self.webViewController else {
907
+ call.reject("Failed to initialize WebViewController")
908
+ return
909
+ }
910
+
911
+ webViewController.instanceId = webViewId
912
+
913
+ // Set HTTP method and body if provided
914
+ if let method = httpMethod {
915
+ webViewController.httpMethod = method
916
+ }
917
+ if let body = httpBody {
918
+ webViewController.httpBody = body
919
+ }
920
+
921
+ // Set dimensions if provided
922
+ if let width = width {
923
+ webViewController.customWidth = CGFloat(width)
924
+ }
925
+ if let height = height {
926
+ webViewController.customHeight = CGFloat(height)
927
+ }
928
+ if let xPos = xPos {
929
+ webViewController.customX = CGFloat(xPos)
930
+ }
931
+ if let yPos = yPos {
932
+ webViewController.customY = CGFloat(yPos)
933
+ }
934
+
935
+ // Set disableOverscroll option
936
+ webViewController.disableOverscroll = disableOverscroll
937
+ webViewController.handleDownloads = handleDownloads
938
+
939
+ // Set native navigation gestures before view loads
940
+ webViewController.activeNativeNavigationForWebview = activeNativeNavigationForWebview
941
+
942
+ // Update the webview's gesture property (if webview already exists)
943
+ webViewController.updateNavigationGestures()
944
+
945
+ if self.bridge?.statusBarVisible == true {
946
+ let subviews = self.bridge?.webView?.superview?.subviews
947
+ if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
948
+ if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
949
+ webViewController.capacitorStatusBar = emptyStatusBar
950
+ emptyStatusBar.removeFromSuperview()
951
+ }
952
+ }
953
+ }
954
+
955
+ webViewController.source = .remote(url)
956
+ webViewController.leftNavigationBarItemTypes = []
957
+
958
+ // Configure close button based on showArrow
959
+ let showArrow = call.getBool("showArrow", false)
960
+ if showArrow {
961
+ // When showArrow is true, put arrow on left
962
+ webViewController.doneBarButtonItemPosition = .left
963
+ webViewController.showArrowAsClose = true
964
+ } else {
965
+ // Default X on right
966
+ webViewController.doneBarButtonItemPosition = toolbarType == "activity" ? .none : .right
967
+ webViewController.showArrowAsClose = false
968
+ }
969
+
970
+ // Configure navigation buttons based on toolbarType
971
+ if toolbarType == "activity" {
972
+ // Activity mode should ONLY have:
973
+ // 1. Close button (if not hidden by doneBarButtonItemPosition)
974
+ // 2. Share button (if shareSubject is provided)
975
+ webViewController.leftNavigationBarItemTypes = [] // Clear any left items
976
+ webViewController.rightNavigaionBarItemTypes = [] // Clear any right items
977
+
978
+ // Only add share button if subject is provided
979
+ if call.getString("shareSubject") != nil {
980
+ // Add share button to right bar
981
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
982
+ print("[DEBUG] Activity mode: Added share button, shareSubject: \(call.getString("shareSubject") ?? "nil")")
983
+ } else {
984
+ // In activity mode, always make the share button visible by setting a default shareSubject
985
+ webViewController.shareSubject = "Share"
986
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
987
+ print("[DEBUG] Activity mode: Setting default shareSubject")
988
+ }
989
+
990
+ // Set done button position based on showArrow
991
+ if showArrow {
992
+ webViewController.doneBarButtonItemPosition = .left
993
+ } else {
994
+ // In activity mode, keep the done button visible even when showArrow is false
995
+ webViewController.doneBarButtonItemPosition = .right
996
+ }
997
+ } else if toolbarType == "navigation" {
998
+ // Navigation mode puts back/forward on the left
999
+ webViewController.leftNavigationBarItemTypes = [.back, .forward]
1000
+ if showReloadButton {
1001
+ webViewController.leftNavigationBarItemTypes.append(.reload)
1002
+ }
1003
+
1004
+ // Only add share button if subject is provided
1005
+ if call.getString("shareSubject") != nil {
1006
+ // Add share button to right navigation bar
1007
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
1008
+ }
1009
+ } else {
1010
+ // Other modes may have reload button
1011
+ if showReloadButton {
1012
+ webViewController.leftNavigationBarItemTypes.append(.reload)
1013
+ }
1014
+
1015
+ // Only add share button if subject is provided
1016
+ if call.getString("shareSubject") != nil {
1017
+ // Add share button to right navigation bar
1018
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
1019
+ }
1020
+ }
1021
+
1022
+ // Set buttonNearDoneIcon if provided
1023
+ if let buttonNearDoneIcon = buttonNearDoneIcon {
1024
+ webViewController.buttonNearDoneIcon = buttonNearDoneIcon
1025
+ print("[DEBUG] Button near done icon set: \(buttonNearDoneIcon)")
1026
+ } else if showScreenshotButton {
1027
+ webViewController.buttonNearDoneIcon = UIImage(systemName: "camera")?.withRenderingMode(.alwaysTemplate)
1028
+ }
1029
+ webViewController.showScreenshotButton = showScreenshotButton
1030
+
1031
+ webViewController.capBrowserPlugin = self
1032
+ webViewController.title = call.getString("title", "New Window")
1033
+ // Only set shareSubject if not already set for activity mode
1034
+ if webViewController.shareSubject == nil {
1035
+ webViewController.shareSubject = call.getString("shareSubject")
1036
+ }
1037
+ webViewController.shareDisclaimer = disclaimerContent
1038
+
1039
+ // Debug shareDisclaimer
1040
+ if let disclaimer = disclaimerContent {
1041
+ print("[DEBUG] Share disclaimer set: \(disclaimer)")
1042
+ } else {
1043
+ print("[DEBUG] No share disclaimer set")
1044
+ }
1045
+
1046
+ webViewController.preShowScript = call.getString("preShowScript")
1047
+ webViewController.preShowScriptInjectionTime = call.getString("preShowScriptInjectionTime", "pageLoad")
1048
+
1049
+ // If script should be injected at document start, inject it now
1050
+ if webViewController.preShowScriptInjectionTime == "documentStart" {
1051
+ webViewController.injectPreShowScriptAtDocumentStart()
1052
+ }
1053
+
1054
+ webViewController.websiteTitleInNavigationBar = call.getBool("visibleTitle", true)
1055
+ webViewController.ignoreUntrustedSSLError = ignoreUntrustedSSLError
1056
+
1057
+ // Set Google Pay support
1058
+ webViewController.enableGooglePaySupport = enableGooglePaySupport
1059
+ webViewController.hiddenPopupWindow = hiddenPopupWindow
1060
+ webViewController.opensHidden = hidden
1061
+ webViewController.captureConsoleLogs = captureConsoleLogs
1062
+
1063
+ // Set text zoom if specified
1064
+ if let textZoom = call.getInt("textZoom") {
1065
+ webViewController.textZoom = textZoom
1066
+ }
1067
+
1068
+ // Set closeModal properties after proper initialization
1069
+ if closeModal {
1070
+ webViewController.closeModal = true
1071
+ webViewController.closeModalTitle = self.closeModalTitle ?? closeModalTitle
1072
+ webViewController.closeModalDescription = self.closeModalDescription ?? closeModalDescription
1073
+ webViewController.closeModalOk = self.closeModalOk ?? closeModalOk
1074
+ webViewController.closeModalCancel = self.closeModalCancel ?? closeModalCancel
1075
+ if let pattern = closeModalURLPattern {
1076
+ webViewController.closeModalURLPattern = pattern
1077
+ }
1078
+ }
1079
+
1080
+ self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
1081
+ if let navigationController = self.navigationWebViewController {
1082
+ self.registerWebView(id: webViewId, webView: webViewController, navigationController: navigationController)
1083
+ }
1084
+ self.navigationWebViewController?.navigationBar.isTranslucent = false
1085
+ self.navigationWebViewController?.toolbar.isTranslucent = false
1086
+
1087
+ // Ensure no lines or borders appear by default
1088
+ self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
1089
+ self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
1090
+ self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
1091
+ self.navigationWebViewController?.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any)
1092
+
1093
+ // Handle web view background color
1094
+ webViewController.view.backgroundColor = backgroundColor
1095
+
1096
+ // Handle toolbar color
1097
+ if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
1098
+ // If specific color provided, use it
1099
+ let color = UIColor(hexString: toolbarColor)
1100
+
1101
+ // Apply to status bar and navigation bar area with a single colored view
1102
+ webViewController.setupStatusBarBackground(color: color)
1103
+
1104
+ // Set status bar style based on toolbar color
1105
+ let isDark = self.isDarkColor(color)
1106
+ webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
1107
+ webViewController.updateStatusBarStyle()
1108
+
1109
+ // Apply text color
1110
+ let textColor: UIColor
1111
+ if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
1112
+ textColor = UIColor(hexString: toolbarTextColor)
1113
+ } else {
1114
+ textColor = isDark ? UIColor.white : UIColor.black
1115
+ }
1116
+
1117
+ // Apply tint color to all UI elements without changing background
1118
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
1119
+ webViewController.tintColor = textColor
1120
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
1121
+ } else {
1122
+ // Use system appearance
1123
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
1124
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
1125
+ let textColor: UIColor
1126
+
1127
+ if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
1128
+ textColor = UIColor(hexString: toolbarTextColor)
1129
+ } else {
1130
+ textColor = isDarkMode ? UIColor.white : UIColor.black
1131
+ }
1132
+
1133
+ // Apply colors
1134
+ webViewController.setupStatusBarBackground(color: backgroundColor)
1135
+ webViewController.tintColor = textColor
1136
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
1137
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
1138
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
1139
+ webViewController.updateStatusBarStyle()
1140
+
1141
+ }
1142
+
1143
+ // Configure modal presentation for touch passthrough if custom dimensions are set
1144
+ if width != nil || height != nil {
1145
+ self.navigationWebViewController?.modalPresentationStyle = .overFullScreen
1146
+
1147
+ // Create a pass-through container
1148
+ let containerView = PassThroughView()
1149
+ containerView.backgroundColor = .clear
1150
+
1151
+ // Calculate dimensions - use screen width if only height is provided
1152
+ let finalWidth = width.map { CGFloat($0) } ?? UIScreen.main.bounds.width
1153
+ let finalHeight = height.map { CGFloat($0) } ?? UIScreen.main.bounds.height
1154
+
1155
+ containerView.targetFrame = CGRect(
1156
+ x: CGFloat(xPos ?? 0),
1157
+ y: CGFloat(yPos ?? 0),
1158
+ width: finalWidth,
1159
+ height: finalHeight
1160
+ )
1161
+
1162
+ // Replace the navigation controller's view with our pass-through container
1163
+ if let navController = self.navigationWebViewController,
1164
+ let originalView = navController.view {
1165
+ navController.view = containerView
1166
+ containerView.addSubview(originalView)
1167
+ originalView.frame = CGRect(
1168
+ x: CGFloat(xPos ?? 0),
1169
+ y: CGFloat(yPos ?? 0),
1170
+ width: finalWidth,
1171
+ height: finalHeight
1172
+ )
1173
+ }
1174
+ } else {
1175
+ self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
1176
+ }
1177
+
1178
+ self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
1179
+ if toolbarType == "blank" {
1180
+ self.navigationWebViewController?.navigationBar.isHidden = true
1181
+ webViewController.blankNavigationTab = true
1182
+
1183
+ // Even with hidden navigation bar, we need to set proper status bar appearance
1184
+ // If toolbarColor is explicitly set, use that for status bar style
1185
+ if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
1186
+ let color = UIColor(hexString: toolbarColor)
1187
+ let isDark = self.isDarkColor(color)
1188
+ webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
1189
+ webViewController.updateStatusBarStyle()
1190
+
1191
+ // Apply status bar background color via the special view
1192
+ webViewController.setupStatusBarBackground(color: color)
1193
+
1194
+ // Apply background color to whole view to ensure no gaps
1195
+ webViewController.view.backgroundColor = color
1196
+ self.navigationWebViewController?.view.backgroundColor = color
1197
+
1198
+ // Apply status bar background color
1199
+ if let navController = self.navigationWebViewController {
1200
+ navController.view.backgroundColor = color
1201
+ }
1202
+ } else {
1203
+ // Follow system appearance if no specific color
1204
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
1205
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
1206
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
1207
+ webViewController.updateStatusBarStyle()
1208
+
1209
+ // Apply status bar background color via the special view
1210
+ webViewController.setupStatusBarBackground(color: backgroundColor)
1211
+
1212
+ // Set appropriate background color
1213
+ if let navController = self.navigationWebViewController {
1214
+ navController.view.backgroundColor = backgroundColor
1215
+ }
1216
+ }
1217
+
1218
+ }
1219
+
1220
+ // We don't use the toolbar anymore, always hide it
1221
+ self.navigationWebViewController?.setToolbarHidden(true, animated: false)
1222
+
1223
+ if hidden {
1224
+ guard let webView = webViewController.capableWebView else {
1225
+ call.reject("Failed to get webview for hidden mode")
1226
+ return
1227
+ }
1228
+ // Zero-frame in window hierarchy required for WKWebView JS execution when hidden
1229
+ if !self.attachWebViewToWindow(webView) {
1230
+ call.reject("Failed to get active window for hidden webview")
1231
+ return
1232
+ }
1233
+ } else if !self.isPresentAfterPageLoad {
1234
+ self.presentView(webViewId: webViewId, isAnimated: isAnimated)
1235
+ }
1236
+ call.resolve(["id": webViewId])
1237
+ }
1238
+ }
1239
+
1240
+ @objc func goBack(_ call: CAPPluginCall) {
1241
+ DispatchQueue.main.async {
1242
+ let targetId = call.getString("id") ?? self.activeWebViewId
1243
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1244
+ call.resolve(["canGoBack": false])
1245
+ return
1246
+ }
1247
+
1248
+ let canGoBack = webViewController.goBack()
1249
+ call.resolve(["canGoBack": canGoBack])
1250
+ }
1251
+ }
1252
+
1253
+ @objc func reload(_ call: CAPPluginCall) {
1254
+ DispatchQueue.main.async {
1255
+ let targetId = call.getString("id") ?? self.activeWebViewId
1256
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1257
+ call.reject("WebView is not initialized")
1258
+ return
1259
+ }
1260
+
1261
+ webViewController.reload()
1262
+ call.resolve()
1263
+ }
1264
+ }
1265
+
1266
+ @objc func setUrl(_ call: CAPPluginCall) {
1267
+ guard let urlString = call.getString("url") else {
1268
+ call.reject("Cannot get new url to set")
1269
+ return
1270
+ }
1271
+
1272
+ guard let url = URL(string: urlString) else {
1273
+ call.reject("Invalid URL")
1274
+ return
1275
+ }
1276
+
1277
+ let targetId = call.getString("id")
1278
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1279
+ call.reject("WebView is not initialized")
1280
+ return
1281
+ }
1282
+
1283
+ webViewController.load(remote: url)
1284
+ call.resolve()
1285
+ }
1286
+
1287
+ private func setHiddenState(_ hidden: Bool, targetId: String?, call: CAPPluginCall?) {
1288
+ DispatchQueue.main.async {
1289
+ let resolvedId = targetId ?? self.activeWebViewId
1290
+ guard let webViewController = self.resolveWebViewController(for: resolvedId),
1291
+ let webView = webViewController.capableWebView else {
1292
+ call?.reject("WebView is not initialized")
1293
+ return
1294
+ }
1295
+ let navigationController = self.resolveNavigationController(for: resolvedId)
1296
+
1297
+ self.isHidden = hidden
1298
+
1299
+ if hidden {
1300
+ if let navController = navigationController, navController.presentingViewController != nil {
1301
+ navController.view.isHidden = true
1302
+ navController.view.isUserInteractionEnabled = false
1303
+ if let containerView = navController.view.superview {
1304
+ if self.presentationContainerView == nil || self.presentationContainerView !== containerView {
1305
+ self.presentationContainerView = containerView
1306
+ self.presentationContainerWasInteractive = containerView.isUserInteractionEnabled
1307
+ self.presentationContainerPreviousAlpha = containerView.alpha
1308
+ }
1309
+ containerView.isUserInteractionEnabled = false
1310
+ containerView.alpha = 0
1311
+ }
1312
+ }
1313
+
1314
+ if !self.attachWebViewToWindow(webView) {
1315
+ call?.reject("Failed to get active window for hidden webview")
1316
+ return
1317
+ }
1318
+ } else {
1319
+ if webView.superview !== webViewController.view {
1320
+ self.attachWebViewToController(webViewController, webView: webView)
1321
+ }
1322
+
1323
+ if let navController = navigationController {
1324
+ if let resolvedId {
1325
+ self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navController)
1326
+ }
1327
+ navController.view.isHidden = false
1328
+ navController.view.isUserInteractionEnabled = true
1329
+ if let containerView = self.presentationContainerView {
1330
+ containerView.isUserInteractionEnabled = self.presentationContainerWasInteractive
1331
+ containerView.alpha = self.presentationContainerPreviousAlpha
1332
+ self.presentationContainerView = nil
1333
+ self.presentationContainerWasInteractive = true
1334
+ self.presentationContainerPreviousAlpha = 1
1335
+ }
1336
+
1337
+ if navController.presentingViewController == nil {
1338
+ let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
1339
+ presenter?.present(navController, animated: true, completion: {
1340
+ call?.resolve()
1341
+ })
1342
+ return
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ call?.resolve()
1348
+ }
1349
+ }
1350
+
1351
+ func setHiddenFromJavaScript(_ hidden: Bool, sourceId: String? = nil) {
1352
+ self.setHiddenState(
1353
+ hidden,
1354
+ targetId: ActiveWebViewSupport.resolveVisibilityTarget(
1355
+ originatingWebViewId: sourceId,
1356
+ activeWebViewId: activeWebViewId
1357
+ ),
1358
+ call: nil
1359
+ )
1360
+ }
1361
+
1362
+ @objc func hide(_ call: CAPPluginCall) {
1363
+ self.setHiddenState(true, targetId: call.getString("id"), call: call)
1364
+ }
1365
+
1366
+ @objc func show(_ call: CAPPluginCall) {
1367
+ self.setHiddenState(false, targetId: call.getString("id"), call: call)
1368
+ }
1369
+
1370
+ @objc func executeScript(_ call: CAPPluginCall) {
1371
+ guard let script = call.getString("code") else {
1372
+ call.reject("Cannot get script to execute")
1373
+ return
1374
+ }
1375
+ DispatchQueue.main.async {
1376
+ if let targetId = call.getString("id") {
1377
+ guard let webViewController = self.webViewControllers[targetId] else {
1378
+ call.reject("WebView is not initialized")
1379
+ return
1380
+ }
1381
+ webViewController.executeScript(script: script)
1382
+ call.resolve()
1383
+ return
1384
+ }
1385
+
1386
+ if !self.webViewControllers.isEmpty {
1387
+ self.webViewControllers.values.forEach { $0.executeScript(script: script) }
1388
+ call.resolve()
1389
+ return
1390
+ }
1391
+
1392
+ guard let webViewController = self.webViewController else {
1393
+ call.reject("WebView is not initialized")
1394
+ return
1395
+ }
1396
+
1397
+ webViewController.executeScript(script: script)
1398
+ call.resolve()
1399
+ }
1400
+ }
1401
+
1402
+ @objc func postMessage(_ call: CAPPluginCall) {
1403
+ let eventData = call.getObject("detail", [:])
1404
+ // Check if eventData is empty
1405
+ if eventData.isEmpty {
1406
+ call.reject("Event data must not be empty")
1407
+ return
1408
+ }
1409
+ print("Event data: \(eventData)")
1410
+
1411
+ DispatchQueue.main.async {
1412
+ if let targetId = call.getString("id") {
1413
+ guard let webViewController = self.webViewControllers[targetId] else {
1414
+ call.reject("WebView is not initialized")
1415
+ return
1416
+ }
1417
+ webViewController.postMessageToJS(message: eventData)
1418
+ call.resolve()
1419
+ return
1420
+ }
1421
+
1422
+ if !self.webViewControllers.isEmpty {
1423
+ self.webViewControllers.values.forEach { $0.postMessageToJS(message: eventData) }
1424
+ call.resolve()
1425
+ return
1426
+ }
1427
+
1428
+ guard let webViewController = self.webViewController else {
1429
+ call.reject("WebView is not initialized")
1430
+ return
1431
+ }
1432
+
1433
+ webViewController.postMessageToJS(message: eventData)
1434
+ call.resolve()
1435
+ }
1436
+ }
1437
+
1438
+ @objc func takeScreenshot(_ call: CAPPluginCall) {
1439
+ DispatchQueue.main.async {
1440
+ let targetId = call.getString("id") ?? self.activeWebViewId
1441
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1442
+ call.reject("WebView is not initialized")
1443
+ return
1444
+ }
1445
+
1446
+ webViewController.takeScreenshot { result in
1447
+ switch result {
1448
+ case .success(let screenshot):
1449
+ call.resolve(screenshot)
1450
+ case .failure(let error):
1451
+ call.reject(error.localizedDescription)
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ private func normalizedAuthorizedHosts(from links: [String]) -> [String] {
1458
+ links.compactMap { link in
1459
+ guard let host = URLComponents(string: link)?.host?.lowercased() else {
1460
+ return nil
1461
+ }
1462
+
1463
+ return host.hasPrefix("www.") ? String(host.dropFirst(4)) : host
1464
+ }
1465
+ }
1466
+
1467
+ private func blankTargetInWebViewScript(authorizedAppLinks: [String]) -> String {
1468
+ let authorizedHosts = normalizedAuthorizedHosts(from: authorizedAppLinks)
1469
+ let hostsData = try? JSONSerialization.data(withJSONObject: authorizedHosts)
1470
+ let hostsJSON = hostsData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
1471
+
1472
+ return """
1473
+ (function() {
1474
+ const authorizedHosts = new Set(\(hostsJSON));
1475
+ const normalizeHost = (host) => (host || '').replace(/^www\\./i, '').toLowerCase();
1476
+
1477
+ document.addEventListener('click', function(event) {
1478
+ const element = event.target;
1479
+ if (!element || typeof element.closest !== 'function') {
1480
+ return;
1481
+ }
1482
+
1483
+ const anchor = element.closest('a[target="_blank"][href]');
1484
+ if (!anchor) {
1485
+ return;
1486
+ }
1487
+
1488
+ let nextUrl;
1489
+ try {
1490
+ nextUrl = new URL(anchor.getAttribute('href'), window.location.href);
1491
+ } catch (_) {
1492
+ return;
1493
+ }
1494
+
1495
+ const protocol = nextUrl.protocol.toLowerCase();
1496
+ if (protocol !== 'http:' && protocol !== 'https:') {
1497
+ return;
1498
+ }
1499
+
1500
+ if (authorizedHosts.has(normalizeHost(nextUrl.hostname))) {
1501
+ return;
1502
+ }
1503
+
1504
+ event.preventDefault();
1505
+ setTimeout(function() {
1506
+ window.location.assign(nextUrl.toString());
1507
+ }, 0);
1508
+ }, true);
1509
+ })();
1510
+ """
1511
+ }
1512
+
1513
+ private func documentStartUserScripts(
1514
+ authorizedAppLinks: [String],
1515
+ openBlankTargetInWebView: Bool
1516
+ ) -> [String] {
1517
+ guard openBlankTargetInWebView else {
1518
+ return []
1519
+ }
1520
+
1521
+ return [blankTargetInWebViewScript(authorizedAppLinks: authorizedAppLinks)]
1522
+ }
1523
+
1524
+ func isHexColorCode(_ input: String) -> Bool {
1525
+ let hexColorRegex = "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$"
1526
+
1527
+ do {
1528
+ let regex = try NSRegularExpression(pattern: hexColorRegex)
1529
+ let range = NSRange(location: 0, length: input.utf16.count)
1530
+ if regex.firstMatch(in: input, options: [], range: range) != nil {
1531
+ return true
1532
+ }
1533
+ } catch {
1534
+ print("Error creating regular expression: \(error)")
1535
+ }
1536
+
1537
+ return false
1538
+ }
1539
+
1540
+ @objc func open(_ call: CAPPluginCall) {
1541
+ if !self.isSetupDone {
1542
+ self.setup()
1543
+ }
1544
+
1545
+ let isInspectable = call.getBool("isInspectable", false)
1546
+ let preventDeeplink = call.getBool("preventDeeplink", false)
1547
+ self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
1548
+
1549
+ self.currentPluginCall = call
1550
+
1551
+ guard let urlString = call.getString("url") else {
1552
+ call.reject("Must provide a URL to open")
1553
+ return
1554
+ }
1555
+
1556
+ if urlString.isEmpty {
1557
+ call.reject("URL must not be empty")
1558
+ return
1559
+ }
1560
+
1561
+ let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
1562
+ let credentials = self.readCredentials(call)
1563
+
1564
+ DispatchQueue.main.async {
1565
+ guard let url = URL(string: urlString) else {
1566
+ call.reject("Invalid URL format")
1567
+ return
1568
+ }
1569
+
1570
+ self.webViewController = WKWebViewController.init(url: url, headers: headers, isInspectable: isInspectable, credentials: credentials, preventDeeplink: preventDeeplink, blankNavigationTab: true, enabledSafeBottomMargin: false, enabledSafeTopMargin: true)
1571
+
1572
+ guard let webViewController = self.webViewController else {
1573
+ call.reject("Failed to initialize WebViewController")
1574
+ return
1575
+ }
1576
+
1577
+ if self.bridge?.statusBarVisible == true {
1578
+ let subviews = self.bridge?.webView?.superview?.subviews
1579
+ if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
1580
+ if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
1581
+ webViewController.capacitorStatusBar = emptyStatusBar
1582
+ emptyStatusBar.removeFromSuperview()
1583
+ }
1584
+ }
1585
+ }
1586
+
1587
+ webViewController.source = .remote(url)
1588
+ webViewController.leftNavigationBarItemTypes = [.back, .forward, .reload]
1589
+ webViewController.capBrowserPlugin = self
1590
+ webViewController.hasDynamicTitle = true
1591
+
1592
+ self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
1593
+ self.navigationWebViewController?.navigationBar.isTranslucent = false
1594
+
1595
+ // Ensure no lines or borders appear by default
1596
+ self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
1597
+ self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
1598
+ self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
1599
+
1600
+ // Use system appearance
1601
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
1602
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
1603
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
1604
+
1605
+ // Apply colors
1606
+ webViewController.setupStatusBarBackground(color: backgroundColor)
1607
+ webViewController.tintColor = textColor
1608
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
1609
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
1610
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
1611
+ webViewController.updateStatusBarStyle()
1612
+
1613
+ // Always hide toolbar to ensure no bottom bar
1614
+ self.navigationWebViewController?.setToolbarHidden(true, animated: false)
1615
+
1616
+ self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
1617
+ self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
1618
+
1619
+ if !self.isPresentAfterPageLoad {
1620
+ self.presentView()
1621
+ }
1622
+ call.resolve()
1623
+ }
1624
+ }
1625
+
1626
+ @objc func handleProxyRequest(_ call: CAPPluginCall) {
1627
+ guard let requestId = call.getString("requestId") else {
1628
+ call.reject("requestId is required")
1629
+ return
1630
+ }
1631
+
1632
+ let webviewId = call.getString("webviewId")
1633
+ let phase = call.getString("phase")
1634
+ let decisionObj = call.getObject("decision") ?? call.getObject("response")
1635
+
1636
+ let handler: ProxySchemeHandler?
1637
+ switch ProxyResponseRoutingSupport.resolveTargetHandler(
1638
+ webviewId: webviewId,
1639
+ requestId: requestId,
1640
+ handlers: proxySchemeHandlers
1641
+ ) {
1642
+ case .matched(let proxyHandler):
1643
+ handler = proxyHandler
1644
+ case .ambiguous:
1645
+ call.reject("webviewId is required when multiple webviews are open")
1646
+ return
1647
+ case .missing:
1648
+ handler = nil
1649
+ }
1650
+
1651
+ guard let proxyHandler = handler else {
1652
+ if let webviewId,
1653
+ webViewControllers[webviewId] == nil,
1654
+ navigationControllers[webviewId] == nil {
1655
+ print("[InAppBrowser][Proxy] Ignoring late proxy response for closed webview \(webviewId)")
1656
+ call.resolve()
1657
+ return
1658
+ }
1659
+ call.reject("No proxy handler found")
1660
+ return
1661
+ }
1662
+
1663
+ var responseDict: [String: Any]?
1664
+ if let decisionObj = decisionObj {
1665
+ var dict: [String: Any] = [:]
1666
+ if let cancel = decisionObj["cancel"] as? Bool {
1667
+ dict["cancel"] = cancel
1668
+ }
1669
+ if let request = decisionObj["request"] as? JSObject {
1670
+ var requestDict: [String: Any] = [:]
1671
+ requestDict["url"] = request["url"]
1672
+ requestDict["method"] = request["method"]
1673
+ requestDict["body"] = request["body"]
1674
+ if let headers = request["headers"] as? JSObject {
1675
+ var headersDict: [String: String] = [:]
1676
+ for (key, value) in headers {
1677
+ if let strValue = value as? String {
1678
+ headersDict[key] = strValue
1679
+ }
1680
+ }
1681
+ requestDict["headers"] = headersDict
1682
+ }
1683
+ dict["request"] = requestDict
1684
+ }
1685
+
1686
+ let responsePayload = (decisionObj["response"] as? JSObject) ?? decisionObj
1687
+ if responsePayload["status"] != nil {
1688
+ var responsePayloadDict: [String: Any] = [:]
1689
+ responsePayloadDict["status"] = responsePayload["status"]
1690
+ responsePayloadDict["body"] = responsePayload["body"]
1691
+ if let headers = responsePayload["headers"] as? JSObject {
1692
+ var headersDict: [String: String] = [:]
1693
+ for (key, value) in headers {
1694
+ if let strValue = value as? String {
1695
+ headersDict[key] = strValue
1696
+ }
1697
+ }
1698
+ responsePayloadDict["headers"] = headersDict
1699
+ }
1700
+ if decisionObj["response"] != nil {
1701
+ dict["response"] = responsePayloadDict
1702
+ } else {
1703
+ dict.merge(responsePayloadDict) { _, new in new }
1704
+ }
1705
+ }
1706
+ responseDict = dict
1707
+ }
1708
+
1709
+ proxyHandler.handleResponse(requestId: requestId, phase: phase, responseData: responseDict)
1710
+ call.resolve()
1711
+ }
1712
+
1713
+ @objc func close(_ call: CAPPluginCall) {
1714
+ let isAnimated = call.getBool("isAnimated", true)
1715
+
1716
+ DispatchQueue.main.async {
1717
+ let targetId = call.getString("id") ?? self.activeWebViewId
1718
+ if let targetId,
1719
+ let webViewController = self.webViewControllers[targetId],
1720
+ let navigationController = self.navigationControllers[targetId] {
1721
+ let currentUrl = webViewController.url?.absoluteString ?? ""
1722
+ webViewController.cleanupWebView()
1723
+ self.handleWebViewDidClose(id: targetId, url: currentUrl)
1724
+ navigationController.dismiss(animated: isAnimated, completion: nil)
1725
+ call.resolve()
1726
+ return
1727
+ }
1728
+
1729
+ guard let webViewController = self.webViewController,
1730
+ let navigationController = self.navigationWebViewController else {
1731
+ call.reject("WebView is not initialized")
1732
+ return
1733
+ }
1734
+
1735
+ let currentUrl = webViewController.url?.absoluteString ?? ""
1736
+ let isPresented = navigationController.presentingViewController != nil
1737
+
1738
+ if self.isHidden {
1739
+ webViewController.capableWebView?.removeFromSuperview()
1740
+ webViewController.cleanupWebView()
1741
+ if isPresented {
1742
+ navigationController.dismiss(animated: isAnimated) {
1743
+ self.handleWebViewDidClose(id: "", url: currentUrl)
1744
+ }
1745
+ } else {
1746
+ self.handleWebViewDidClose(id: "", url: currentUrl)
1747
+ }
1748
+ self.isHidden = false
1749
+ call.resolve()
1750
+ return
1751
+ }
1752
+
1753
+ webViewController.cleanupWebView()
1754
+ self.handleWebViewDidClose(id: "", url: currentUrl)
1755
+ navigationController.dismiss(animated: isAnimated, completion: nil)
1756
+ call.resolve()
1757
+ }
1758
+ }
1759
+
1760
+ private func showPrivacyScreen() {
1761
+ if privacyScreen == nil {
1762
+ let newPrivacyScreen = UIImageView()
1763
+ self.privacyScreen = newPrivacyScreen
1764
+ if let launchImage = UIImage(named: "LaunchImage") {
1765
+ newPrivacyScreen.image = launchImage
1766
+ newPrivacyScreen.frame = UIScreen.main.bounds
1767
+ newPrivacyScreen.contentMode = .scaleAspectFill
1768
+ newPrivacyScreen.isUserInteractionEnabled = false
1769
+ } else if let launchImage = UIImage(named: "Splash") {
1770
+ newPrivacyScreen.image = launchImage
1771
+ newPrivacyScreen.frame = UIScreen.main.bounds
1772
+ newPrivacyScreen.contentMode = .scaleAspectFill
1773
+ newPrivacyScreen.isUserInteractionEnabled = false
1774
+ }
1775
+ }
1776
+ if let screen = self.privacyScreen {
1777
+ self.navigationWebViewController?.view.addSubview(screen)
1778
+ }
1779
+ }
1780
+
1781
+ private func hidePrivacyScreen() {
1782
+ self.privacyScreen?.removeFromSuperview()
1783
+ }
1784
+
1785
+ @objc func appDidBecomeActive(_ notification: NSNotification) {
1786
+ self.hidePrivacyScreen()
1787
+ }
1788
+
1789
+ @objc func appWillResignActive(_ notification: NSNotification) {
1790
+ self.showPrivacyScreen()
1791
+ }
1792
+
1793
+ private func readCredentials(_ call: CAPPluginCall) -> WKWebViewCredentials? {
1794
+ var credentials: WKWebViewCredentials?
1795
+ let credentialsDict = call.getObject("credentials", [:]).mapValues { String(describing: $0 as Any) }
1796
+ if !credentialsDict.isEmpty, let username = credentialsDict["username"], let password = credentialsDict["password"] {
1797
+ credentials = WKWebViewCredentials(username: username, password: password)
1798
+ }
1799
+ return credentials
1800
+ }
1801
+
1802
+ private func isDarkColor(_ color: UIColor) -> Bool {
1803
+ let components = color.cgColor.components ?? []
1804
+ let red = components[0]
1805
+ let green = components[1]
1806
+ let blue = components[2]
1807
+ let brightness = (red * 299 + green * 587 + blue * 114) / 1000
1808
+ return brightness < 0.5
1809
+ }
1810
+
1811
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
1812
+ call.resolve(["version": self.pluginVersion])
1813
+ }
1814
+
1815
+ @objc func updateDimensions(_ call: CAPPluginCall) {
1816
+ let width = call.getFloat("width")
1817
+ let height = call.getFloat("height")
1818
+ let xPos = call.getFloat("x")
1819
+ let yPos = call.getFloat("y")
1820
+
1821
+ DispatchQueue.main.async {
1822
+ let targetId = call.getString("id") ?? self.activeWebViewId
1823
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1824
+ call.reject("WebView is not initialized")
1825
+ return
1826
+ }
1827
+
1828
+ webViewController.updateDimensions(
1829
+ width: width.map { CGFloat($0) },
1830
+ height: height.map { CGFloat($0) },
1831
+ xPos: xPos.map { CGFloat($0) },
1832
+ yPos: yPos.map { CGFloat($0) }
1833
+ )
1834
+
1835
+ call.resolve()
1836
+ }
1837
+ }
1838
+
1839
+ @objc func setEnabledSafeTopMargin(_ call: CAPPluginCall) {
1840
+ if call.options["enabled"] == nil {
1841
+ print("[InAppBrowser][Warning] setEnabledSafeTopMargin called without 'enabled'; defaulting to true")
1842
+ }
1843
+ let enabled = call.getBool("enabled", true)
1844
+ DispatchQueue.main.async {
1845
+ let targetId = call.getString("id") ?? self.activeWebViewId
1846
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1847
+ call.reject("WebView is not initialized")
1848
+ return
1849
+ }
1850
+ webViewController.updateSafeTopMargin(enabled)
1851
+ call.resolve()
1852
+ }
1853
+ }
1854
+
1855
+ @objc func setEnabledSafeBottomMargin(_ call: CAPPluginCall) {
1856
+ if call.options["enabled"] == nil {
1857
+ print("[InAppBrowser][Warning] setEnabledSafeBottomMargin called without 'enabled'; defaulting to false")
1858
+ }
1859
+ let enabled = call.getBool("enabled", false)
1860
+ DispatchQueue.main.async {
1861
+ let targetId = call.getString("id") ?? self.activeWebViewId
1862
+ guard let webViewController = self.resolveWebViewController(for: targetId) else {
1863
+ call.reject("WebView is not initialized")
1864
+ return
1865
+ }
1866
+ webViewController.updateSafeBottomMargin(enabled)
1867
+ call.resolve()
1868
+ }
1869
+ }
1870
+
1871
+ @objc func openSecureWindow(_ call: CAPPluginCall) {
1872
+ guard let urlString = call.getString("authEndpoint") else {
1873
+ call.reject("authEndpoint is required")
1874
+ return
1875
+ }
1876
+
1877
+ guard let url = URL(string: urlString) else {
1878
+ call.reject("Invalid URL")
1879
+ return
1880
+ }
1881
+
1882
+ guard let redirectUri = call.getString("redirectUri") else {
1883
+ call.reject("Redirect URI is required")
1884
+ return
1885
+ }
1886
+
1887
+ // Store the call for later resolution
1888
+ self.openSecureWindowCall = call
1889
+
1890
+ let prefersEphemeral = call.getBool("prefersEphemeralWebBrowserSession") ?? false
1891
+
1892
+ // Open the URL in a secure browser window
1893
+ DispatchQueue.main.async {
1894
+ let session = ASWebAuthenticationSession(url: url, callbackURLScheme: url.scheme) {
1895
+ callbackURL, error in
1896
+
1897
+ // Clean up the stored call
1898
+ self.openSecureWindowCall = nil
1899
+
1900
+ if let error = error {
1901
+ // Handle error (e.g., user cancelled)
1902
+ call.reject(error.localizedDescription)
1903
+ return
1904
+ }
1905
+
1906
+ guard let callbackURL = callbackURL else {
1907
+ call.reject("No callback URL received")
1908
+ return
1909
+ }
1910
+
1911
+ if !callbackURL.absoluteString.hasPrefix(redirectUri) {
1912
+ call.reject("Redirect URI does not match, expected " + redirectUri + " but got " + callbackURL.absoluteString)
1913
+ return
1914
+ }
1915
+
1916
+ // Resolve the call with the callback URL
1917
+ call.resolve(["redirectedUri": callbackURL.absoluteString])
1918
+ }
1919
+
1920
+ // Present the session
1921
+ session.prefersEphemeralWebBrowserSession = prefersEphemeral
1922
+ session.presentationContextProvider = self
1923
+ session.start()
1924
+ }
1925
+ }
1926
+ }
1927
+
1928
+ extension InAppBrowserPlugin: ASWebAuthenticationPresentationContextProviding {
1929
+ public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
1930
+ return self.bridge?.viewController?.view.window ?? ASPresentationAnchor()
1931
+ }
1932
+ }