@capgo/inappbrowser 8.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +3023 -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/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +952 -0
  34. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +2006 -0
  35. package/package.json +30 -30
  36. package/ios/Plugin/Assets.xcassets/Back.imageset/Back.png +0 -0
  37. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@2x.png +0 -0
  38. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@3x.png +0 -0
  39. package/ios/Plugin/Assets.xcassets/Back.imageset/Contents.json +0 -26
  40. package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
  41. package/ios/Plugin/Assets.xcassets/Forward.imageset/Contents.json +0 -26
  42. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward.png +0 -0
  43. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@2x.png +0 -0
  44. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@3x.png +0 -0
  45. package/ios/Plugin/InAppBrowserPlugin.h +0 -10
  46. package/ios/Plugin/InAppBrowserPlugin.m +0 -17
  47. package/ios/Plugin/InAppBrowserPlugin.swift +0 -203
  48. package/ios/Plugin/Info.plist +0 -24
  49. package/ios/Plugin/WKWebViewController.swift +0 -784
  50. /package/ios/{Plugin → Sources/InAppBrowserPlugin}/Enums.swift +0 -0
@@ -0,0 +1,2006 @@
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 s = self.source {
739
+ self.load(source: s)
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
+ if #available(iOS 11.0, *) {
757
+ let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
758
+ bottomPadding = window?.safeAreaInsets.bottom ?? 0.0
759
+ topPadding = window?.safeAreaInsets.top ?? 0.0
760
+ }
761
+ if UIDevice.current.orientation.isPortrait {
762
+ // Don't force toolbar visibility
763
+ if self.viewHeightPortrait == nil {
764
+ self.viewHeightPortrait = self.view.safeAreaLayoutGuide.layoutFrame.size.height
765
+ self.viewHeightPortrait! += bottomPadding
766
+ if self.navigationController?.navigationBar.isHidden == true {
767
+ self.viewHeightPortrait! += topPadding
768
+ }
769
+ }
770
+ self.currentViewHeight = self.viewHeightPortrait
771
+ } else if UIDevice.current.orientation.isLandscape {
772
+ // Don't force toolbar visibility
773
+ if self.viewHeightLandscape == nil {
774
+ self.viewHeightLandscape = self.view.safeAreaLayoutGuide.layoutFrame.size.height
775
+ self.viewHeightLandscape! += bottomPadding
776
+ if self.navigationController?.navigationBar.isHidden == true {
777
+ self.viewHeightLandscape! += topPadding
778
+ }
779
+ }
780
+ self.currentViewHeight = self.viewHeightLandscape
781
+ }
782
+ }
783
+
784
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
785
+ // self.view.frame.size.height = self.currentViewHeight!
786
+ }
787
+
788
+ override open func viewWillLayoutSubviews() {
789
+ restateViewHeight()
790
+ // Don't override frame height when enabledSafeBottomMargin is true, as it would override our constraints
791
+ if self.currentViewHeight != nil && !self.enabledSafeBottomMargin {
792
+ self.view.frame.size.height = self.currentViewHeight!
793
+ }
794
+ }
795
+
796
+ override open func viewWillAppear(_ animated: Bool) {
797
+ super.viewWillAppear(animated)
798
+ if !self.viewWasPresented {
799
+ self.setupViewElements()
800
+ setUpState()
801
+ self.viewWasPresented = true
802
+
803
+ // Apply custom dimensions if specified
804
+ applyCustomDimensions()
805
+ }
806
+
807
+ // Force update button appearances
808
+ updateButtonTintColors()
809
+
810
+ // Ensure status bar appearance is correct when view appears
811
+ // Make sure we have the latest tint color
812
+ if let tintColor = self.tintColor {
813
+ // Update the status bar background if needed
814
+ if let navController = navigationController, let backgroundColor = navController.navigationBar.backgroundColor ?? statusBarBackgroundView?.backgroundColor {
815
+ setupStatusBarBackground(color: backgroundColor)
816
+ } else {
817
+ setupStatusBarBackground(color: UIColor.white)
818
+ }
819
+ }
820
+
821
+ // Update status bar style
822
+ updateStatusBarStyle()
823
+
824
+ // Special handling for blank toolbar mode
825
+ if blankNavigationTab && statusBarBackgroundView != nil {
826
+ if let color = statusBarBackgroundView?.backgroundColor {
827
+ // Set view color to match status bar
828
+ view.backgroundColor = color
829
+ }
830
+ }
831
+ }
832
+
833
+ override open func viewDidAppear(_ animated: Bool) {
834
+ super.viewDidAppear(animated)
835
+
836
+ // Force add buttonNearDone if it's not visible yet
837
+ if buttonNearDoneIcon != nil {
838
+ // Check if button already exists in the navigation bar
839
+ let buttonExists = navigationItem.rightBarButtonItems?.contains { item in
840
+ return item.action == #selector(buttonNearDoneDidClick)
841
+ } ?? false
842
+
843
+ if !buttonExists {
844
+ // Create and add the button directly
845
+ let buttonItem = UIBarButtonItem(
846
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
847
+ style: .plain,
848
+ target: self,
849
+ action: #selector(buttonNearDoneDidClick)
850
+ )
851
+
852
+ // Apply tint color
853
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
854
+ buttonItem.tintColor = tintColor
855
+ }
856
+
857
+ // Add to right items
858
+ if navigationItem.rightBarButtonItems == nil {
859
+ navigationItem.rightBarButtonItems = [buttonItem]
860
+ } else {
861
+ var items = navigationItem.rightBarButtonItems ?? []
862
+ items.append(buttonItem)
863
+ navigationItem.rightBarButtonItems = items
864
+ }
865
+
866
+ print("[DEBUG] Force added buttonNearDone in viewDidAppear")
867
+ }
868
+ }
869
+ }
870
+
871
+ override open func viewWillDisappear(_ animated: Bool) {
872
+ super.viewWillDisappear(animated)
873
+ rollbackState()
874
+ }
875
+
876
+ override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
877
+ switch keyPath {
878
+ case estimatedProgressKeyPath?:
879
+ DispatchQueue.main.async {
880
+ guard let estimatedProgress = self.webView?.estimatedProgress else {
881
+ return
882
+ }
883
+ self.progressView?.alpha = 1
884
+ self.progressView?.setProgress(Float(estimatedProgress), animated: true)
885
+
886
+ if estimatedProgress >= 1.0 {
887
+ UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: {
888
+ self.progressView?.alpha = 0
889
+ }, completion: {
890
+ _ in
891
+ self.progressView?.setProgress(0, animated: false)
892
+ })
893
+ }
894
+ }
895
+ case titleKeyPath?:
896
+ if self.hasDynamicTitle {
897
+ self.navigationItem.title = webView?.url?.host
898
+ }
899
+ case "URL":
900
+
901
+ self.capBrowserPlugin?.notifyListeners("urlChangeEvent", data: ["url": webView?.url?.absoluteString ?? ""])
902
+ self.injectJavaScriptInterface()
903
+ default:
904
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
905
+ }
906
+ }
907
+ }
908
+
909
+ // MARK: - Public Methods
910
+ public extension WKWebViewController {
911
+
912
+ func load(source s: WKWebSource) {
913
+ switch s {
914
+ case .remote(let url):
915
+ self.load(remote: url)
916
+ case .file(let url, access: let access):
917
+ self.load(file: url, access: access)
918
+ case .string(let str, base: let base):
919
+ self.load(string: str, base: base)
920
+ }
921
+ }
922
+
923
+ func load(remote: URL) {
924
+ DispatchQueue.main.async {
925
+ self.webView?.load(self.createRequest(url: remote))
926
+ }
927
+ }
928
+
929
+ func load(file: URL, access: URL) {
930
+ webView?.loadFileURL(file, allowingReadAccessTo: access)
931
+ }
932
+
933
+ func load(string: String, base: URL? = nil) {
934
+ webView?.loadHTMLString(string, baseURL: base)
935
+ }
936
+
937
+ func goBackToFirstPage() {
938
+ if let firstPageItem = webView?.backForwardList.backList.first {
939
+ webView?.go(to: firstPageItem)
940
+ }
941
+ }
942
+ func reload() {
943
+ webView?.reload()
944
+ }
945
+
946
+ func executeScript(script: String, completion: ((Any?, Error?) -> Void)? = nil) {
947
+ DispatchQueue.main.async { [weak self] in
948
+ self?.webView?.evaluateJavaScript(script, completionHandler: completion)
949
+ }
950
+ }
951
+
952
+ func applyTextZoom(_ zoomPercent: Int) {
953
+ let script = """
954
+ document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust = '\(zoomPercent)%';
955
+ document.getElementsByTagName('body')[0].style.textSizeAdjust = '\(zoomPercent)%';
956
+ """
957
+
958
+ executeScript(script: script)
959
+ }
960
+
961
+ func injectPreShowScriptAtDocumentStart() {
962
+ guard let preShowScript = self.preShowScript,
963
+ !preShowScript.isEmpty,
964
+ self.preShowScriptInjectionTime == "documentStart",
965
+ let webView = self.webView else {
966
+ return
967
+ }
968
+
969
+ let userScript = WKUserScript(
970
+ source: preShowScript,
971
+ injectionTime: .atDocumentStart,
972
+ forMainFrameOnly: false
973
+ )
974
+ webView.configuration.userContentController.addUserScript(userScript)
975
+ print("[InAppBrowser] Injected preShowScript at document start")
976
+
977
+ // Reload the webview so the script executes at document start
978
+ if let currentURL = webView.url {
979
+ load(remote: currentURL)
980
+ } else if let source = self.source {
981
+ load(source: source)
982
+ }
983
+ }
984
+
985
+ func updateNavigationGestures() {
986
+ self.webView?.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
987
+ }
988
+
989
+ open func cleanupWebView() {
990
+ guard let webView = self.webView else { return }
991
+ webView.stopLoading()
992
+ // Break delegate callbacks early
993
+ webView.navigationDelegate = nil
994
+ webView.uiDelegate = nil
995
+ webView.loadHTMLString("", baseURL: nil)
996
+
997
+ webView.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
998
+ if websiteTitleInNavigationBar {
999
+ webView.removeObserver(self, forKeyPath: titleKeyPath)
1000
+ }
1001
+ webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
1002
+
1003
+ webView.configuration.userContentController.removeAllUserScripts()
1004
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "messageHandler")
1005
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "close")
1006
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptSuccess")
1007
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptError")
1008
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "magicPrint")
1009
+
1010
+ webView.removeFromSuperview()
1011
+ // Also clean progress bar view if present
1012
+ progressView?.removeFromSuperview()
1013
+ progressView = nil
1014
+ self.webView = nil
1015
+ }
1016
+ }
1017
+
1018
+ // MARK: - Fileprivate Methods
1019
+ fileprivate extension WKWebViewController {
1020
+ var availableCookies: [HTTPCookie]? {
1021
+ return cookies?.filter {
1022
+ cookie in
1023
+ var result = true
1024
+ let url = self.source?.remoteURL
1025
+ if let host = url?.host, !cookie.domain.hasSuffix(host) {
1026
+ result = false
1027
+ }
1028
+ if cookie.isSecure && url?.scheme != "https" {
1029
+ result = false
1030
+ }
1031
+
1032
+ return result
1033
+ }
1034
+ }
1035
+ func createRequest(url: URL) -> URLRequest {
1036
+ var request = URLRequest(url: url)
1037
+
1038
+ // Set up headers
1039
+ if let headers = headers {
1040
+ for (field, value) in headers {
1041
+ request.addValue(value, forHTTPHeaderField: field)
1042
+ }
1043
+ }
1044
+
1045
+ // Set up Cookies
1046
+ if let cookies = availableCookies, let value = HTTPCookie.requestHeaderFields(with: cookies)[cookieKey] {
1047
+ request.addValue(value, forHTTPHeaderField: cookieKey)
1048
+ }
1049
+
1050
+ return request
1051
+ }
1052
+
1053
+ func setUpProgressView() {
1054
+ let progressView = UIProgressView(progressViewStyle: .default)
1055
+ progressView.trackTintColor = UIColor(white: 1, alpha: 0)
1056
+ self.progressView = progressView
1057
+ // updateProgressViewFrame()
1058
+ }
1059
+
1060
+ func setUpConstraints() {
1061
+ if !(self.navigationController?.navigationBar.isHidden)! {
1062
+ self.progressView?.frame.origin.y = CGFloat((self.navigationController?.navigationBar.frame.height)!)
1063
+ self.navigationController?.navigationBar.addSubview(self.progressView!)
1064
+ }
1065
+ }
1066
+
1067
+ func addBarButtonItems() {
1068
+ func barButtonItem(_ type: BarButtonItemType) -> UIBarButtonItem? {
1069
+ switch type {
1070
+ case .back:
1071
+ return backBarButtonItem
1072
+ case .forward:
1073
+ return forwardBarButtonItem
1074
+ case .reload:
1075
+ return reloadBarButtonItem
1076
+ case .stop:
1077
+ return stopBarButtonItem
1078
+ case .activity:
1079
+ return activityBarButtonItem
1080
+ case .done:
1081
+ return doneBarButtonItem
1082
+ case .flexibleSpace:
1083
+ return flexibleSpaceBarButtonItem
1084
+ case .custom(let icon, let title, let action):
1085
+ let item: BlockBarButtonItem
1086
+ if let icon = icon {
1087
+ item = BlockBarButtonItem(image: icon, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1088
+ } else {
1089
+ item = BlockBarButtonItem(title: title, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1090
+ }
1091
+ item.block = action
1092
+ return item
1093
+ }
1094
+ }
1095
+
1096
+ switch doneBarButtonItemPosition {
1097
+ case .left:
1098
+ if !leftNavigationBarItemTypes.contains(where: { type in
1099
+ switch type {
1100
+ case .done:
1101
+ return true
1102
+ default:
1103
+ return false
1104
+ }
1105
+ }) {
1106
+ leftNavigationBarItemTypes.insert(.done, at: 0)
1107
+ }
1108
+ case .right:
1109
+ if !rightNavigaionBarItemTypes.contains(where: { type in
1110
+ switch type {
1111
+ case .done:
1112
+ return true
1113
+ default:
1114
+ return false
1115
+ }
1116
+ }) {
1117
+ rightNavigaionBarItemTypes.insert(.done, at: 0)
1118
+ }
1119
+ case .none:
1120
+ break
1121
+ }
1122
+
1123
+ navigationItem.leftBarButtonItems = leftNavigationBarItemTypes.map {
1124
+ barButtonItemType in
1125
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1126
+ return barButtonItem
1127
+ }
1128
+ return UIBarButtonItem()
1129
+ }
1130
+
1131
+ var rightBarButtons = rightNavigaionBarItemTypes.map {
1132
+ barButtonItemType in
1133
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1134
+ return barButtonItem
1135
+ }
1136
+ return UIBarButtonItem()
1137
+ }
1138
+
1139
+ // If we have buttonNearDoneIcon and the first (or only) right button is the done button
1140
+ if buttonNearDoneIcon != nil &&
1141
+ ((rightBarButtons.count == 1 && rightBarButtons[0] == doneBarButtonItem) ||
1142
+ (rightBarButtons.isEmpty && doneBarButtonItemPosition == .right) ||
1143
+ rightBarButtons.contains(doneBarButtonItem)) {
1144
+
1145
+ // Check if button already exists to avoid duplicates
1146
+ let buttonExists = rightBarButtons.contains { item in
1147
+ let selector = #selector(buttonNearDoneDidClick)
1148
+ return item.action == selector
1149
+ }
1150
+
1151
+ if !buttonExists {
1152
+ // Create button with proper tint and template rendering mode
1153
+ let buttonItem = UIBarButtonItem(
1154
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
1155
+ style: .plain,
1156
+ target: self,
1157
+ action: #selector(buttonNearDoneDidClick)
1158
+ )
1159
+
1160
+ // Apply tint from navigation bar or from tintColor property
1161
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
1162
+ buttonItem.tintColor = tintColor
1163
+ }
1164
+
1165
+ // Make sure the done button is there before adding this one
1166
+ if rightBarButtons.isEmpty && doneBarButtonItemPosition == .right {
1167
+ rightBarButtons.append(doneBarButtonItem)
1168
+ }
1169
+
1170
+ // Add the button
1171
+ rightBarButtons.append(buttonItem)
1172
+
1173
+ print("[DEBUG] Added buttonNearDone to right bar buttons, icon: \(String(describing: buttonNearDoneIcon))")
1174
+ } else {
1175
+ print("[DEBUG] buttonNearDone already exists in right bar buttons")
1176
+ }
1177
+ }
1178
+
1179
+ navigationItem.rightBarButtonItems = rightBarButtons
1180
+
1181
+ // After all buttons are set up, apply tint color
1182
+ updateButtonTintColors()
1183
+ }
1184
+
1185
+ func updateBarButtonItems() {
1186
+ // Update navigation buttons (completely separate from close button)
1187
+ backBarButtonItem.isEnabled = webView?.canGoBack ?? false
1188
+ forwardBarButtonItem.isEnabled = webView?.canGoForward ?? false
1189
+
1190
+ let updateReloadBarButtonItem: (UIBarButtonItem, Bool) -> UIBarButtonItem = {
1191
+ [unowned self] barButtonItem, isLoading in
1192
+ switch barButtonItem {
1193
+ case self.reloadBarButtonItem:
1194
+ fallthrough
1195
+ case self.stopBarButtonItem:
1196
+ return isLoading ? self.stopBarButtonItem : self.reloadBarButtonItem
1197
+ default:
1198
+ break
1199
+ }
1200
+ return barButtonItem
1201
+ }
1202
+
1203
+ let isLoading = webView?.isLoading ?? false
1204
+ navigationItem.leftBarButtonItems = navigationItem.leftBarButtonItems?.map {
1205
+ barButtonItem -> UIBarButtonItem in
1206
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1207
+ }
1208
+
1209
+ navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems?.map {
1210
+ barButtonItem -> UIBarButtonItem in
1211
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1212
+ }
1213
+ }
1214
+
1215
+ func setUpState() {
1216
+ navigationController?.setNavigationBarHidden(false, animated: true)
1217
+
1218
+ // Always hide toolbar since we never want it
1219
+ navigationController?.setToolbarHidden(true, animated: true)
1220
+
1221
+ // Set tint colors but don't override specific colors
1222
+ if tintColor == nil {
1223
+ // Use system appearance if no specific tint color is set
1224
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
1225
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
1226
+
1227
+ navigationController?.navigationBar.tintColor = textColor
1228
+ progressView?.progressTintColor = textColor
1229
+ } else {
1230
+ progressView?.progressTintColor = tintColor
1231
+ navigationController?.navigationBar.tintColor = tintColor
1232
+ }
1233
+ }
1234
+
1235
+ func rollbackState() {
1236
+ progressView?.progress = 0
1237
+
1238
+ navigationController?.navigationBar.tintColor = previousNavigationBarState.tintColor
1239
+
1240
+ navigationController?.setNavigationBarHidden(previousNavigationBarState.hidden, animated: true)
1241
+ }
1242
+
1243
+ func checkRequestCookies(_ request: URLRequest, cookies: [HTTPCookie]) -> Bool {
1244
+ if cookies.count <= 0 {
1245
+ return true
1246
+ }
1247
+ guard let headerFields = request.allHTTPHeaderFields, let cookieString = headerFields[cookieKey] else {
1248
+ return false
1249
+ }
1250
+
1251
+ let requestCookies = cookieString.components(separatedBy: ";").map {
1252
+ $0.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=", maxSplits: 1).map(String.init)
1253
+ }
1254
+
1255
+ var valid = false
1256
+ for cookie in cookies {
1257
+ valid = requestCookies.filter {
1258
+ $0[0] == cookie.name && $0[1] == cookie.value
1259
+ }.count > 0
1260
+ if !valid {
1261
+ break
1262
+ }
1263
+ }
1264
+ return valid
1265
+ }
1266
+
1267
+ private func tryOpenCustomScheme(_ url: URL) -> Bool {
1268
+ let app = UIApplication.shared
1269
+
1270
+ if app.canOpenURL(url) {
1271
+ app.open(url, options: [:], completionHandler: nil)
1272
+ return true // external app opened -> cancel WebView load
1273
+ }
1274
+
1275
+ // Cannot open scheme: notify and still block WebView (avoid rendering garbage / errors)
1276
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1277
+ return true
1278
+ }
1279
+
1280
+ private func tryOpenUniversalLink(_ url: URL, completion: @escaping (Bool) -> Void) {
1281
+ // Only for http(s):// and authorized hosts
1282
+ UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in
1283
+ completion(opened) // true => app opened, false => no associated app
1284
+ }
1285
+ }
1286
+
1287
+ func openURLWithApp(_ url: URL) -> Bool {
1288
+ let application = UIApplication.shared
1289
+ if application.canOpenURL(url) {
1290
+ application.open(url, options: [:], completionHandler: nil)
1291
+ return true
1292
+ }
1293
+
1294
+ return false
1295
+ }
1296
+
1297
+ private func normalizeHost(_ host: String?) -> String? {
1298
+ guard var h = host?.lowercased() else { return nil }
1299
+ if h.hasPrefix("www.") { h.removeFirst(4) }
1300
+ return h
1301
+ }
1302
+
1303
+ func isUrlAuthorized(_ url: URL, authorizedLinks: [String]) -> Bool {
1304
+ guard !authorizedLinks.isEmpty else { return false }
1305
+
1306
+ let urlHostNorm = normalizeHost(url.host)
1307
+ for auth in authorizedLinks {
1308
+ guard let comp = URLComponents(string: auth) else { continue }
1309
+ let authHostNorm = normalizeHost(comp.host)
1310
+ if urlHostNorm == authHostNorm {
1311
+ return true
1312
+ }
1313
+ }
1314
+
1315
+ return false
1316
+ }
1317
+
1318
+ /// Attempts to open URL in an external app if it's a custom scheme OR an authorized universal link.
1319
+ /// Returns via completion whether an external app was opened (true) or not (false).
1320
+ private func handleURLWithApp(_ url: URL, targetFrame: WKFrameInfo?, completion: @escaping (Bool) -> Void) {
1321
+
1322
+ // If preventDeeplink is true, don't try to open URLs in external apps
1323
+ if preventDeeplink {
1324
+ print("[InAppBrowser] preventDeeplink is true, won't try to open URLs in external apps")
1325
+ completion(false)
1326
+ return
1327
+ }
1328
+
1329
+ let scheme = url.scheme?.lowercased() ?? ""
1330
+ let host = url.host?.lowercased() ?? ""
1331
+
1332
+ print("[InAppBrowser] scheme \(scheme), host \(host)")
1333
+
1334
+ // Don't try to open internal WebKit URLs externally (about:, data:, blob:, etc.)
1335
+ let internalSchemes = ["about", "data", "blob", "javascript"]
1336
+ if internalSchemes.contains(scheme) {
1337
+ print("[InAppBrowser] internal WebKit scheme detected, allowing navigation")
1338
+ completion(false)
1339
+ return
1340
+ }
1341
+
1342
+ // Handle all non-http(s) schemes by default
1343
+ if scheme != "http" && scheme != "https" && scheme != "file" {
1344
+ print("[InAppBrowser] not http(s) scheme, try to open URLs in external apps")
1345
+ completion(tryOpenCustomScheme(url))
1346
+ return
1347
+ }
1348
+
1349
+ // Also handle specific hosts and schemes from UrlsHandledByApp
1350
+ let hosts = UrlsHandledByApp.hosts
1351
+ let schemes = UrlsHandledByApp.schemes
1352
+ let blank = UrlsHandledByApp.blank
1353
+
1354
+ if hosts.contains(host) {
1355
+ print("[InAppBrowser] host \(host) matches one in UrlsHandledByApp, try to open URLs in external apps")
1356
+ completion(tryOpenCustomScheme(url))
1357
+ return
1358
+ }
1359
+ if schemes.contains(scheme) {
1360
+ print("[InAppBrowser] scheme \(scheme) matches one in UrlsHandledByApp, try to open URLs in external apps")
1361
+ completion(tryOpenCustomScheme(url))
1362
+ return
1363
+ }
1364
+ if blank && targetFrame == nil {
1365
+ print("[InAppBrowser] is blank and targetFrame is nil, try to open URLs in external apps")
1366
+ completion(tryOpenCustomScheme(url))
1367
+ return
1368
+ }
1369
+
1370
+ // Authorized Universal Link hosts: prefer app via universalLinksOnly
1371
+ print("[InAppBrowser] Authorized App Links: \(self.authorizedAppLinks)")
1372
+ if isUrlAuthorized(url, authorizedLinks: self.authorizedAppLinks) {
1373
+ print("[InAppBrowser] Authorized Universal Link detected \(scheme + host), try to open URLs in external apps")
1374
+ tryOpenUniversalLink(url) { opened in
1375
+ print("[InAppBrowser] Handle as Universal Link: \(opened)")
1376
+ completion(opened) // opened => cancel navigation; not opened => allow WebView
1377
+ }
1378
+ return
1379
+ }
1380
+
1381
+ // Default: let WebView load
1382
+ print("[InAppBrowser] default completion handler: false")
1383
+ completion(false)
1384
+ }
1385
+
1386
+ @objc func backDidClick(sender: AnyObject) {
1387
+ // Only handle back navigation, not closing
1388
+ if webView?.canGoBack ?? false {
1389
+ webView?.goBack()
1390
+ }
1391
+ }
1392
+
1393
+ // Public method for safe back navigation
1394
+ public func goBack() -> Bool {
1395
+ if webView?.canGoBack ?? false {
1396
+ webView?.goBack()
1397
+ return true
1398
+ }
1399
+ return false
1400
+ }
1401
+
1402
+ @objc func forwardDidClick(sender: AnyObject) {
1403
+ webView?.goForward()
1404
+ }
1405
+
1406
+ @objc func buttonNearDoneDidClick(sender: AnyObject) {
1407
+ self.capBrowserPlugin?.notifyListeners("buttonNearDoneClick", data: [:])
1408
+ }
1409
+
1410
+ @objc func reloadDidClick(sender: AnyObject) {
1411
+ webView?.stopLoading()
1412
+ if webView?.url != nil {
1413
+ webView?.reload()
1414
+ } else if let s = self.source {
1415
+ self.load(source: s)
1416
+ }
1417
+ }
1418
+
1419
+ @objc func stopDidClick(sender: AnyObject) {
1420
+ webView?.stopLoading()
1421
+ }
1422
+
1423
+ @objc func activityDidClick(sender: AnyObject) {
1424
+ print("[DEBUG] Activity button clicked, shareSubject: \(self.shareSubject ?? "nil")")
1425
+
1426
+ guard let s = self.source else {
1427
+ print("[DEBUG] Activity button: No source available")
1428
+ return
1429
+ }
1430
+
1431
+ let items: [Any]
1432
+ switch s {
1433
+ case .remote(let u):
1434
+ items = [u]
1435
+ case .file(let u, access: _):
1436
+ items = [u]
1437
+ case .string(let str, base: _):
1438
+ items = [str]
1439
+ }
1440
+ showDisclaimer(items: items, sender: sender)
1441
+ }
1442
+
1443
+ func showDisclaimer(items: [Any], sender: AnyObject) {
1444
+ // Show disclaimer dialog before sharing if shareDisclaimer is set
1445
+ if let disclaimer = self.shareDisclaimer, !disclaimer.isEmpty {
1446
+ // Create and show the alert
1447
+ let alert = UIAlertController(
1448
+ title: disclaimer["title"] as? String ?? "Title",
1449
+ message: disclaimer["message"] as? String ?? "Message",
1450
+ preferredStyle: UIAlertController.Style.alert)
1451
+ let currentUrl = self.webView?.url?.absoluteString ?? ""
1452
+
1453
+ // Add confirm button that continues with sharing
1454
+ alert.addAction(UIAlertAction(
1455
+ title: disclaimer["confirmBtn"] as? String ?? "Confirm",
1456
+ style: UIAlertAction.Style.default,
1457
+ handler: { _ in
1458
+ // Notify that confirm was clicked
1459
+ self.capBrowserPlugin?.notifyListeners("confirmBtnClicked", data: ["url": currentUrl])
1460
+
1461
+ // Show the share dialog
1462
+ self.showShareSheet(items: items, sender: sender)
1463
+ }
1464
+ ))
1465
+
1466
+ // Add cancel button
1467
+ alert.addAction(UIAlertAction(
1468
+ title: disclaimer["cancelBtn"] as? String ?? "Cancel",
1469
+ style: UIAlertAction.Style.cancel,
1470
+ handler: nil
1471
+ ))
1472
+
1473
+ // Present the alert
1474
+ self.present(alert, animated: true, completion: nil)
1475
+ } else {
1476
+ // No disclaimer, directly show share sheet
1477
+ showShareSheet(items: items, sender: sender)
1478
+ }
1479
+ }
1480
+
1481
+ // Separated the actual sharing functionality
1482
+ private func showShareSheet(items: [Any], sender: AnyObject) {
1483
+ let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
1484
+ activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
1485
+ activityViewController.popoverPresentationController?.barButtonItem = (sender as! UIBarButtonItem)
1486
+ self.present(activityViewController, animated: true, completion: nil)
1487
+ }
1488
+
1489
+ func closeView() {
1490
+ var canDismiss = true
1491
+ if let url = self.source?.url {
1492
+ canDismiss = delegate?.webViewController?(self, canDismiss: url) ?? true
1493
+ }
1494
+ if canDismiss {
1495
+ let currentUrl = webView?.url?.absoluteString ?? ""
1496
+ cleanupWebView()
1497
+ self.capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1498
+ dismiss(animated: true, completion: nil)
1499
+ }
1500
+ }
1501
+
1502
+ @objc func doneDidClick(sender: AnyObject) {
1503
+ // check if closeModal is true, if true display alert before close
1504
+ if self.closeModal {
1505
+ let currentUrl = webView?.url?.absoluteString ?? ""
1506
+ let alert = UIAlertController(title: self.closeModalTitle, message: self.closeModalDescription, preferredStyle: UIAlertController.Style.alert)
1507
+ alert.addAction(UIAlertAction(title: self.closeModalOk, style: UIAlertAction.Style.default, handler: { _ in
1508
+ // Notify that confirm was clicked
1509
+ self.capBrowserPlugin?.notifyListeners("confirmBtnClicked", data: ["url": currentUrl])
1510
+ self.closeView()
1511
+ }))
1512
+ alert.addAction(UIAlertAction(title: self.closeModalCancel, style: UIAlertAction.Style.default, handler: nil))
1513
+ self.present(alert, animated: true, completion: nil)
1514
+ } else {
1515
+ self.closeView()
1516
+ }
1517
+
1518
+ }
1519
+
1520
+ @objc func customDidClick(sender: BlockBarButtonItem) {
1521
+ sender.block?(self)
1522
+ }
1523
+
1524
+ func canRotate() {}
1525
+
1526
+ func close() {
1527
+ let currentUrl = webView?.url?.absoluteString ?? ""
1528
+ cleanupWebView()
1529
+ capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1530
+ dismiss(animated: true, completion: nil)
1531
+ }
1532
+
1533
+ open func setUpNavigationBarAppearance() {
1534
+ // Set up basic bar appearance
1535
+ if let navBar = navigationController?.navigationBar {
1536
+ // Make navigation bar transparent
1537
+ navBar.setBackgroundImage(UIImage(), for: .default)
1538
+ navBar.shadowImage = UIImage()
1539
+ navBar.isTranslucent = true
1540
+
1541
+ // Ensure tint colors are applied properly
1542
+ if navBar.tintColor == nil {
1543
+ navBar.tintColor = tintColor ?? .black
1544
+ }
1545
+
1546
+ // Ensure text colors are set
1547
+ if navBar.titleTextAttributes == nil {
1548
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor ?? .black]
1549
+ }
1550
+
1551
+ // Ensure the navigation bar buttons are properly visible
1552
+ for item in navBar.items ?? [] {
1553
+ for barButton in (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? []) {
1554
+ barButton.tintColor = tintColor ?? navBar.tintColor ?? .black
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // Force button colors to update
1560
+ updateButtonTintColors()
1561
+ }
1562
+ }
1563
+
1564
+ // MARK: - WKUIDelegate
1565
+ extension WKWebViewController: WKUIDelegate {
1566
+ public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
1567
+ // Create a strong reference to the completion handler to ensure it's called
1568
+ let strongCompletionHandler = completionHandler
1569
+
1570
+ // Ensure UI updates are on the main thread
1571
+ DispatchQueue.main.async { [weak self] in
1572
+ guard let self = self else {
1573
+ // View controller was deallocated
1574
+ strongCompletionHandler()
1575
+ return
1576
+ }
1577
+
1578
+ // Check if view is available and ready for presentation
1579
+ guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
1580
+ print("[InAppBrowser] Cannot present alert - view not in window hierarchy or being dismissed")
1581
+ strongCompletionHandler()
1582
+ return
1583
+ }
1584
+
1585
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
1586
+ alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
1587
+ strongCompletionHandler()
1588
+ }))
1589
+
1590
+ // Try to present the alert
1591
+ do {
1592
+ self.present(alertController, animated: true, completion: nil)
1593
+ } catch {
1594
+ // This won't typically be triggered as present doesn't throw,
1595
+ // but adding as a safeguard
1596
+ print("[InAppBrowser] Error presenting alert: \(error)")
1597
+ strongCompletionHandler()
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
1603
+ // Handle target="_blank" links and popup windows
1604
+ // When preventDeeplink is true, we should load these in the same webview instead of opening externally
1605
+ if let url = navigationAction.request.url {
1606
+ print("[InAppBrowser] Handling popup/new window request for URL: \(url.absoluteString)")
1607
+
1608
+ // If preventDeeplink is true, load the URL in the current webview
1609
+ if preventDeeplink {
1610
+ print("[InAppBrowser] preventDeeplink is true, loading popup URL in current webview")
1611
+ DispatchQueue.main.async { [weak self] in
1612
+ self?.load(remote: url)
1613
+ }
1614
+ return nil
1615
+ }
1616
+
1617
+ // Otherwise, check if we should handle it externally
1618
+ // But since preventDeeplink is false here, we won't block it
1619
+ return nil
1620
+ }
1621
+
1622
+ return nil
1623
+ }
1624
+
1625
+ @available(iOS 15.0, *)
1626
+ public func webView(_ webView: WKWebView, requestGeolocationPermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
1627
+ print("[InAppBrowser] Geolocation permission requested for origin: \(origin.host)")
1628
+
1629
+ // Grant geolocation permission automatically for openWebView
1630
+ // This allows websites to access location when opened with openWebView
1631
+ decisionHandler(.grant)
1632
+ }
1633
+ }
1634
+
1635
+ // MARK: - Host Blocking Utilities
1636
+ extension WKWebViewController {
1637
+
1638
+ /// Checks if a host should be blocked based on the configured blocked hosts patterns
1639
+ /// - Parameter host: The host to check
1640
+ /// - Returns: true if the host should be blocked, false otherwise
1641
+ private func shouldBlockHost(_ host: String) -> Bool {
1642
+ guard !host.isEmpty else { return false }
1643
+
1644
+ let normalizedHost = host.lowercased()
1645
+
1646
+ return blockedHosts.contains { blockPattern in
1647
+ return matchesBlockPattern(host: normalizedHost, pattern: blockPattern.lowercased())
1648
+ }
1649
+ }
1650
+
1651
+ /// Matches a host against a blocking pattern (supports wildcards)
1652
+ /// - Parameters:
1653
+ /// - host: The normalized host to check
1654
+ /// - pattern: The normalized blocking pattern
1655
+ /// - Returns: true if the host matches the pattern
1656
+ private func matchesBlockPattern(host: String, pattern: String) -> Bool {
1657
+ guard !pattern.isEmpty else { return false }
1658
+
1659
+ // Exact match - fastest check first
1660
+ if host == pattern {
1661
+ return true
1662
+ }
1663
+
1664
+ // No wildcards - already checked exact match above
1665
+ guard pattern.contains("*") else {
1666
+ return false
1667
+ }
1668
+
1669
+ // Handle wildcard patterns
1670
+ if pattern.hasPrefix("*.") {
1671
+ return matchesWildcardDomain(host: host, pattern: pattern)
1672
+ } else if pattern.contains("*") {
1673
+ return matchesRegexPattern(host: host, pattern: pattern)
1674
+ }
1675
+
1676
+ return false
1677
+ }
1678
+
1679
+ /// Handles simple subdomain wildcard patterns like "*.example.com"
1680
+ /// - Parameters:
1681
+ /// - host: The host to check
1682
+ /// - pattern: The wildcard pattern starting with "*."
1683
+ /// - Returns: true if the host matches the wildcard domain
1684
+ private func matchesWildcardDomain(host: String, pattern: String) -> Bool {
1685
+ let domain = String(pattern.dropFirst(2)) // Remove "*."
1686
+
1687
+ guard !domain.isEmpty else { return false }
1688
+
1689
+ // Match exact domain or any subdomain
1690
+ return host == domain || host.hasSuffix("." + domain)
1691
+ }
1692
+
1693
+ /// Handles complex regex patterns with multiple wildcards
1694
+ /// - Parameters:
1695
+ /// - host: The host to check
1696
+ /// - pattern: The pattern with wildcards to convert to regex
1697
+ /// - Returns: true if the host matches the regex pattern
1698
+ private func matchesRegexPattern(host: String, pattern: String) -> Bool {
1699
+ // Escape everything, then re-enable '*' as a wildcard
1700
+ let escaped = NSRegularExpression.escapedPattern(for: pattern)
1701
+ let wildcardEnabled = escaped.replacingOccurrences(of: "\\*", with: ".*")
1702
+ let regexPattern = "^\(wildcardEnabled)$"
1703
+
1704
+ do {
1705
+ let regex = try NSRegularExpression(pattern: regexPattern, options: [])
1706
+ let range = NSRange(location: 0, length: host.utf16.count)
1707
+ return regex.firstMatch(in: host, options: [], range: range) != nil
1708
+ } catch {
1709
+ print("[InAppBrowser] Invalid regex pattern '\(regexPattern)': \(error)")
1710
+ return false
1711
+ }
1712
+ }
1713
+ }
1714
+
1715
+ // MARK: - WKNavigationDelegate
1716
+ extension WKWebViewController: WKNavigationDelegate {
1717
+ internal func injectPreShowScript() {
1718
+ if preShowSemaphore != nil {
1719
+ return
1720
+ }
1721
+
1722
+ // Safely construct script template with proper escaping
1723
+ let userScript = self.preShowScript ?? ""
1724
+
1725
+ // Build script using safe concatenation to avoid multi-line string issues
1726
+ let scriptTemplate = [
1727
+ "async function preShowFunction() {",
1728
+ userScript,
1729
+ "}",
1730
+ "preShowFunction().then(",
1731
+ " () => window.webkit.messageHandlers.preShowScriptSuccess.postMessage({})",
1732
+ ").catch(",
1733
+ " err => {",
1734
+ " console.error('Preshow error', err);",
1735
+ " window.webkit.messageHandlers.preShowScriptError.postMessage(JSON.stringify(err, Object.getOwnPropertyNames(err)));",
1736
+ " }",
1737
+ ")"
1738
+ ]
1739
+
1740
+ let script = scriptTemplate.joined(separator: "\n")
1741
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript script: \(script)")
1742
+
1743
+ self.preShowSemaphore = DispatchSemaphore(value: 0)
1744
+ self.executeScript(script: script) // this will run on the main thread
1745
+
1746
+ defer {
1747
+ self.preShowSemaphore = nil
1748
+ self.preShowError = nil
1749
+ }
1750
+
1751
+ if self.preShowSemaphore?.wait(timeout: .now() + 10) == .timedOut {
1752
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript running for over 10 seconds. The plugin will not wait any longer!")
1753
+ return
1754
+ }
1755
+
1756
+ // "async function preShowFunction() {\n" +
1757
+ // self.preShowScript + "\n" +
1758
+ // "};\n" +
1759
+ // "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
1760
+
1761
+ }
1762
+
1763
+ public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
1764
+ updateBarButtonItems()
1765
+ self.progressView?.progress = 0
1766
+ if let u = webView.url {
1767
+ self.url = u
1768
+ delegate?.webViewController?(self, didStart: u)
1769
+ }
1770
+ }
1771
+ public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
1772
+ if !didpageInit && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
1773
+ // Only inject preShowScript if it wasn't already injected at document start
1774
+ let shouldInjectScript = self.preShowScript != nil &&
1775
+ !self.preShowScript!.isEmpty &&
1776
+ self.preShowScriptInjectionTime != "documentStart"
1777
+
1778
+ if shouldInjectScript {
1779
+ // injectPreShowScript will block, don't execute on the main thread
1780
+ DispatchQueue.global(qos: .userInitiated).async {
1781
+ self.injectPreShowScript()
1782
+ DispatchQueue.main.async { [weak self] in
1783
+ self?.capBrowserPlugin?.presentView()
1784
+ }
1785
+ }
1786
+ } else {
1787
+ self.capBrowserPlugin?.presentView()
1788
+ }
1789
+ } else if self.preShowScript != nil &&
1790
+ !self.preShowScript!.isEmpty &&
1791
+ self.capBrowserPlugin?.isPresentAfterPageLoad == true &&
1792
+ self.preShowScriptInjectionTime != "documentStart" {
1793
+ // Only inject if not already injected at document start
1794
+ DispatchQueue.global(qos: .userInitiated).async {
1795
+ self.injectPreShowScript()
1796
+ }
1797
+ }
1798
+
1799
+ // Apply text zoom if set
1800
+ if let zoom = self.textZoom {
1801
+ applyTextZoom(zoom)
1802
+ }
1803
+
1804
+ didpageInit = true
1805
+ updateBarButtonItems()
1806
+ self.progressView?.progress = 0
1807
+ if let url = webView.url {
1808
+ self.url = url
1809
+ delegate?.webViewController?(self, didFinish: url)
1810
+ }
1811
+ self.injectJavaScriptInterface()
1812
+ self.capBrowserPlugin?.notifyListeners("browserPageLoaded", data: [:])
1813
+ }
1814
+
1815
+ public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
1816
+ updateBarButtonItems()
1817
+ self.progressView?.progress = 0
1818
+ if let url = webView.url {
1819
+ self.url = url
1820
+ delegate?.webViewController?(self, didFail: url, withError: error)
1821
+ }
1822
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1823
+ }
1824
+
1825
+ public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
1826
+ updateBarButtonItems()
1827
+ self.progressView?.progress = 0
1828
+ if let url = webView.url {
1829
+ self.url = url
1830
+ delegate?.webViewController?(self, didFail: url, withError: error)
1831
+ }
1832
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1833
+ }
1834
+
1835
+ public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
1836
+ if let credentials = credentials,
1837
+ challenge.protectionSpace.receivesCredentialSecurely,
1838
+ 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) {
1839
+ let urlCredential = URLCredential(user: credentials.username, password: credentials.password, persistence: .none)
1840
+ completionHandler(.useCredential, urlCredential)
1841
+ } else if let bypassedSSLHosts = bypassedSSLHosts, bypassedSSLHosts.contains(challenge.protectionSpace.host) {
1842
+ let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
1843
+ completionHandler(.useCredential, credential)
1844
+ } else {
1845
+ guard self.ignoreUntrustedSSLError else {
1846
+ completionHandler(.performDefaultHandling, nil)
1847
+ return
1848
+ }
1849
+ /* allows to open links with self-signed certificates
1850
+ Follow Apple's guidelines https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication
1851
+ */
1852
+ guard let serverTrust = challenge.protectionSpace.serverTrust else {
1853
+ completionHandler(.useCredential, nil)
1854
+ return
1855
+ }
1856
+ let credential = URLCredential(trust: serverTrust)
1857
+ completionHandler(.useCredential, credential)
1858
+ }
1859
+ self.injectJavaScriptInterface()
1860
+ }
1861
+
1862
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
1863
+ var actionPolicy: WKNavigationActionPolicy = self.preventDeeplink ? .preventDeeplinkActionPolicy : .allow
1864
+
1865
+ guard let url = navigationAction.request.url else {
1866
+ print("[InAppBrowser] Cannot determine URL from navigationAction")
1867
+ decisionHandler(actionPolicy)
1868
+ return
1869
+ }
1870
+
1871
+ if url.absoluteString.contains("apps.apple.com") {
1872
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
1873
+ decisionHandler(.cancel)
1874
+ return
1875
+ }
1876
+
1877
+ if !self.allowsFileURL, url.isFileURL {
1878
+ print("[InAppBrowser] Cannot handle file URLs")
1879
+ decisionHandler(.cancel)
1880
+ return
1881
+ }
1882
+
1883
+ // Defer the rest of the logic until the async external-app handling checks completes.
1884
+ handleURLWithApp(url, targetFrame: navigationAction.targetFrame) { [weak self] openedExternally in
1885
+ guard let self else {
1886
+ decisionHandler(.cancel)
1887
+ return
1888
+ }
1889
+
1890
+ if openedExternally {
1891
+ decisionHandler(.cancel)
1892
+ return
1893
+ }
1894
+
1895
+ let host = url.host ?? ""
1896
+
1897
+ if host == self.source?.url?.host,
1898
+ let cookies = self.availableCookies,
1899
+ !self.checkRequestCookies(navigationAction.request, cookies: cookies) {
1900
+ self.load(remote: url)
1901
+ decisionHandler(.cancel)
1902
+ return
1903
+ }
1904
+
1905
+ if self.shouldBlockHost(host) {
1906
+ print("[InAppBrowser] Blocked host detected: \(host)")
1907
+ self.capBrowserPlugin?.notifyListeners("urlChangeEvent", data: ["url": url.absoluteString])
1908
+ decisionHandler(.cancel)
1909
+ return
1910
+ }
1911
+
1912
+ if let navigationType = NavigationType(rawValue: navigationAction.navigationType.rawValue),
1913
+ let result = self.delegate?.webViewController?(self, decidePolicy: url, navigationType: navigationType) {
1914
+ actionPolicy = result ? .allow : .cancel
1915
+ }
1916
+
1917
+ self.injectJavaScriptInterface()
1918
+ decisionHandler(actionPolicy)
1919
+ }
1920
+ }
1921
+
1922
+ // MARK: - Dimension Management
1923
+
1924
+ /// Apply custom dimensions to the view if specified
1925
+ open func applyCustomDimensions() {
1926
+ guard let navigationController = navigationController else { return }
1927
+
1928
+ // Apply custom dimensions if both width and height are specified
1929
+ if let width = customWidth, let height = customHeight {
1930
+ let x = customX ?? 0
1931
+ let y = customY ?? 0
1932
+
1933
+ // Set the frame for the navigation controller's view
1934
+ navigationController.view.frame = CGRect(x: x, y: y, width: width, height: height)
1935
+ }
1936
+ // If only height is specified, use fullscreen width
1937
+ else if let height = customHeight, customWidth == nil {
1938
+ let x = customX ?? 0
1939
+ let y = customY ?? 0
1940
+ let screenWidth = UIScreen.main.bounds.width
1941
+
1942
+ // Set the frame with fullscreen width and custom height
1943
+ navigationController.view.frame = CGRect(x: x, y: y, width: screenWidth, height: height)
1944
+ }
1945
+ // Otherwise, use default fullscreen behavior (no action needed)
1946
+ }
1947
+
1948
+ /// Update dimensions at runtime
1949
+ open func updateDimensions(width: CGFloat?, height: CGFloat?, x: CGFloat?, y: CGFloat?) {
1950
+ // Update stored dimensions
1951
+ if let width = width {
1952
+ customWidth = width
1953
+ }
1954
+ if let height = height {
1955
+ customHeight = height
1956
+ }
1957
+ if let x = x {
1958
+ customX = x
1959
+ }
1960
+ if let y = y {
1961
+ customY = y
1962
+ }
1963
+
1964
+ // Apply the new dimensions
1965
+ applyCustomDimensions()
1966
+ }
1967
+ }
1968
+
1969
+ class BlockBarButtonItem: UIBarButtonItem {
1970
+
1971
+ var block: ((WKWebViewController) -> Void)?
1972
+ }
1973
+
1974
+ /// Custom view that passes touches outside a target frame to the underlying view
1975
+ class PassThroughView: UIView {
1976
+ var targetFrame: CGRect?
1977
+
1978
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
1979
+ // If we have a target frame and the touch is outside it, pass through
1980
+ if let frame = targetFrame {
1981
+ if !frame.contains(point) {
1982
+ return nil // Pass through to underlying views
1983
+ }
1984
+ }
1985
+
1986
+ // Otherwise, handle normally
1987
+ return super.hitTest(point, with: event)
1988
+ }
1989
+ }
1990
+
1991
+ extension WKNavigationActionPolicy {
1992
+ static let preventDeeplinkActionPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!
1993
+ }
1994
+
1995
+ class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
1996
+ weak var delegate: WKScriptMessageHandler?
1997
+
1998
+ init(_ delegate: WKScriptMessageHandler) {
1999
+ self.delegate = delegate
2000
+ super.init()
2001
+ }
2002
+
2003
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
2004
+ self.delegate?.userContentController(userContentController, didReceive: message)
2005
+ }
2006
+ }