@capgo/inappbrowser 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CapgoInappbrowser.podspec +2 -2
  2. package/LICENSE +373 -21
  3. package/Package.swift +28 -0
  4. package/README.md +600 -74
  5. package/android/build.gradle +17 -16
  6. package/android/src/main/AndroidManifest.xml +14 -2
  7. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +952 -204
  8. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +478 -81
  9. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +10 -4
  10. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +3021 -226
  11. package/android/src/main/res/drawable/ic_refresh.xml +9 -0
  12. package/android/src/main/res/drawable/ic_share.xml +10 -0
  13. package/android/src/main/res/layout/activity_browser.xml +10 -0
  14. package/android/src/main/res/layout/content_browser.xml +3 -2
  15. package/android/src/main/res/layout/tool_bar.xml +44 -7
  16. package/android/src/main/res/values/strings.xml +4 -0
  17. package/android/src/main/res/values/themes.xml +27 -0
  18. package/android/src/main/res/xml/file_paths.xml +14 -0
  19. package/dist/docs.json +1289 -149
  20. package/dist/esm/definitions.d.ts +614 -25
  21. package/dist/esm/definitions.js +17 -1
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/index.d.ts +2 -2
  24. package/dist/esm/index.js +4 -4
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/web.d.ts +16 -3
  27. package/dist/esm/web.js +43 -7
  28. package/dist/esm/web.js.map +1 -1
  29. package/dist/plugin.cjs.js +60 -8
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/plugin.js +60 -8
  32. package/dist/plugin.js.map +1 -1
  33. package/ios/{Plugin → Sources/InAppBrowserPlugin}/Enums.swift +5 -5
  34. package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +954 -0
  35. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +2003 -0
  36. package/package.json +32 -30
  37. package/ios/Plugin/Assets.xcassets/Back.imageset/Back.png +0 -0
  38. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@2x.png +0 -0
  39. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@3x.png +0 -0
  40. package/ios/Plugin/Assets.xcassets/Back.imageset/Contents.json +0 -26
  41. package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
  42. package/ios/Plugin/Assets.xcassets/Forward.imageset/Contents.json +0 -26
  43. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward.png +0 -0
  44. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@2x.png +0 -0
  45. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@3x.png +0 -0
  46. package/ios/Plugin/InAppBrowserPlugin.h +0 -10
  47. package/ios/Plugin/InAppBrowserPlugin.m +0 -17
  48. package/ios/Plugin/InAppBrowserPlugin.swift +0 -203
  49. package/ios/Plugin/Info.plist +0 -24
  50. package/ios/Plugin/WKWebViewController.swift +0 -784
@@ -0,0 +1,2003 @@
1
+ //
2
+ // WKWebViewController.swift
3
+ // Sample
4
+ //
5
+ // Created by Meniny on 2018-01-20.
6
+ // Copyright © 2018年 Meniny. All rights reserved.
7
+ //
8
+
9
+ import UIKit
10
+ import WebKit
11
+
12
+ private let estimatedProgressKeyPath = "estimatedProgress"
13
+ private let titleKeyPath = "title"
14
+ private let cookieKey = "Cookie"
15
+
16
+ private struct UrlsHandledByApp {
17
+ static var hosts = ["itunes.apple.com"]
18
+ static var schemes = ["tel", "mailto", "sms"]
19
+ static var blank = true
20
+ }
21
+
22
+ public struct WKWebViewCredentials {
23
+ var username: String
24
+ var password: String
25
+ }
26
+
27
+ @objc public protocol WKWebViewControllerDelegate {
28
+ @objc optional func webViewController(_ controller: WKWebViewController, canDismiss url: URL) -> Bool
29
+
30
+ @objc optional func webViewController(_ controller: WKWebViewController, didStart url: URL)
31
+ @objc optional func webViewController(_ controller: WKWebViewController, didFinish url: URL)
32
+ @objc optional func webViewController(_ controller: WKWebViewController, didFail url: URL, withError error: Error)
33
+ @objc optional func webViewController(_ controller: WKWebViewController, decidePolicy url: URL, navigationType: NavigationType) -> Bool
34
+ }
35
+
36
+ extension Dictionary {
37
+ func mapKeys<T>(_ transform: (Key) throws -> T) rethrows -> [T: Value] {
38
+ var dictionary = [T: Value]()
39
+ for (key, value) in self {
40
+ dictionary[try transform(key)] = value
41
+ }
42
+ return dictionary
43
+ }
44
+ }
45
+
46
+ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
47
+
48
+ public init() {
49
+ super.init(nibName: nil, bundle: nil)
50
+ }
51
+
52
+ public required init?(coder aDecoder: NSCoder) {
53
+ super.init(coder: aDecoder)
54
+ }
55
+
56
+ public init(source: WKWebSource?, credentials: WKWebViewCredentials? = nil) {
57
+ super.init(nibName: nil, bundle: nil)
58
+ self.source = source
59
+ self.credentials = credentials
60
+ self.initWebview()
61
+ }
62
+
63
+ public init(url: URL, credentials: WKWebViewCredentials? = nil) {
64
+ super.init(nibName: nil, bundle: nil)
65
+ self.source = .remote(url)
66
+ self.credentials = credentials
67
+ self.initWebview()
68
+ }
69
+
70
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool) {
71
+ super.init(nibName: nil, bundle: nil)
72
+ self.source = .remote(url)
73
+ self.credentials = credentials
74
+ self.setHeaders(headers: headers)
75
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
76
+ self.initWebview(isInspectable: isInspectable)
77
+ }
78
+
79
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool) {
80
+ super.init(nibName: nil, bundle: nil)
81
+ self.blankNavigationTab = blankNavigationTab
82
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
83
+ self.source = .remote(url)
84
+ self.credentials = credentials
85
+ self.setHeaders(headers: headers)
86
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
87
+ self.initWebview(isInspectable: isInspectable)
88
+ }
89
+
90
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, blockedHosts: [String]) {
91
+ super.init(nibName: nil, bundle: nil)
92
+ self.blankNavigationTab = blankNavigationTab
93
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
94
+ self.source = .remote(url)
95
+ self.credentials = credentials
96
+ self.setHeaders(headers: headers)
97
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
98
+ self.setBlockedHosts(blockedHosts: blockedHosts)
99
+ self.initWebview(isInspectable: isInspectable)
100
+ }
101
+
102
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, blockedHosts: [String], authorizedAppLinks: [String]) {
103
+ super.init(nibName: nil, bundle: nil)
104
+ self.blankNavigationTab = blankNavigationTab
105
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
106
+ self.source = .remote(url)
107
+ self.credentials = credentials
108
+ self.setHeaders(headers: headers)
109
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
110
+ self.setBlockedHosts(blockedHosts: blockedHosts)
111
+ self.setAuthorizedAppLinks(authorizedAppLinks: authorizedAppLinks)
112
+ self.initWebview(isInspectable: isInspectable)
113
+ }
114
+
115
+ open var hasDynamicTitle = false
116
+ open var source: WKWebSource?
117
+ /// use `source` instead
118
+ open internal(set) var url: URL?
119
+ open var tintColor: UIColor?
120
+ open var allowsFileURL = true
121
+ open var delegate: WKWebViewControllerDelegate?
122
+ open var bypassedSSLHosts: [String]?
123
+ open var cookies: [HTTPCookie]?
124
+ open var headers: [String: String]?
125
+ open var capBrowserPlugin: InAppBrowserPlugin?
126
+ var shareDisclaimer: [String: Any]?
127
+ var shareSubject: String?
128
+ var didpageInit = false
129
+ var viewHeightLandscape: CGFloat?
130
+ var viewHeightPortrait: CGFloat?
131
+ var currentViewHeight: CGFloat?
132
+ open var closeModal = false
133
+ open var closeModalTitle = ""
134
+ open var closeModalDescription = ""
135
+ open var closeModalOk = ""
136
+ open var closeModalCancel = ""
137
+ open var ignoreUntrustedSSLError = false
138
+ open var enableGooglePaySupport = false
139
+ var viewWasPresented = false
140
+ var preventDeeplink: Bool = false
141
+ var blankNavigationTab: Bool = false
142
+ var capacitorStatusBar: UIView?
143
+ var enabledSafeBottomMargin: Bool = false
144
+ var blockedHosts: [String] = []
145
+ var authorizedAppLinks: [String] = []
146
+ var activeNativeNavigationForWebview: Bool = true
147
+
148
+ // Dimension properties
149
+ var customWidth: CGFloat?
150
+ var customHeight: CGFloat?
151
+ var customX: CGFloat?
152
+ var customY: CGFloat?
153
+
154
+ internal var preShowSemaphore: DispatchSemaphore?
155
+ internal var preShowError: String?
156
+ private var isWebViewInitialized = false
157
+
158
+ func setHeaders(headers: [String: String]) {
159
+ self.headers = headers
160
+ let lowercasedHeaders = headers.mapKeys { $0.lowercased() }
161
+ let userAgent = lowercasedHeaders["user-agent"]
162
+ self.headers?.removeValue(forKey: "User-Agent")
163
+ self.headers?.removeValue(forKey: "user-agent")
164
+
165
+ if let userAgent = userAgent {
166
+ self.customUserAgent = userAgent
167
+ }
168
+ }
169
+
170
+ func setPreventDeeplink(preventDeeplink: Bool) {
171
+ self.preventDeeplink = preventDeeplink
172
+ }
173
+
174
+ func setBlockedHosts(blockedHosts: [String]) {
175
+ self.blockedHosts = blockedHosts
176
+ }
177
+
178
+ func setAuthorizedAppLinks(authorizedAppLinks: [String]) {
179
+ self.authorizedAppLinks = authorizedAppLinks
180
+ }
181
+
182
+ internal var customUserAgent: String? {
183
+ didSet {
184
+ guard let agent = userAgent else {
185
+ return
186
+ }
187
+ webView?.customUserAgent = agent
188
+ }
189
+ }
190
+
191
+ open var userAgent: String? {
192
+ didSet {
193
+ guard let originalUserAgent = originalUserAgent, let userAgent = userAgent else {
194
+ return
195
+ }
196
+ webView?.customUserAgent = [originalUserAgent, userAgent].joined(separator: " ")
197
+ }
198
+ }
199
+
200
+ open var pureUserAgent: String? {
201
+ didSet {
202
+ guard let agent = pureUserAgent else {
203
+ return
204
+ }
205
+ webView?.customUserAgent = agent
206
+ }
207
+ }
208
+
209
+ open var websiteTitleInNavigationBar = true
210
+ open var doneBarButtonItemPosition: NavigationBarPosition = .right
211
+ open var showArrowAsClose = false
212
+ open var preShowScript: String?
213
+ open var preShowScriptInjectionTime: String = "pageLoad" // "documentStart" or "pageLoad"
214
+ open var leftNavigationBarItemTypes: [BarButtonItemType] = []
215
+ open var rightNavigaionBarItemTypes: [BarButtonItemType] = []
216
+
217
+ // Status bar style to be applied
218
+ open var statusBarStyle: UIStatusBarStyle = .default
219
+
220
+ // Status bar background view
221
+ private var statusBarBackgroundView: UIView?
222
+
223
+ // Status bar height
224
+ private var statusBarHeight: CGFloat {
225
+ return UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
226
+ }
227
+
228
+ // Make status bar background with colored view underneath
229
+ open func setupStatusBarBackground(color: UIColor) {
230
+ // Remove any existing status bar view
231
+ statusBarBackgroundView?.removeFromSuperview()
232
+
233
+ // Create a new view to cover both status bar and navigation bar
234
+ statusBarBackgroundView = UIView()
235
+
236
+ if let navView = navigationController?.view {
237
+ // Add to back of view hierarchy
238
+ navView.insertSubview(statusBarBackgroundView!, at: 0)
239
+ statusBarBackgroundView?.translatesAutoresizingMaskIntoConstraints = false
240
+
241
+ // Calculate total height - status bar + navigation bar
242
+ let navBarHeight = navigationController?.navigationBar.frame.height ?? 44
243
+ let totalHeight = (navigationController?.view.safeAreaInsets.top ?? CGFloat(0)) + navBarHeight
244
+
245
+ // Position from top of screen to bottom of navigation bar
246
+ NSLayoutConstraint.activate([
247
+ statusBarBackgroundView!.topAnchor.constraint(equalTo: navView.topAnchor),
248
+ statusBarBackgroundView!.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
249
+ statusBarBackgroundView!.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
250
+ statusBarBackgroundView!.heightAnchor.constraint(equalToConstant: totalHeight)
251
+ ])
252
+
253
+ // Set background color
254
+ statusBarBackgroundView?.backgroundColor = color
255
+
256
+ // Make navigation bar transparent to show our view underneath
257
+ navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
258
+ navigationController?.navigationBar.shadowImage = UIImage()
259
+ navigationController?.navigationBar.isTranslucent = true
260
+ navigationController?.navigationBar.isTranslucent = true
261
+ }
262
+ }
263
+
264
+ // Override to use our custom status bar style
265
+ override open var preferredStatusBarStyle: UIStatusBarStyle {
266
+ return statusBarStyle
267
+ }
268
+
269
+ // Force status bar style update when needed
270
+ open func updateStatusBarStyle() {
271
+ setNeedsStatusBarAppearanceUpdate()
272
+ }
273
+
274
+ open var backBarButtonItemImage: UIImage?
275
+ open var forwardBarButtonItemImage: UIImage?
276
+ open var reloadBarButtonItemImage: UIImage?
277
+ open var stopBarButtonItemImage: UIImage?
278
+ open var activityBarButtonItemImage: UIImage?
279
+
280
+ open var buttonNearDoneIcon: UIImage?
281
+
282
+ fileprivate var webView: WKWebView?
283
+ fileprivate var progressView: UIProgressView?
284
+
285
+ fileprivate var previousNavigationBarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
286
+ fileprivate var previousToolbarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
287
+
288
+ fileprivate var originalUserAgent: String?
289
+
290
+ fileprivate lazy var backBarButtonItem: UIBarButtonItem = {
291
+ let navBackImage = UIImage(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
292
+ let barButtonItem = UIBarButtonItem(image: navBackImage, style: .plain, target: self, action: #selector(backDidClick(sender:)))
293
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
294
+ barButtonItem.tintColor = tintColor
295
+ }
296
+ return barButtonItem
297
+ }()
298
+
299
+ fileprivate lazy var forwardBarButtonItem: UIBarButtonItem = {
300
+ let forwardImage = UIImage(systemName: "chevron.forward")?.withRenderingMode(.alwaysTemplate)
301
+ let barButtonItem = UIBarButtonItem(image: forwardImage, style: .plain, target: self, action: #selector(forwardDidClick(sender:)))
302
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
303
+ barButtonItem.tintColor = tintColor
304
+ }
305
+ return barButtonItem
306
+ }()
307
+
308
+ fileprivate lazy var reloadBarButtonItem: UIBarButtonItem = {
309
+ if let image = reloadBarButtonItemImage {
310
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(reloadDidClick(sender:)))
311
+ } else {
312
+ return UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reloadDidClick(sender:)))
313
+ }
314
+ }()
315
+
316
+ fileprivate lazy var stopBarButtonItem: UIBarButtonItem = {
317
+ if let image = stopBarButtonItemImage {
318
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(stopDidClick(sender:)))
319
+ } else {
320
+ return UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(stopDidClick(sender:)))
321
+ }
322
+ }()
323
+
324
+ fileprivate lazy var activityBarButtonItem: UIBarButtonItem = {
325
+ // Check if custom image is provided
326
+ if let image = activityBarButtonItemImage {
327
+ let button = UIBarButtonItem(image: image.withRenderingMode(.alwaysTemplate),
328
+ style: .plain,
329
+ target: self,
330
+ action: #selector(activityDidClick(sender:)))
331
+
332
+ // Apply tint from navigation bar or from tintColor property
333
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
334
+ button.tintColor = tintColor
335
+ }
336
+
337
+ print("[DEBUG] Created activity button with custom image")
338
+ return button
339
+ } else {
340
+ // Use system share icon
341
+ let button = UIBarButtonItem(barButtonSystemItem: .action,
342
+ target: self,
343
+ action: #selector(activityDidClick(sender:)))
344
+
345
+ // Apply tint from navigation bar or from tintColor property
346
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
347
+ button.tintColor = tintColor
348
+ }
349
+
350
+ print("[DEBUG] Created activity button with system action icon")
351
+ return button
352
+ }
353
+ }()
354
+
355
+ fileprivate lazy var doneBarButtonItem: UIBarButtonItem = {
356
+ if showArrowAsClose {
357
+ // Show chevron icon when showArrowAsClose is true (originally was arrow.left)
358
+ let chevronImage = UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate)
359
+ let barButtonItem = UIBarButtonItem(image: chevronImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
360
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
361
+ barButtonItem.tintColor = tintColor
362
+ }
363
+ return barButtonItem
364
+ } else {
365
+ // Show X icon by default
366
+ let xImage = UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate)
367
+ let barButtonItem = UIBarButtonItem(image: xImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
368
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
369
+ barButtonItem.tintColor = tintColor
370
+ }
371
+ return barButtonItem
372
+ }
373
+ }()
374
+
375
+ fileprivate lazy var flexibleSpaceBarButtonItem: UIBarButtonItem = {
376
+ return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
377
+ }()
378
+
379
+ fileprivate var credentials: WKWebViewCredentials?
380
+
381
+ var textZoom: Int?
382
+
383
+ var capableWebView: WKWebView? {
384
+ return webView
385
+ }
386
+
387
+ deinit {
388
+ webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
389
+ if websiteTitleInNavigationBar {
390
+ webView?.removeObserver(self, forKeyPath: titleKeyPath)
391
+ }
392
+ webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
393
+ }
394
+
395
+ override open func viewDidDisappear(_ animated: Bool) {
396
+ super.viewDidDisappear(animated)
397
+
398
+ if self.isBeingDismissed || self.isMovingFromParent {
399
+ self.cleanupWebView()
400
+ }
401
+
402
+ if let capacitorStatusBar = capacitorStatusBar {
403
+ self.capBrowserPlugin?.bridge?.webView?.superview?.addSubview(capacitorStatusBar)
404
+ self.capBrowserPlugin?.bridge?.webView?.frame.origin.y = capacitorStatusBar.frame.height
405
+ }
406
+ }
407
+
408
+ override open func viewDidLoad() {
409
+ super.viewDidLoad()
410
+ if self.webView == nil {
411
+ self.initWebview()
412
+ }
413
+
414
+ // Apply navigation gestures setting
415
+ updateNavigationGestures()
416
+
417
+ // Force all buttons to use tint color
418
+ updateButtonTintColors()
419
+
420
+ // Extra call to ensure buttonNearDone is visible
421
+ if buttonNearDoneIcon != nil {
422
+ // Delay slightly to ensure navigation items are set up
423
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
424
+ self?.updateButtonTintColors()
425
+
426
+ // Force update UI if needed
427
+ self?.navigationController?.navigationBar.setNeedsLayout()
428
+ }
429
+ }
430
+ }
431
+
432
+ func updateButtonTintColors() {
433
+ // Ensure all button items use the navigation bar's tint color
434
+ if let tintColor = navigationController?.navigationBar.tintColor {
435
+ backBarButtonItem.tintColor = tintColor
436
+ forwardBarButtonItem.tintColor = tintColor
437
+ reloadBarButtonItem.tintColor = tintColor
438
+ stopBarButtonItem.tintColor = tintColor
439
+ activityBarButtonItem.tintColor = tintColor
440
+ doneBarButtonItem.tintColor = tintColor
441
+
442
+ // Update navigation items
443
+ if let leftItems = navigationItem.leftBarButtonItems {
444
+ for item in leftItems {
445
+ item.tintColor = tintColor
446
+ }
447
+ }
448
+
449
+ if let rightItems = navigationItem.rightBarButtonItems {
450
+ for item in rightItems {
451
+ item.tintColor = tintColor
452
+ }
453
+ }
454
+
455
+ // Create buttonNearDone button with the correct tint color if it doesn't already exist
456
+ if buttonNearDoneIcon != nil &&
457
+ navigationItem.rightBarButtonItems?.count == 1 &&
458
+ navigationItem.rightBarButtonItems?.first == doneBarButtonItem {
459
+
460
+ // Create a properly tinted button
461
+ let buttonItem = UIBarButtonItem(image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
462
+ style: .plain,
463
+ target: self,
464
+ action: #selector(buttonNearDoneDidClick))
465
+ buttonItem.tintColor = tintColor
466
+
467
+ // Add it to right items
468
+ navigationItem.rightBarButtonItems?.append(buttonItem)
469
+ }
470
+ }
471
+ }
472
+
473
+ override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
474
+ super.traitCollectionDidChange(previousTraitCollection)
475
+
476
+ // Update colors when appearance changes
477
+ if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
478
+ // Update tint colors
479
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
480
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
481
+
482
+ if let navBar = navigationController?.navigationBar {
483
+ if navBar.backgroundColor == UIColor.black || navBar.backgroundColor == UIColor.white {
484
+ navBar.backgroundColor = isDarkMode ? UIColor.black : UIColor.white
485
+ navBar.tintColor = textColor
486
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
487
+
488
+ // Update all buttons
489
+ updateButtonTintColors()
490
+ }
491
+ }
492
+ }
493
+ }
494
+
495
+ open func setCredentials(credentials: WKWebViewCredentials?) {
496
+ self.credentials = credentials
497
+ }
498
+
499
+ // Method to send a message from Swift to JavaScript
500
+ open func postMessageToJS(message: [String: Any]) {
501
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: message, options: []),
502
+ let jsonString = String(data: jsonData, encoding: .utf8) else {
503
+ print("[InAppBrowser] Failed to serialize message to JSON")
504
+ return
505
+ }
506
+
507
+ // Safely build the script to avoid any potential issues
508
+ let script = "window.dispatchEvent(new CustomEvent('messageFromNative', { detail: \(jsonString) }));"
509
+
510
+ DispatchQueue.main.async {
511
+ self.webView?.evaluateJavaScript(script) { _, error in
512
+ if let error = error {
513
+ print("[InAppBrowser] JavaScript evaluation error in postMessageToJS: \(error)")
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ // Method to receive messages from JavaScript
520
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
521
+ if message.name == "messageHandler" {
522
+ if let messageBody = message.body as? [String: Any] {
523
+ print("Received message from JavaScript:", messageBody)
524
+ self.capBrowserPlugin?.notifyListeners("messageFromWebview", data: messageBody)
525
+ } else {
526
+ print("Received non-dictionary message from JavaScript:", message.body)
527
+ self.capBrowserPlugin?.notifyListeners("messageFromWebview", data: ["rawMessage": String(describing: message.body)])
528
+ }
529
+ } else if message.name == "preShowScriptSuccess" {
530
+ guard let semaphore = preShowSemaphore else {
531
+ print("[InAppBrowser - preShowScriptSuccess]: Semaphore not found")
532
+ return
533
+ }
534
+
535
+ semaphore.signal()
536
+ } else if message.name == "preShowScriptError" {
537
+ guard let semaphore = preShowSemaphore else {
538
+ print("[InAppBrowser - preShowScriptError]: Semaphore not found")
539
+ return
540
+ }
541
+ print("[InAppBrowser - preShowScriptError]: Error!!!!")
542
+ semaphore.signal()
543
+ } else if message.name == "close" {
544
+ closeView()
545
+ } else if message.name == "magicPrint" {
546
+ if let webView = self.webView {
547
+ let printController = UIPrintInteractionController.shared
548
+
549
+ let printInfo = UIPrintInfo(dictionary: nil)
550
+ printInfo.outputType = .general
551
+ printInfo.jobName = "Print Job"
552
+
553
+ printController.printInfo = printInfo
554
+ printController.printFormatter = webView.viewPrintFormatter()
555
+
556
+ printController.present(animated: true, completionHandler: nil)
557
+ }
558
+ }
559
+ }
560
+
561
+ func injectJavaScriptInterface() {
562
+ let script = """
563
+ if (!window.mobileApp) {
564
+ window.mobileApp = {
565
+ postMessage: function(message) {
566
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.messageHandler) {
567
+ window.webkit.messageHandlers.messageHandler.postMessage(message);
568
+ }
569
+ },
570
+ close: function() {
571
+ window.webkit.messageHandlers.close.postMessage(null);
572
+ }
573
+ };
574
+ }
575
+ """
576
+ DispatchQueue.main.async {
577
+ self.webView?.evaluateJavaScript(script) { result, error in
578
+ if let error = error {
579
+ print("JavaScript evaluation error: \(error)")
580
+ } else if let result = result {
581
+ print("JavaScript result: \(result)")
582
+ } else {
583
+ print("JavaScript executed with no result")
584
+ }
585
+ }
586
+ }
587
+ }
588
+
589
+ open func initWebview(isInspectable: Bool = true) {
590
+ if self.isWebViewInitialized {
591
+ return
592
+ }
593
+ self.isWebViewInitialized = true
594
+ self.view.backgroundColor = UIColor.white
595
+
596
+ self.extendedLayoutIncludesOpaqueBars = true
597
+ self.edgesForExtendedLayout = [.bottom]
598
+
599
+ let webConfiguration = WKWebViewConfiguration()
600
+ let userContentController = WKUserContentController()
601
+
602
+ let weakHandler = WeakScriptMessageHandler(self)
603
+ userContentController.add(weakHandler, name: "messageHandler")
604
+ userContentController.add(weakHandler, name: "preShowScriptError")
605
+ userContentController.add(weakHandler, name: "preShowScriptSuccess")
606
+ userContentController.add(weakHandler, name: "close")
607
+ userContentController.add(weakHandler, name: "magicPrint")
608
+
609
+ // Inject JavaScript to override window.print
610
+ let script = WKUserScript(
611
+ source: """
612
+ window.print = function() {
613
+ window.webkit.messageHandlers.magicPrint.postMessage('magicPrint');
614
+ };
615
+ """,
616
+ injectionTime: .atDocumentStart,
617
+ forMainFrameOnly: false
618
+ )
619
+ userContentController.addUserScript(script)
620
+
621
+ webConfiguration.allowsInlineMediaPlayback = true
622
+ webConfiguration.userContentController = userContentController
623
+ webConfiguration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
624
+ webConfiguration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
625
+
626
+ // Enable background task processing
627
+ webConfiguration.processPool = WKProcessPool()
628
+
629
+ // Enable JavaScript to run automatically (needed for preShowScript and Firebase polyfill)
630
+ webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
631
+
632
+ // Enhanced configuration for Google Pay support (only when enabled)
633
+ if enableGooglePaySupport {
634
+ print("[InAppBrowser] Enabling Google Pay support features for iOS")
635
+
636
+ // Allow arbitrary loads in web views for Payment Request API
637
+ webConfiguration.setValue(true, forKey: "allowsArbitraryLoads")
638
+
639
+ // Inject Google Pay support script
640
+ let googlePayScript = WKUserScript(
641
+ source: """
642
+ console.log('[InAppBrowser] Injecting Google Pay support for iOS');
643
+
644
+ // Enhanced window.open for Google Pay
645
+ (function() {
646
+ const originalWindowOpen = window.open;
647
+ window.open = function(url, target, features) {
648
+ console.log('[InAppBrowser iOS] Enhanced window.open called:', url, target, features);
649
+
650
+ // For Google Pay URLs, handle popup properly
651
+ if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
652
+ console.log('[InAppBrowser iOS] Google Pay popup detected');
653
+ return originalWindowOpen.call(window, url, target || '_blank', features);
654
+ }
655
+
656
+ return originalWindowOpen.call(window, url, target, features);
657
+ };
658
+
659
+ // Add Cross-Origin-Opener-Policy meta tag if not present
660
+ if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
661
+ const meta = document.createElement('meta');
662
+ meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
663
+ meta.setAttribute('content', 'same-origin-allow-popups');
664
+ if (document.head) {
665
+ document.head.appendChild(meta);
666
+ console.log('[InAppBrowser iOS] Added Cross-Origin-Opener-Policy meta tag');
667
+ }
668
+ }
669
+
670
+ console.log('[InAppBrowser iOS] Google Pay support enhancements complete');
671
+ })();
672
+ """,
673
+ injectionTime: .atDocumentStart,
674
+ forMainFrameOnly: false
675
+ )
676
+ userContentController.addUserScript(googlePayScript)
677
+ }
678
+
679
+ let webView = WKWebView(frame: .zero, configuration: webConfiguration)
680
+
681
+ // if webView.responds(to: Selector(("setInspectable:"))) {
682
+ // // Fix: https://stackoverflow.com/questions/76216183/how-to-debug-wkwebview-in-ios-16-4-1-using-xcode-14-2/76603043#76603043
683
+ // webView.perform(Selector(("setInspectable:")), with: isInspectable)
684
+ // }
685
+
686
+ if #available(iOS 16.4, *) {
687
+ webView.isInspectable = true
688
+ } else {
689
+ // Fallback on earlier versions
690
+ }
691
+
692
+ // First add the webView to view hierarchy
693
+ self.view.addSubview(webView)
694
+
695
+ // Then set up constraints
696
+ webView.translatesAutoresizingMaskIntoConstraints = false
697
+ var bottomPadding = self.view.bottomAnchor
698
+
699
+ if self.enabledSafeBottomMargin {
700
+ bottomPadding = self.view.safeAreaLayoutGuide.bottomAnchor
701
+ }
702
+
703
+ NSLayoutConstraint.activate([
704
+ webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
705
+ webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
706
+ webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
707
+ webView.bottomAnchor.constraint(equalTo: bottomPadding)
708
+ ])
709
+
710
+ webView.uiDelegate = self
711
+ webView.navigationDelegate = self
712
+
713
+ webView.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
714
+ webView.isMultipleTouchEnabled = true
715
+
716
+ webView.addObserver(self, forKeyPath: estimatedProgressKeyPath, options: .new, context: nil)
717
+ if websiteTitleInNavigationBar {
718
+ webView.addObserver(self, forKeyPath: titleKeyPath, options: .new, context: nil)
719
+ }
720
+ webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
721
+
722
+ if !self.blankNavigationTab {
723
+ self.view.addSubview(webView)
724
+ // Then set up constraints
725
+ webView.translatesAutoresizingMaskIntoConstraints = false
726
+ }
727
+ self.webView = webView
728
+
729
+ self.webView?.customUserAgent = self.customUserAgent ?? self.userAgent ?? self.originalUserAgent
730
+
731
+ self.navigationItem.title = self.navigationItem.title ?? self.source?.absoluteString
732
+
733
+ if let navigation = self.navigationController {
734
+ self.previousNavigationBarState = (navigation.navigationBar.tintColor, navigation.navigationBar.isHidden)
735
+ self.previousToolbarState = (navigation.toolbar.tintColor, navigation.toolbar.isHidden)
736
+ }
737
+
738
+ if let sourceValue = self.source {
739
+ self.load(source: sourceValue)
740
+ } else {
741
+ print("[\(type(of: self))][Error] Invalid url")
742
+ }
743
+ }
744
+
745
+ open func setupViewElements() {
746
+ self.setUpProgressView()
747
+ self.setUpConstraints()
748
+ self.setUpNavigationBarAppearance()
749
+ self.addBarButtonItems()
750
+ self.updateBarButtonItems()
751
+ }
752
+
753
+ @objc func restateViewHeight() {
754
+ var bottomPadding = CGFloat(0.0)
755
+ var topPadding = CGFloat(0.0)
756
+ let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
757
+ bottomPadding = window?.safeAreaInsets.bottom ?? 0.0
758
+ topPadding = window?.safeAreaInsets.top ?? 0.0
759
+ if UIDevice.current.orientation.isPortrait {
760
+ // Don't force toolbar visibility
761
+ if self.viewHeightPortrait == nil {
762
+ self.viewHeightPortrait = self.view.safeAreaLayoutGuide.layoutFrame.size.height
763
+ self.viewHeightPortrait! += bottomPadding
764
+ if self.navigationController?.navigationBar.isHidden == true {
765
+ self.viewHeightPortrait! += topPadding
766
+ }
767
+ }
768
+ self.currentViewHeight = self.viewHeightPortrait
769
+ } else if UIDevice.current.orientation.isLandscape {
770
+ // Don't force toolbar visibility
771
+ if self.viewHeightLandscape == nil {
772
+ self.viewHeightLandscape = self.view.safeAreaLayoutGuide.layoutFrame.size.height
773
+ self.viewHeightLandscape! += bottomPadding
774
+ if self.navigationController?.navigationBar.isHidden == true {
775
+ self.viewHeightLandscape! += topPadding
776
+ }
777
+ }
778
+ self.currentViewHeight = self.viewHeightLandscape
779
+ }
780
+ }
781
+
782
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
783
+ // self.view.frame.size.height = self.currentViewHeight!
784
+ }
785
+
786
+ override open func viewWillLayoutSubviews() {
787
+ restateViewHeight()
788
+ // Don't override frame height when enabledSafeBottomMargin is true, as it would override our constraints
789
+ if self.currentViewHeight != nil && !self.enabledSafeBottomMargin {
790
+ self.view.frame.size.height = self.currentViewHeight!
791
+ }
792
+ }
793
+
794
+ override open func viewWillAppear(_ animated: Bool) {
795
+ super.viewWillAppear(animated)
796
+ if !self.viewWasPresented {
797
+ self.setupViewElements()
798
+ setUpState()
799
+ self.viewWasPresented = true
800
+
801
+ // Apply custom dimensions if specified
802
+ applyCustomDimensions()
803
+ }
804
+
805
+ // Force update button appearances
806
+ updateButtonTintColors()
807
+
808
+ // Ensure status bar appearance is correct when view appears
809
+ // Make sure we have the latest tint color
810
+ if let tintColor = self.tintColor {
811
+ // Update the status bar background if needed
812
+ if let navController = navigationController, let backgroundColor = navController.navigationBar.backgroundColor ?? statusBarBackgroundView?.backgroundColor {
813
+ setupStatusBarBackground(color: backgroundColor)
814
+ } else {
815
+ setupStatusBarBackground(color: UIColor.white)
816
+ }
817
+ }
818
+
819
+ // Update status bar style
820
+ updateStatusBarStyle()
821
+
822
+ // Special handling for blank toolbar mode
823
+ if blankNavigationTab && statusBarBackgroundView != nil {
824
+ if let color = statusBarBackgroundView?.backgroundColor {
825
+ // Set view color to match status bar
826
+ view.backgroundColor = color
827
+ }
828
+ }
829
+ }
830
+
831
+ override open func viewDidAppear(_ animated: Bool) {
832
+ super.viewDidAppear(animated)
833
+
834
+ // Force add buttonNearDone if it's not visible yet
835
+ if buttonNearDoneIcon != nil {
836
+ // Check if button already exists in the navigation bar
837
+ let buttonExists = navigationItem.rightBarButtonItems?.contains { item in
838
+ return item.action == #selector(buttonNearDoneDidClick)
839
+ } ?? false
840
+
841
+ if !buttonExists {
842
+ // Create and add the button directly
843
+ let buttonItem = UIBarButtonItem(
844
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
845
+ style: .plain,
846
+ target: self,
847
+ action: #selector(buttonNearDoneDidClick)
848
+ )
849
+
850
+ // Apply tint color
851
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
852
+ buttonItem.tintColor = tintColor
853
+ }
854
+
855
+ // Add to right items
856
+ if navigationItem.rightBarButtonItems == nil {
857
+ navigationItem.rightBarButtonItems = [buttonItem]
858
+ } else {
859
+ var items = navigationItem.rightBarButtonItems ?? []
860
+ items.append(buttonItem)
861
+ navigationItem.rightBarButtonItems = items
862
+ }
863
+
864
+ print("[DEBUG] Force added buttonNearDone in viewDidAppear")
865
+ }
866
+ }
867
+ }
868
+
869
+ override open func viewWillDisappear(_ animated: Bool) {
870
+ super.viewWillDisappear(animated)
871
+ rollbackState()
872
+ }
873
+
874
+ override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
875
+ switch keyPath {
876
+ case estimatedProgressKeyPath?:
877
+ DispatchQueue.main.async {
878
+ guard let estimatedProgress = self.webView?.estimatedProgress else {
879
+ return
880
+ }
881
+ self.progressView?.alpha = 1
882
+ self.progressView?.setProgress(Float(estimatedProgress), animated: true)
883
+
884
+ if estimatedProgress >= 1.0 {
885
+ UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: {
886
+ self.progressView?.alpha = 0
887
+ }, completion: {
888
+ _ in
889
+ self.progressView?.setProgress(0, animated: false)
890
+ })
891
+ }
892
+ }
893
+ case titleKeyPath?:
894
+ if self.hasDynamicTitle {
895
+ self.navigationItem.title = webView?.url?.host
896
+ }
897
+ case "URL":
898
+
899
+ self.capBrowserPlugin?.notifyListeners("urlChangeEvent", data: ["url": webView?.url?.absoluteString ?? ""])
900
+ self.injectJavaScriptInterface()
901
+ default:
902
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
903
+ }
904
+ }
905
+ }
906
+
907
+ // MARK: - Public Methods
908
+ public extension WKWebViewController {
909
+
910
+ func load(source sourceValue: WKWebSource) {
911
+ switch sourceValue {
912
+ case .remote(let url):
913
+ self.load(remote: url)
914
+ case .file(let url, access: let access):
915
+ self.load(file: url, access: access)
916
+ case .string(let str, base: let base):
917
+ self.load(string: str, base: base)
918
+ }
919
+ }
920
+
921
+ func load(remote: URL) {
922
+ DispatchQueue.main.async {
923
+ self.webView?.load(self.createRequest(url: remote))
924
+ }
925
+ }
926
+
927
+ func load(file: URL, access: URL) {
928
+ webView?.loadFileURL(file, allowingReadAccessTo: access)
929
+ }
930
+
931
+ func load(string: String, base: URL? = nil) {
932
+ webView?.loadHTMLString(string, baseURL: base)
933
+ }
934
+
935
+ func goBackToFirstPage() {
936
+ if let firstPageItem = webView?.backForwardList.backList.first {
937
+ webView?.go(to: firstPageItem)
938
+ }
939
+ }
940
+ func reload() {
941
+ webView?.reload()
942
+ }
943
+
944
+ func executeScript(script: String, completion: ((Any?, Error?) -> Void)? = nil) {
945
+ DispatchQueue.main.async { [weak self] in
946
+ self?.webView?.evaluateJavaScript(script, completionHandler: completion)
947
+ }
948
+ }
949
+
950
+ func applyTextZoom(_ zoomPercent: Int) {
951
+ let script = """
952
+ document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust = '\(zoomPercent)%';
953
+ document.getElementsByTagName('body')[0].style.textSizeAdjust = '\(zoomPercent)%';
954
+ """
955
+
956
+ executeScript(script: script)
957
+ }
958
+
959
+ func injectPreShowScriptAtDocumentStart() {
960
+ guard let preShowScript = self.preShowScript,
961
+ !preShowScript.isEmpty,
962
+ self.preShowScriptInjectionTime == "documentStart",
963
+ let webView = self.webView else {
964
+ return
965
+ }
966
+
967
+ let userScript = WKUserScript(
968
+ source: preShowScript,
969
+ injectionTime: .atDocumentStart,
970
+ forMainFrameOnly: false
971
+ )
972
+ webView.configuration.userContentController.addUserScript(userScript)
973
+ print("[InAppBrowser] Injected preShowScript at document start")
974
+
975
+ // Reload the webview so the script executes at document start
976
+ if let currentURL = webView.url {
977
+ load(remote: currentURL)
978
+ } else if let source = self.source {
979
+ load(source: source)
980
+ }
981
+ }
982
+
983
+ func updateNavigationGestures() {
984
+ self.webView?.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
985
+ }
986
+
987
+ open func cleanupWebView() {
988
+ guard let webView = self.webView else { return }
989
+ webView.stopLoading()
990
+ // Break delegate callbacks early
991
+ webView.navigationDelegate = nil
992
+ webView.uiDelegate = nil
993
+ webView.loadHTMLString("", baseURL: nil)
994
+
995
+ webView.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
996
+ if websiteTitleInNavigationBar {
997
+ webView.removeObserver(self, forKeyPath: titleKeyPath)
998
+ }
999
+ webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
1000
+
1001
+ webView.configuration.userContentController.removeAllUserScripts()
1002
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "messageHandler")
1003
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "close")
1004
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptSuccess")
1005
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptError")
1006
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "magicPrint")
1007
+
1008
+ webView.removeFromSuperview()
1009
+ // Also clean progress bar view if present
1010
+ progressView?.removeFromSuperview()
1011
+ progressView = nil
1012
+ self.webView = nil
1013
+ }
1014
+ }
1015
+
1016
+ // MARK: - Fileprivate Methods
1017
+ fileprivate extension WKWebViewController {
1018
+ var availableCookies: [HTTPCookie]? {
1019
+ return cookies?.filter {
1020
+ cookie in
1021
+ var result = true
1022
+ let url = self.source?.remoteURL
1023
+ if let host = url?.host, !cookie.domain.hasSuffix(host) {
1024
+ result = false
1025
+ }
1026
+ if cookie.isSecure && url?.scheme != "https" {
1027
+ result = false
1028
+ }
1029
+
1030
+ return result
1031
+ }
1032
+ }
1033
+ func createRequest(url: URL) -> URLRequest {
1034
+ var request = URLRequest(url: url)
1035
+
1036
+ // Set up headers
1037
+ if let headers = headers {
1038
+ for (field, value) in headers {
1039
+ request.addValue(value, forHTTPHeaderField: field)
1040
+ }
1041
+ }
1042
+
1043
+ // Set up Cookies
1044
+ if let cookies = availableCookies, let value = HTTPCookie.requestHeaderFields(with: cookies)[cookieKey] {
1045
+ request.addValue(value, forHTTPHeaderField: cookieKey)
1046
+ }
1047
+
1048
+ return request
1049
+ }
1050
+
1051
+ func setUpProgressView() {
1052
+ let progressView = UIProgressView(progressViewStyle: .default)
1053
+ progressView.trackTintColor = UIColor(white: 1, alpha: 0)
1054
+ self.progressView = progressView
1055
+ // updateProgressViewFrame()
1056
+ }
1057
+
1058
+ func setUpConstraints() {
1059
+ if !(self.navigationController?.navigationBar.isHidden)! {
1060
+ self.progressView?.frame.origin.y = CGFloat((self.navigationController?.navigationBar.frame.height)!)
1061
+ self.navigationController?.navigationBar.addSubview(self.progressView!)
1062
+ }
1063
+ }
1064
+
1065
+ func addBarButtonItems() {
1066
+ func barButtonItem(_ type: BarButtonItemType) -> UIBarButtonItem? {
1067
+ switch type {
1068
+ case .back:
1069
+ return backBarButtonItem
1070
+ case .forward:
1071
+ return forwardBarButtonItem
1072
+ case .reload:
1073
+ return reloadBarButtonItem
1074
+ case .stop:
1075
+ return stopBarButtonItem
1076
+ case .activity:
1077
+ return activityBarButtonItem
1078
+ case .done:
1079
+ return doneBarButtonItem
1080
+ case .flexibleSpace:
1081
+ return flexibleSpaceBarButtonItem
1082
+ case .custom(let icon, let title, let action):
1083
+ let item: BlockBarButtonItem
1084
+ if let icon = icon {
1085
+ item = BlockBarButtonItem(image: icon, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1086
+ } else {
1087
+ item = BlockBarButtonItem(title: title, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1088
+ }
1089
+ item.block = action
1090
+ return item
1091
+ }
1092
+ }
1093
+
1094
+ switch doneBarButtonItemPosition {
1095
+ case .left:
1096
+ if !leftNavigationBarItemTypes.contains(where: { type in
1097
+ switch type {
1098
+ case .done:
1099
+ return true
1100
+ default:
1101
+ return false
1102
+ }
1103
+ }) {
1104
+ leftNavigationBarItemTypes.insert(.done, at: 0)
1105
+ }
1106
+ case .right:
1107
+ if !rightNavigaionBarItemTypes.contains(where: { type in
1108
+ switch type {
1109
+ case .done:
1110
+ return true
1111
+ default:
1112
+ return false
1113
+ }
1114
+ }) {
1115
+ rightNavigaionBarItemTypes.insert(.done, at: 0)
1116
+ }
1117
+ case .none:
1118
+ break
1119
+ }
1120
+
1121
+ navigationItem.leftBarButtonItems = leftNavigationBarItemTypes.map {
1122
+ barButtonItemType in
1123
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1124
+ return barButtonItem
1125
+ }
1126
+ return UIBarButtonItem()
1127
+ }
1128
+
1129
+ var rightBarButtons = rightNavigaionBarItemTypes.map {
1130
+ barButtonItemType in
1131
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1132
+ return barButtonItem
1133
+ }
1134
+ return UIBarButtonItem()
1135
+ }
1136
+
1137
+ // If we have buttonNearDoneIcon and the first (or only) right button is the done button
1138
+ if buttonNearDoneIcon != nil &&
1139
+ ((rightBarButtons.count == 1 && rightBarButtons[0] == doneBarButtonItem) ||
1140
+ (rightBarButtons.isEmpty && doneBarButtonItemPosition == .right) ||
1141
+ rightBarButtons.contains(doneBarButtonItem)) {
1142
+
1143
+ // Check if button already exists to avoid duplicates
1144
+ let buttonExists = rightBarButtons.contains { item in
1145
+ let selector = #selector(buttonNearDoneDidClick)
1146
+ return item.action == selector
1147
+ }
1148
+
1149
+ if !buttonExists {
1150
+ // Create button with proper tint and template rendering mode
1151
+ let buttonItem = UIBarButtonItem(
1152
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
1153
+ style: .plain,
1154
+ target: self,
1155
+ action: #selector(buttonNearDoneDidClick)
1156
+ )
1157
+
1158
+ // Apply tint from navigation bar or from tintColor property
1159
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
1160
+ buttonItem.tintColor = tintColor
1161
+ }
1162
+
1163
+ // Make sure the done button is there before adding this one
1164
+ if rightBarButtons.isEmpty && doneBarButtonItemPosition == .right {
1165
+ rightBarButtons.append(doneBarButtonItem)
1166
+ }
1167
+
1168
+ // Add the button
1169
+ rightBarButtons.append(buttonItem)
1170
+
1171
+ print("[DEBUG] Added buttonNearDone to right bar buttons, icon: \(String(describing: buttonNearDoneIcon))")
1172
+ } else {
1173
+ print("[DEBUG] buttonNearDone already exists in right bar buttons")
1174
+ }
1175
+ }
1176
+
1177
+ navigationItem.rightBarButtonItems = rightBarButtons
1178
+
1179
+ // After all buttons are set up, apply tint color
1180
+ updateButtonTintColors()
1181
+ }
1182
+
1183
+ func updateBarButtonItems() {
1184
+ // Update navigation buttons (completely separate from close button)
1185
+ backBarButtonItem.isEnabled = webView?.canGoBack ?? false
1186
+ forwardBarButtonItem.isEnabled = webView?.canGoForward ?? false
1187
+
1188
+ let updateReloadBarButtonItem: (UIBarButtonItem, Bool) -> UIBarButtonItem = {
1189
+ [weak self] barButtonItem, isLoading in
1190
+ guard let self = self else { return barButtonItem }
1191
+ switch barButtonItem {
1192
+ case self.reloadBarButtonItem, self.stopBarButtonItem:
1193
+ return isLoading ? self.stopBarButtonItem : self.reloadBarButtonItem
1194
+ default:
1195
+ return barButtonItem
1196
+ }
1197
+ }
1198
+
1199
+ let isLoading = webView?.isLoading ?? false
1200
+ navigationItem.leftBarButtonItems = navigationItem.leftBarButtonItems?.map {
1201
+ barButtonItem -> UIBarButtonItem in
1202
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1203
+ }
1204
+
1205
+ navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems?.map {
1206
+ barButtonItem -> UIBarButtonItem in
1207
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1208
+ }
1209
+ }
1210
+
1211
+ func setUpState() {
1212
+ navigationController?.setNavigationBarHidden(false, animated: true)
1213
+
1214
+ // Always hide toolbar since we never want it
1215
+ navigationController?.setToolbarHidden(true, animated: true)
1216
+
1217
+ // Set tint colors but don't override specific colors
1218
+ if tintColor == nil {
1219
+ // Use system appearance if no specific tint color is set
1220
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
1221
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
1222
+
1223
+ navigationController?.navigationBar.tintColor = textColor
1224
+ progressView?.progressTintColor = textColor
1225
+ } else {
1226
+ progressView?.progressTintColor = tintColor
1227
+ navigationController?.navigationBar.tintColor = tintColor
1228
+ }
1229
+ }
1230
+
1231
+ func rollbackState() {
1232
+ progressView?.progress = 0
1233
+
1234
+ navigationController?.navigationBar.tintColor = previousNavigationBarState.tintColor
1235
+
1236
+ navigationController?.setNavigationBarHidden(previousNavigationBarState.hidden, animated: true)
1237
+ }
1238
+
1239
+ func checkRequestCookies(_ request: URLRequest, cookies: [HTTPCookie]) -> Bool {
1240
+ if cookies.count <= 0 {
1241
+ return true
1242
+ }
1243
+ guard let headerFields = request.allHTTPHeaderFields, let cookieString = headerFields[cookieKey] else {
1244
+ return false
1245
+ }
1246
+
1247
+ let requestCookies = cookieString.components(separatedBy: ";").map {
1248
+ $0.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=", maxSplits: 1).map(String.init)
1249
+ }
1250
+
1251
+ var valid = false
1252
+ for cookie in cookies {
1253
+ valid = requestCookies.filter {
1254
+ $0[0] == cookie.name && $0[1] == cookie.value
1255
+ }.count > 0
1256
+ if !valid {
1257
+ break
1258
+ }
1259
+ }
1260
+ return valid
1261
+ }
1262
+
1263
+ private func tryOpenCustomScheme(_ url: URL) -> Bool {
1264
+ let app = UIApplication.shared
1265
+
1266
+ if app.canOpenURL(url) {
1267
+ app.open(url, options: [:], completionHandler: nil)
1268
+ return true // external app opened -> cancel WebView load
1269
+ }
1270
+
1271
+ // Cannot open scheme: notify and still block WebView (avoid rendering garbage / errors)
1272
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1273
+ return true
1274
+ }
1275
+
1276
+ private func tryOpenUniversalLink(_ url: URL, completion: @escaping (Bool) -> Void) {
1277
+ // Only for http(s):// and authorized hosts
1278
+ UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in
1279
+ completion(opened) // true => app opened, false => no associated app
1280
+ }
1281
+ }
1282
+
1283
+ func openURLWithApp(_ url: URL) -> Bool {
1284
+ let application = UIApplication.shared
1285
+ if application.canOpenURL(url) {
1286
+ application.open(url, options: [:], completionHandler: nil)
1287
+ return true
1288
+ }
1289
+
1290
+ return false
1291
+ }
1292
+
1293
+ private func normalizeHost(_ host: String?) -> String? {
1294
+ guard var hostValue = host?.lowercased() else { return nil }
1295
+ if hostValue.hasPrefix("www.") { hostValue.removeFirst(4) }
1296
+ return hostValue
1297
+ }
1298
+
1299
+ func isUrlAuthorized(_ url: URL, authorizedLinks: [String]) -> Bool {
1300
+ guard !authorizedLinks.isEmpty else { return false }
1301
+
1302
+ let urlHostNorm = normalizeHost(url.host)
1303
+ for auth in authorizedLinks {
1304
+ guard let comp = URLComponents(string: auth) else { continue }
1305
+ let authHostNorm = normalizeHost(comp.host)
1306
+ if urlHostNorm == authHostNorm {
1307
+ return true
1308
+ }
1309
+ }
1310
+
1311
+ return false
1312
+ }
1313
+
1314
+ /// Attempts to open URL in an external app if it's a custom scheme OR an authorized universal link.
1315
+ /// Returns via completion whether an external app was opened (true) or not (false).
1316
+ private func handleURLWithApp(_ url: URL, targetFrame: WKFrameInfo?, completion: @escaping (Bool) -> Void) {
1317
+
1318
+ // If preventDeeplink is true, don't try to open URLs in external apps
1319
+ if preventDeeplink {
1320
+ print("[InAppBrowser] preventDeeplink is true, won't try to open URLs in external apps")
1321
+ completion(false)
1322
+ return
1323
+ }
1324
+
1325
+ let scheme = url.scheme?.lowercased() ?? ""
1326
+ let host = url.host?.lowercased() ?? ""
1327
+
1328
+ print("[InAppBrowser] scheme \(scheme), host \(host)")
1329
+
1330
+ // Don't try to open internal WebKit URLs externally (about:, data:, blob:, etc.)
1331
+ let internalSchemes = ["about", "data", "blob", "javascript"]
1332
+ if internalSchemes.contains(scheme) {
1333
+ print("[InAppBrowser] internal WebKit scheme detected, allowing navigation")
1334
+ completion(false)
1335
+ return
1336
+ }
1337
+
1338
+ // Handle all non-http(s) schemes by default
1339
+ if scheme != "http" && scheme != "https" && scheme != "file" {
1340
+ print("[InAppBrowser] not http(s) scheme, try to open URLs in external apps")
1341
+ completion(tryOpenCustomScheme(url))
1342
+ return
1343
+ }
1344
+
1345
+ // Also handle specific hosts and schemes from UrlsHandledByApp
1346
+ let hosts = UrlsHandledByApp.hosts
1347
+ let schemes = UrlsHandledByApp.schemes
1348
+ let blank = UrlsHandledByApp.blank
1349
+
1350
+ if hosts.contains(host) {
1351
+ print("[InAppBrowser] host \(host) matches one in UrlsHandledByApp, try to open URLs in external apps")
1352
+ completion(tryOpenCustomScheme(url))
1353
+ return
1354
+ }
1355
+ if schemes.contains(scheme) {
1356
+ print("[InAppBrowser] scheme \(scheme) matches one in UrlsHandledByApp, try to open URLs in external apps")
1357
+ completion(tryOpenCustomScheme(url))
1358
+ return
1359
+ }
1360
+ if blank && targetFrame == nil {
1361
+ print("[InAppBrowser] is blank and targetFrame is nil, try to open URLs in external apps")
1362
+ completion(tryOpenCustomScheme(url))
1363
+ return
1364
+ }
1365
+
1366
+ // Authorized Universal Link hosts: prefer app via universalLinksOnly
1367
+ print("[InAppBrowser] Authorized App Links: \(self.authorizedAppLinks)")
1368
+ if isUrlAuthorized(url, authorizedLinks: self.authorizedAppLinks) {
1369
+ print("[InAppBrowser] Authorized Universal Link detected \(scheme + host), try to open URLs in external apps")
1370
+ tryOpenUniversalLink(url) { opened in
1371
+ print("[InAppBrowser] Handle as Universal Link: \(opened)")
1372
+ completion(opened) // opened => cancel navigation; not opened => allow WebView
1373
+ }
1374
+ return
1375
+ }
1376
+
1377
+ // Default: let WebView load
1378
+ print("[InAppBrowser] default completion handler: false")
1379
+ completion(false)
1380
+ }
1381
+
1382
+ @objc func backDidClick(sender: AnyObject) {
1383
+ // Only handle back navigation, not closing
1384
+ if webView?.canGoBack ?? false {
1385
+ webView?.goBack()
1386
+ }
1387
+ }
1388
+
1389
+ // Public method for safe back navigation
1390
+ public func goBack() -> Bool {
1391
+ if webView?.canGoBack ?? false {
1392
+ webView?.goBack()
1393
+ return true
1394
+ }
1395
+ return false
1396
+ }
1397
+
1398
+ @objc func forwardDidClick(sender: AnyObject) {
1399
+ webView?.goForward()
1400
+ }
1401
+
1402
+ @objc func buttonNearDoneDidClick(sender: AnyObject) {
1403
+ self.capBrowserPlugin?.notifyListeners("buttonNearDoneClick", data: [:])
1404
+ }
1405
+
1406
+ @objc func reloadDidClick(sender: AnyObject) {
1407
+ webView?.stopLoading()
1408
+ if webView?.url != nil {
1409
+ webView?.reload()
1410
+ } else if let sourceValue = self.source {
1411
+ self.load(source: sourceValue)
1412
+ }
1413
+ }
1414
+
1415
+ @objc func stopDidClick(sender: AnyObject) {
1416
+ webView?.stopLoading()
1417
+ }
1418
+
1419
+ @objc func activityDidClick(sender: AnyObject) {
1420
+ print("[DEBUG] Activity button clicked, shareSubject: \(self.shareSubject ?? "nil")")
1421
+
1422
+ guard let sourceValue = self.source else {
1423
+ print("[DEBUG] Activity button: No source available")
1424
+ return
1425
+ }
1426
+
1427
+ let items: [Any]
1428
+ switch sourceValue {
1429
+ case .remote(let urlValue):
1430
+ items = [urlValue]
1431
+ case .file(let urlValue, access: _):
1432
+ items = [urlValue]
1433
+ case .string(let str, base: _):
1434
+ items = [str]
1435
+ }
1436
+ showDisclaimer(items: items, sender: sender)
1437
+ }
1438
+
1439
+ func showDisclaimer(items: [Any], sender: AnyObject) {
1440
+ // Show disclaimer dialog before sharing if shareDisclaimer is set
1441
+ if let disclaimer = self.shareDisclaimer, !disclaimer.isEmpty {
1442
+ // Create and show the alert
1443
+ let alert = UIAlertController(
1444
+ title: disclaimer["title"] as? String ?? "Title",
1445
+ message: disclaimer["message"] as? String ?? "Message",
1446
+ preferredStyle: UIAlertController.Style.alert)
1447
+ let currentUrl = self.webView?.url?.absoluteString ?? ""
1448
+
1449
+ // Add confirm button that continues with sharing
1450
+ alert.addAction(UIAlertAction(
1451
+ title: disclaimer["confirmBtn"] as? String ?? "Confirm",
1452
+ style: UIAlertAction.Style.default,
1453
+ handler: { _ in
1454
+ // Notify that confirm was clicked
1455
+ self.capBrowserPlugin?.notifyListeners("confirmBtnClicked", data: ["url": currentUrl])
1456
+
1457
+ // Show the share dialog
1458
+ self.showShareSheet(items: items, sender: sender)
1459
+ }
1460
+ ))
1461
+
1462
+ // Add cancel button
1463
+ alert.addAction(UIAlertAction(
1464
+ title: disclaimer["cancelBtn"] as? String ?? "Cancel",
1465
+ style: UIAlertAction.Style.cancel,
1466
+ handler: nil
1467
+ ))
1468
+
1469
+ // Present the alert
1470
+ self.present(alert, animated: true, completion: nil)
1471
+ } else {
1472
+ // No disclaimer, directly show share sheet
1473
+ showShareSheet(items: items, sender: sender)
1474
+ }
1475
+ }
1476
+
1477
+ // Separated the actual sharing functionality
1478
+ private func showShareSheet(items: [Any], sender: AnyObject) {
1479
+ let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
1480
+ activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
1481
+ if let barButtonItem = sender as? UIBarButtonItem {
1482
+ activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
1483
+ }
1484
+ self.present(activityViewController, animated: true, completion: nil)
1485
+ }
1486
+
1487
+ func closeView() {
1488
+ var canDismiss = true
1489
+ if let url = self.source?.url {
1490
+ canDismiss = delegate?.webViewController?(self, canDismiss: url) ?? true
1491
+ }
1492
+ if canDismiss {
1493
+ let currentUrl = webView?.url?.absoluteString ?? ""
1494
+ cleanupWebView()
1495
+ self.capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1496
+ dismiss(animated: true, completion: nil)
1497
+ }
1498
+ }
1499
+
1500
+ @objc func doneDidClick(sender: AnyObject) {
1501
+ // check if closeModal is true, if true display alert before close
1502
+ if self.closeModal {
1503
+ let currentUrl = webView?.url?.absoluteString ?? ""
1504
+ let alert = UIAlertController(title: self.closeModalTitle, message: self.closeModalDescription, preferredStyle: UIAlertController.Style.alert)
1505
+ alert.addAction(UIAlertAction(title: self.closeModalOk, style: UIAlertAction.Style.default, handler: { _ in
1506
+ // Notify that confirm was clicked
1507
+ self.capBrowserPlugin?.notifyListeners("confirmBtnClicked", data: ["url": currentUrl])
1508
+ self.closeView()
1509
+ }))
1510
+ alert.addAction(UIAlertAction(title: self.closeModalCancel, style: UIAlertAction.Style.default, handler: nil))
1511
+ self.present(alert, animated: true, completion: nil)
1512
+ } else {
1513
+ self.closeView()
1514
+ }
1515
+
1516
+ }
1517
+
1518
+ @objc func customDidClick(sender: BlockBarButtonItem) {
1519
+ sender.block?(self)
1520
+ }
1521
+
1522
+ func canRotate() {}
1523
+
1524
+ func close() {
1525
+ let currentUrl = webView?.url?.absoluteString ?? ""
1526
+ cleanupWebView()
1527
+ capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1528
+ dismiss(animated: true, completion: nil)
1529
+ }
1530
+
1531
+ open func setUpNavigationBarAppearance() {
1532
+ // Set up basic bar appearance
1533
+ if let navBar = navigationController?.navigationBar {
1534
+ // Make navigation bar transparent
1535
+ navBar.setBackgroundImage(UIImage(), for: .default)
1536
+ navBar.shadowImage = UIImage()
1537
+ navBar.isTranslucent = true
1538
+
1539
+ // Ensure tint colors are applied properly
1540
+ if navBar.tintColor == nil {
1541
+ navBar.tintColor = tintColor ?? .black
1542
+ }
1543
+
1544
+ // Ensure text colors are set
1545
+ if navBar.titleTextAttributes == nil {
1546
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor ?? .black]
1547
+ }
1548
+
1549
+ // Ensure the navigation bar buttons are properly visible
1550
+ for item in navBar.items ?? [] {
1551
+ for barButton in (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? []) {
1552
+ barButton.tintColor = tintColor ?? navBar.tintColor ?? .black
1553
+ }
1554
+ }
1555
+ }
1556
+
1557
+ // Force button colors to update
1558
+ updateButtonTintColors()
1559
+ }
1560
+ }
1561
+
1562
+ // MARK: - WKUIDelegate
1563
+ extension WKWebViewController: WKUIDelegate {
1564
+ public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
1565
+ // Create a strong reference to the completion handler to ensure it's called
1566
+ let strongCompletionHandler = completionHandler
1567
+
1568
+ // Ensure UI updates are on the main thread
1569
+ DispatchQueue.main.async { [weak self] in
1570
+ guard let self = self else {
1571
+ // View controller was deallocated
1572
+ strongCompletionHandler()
1573
+ return
1574
+ }
1575
+
1576
+ // Check if view is available and ready for presentation
1577
+ guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
1578
+ print("[InAppBrowser] Cannot present alert - view not in window hierarchy or being dismissed")
1579
+ strongCompletionHandler()
1580
+ return
1581
+ }
1582
+
1583
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
1584
+ alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
1585
+ strongCompletionHandler()
1586
+ }))
1587
+
1588
+ // Try to present the alert
1589
+ do {
1590
+ self.present(alertController, animated: true, completion: nil)
1591
+ } catch {
1592
+ // This won't typically be triggered as present doesn't throw,
1593
+ // but adding as a safeguard
1594
+ print("[InAppBrowser] Error presenting alert: \(error)")
1595
+ strongCompletionHandler()
1596
+ }
1597
+ }
1598
+ }
1599
+
1600
+ public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
1601
+ // Handle target="_blank" links and popup windows
1602
+ // When preventDeeplink is true, we should load these in the same webview instead of opening externally
1603
+ if let url = navigationAction.request.url {
1604
+ print("[InAppBrowser] Handling popup/new window request for URL: \(url.absoluteString)")
1605
+
1606
+ // If preventDeeplink is true, load the URL in the current webview
1607
+ if preventDeeplink {
1608
+ print("[InAppBrowser] preventDeeplink is true, loading popup URL in current webview")
1609
+ DispatchQueue.main.async { [weak self] in
1610
+ self?.load(remote: url)
1611
+ }
1612
+ return nil
1613
+ }
1614
+
1615
+ // Otherwise, check if we should handle it externally
1616
+ // But since preventDeeplink is false here, we won't block it
1617
+ return nil
1618
+ }
1619
+
1620
+ return nil
1621
+ }
1622
+
1623
+ @available(iOS 15.0, *)
1624
+ public func webView(_ webView: WKWebView, requestGeolocationPermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
1625
+ print("[InAppBrowser] Geolocation permission requested for origin: \(origin.host)")
1626
+
1627
+ // Grant geolocation permission automatically for openWebView
1628
+ // This allows websites to access location when opened with openWebView
1629
+ decisionHandler(.grant)
1630
+ }
1631
+ }
1632
+
1633
+ // MARK: - Host Blocking Utilities
1634
+ extension WKWebViewController {
1635
+
1636
+ /// Checks if a host should be blocked based on the configured blocked hosts patterns
1637
+ /// - Parameter host: The host to check
1638
+ /// - Returns: true if the host should be blocked, false otherwise
1639
+ private func shouldBlockHost(_ host: String) -> Bool {
1640
+ guard !host.isEmpty else { return false }
1641
+
1642
+ let normalizedHost = host.lowercased()
1643
+
1644
+ return blockedHosts.contains { blockPattern in
1645
+ return matchesBlockPattern(host: normalizedHost, pattern: blockPattern.lowercased())
1646
+ }
1647
+ }
1648
+
1649
+ /// Matches a host against a blocking pattern (supports wildcards)
1650
+ /// - Parameters:
1651
+ /// - host: The normalized host to check
1652
+ /// - pattern: The normalized blocking pattern
1653
+ /// - Returns: true if the host matches the pattern
1654
+ private func matchesBlockPattern(host: String, pattern: String) -> Bool {
1655
+ guard !pattern.isEmpty else { return false }
1656
+
1657
+ // Exact match - fastest check first
1658
+ if host == pattern {
1659
+ return true
1660
+ }
1661
+
1662
+ // No wildcards - already checked exact match above
1663
+ guard pattern.contains("*") else {
1664
+ return false
1665
+ }
1666
+
1667
+ // Handle wildcard patterns
1668
+ if pattern.hasPrefix("*.") {
1669
+ return matchesWildcardDomain(host: host, pattern: pattern)
1670
+ } else if pattern.contains("*") {
1671
+ return matchesRegexPattern(host: host, pattern: pattern)
1672
+ }
1673
+
1674
+ return false
1675
+ }
1676
+
1677
+ /// Handles simple subdomain wildcard patterns like "*.example.com"
1678
+ /// - Parameters:
1679
+ /// - host: The host to check
1680
+ /// - pattern: The wildcard pattern starting with "*."
1681
+ /// - Returns: true if the host matches the wildcard domain
1682
+ private func matchesWildcardDomain(host: String, pattern: String) -> Bool {
1683
+ let domain = String(pattern.dropFirst(2)) // Remove "*."
1684
+
1685
+ guard !domain.isEmpty else { return false }
1686
+
1687
+ // Match exact domain or any subdomain
1688
+ return host == domain || host.hasSuffix("." + domain)
1689
+ }
1690
+
1691
+ /// Handles complex regex patterns with multiple wildcards
1692
+ /// - Parameters:
1693
+ /// - host: The host to check
1694
+ /// - pattern: The pattern with wildcards to convert to regex
1695
+ /// - Returns: true if the host matches the regex pattern
1696
+ private func matchesRegexPattern(host: String, pattern: String) -> Bool {
1697
+ // Escape everything, then re-enable '*' as a wildcard
1698
+ let escaped = NSRegularExpression.escapedPattern(for: pattern)
1699
+ let wildcardEnabled = escaped.replacingOccurrences(of: "\\*", with: ".*")
1700
+ let regexPattern = "^\(wildcardEnabled)$"
1701
+
1702
+ do {
1703
+ let regex = try NSRegularExpression(pattern: regexPattern, options: [])
1704
+ let range = NSRange(location: 0, length: host.utf16.count)
1705
+ return regex.firstMatch(in: host, options: [], range: range) != nil
1706
+ } catch {
1707
+ print("[InAppBrowser] Invalid regex pattern '\(regexPattern)': \(error)")
1708
+ return false
1709
+ }
1710
+ }
1711
+ }
1712
+
1713
+ // MARK: - WKNavigationDelegate
1714
+ extension WKWebViewController: WKNavigationDelegate {
1715
+ internal func injectPreShowScript() {
1716
+ if preShowSemaphore != nil {
1717
+ return
1718
+ }
1719
+
1720
+ // Safely construct script template with proper escaping
1721
+ let userScript = self.preShowScript ?? ""
1722
+
1723
+ // Build script using safe concatenation to avoid multi-line string issues
1724
+ let scriptTemplate = [
1725
+ "async function preShowFunction() {",
1726
+ userScript,
1727
+ "}",
1728
+ "preShowFunction().then(",
1729
+ " () => window.webkit.messageHandlers.preShowScriptSuccess.postMessage({})",
1730
+ ").catch(",
1731
+ " err => {",
1732
+ " console.error('Preshow error', err);",
1733
+ " window.webkit.messageHandlers.preShowScriptError.postMessage(JSON.stringify(err, Object.getOwnPropertyNames(err)));",
1734
+ " }",
1735
+ ")"
1736
+ ]
1737
+
1738
+ let script = scriptTemplate.joined(separator: "\n")
1739
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript script: \(script)")
1740
+
1741
+ self.preShowSemaphore = DispatchSemaphore(value: 0)
1742
+ self.executeScript(script: script) // this will run on the main thread
1743
+
1744
+ defer {
1745
+ self.preShowSemaphore = nil
1746
+ self.preShowError = nil
1747
+ }
1748
+
1749
+ if self.preShowSemaphore?.wait(timeout: .now() + 10) == .timedOut {
1750
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript running for over 10 seconds. The plugin will not wait any longer!")
1751
+ return
1752
+ }
1753
+
1754
+ // "async function preShowFunction() {\n" +
1755
+ // self.preShowScript + "\n" +
1756
+ // "};\n" +
1757
+ // "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
1758
+
1759
+ }
1760
+
1761
+ public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
1762
+ updateBarButtonItems()
1763
+ self.progressView?.progress = 0
1764
+ if let urlValue = webView.url {
1765
+ self.url = urlValue
1766
+ delegate?.webViewController?(self, didStart: urlValue)
1767
+ }
1768
+ }
1769
+ public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
1770
+ if !didpageInit && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
1771
+ // Only inject preShowScript if it wasn't already injected at document start
1772
+ let shouldInjectScript = self.preShowScript.map { !$0.isEmpty } ?? false &&
1773
+ self.preShowScriptInjectionTime != "documentStart"
1774
+
1775
+ if shouldInjectScript {
1776
+ // injectPreShowScript will block, don't execute on the main thread
1777
+ DispatchQueue.global(qos: .userInitiated).async {
1778
+ self.injectPreShowScript()
1779
+ DispatchQueue.main.async { [weak self] in
1780
+ self?.capBrowserPlugin?.presentView()
1781
+ }
1782
+ }
1783
+ } else {
1784
+ self.capBrowserPlugin?.presentView()
1785
+ }
1786
+ } else if self.preShowScript != nil &&
1787
+ !self.preShowScript!.isEmpty &&
1788
+ self.capBrowserPlugin?.isPresentAfterPageLoad == true &&
1789
+ self.preShowScriptInjectionTime != "documentStart" {
1790
+ // Only inject if not already injected at document start
1791
+ DispatchQueue.global(qos: .userInitiated).async {
1792
+ self.injectPreShowScript()
1793
+ }
1794
+ }
1795
+
1796
+ // Apply text zoom if set
1797
+ if let zoom = self.textZoom {
1798
+ applyTextZoom(zoom)
1799
+ }
1800
+
1801
+ didpageInit = true
1802
+ updateBarButtonItems()
1803
+ self.progressView?.progress = 0
1804
+ if let url = webView.url {
1805
+ self.url = url
1806
+ delegate?.webViewController?(self, didFinish: url)
1807
+ }
1808
+ self.injectJavaScriptInterface()
1809
+ self.capBrowserPlugin?.notifyListeners("browserPageLoaded", data: [:])
1810
+ }
1811
+
1812
+ public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
1813
+ updateBarButtonItems()
1814
+ self.progressView?.progress = 0
1815
+ if let url = webView.url {
1816
+ self.url = url
1817
+ delegate?.webViewController?(self, didFail: url, withError: error)
1818
+ }
1819
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1820
+ }
1821
+
1822
+ public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
1823
+ updateBarButtonItems()
1824
+ self.progressView?.progress = 0
1825
+ if let url = webView.url {
1826
+ self.url = url
1827
+ delegate?.webViewController?(self, didFail: url, withError: error)
1828
+ }
1829
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1830
+ }
1831
+
1832
+ public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
1833
+ if let credentials = credentials,
1834
+ challenge.protectionSpace.receivesCredentialSecurely,
1835
+ let url = webView.url, challenge.protectionSpace.host == url.host, challenge.protectionSpace.protocol == url.scheme, challenge.protectionSpace.port == url.port ?? (url.scheme == "https" ? 443 : url.scheme == "http" ? 80 : nil) {
1836
+ let urlCredential = URLCredential(user: credentials.username, password: credentials.password, persistence: .none)
1837
+ completionHandler(.useCredential, urlCredential)
1838
+ } else if let bypassedSSLHosts = bypassedSSLHosts, bypassedSSLHosts.contains(challenge.protectionSpace.host) {
1839
+ let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
1840
+ completionHandler(.useCredential, credential)
1841
+ } else {
1842
+ guard self.ignoreUntrustedSSLError else {
1843
+ completionHandler(.performDefaultHandling, nil)
1844
+ return
1845
+ }
1846
+ /* allows to open links with self-signed certificates
1847
+ Follow Apple's guidelines https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication
1848
+ */
1849
+ guard let serverTrust = challenge.protectionSpace.serverTrust else {
1850
+ completionHandler(.useCredential, nil)
1851
+ return
1852
+ }
1853
+ let credential = URLCredential(trust: serverTrust)
1854
+ completionHandler(.useCredential, credential)
1855
+ }
1856
+ self.injectJavaScriptInterface()
1857
+ }
1858
+
1859
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
1860
+ var actionPolicy: WKNavigationActionPolicy = self.preventDeeplink ? .preventDeeplinkActionPolicy : .allow
1861
+
1862
+ guard let url = navigationAction.request.url else {
1863
+ print("[InAppBrowser] Cannot determine URL from navigationAction")
1864
+ decisionHandler(actionPolicy)
1865
+ return
1866
+ }
1867
+
1868
+ if url.absoluteString.contains("apps.apple.com") {
1869
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
1870
+ decisionHandler(.cancel)
1871
+ return
1872
+ }
1873
+
1874
+ if !self.allowsFileURL, url.isFileURL {
1875
+ print("[InAppBrowser] Cannot handle file URLs")
1876
+ decisionHandler(.cancel)
1877
+ return
1878
+ }
1879
+
1880
+ // Defer the rest of the logic until the async external-app handling checks completes.
1881
+ handleURLWithApp(url, targetFrame: navigationAction.targetFrame) { [weak self] openedExternally in
1882
+ guard let self else {
1883
+ decisionHandler(.cancel)
1884
+ return
1885
+ }
1886
+
1887
+ if openedExternally {
1888
+ decisionHandler(.cancel)
1889
+ return
1890
+ }
1891
+
1892
+ let host = url.host ?? ""
1893
+
1894
+ if host == self.source?.url?.host,
1895
+ let cookies = self.availableCookies,
1896
+ !self.checkRequestCookies(navigationAction.request, cookies: cookies) {
1897
+ self.load(remote: url)
1898
+ decisionHandler(.cancel)
1899
+ return
1900
+ }
1901
+
1902
+ if self.shouldBlockHost(host) {
1903
+ print("[InAppBrowser] Blocked host detected: \(host)")
1904
+ self.capBrowserPlugin?.notifyListeners("urlChangeEvent", data: ["url": url.absoluteString])
1905
+ decisionHandler(.cancel)
1906
+ return
1907
+ }
1908
+
1909
+ if let navigationType = NavigationType(rawValue: navigationAction.navigationType.rawValue),
1910
+ let result = self.delegate?.webViewController?(self, decidePolicy: url, navigationType: navigationType) {
1911
+ actionPolicy = result ? .allow : .cancel
1912
+ }
1913
+
1914
+ self.injectJavaScriptInterface()
1915
+ decisionHandler(actionPolicy)
1916
+ }
1917
+ }
1918
+
1919
+ // MARK: - Dimension Management
1920
+
1921
+ /// Apply custom dimensions to the view if specified
1922
+ open func applyCustomDimensions() {
1923
+ guard let navigationController = navigationController else { return }
1924
+
1925
+ // Apply custom dimensions if both width and height are specified
1926
+ if let width = customWidth, let height = customHeight {
1927
+ let xPos = customX ?? 0
1928
+ let yPos = customY ?? 0
1929
+
1930
+ // Set the frame for the navigation controller's view
1931
+ navigationController.view.frame = CGRect(x: xPos, y: yPos, width: width, height: height)
1932
+ }
1933
+ // If only height is specified, use fullscreen width
1934
+ else if let height = customHeight, customWidth == nil {
1935
+ let xPos = customX ?? 0
1936
+ let yPos = customY ?? 0
1937
+ let screenWidth = UIScreen.main.bounds.width
1938
+
1939
+ // Set the frame with fullscreen width and custom height
1940
+ navigationController.view.frame = CGRect(x: xPos, y: yPos, width: screenWidth, height: height)
1941
+ }
1942
+ // Otherwise, use default fullscreen behavior (no action needed)
1943
+ }
1944
+
1945
+ /// Update dimensions at runtime
1946
+ open func updateDimensions(width: CGFloat?, height: CGFloat?, xPos: CGFloat?, yPos: CGFloat?) {
1947
+ // Update stored dimensions
1948
+ if let width = width {
1949
+ customWidth = width
1950
+ }
1951
+ if let height = height {
1952
+ customHeight = height
1953
+ }
1954
+ if let xPos = xPos {
1955
+ customX = xPos
1956
+ }
1957
+ if let yPos = yPos {
1958
+ customY = yPos
1959
+ }
1960
+
1961
+ // Apply the new dimensions
1962
+ applyCustomDimensions()
1963
+ }
1964
+ }
1965
+
1966
+ class BlockBarButtonItem: UIBarButtonItem {
1967
+
1968
+ var block: ((WKWebViewController) -> Void)?
1969
+ }
1970
+
1971
+ /// Custom view that passes touches outside a target frame to the underlying view
1972
+ class PassThroughView: UIView {
1973
+ var targetFrame: CGRect?
1974
+
1975
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
1976
+ // If we have a target frame and the touch is outside it, pass through
1977
+ if let frame = targetFrame {
1978
+ if !frame.contains(point) {
1979
+ return nil // Pass through to underlying views
1980
+ }
1981
+ }
1982
+
1983
+ // Otherwise, handle normally
1984
+ return super.hitTest(point, with: event)
1985
+ }
1986
+ }
1987
+
1988
+ extension WKNavigationActionPolicy {
1989
+ static let preventDeeplinkActionPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!
1990
+ }
1991
+
1992
+ class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
1993
+ weak var delegate: WKScriptMessageHandler?
1994
+
1995
+ init(_ delegate: WKScriptMessageHandler) {
1996
+ self.delegate = delegate
1997
+ super.init()
1998
+ }
1999
+
2000
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
2001
+ self.delegate?.userContentController(userContentController, didReceive: message)
2002
+ }
2003
+ }