@capgo/inappbrowser 7.16.18 → 7.16.19

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.
@@ -0,0 +1,1608 @@
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
+ open var hasDynamicTitle = false
91
+ open var source: WKWebSource?
92
+ /// use `source` instead
93
+ open internal(set) var url: URL?
94
+ open var tintColor: UIColor?
95
+ open var allowsFileURL = true
96
+ open var delegate: WKWebViewControllerDelegate?
97
+ open var bypassedSSLHosts: [String]?
98
+ open var cookies: [HTTPCookie]?
99
+ open var headers: [String: String]?
100
+ open var capBrowserPlugin: InAppBrowserPlugin?
101
+ var shareDisclaimer: [String: Any]?
102
+ var shareSubject: String?
103
+ var didpageInit = false
104
+ var viewHeightLandscape: CGFloat?
105
+ var viewHeightPortrait: CGFloat?
106
+ var currentViewHeight: CGFloat?
107
+ open var closeModal = false
108
+ open var closeModalTitle = ""
109
+ open var closeModalDescription = ""
110
+ open var closeModalOk = ""
111
+ open var closeModalCancel = ""
112
+ open var ignoreUntrustedSSLError = false
113
+ open var enableGooglePaySupport = false
114
+ var viewWasPresented = false
115
+ var preventDeeplink: Bool = false
116
+ var blankNavigationTab: Bool = false
117
+ var capacitorStatusBar: UIView?
118
+ var enabledSafeBottomMargin: Bool = false
119
+
120
+ internal var preShowSemaphore: DispatchSemaphore?
121
+ internal var preShowError: String?
122
+
123
+ func setHeaders(headers: [String: String]) {
124
+ self.headers = headers
125
+ let lowercasedHeaders = headers.mapKeys { $0.lowercased() }
126
+ let userAgent = lowercasedHeaders["user-agent"]
127
+ self.headers?.removeValue(forKey: "User-Agent")
128
+ self.headers?.removeValue(forKey: "user-agent")
129
+
130
+ if let userAgent = userAgent {
131
+ self.customUserAgent = userAgent
132
+ }
133
+ }
134
+
135
+ func setPreventDeeplink(preventDeeplink: Bool) {
136
+ self.preventDeeplink = preventDeeplink
137
+ }
138
+
139
+ internal var customUserAgent: String? {
140
+ didSet {
141
+ guard let agent = userAgent else {
142
+ return
143
+ }
144
+ webView?.customUserAgent = agent
145
+ }
146
+ }
147
+
148
+ open var userAgent: String? {
149
+ didSet {
150
+ guard let originalUserAgent = originalUserAgent, let userAgent = userAgent else {
151
+ return
152
+ }
153
+ webView?.customUserAgent = [originalUserAgent, userAgent].joined(separator: " ")
154
+ }
155
+ }
156
+
157
+ open var pureUserAgent: String? {
158
+ didSet {
159
+ guard let agent = pureUserAgent else {
160
+ return
161
+ }
162
+ webView?.customUserAgent = agent
163
+ }
164
+ }
165
+
166
+ open var websiteTitleInNavigationBar = true
167
+ open var doneBarButtonItemPosition: NavigationBarPosition = .right
168
+ open var showArrowAsClose = false
169
+ open var preShowScript: String?
170
+ open var leftNavigationBarItemTypes: [BarButtonItemType] = []
171
+ open var rightNavigaionBarItemTypes: [BarButtonItemType] = []
172
+
173
+ // Status bar style to be applied
174
+ open var statusBarStyle: UIStatusBarStyle = .default
175
+
176
+ // Status bar background view
177
+ private var statusBarBackgroundView: UIView?
178
+
179
+ // Status bar height
180
+ private var statusBarHeight: CGFloat {
181
+ return UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
182
+ }
183
+
184
+ // Make status bar background with colored view underneath
185
+ open func setupStatusBarBackground(color: UIColor) {
186
+ // Remove any existing status bar view
187
+ statusBarBackgroundView?.removeFromSuperview()
188
+
189
+ // Create a new view to cover both status bar and navigation bar
190
+ statusBarBackgroundView = UIView()
191
+
192
+ if let navView = navigationController?.view {
193
+ // Add to back of view hierarchy
194
+ navView.insertSubview(statusBarBackgroundView!, at: 0)
195
+ statusBarBackgroundView?.translatesAutoresizingMaskIntoConstraints = false
196
+
197
+ // Calculate total height - status bar + navigation bar
198
+ let navBarHeight = navigationController?.navigationBar.frame.height ?? 44
199
+ let totalHeight = statusBarHeight + navBarHeight
200
+
201
+ // Position from top of screen to bottom of navigation bar
202
+ NSLayoutConstraint.activate([
203
+ statusBarBackgroundView!.topAnchor.constraint(equalTo: navView.topAnchor),
204
+ statusBarBackgroundView!.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
205
+ statusBarBackgroundView!.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
206
+ statusBarBackgroundView!.heightAnchor.constraint(equalToConstant: totalHeight)
207
+ ])
208
+
209
+ // Set background color
210
+ statusBarBackgroundView?.backgroundColor = color
211
+
212
+ // Make navigation bar transparent to show our view underneath
213
+ navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
214
+ navigationController?.navigationBar.shadowImage = UIImage()
215
+ navigationController?.navigationBar.isTranslucent = true
216
+ }
217
+ }
218
+
219
+ // Override to use our custom status bar style
220
+ override open var preferredStatusBarStyle: UIStatusBarStyle {
221
+ return statusBarStyle
222
+ }
223
+
224
+ // Force status bar style update when needed
225
+ open func updateStatusBarStyle() {
226
+ setNeedsStatusBarAppearanceUpdate()
227
+ }
228
+
229
+ open var backBarButtonItemImage: UIImage?
230
+ open var forwardBarButtonItemImage: UIImage?
231
+ open var reloadBarButtonItemImage: UIImage?
232
+ open var stopBarButtonItemImage: UIImage?
233
+ open var activityBarButtonItemImage: UIImage?
234
+
235
+ open var buttonNearDoneIcon: UIImage?
236
+
237
+ fileprivate var webView: WKWebView?
238
+ fileprivate var progressView: UIProgressView?
239
+
240
+ fileprivate var previousNavigationBarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
241
+ fileprivate var previousToolbarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
242
+
243
+ fileprivate var originalUserAgent: String?
244
+
245
+ fileprivate lazy var backBarButtonItem: UIBarButtonItem = {
246
+ let navBackImage = UIImage(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
247
+ let barButtonItem = UIBarButtonItem(image: navBackImage, style: .plain, target: self, action: #selector(backDidClick(sender:)))
248
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
249
+ barButtonItem.tintColor = tintColor
250
+ }
251
+ return barButtonItem
252
+ }()
253
+
254
+ fileprivate lazy var forwardBarButtonItem: UIBarButtonItem = {
255
+ let forwardImage = UIImage(systemName: "chevron.forward")?.withRenderingMode(.alwaysTemplate)
256
+ let barButtonItem = UIBarButtonItem(image: forwardImage, style: .plain, target: self, action: #selector(forwardDidClick(sender:)))
257
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
258
+ barButtonItem.tintColor = tintColor
259
+ }
260
+ return barButtonItem
261
+ }()
262
+
263
+ fileprivate lazy var reloadBarButtonItem: UIBarButtonItem = {
264
+ if let image = reloadBarButtonItemImage {
265
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(reloadDidClick(sender:)))
266
+ } else {
267
+ return UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reloadDidClick(sender:)))
268
+ }
269
+ }()
270
+
271
+ fileprivate lazy var stopBarButtonItem: UIBarButtonItem = {
272
+ if let image = stopBarButtonItemImage {
273
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(stopDidClick(sender:)))
274
+ } else {
275
+ return UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(stopDidClick(sender:)))
276
+ }
277
+ }()
278
+
279
+ fileprivate lazy var activityBarButtonItem: UIBarButtonItem = {
280
+ // Check if custom image is provided
281
+ if let image = activityBarButtonItemImage {
282
+ let button = UIBarButtonItem(image: image.withRenderingMode(.alwaysTemplate),
283
+ style: .plain,
284
+ target: self,
285
+ action: #selector(activityDidClick(sender:)))
286
+
287
+ // Apply tint from navigation bar or from tintColor property
288
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
289
+ button.tintColor = tintColor
290
+ }
291
+
292
+ print("[DEBUG] Created activity button with custom image")
293
+ return button
294
+ } else {
295
+ // Use system share icon
296
+ let button = UIBarButtonItem(barButtonSystemItem: .action,
297
+ target: self,
298
+ action: #selector(activityDidClick(sender:)))
299
+
300
+ // Apply tint from navigation bar or from tintColor property
301
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
302
+ button.tintColor = tintColor
303
+ }
304
+
305
+ print("[DEBUG] Created activity button with system action icon")
306
+ return button
307
+ }
308
+ }()
309
+
310
+ fileprivate lazy var doneBarButtonItem: UIBarButtonItem = {
311
+ if showArrowAsClose {
312
+ // Show chevron icon when showArrowAsClose is true (originally was arrow.left)
313
+ let chevronImage = UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate)
314
+ let barButtonItem = UIBarButtonItem(image: chevronImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
315
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
316
+ barButtonItem.tintColor = tintColor
317
+ }
318
+ return barButtonItem
319
+ } else {
320
+ // Show X icon by default
321
+ let xImage = UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate)
322
+ let barButtonItem = UIBarButtonItem(image: xImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
323
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
324
+ barButtonItem.tintColor = tintColor
325
+ }
326
+ return barButtonItem
327
+ }
328
+ }()
329
+
330
+ fileprivate lazy var flexibleSpaceBarButtonItem: UIBarButtonItem = {
331
+ return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
332
+ }()
333
+
334
+ fileprivate var credentials: WKWebViewCredentials?
335
+
336
+ var textZoom: Int?
337
+
338
+ var capableWebView: WKWebView? {
339
+ return webView
340
+ }
341
+
342
+ deinit {
343
+ webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
344
+ if websiteTitleInNavigationBar {
345
+ webView?.removeObserver(self, forKeyPath: titleKeyPath)
346
+ }
347
+ webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
348
+ }
349
+
350
+ override open func viewDidDisappear(_ animated: Bool) {
351
+ super.viewDidDisappear(animated)
352
+
353
+ if let capacitorStatusBar = capacitorStatusBar {
354
+ self.capBrowserPlugin?.bridge?.webView?.superview?.addSubview(capacitorStatusBar)
355
+ self.capBrowserPlugin?.bridge?.webView?.frame.origin.y = capacitorStatusBar.frame.height
356
+ }
357
+ }
358
+
359
+ override open func viewDidLoad() {
360
+ super.viewDidLoad()
361
+ if self.webView == nil {
362
+ self.initWebview()
363
+ }
364
+
365
+ // Force all buttons to use tint color
366
+ updateButtonTintColors()
367
+
368
+ // Extra call to ensure buttonNearDone is visible
369
+ if buttonNearDoneIcon != nil {
370
+ // Delay slightly to ensure navigation items are set up
371
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
372
+ self?.updateButtonTintColors()
373
+
374
+ // Force update UI if needed
375
+ self?.navigationController?.navigationBar.setNeedsLayout()
376
+ }
377
+ }
378
+ }
379
+
380
+ func updateButtonTintColors() {
381
+ // Ensure all button items use the navigation bar's tint color
382
+ if let tintColor = navigationController?.navigationBar.tintColor {
383
+ backBarButtonItem.tintColor = tintColor
384
+ forwardBarButtonItem.tintColor = tintColor
385
+ reloadBarButtonItem.tintColor = tintColor
386
+ stopBarButtonItem.tintColor = tintColor
387
+ activityBarButtonItem.tintColor = tintColor
388
+ doneBarButtonItem.tintColor = tintColor
389
+
390
+ // Update navigation items
391
+ if let leftItems = navigationItem.leftBarButtonItems {
392
+ for item in leftItems {
393
+ item.tintColor = tintColor
394
+ }
395
+ }
396
+
397
+ if let rightItems = navigationItem.rightBarButtonItems {
398
+ for item in rightItems {
399
+ item.tintColor = tintColor
400
+ }
401
+ }
402
+
403
+ // Create buttonNearDone button with the correct tint color if it doesn't already exist
404
+ if buttonNearDoneIcon != nil &&
405
+ navigationItem.rightBarButtonItems?.count == 1 &&
406
+ navigationItem.rightBarButtonItems?.first == doneBarButtonItem {
407
+
408
+ // Create a properly tinted button
409
+ let buttonItem = UIBarButtonItem(image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
410
+ style: .plain,
411
+ target: self,
412
+ action: #selector(buttonNearDoneDidClick))
413
+ buttonItem.tintColor = tintColor
414
+
415
+ // Add it to right items
416
+ navigationItem.rightBarButtonItems?.append(buttonItem)
417
+ }
418
+ }
419
+ }
420
+
421
+ override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
422
+ super.traitCollectionDidChange(previousTraitCollection)
423
+
424
+ // Update colors when appearance changes
425
+ if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
426
+ // Update tint colors
427
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
428
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
429
+
430
+ if let navBar = navigationController?.navigationBar {
431
+ if navBar.backgroundColor == UIColor.black || navBar.backgroundColor == UIColor.white {
432
+ navBar.backgroundColor = isDarkMode ? UIColor.black : UIColor.white
433
+ navBar.tintColor = textColor
434
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
435
+
436
+ // Update all buttons
437
+ updateButtonTintColors()
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ open func setCredentials(credentials: WKWebViewCredentials?) {
444
+ self.credentials = credentials
445
+ }
446
+
447
+ // Method to send a message from Swift to JavaScript
448
+ open func postMessageToJS(message: [String: Any]) {
449
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: message, options: []),
450
+ let jsonString = String(data: jsonData, encoding: .utf8) else {
451
+ print("[InAppBrowser] Failed to serialize message to JSON")
452
+ return
453
+ }
454
+
455
+ // Safely build the script to avoid any potential issues
456
+ let script = "window.dispatchEvent(new CustomEvent('messageFromNative', { detail: \(jsonString) }));"
457
+
458
+ DispatchQueue.main.async {
459
+ self.webView?.evaluateJavaScript(script) { _, error in
460
+ if let error = error {
461
+ print("[InAppBrowser] JavaScript evaluation error in postMessageToJS: \(error)")
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ // Method to receive messages from JavaScript
468
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
469
+ if message.name == "messageHandler" {
470
+ if let messageBody = message.body as? [String: Any] {
471
+ print("Received message from JavaScript:", messageBody)
472
+ self.capBrowserPlugin?.notifyListeners("messageFromWebview", data: messageBody)
473
+ } else {
474
+ print("Received non-dictionary message from JavaScript:", message.body)
475
+ self.capBrowserPlugin?.notifyListeners("messageFromWebview", data: ["rawMessage": String(describing: message.body)])
476
+ }
477
+ } else if message.name == "preShowScriptSuccess" {
478
+ guard let semaphore = preShowSemaphore else {
479
+ print("[InAppBrowser - preShowScriptSuccess]: Semaphore not found")
480
+ return
481
+ }
482
+
483
+ semaphore.signal()
484
+ } else if message.name == "preShowScriptError" {
485
+ guard let semaphore = preShowSemaphore else {
486
+ print("[InAppBrowser - preShowScriptError]: Semaphore not found")
487
+ return
488
+ }
489
+ print("[InAppBrowser - preShowScriptError]: Error!!!!")
490
+ semaphore.signal()
491
+ } else if message.name == "close" {
492
+ closeView()
493
+ } else if message.name == "magicPrint" {
494
+ if let webView = self.webView {
495
+ let printController = UIPrintInteractionController.shared
496
+
497
+ let printInfo = UIPrintInfo(dictionary: nil)
498
+ printInfo.outputType = .general
499
+ printInfo.jobName = "Print Job"
500
+
501
+ printController.printInfo = printInfo
502
+ printController.printFormatter = webView.viewPrintFormatter()
503
+
504
+ printController.present(animated: true, completionHandler: nil)
505
+ }
506
+ }
507
+ }
508
+
509
+ func injectJavaScriptInterface() {
510
+ let script = """
511
+ if (!window.mobileApp) {
512
+ window.mobileApp = {
513
+ postMessage: function(message) {
514
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.messageHandler) {
515
+ window.webkit.messageHandlers.messageHandler.postMessage(message);
516
+ }
517
+ },
518
+ close: function() {
519
+ window.webkit.messageHandlers.close.postMessage(null);
520
+ }
521
+ };
522
+ }
523
+ """
524
+ DispatchQueue.main.async {
525
+ self.webView?.evaluateJavaScript(script) { result, error in
526
+ if let error = error {
527
+ print("JavaScript evaluation error: \(error)")
528
+ } else if let result = result {
529
+ print("JavaScript result: \(result)")
530
+ } else {
531
+ print("JavaScript executed with no result")
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ open func initWebview(isInspectable: Bool = true) {
538
+ self.view.backgroundColor = UIColor.white
539
+
540
+ self.extendedLayoutIncludesOpaqueBars = true
541
+ self.edgesForExtendedLayout = [.bottom]
542
+
543
+ let webConfiguration = WKWebViewConfiguration()
544
+ let userContentController = WKUserContentController()
545
+ userContentController.add(self, name: "messageHandler")
546
+ userContentController.add(self, name: "preShowScriptError")
547
+ userContentController.add(self, name: "preShowScriptSuccess")
548
+ userContentController.add(self, name: "close")
549
+ userContentController.add(self, name: "magicPrint")
550
+
551
+ // Inject JavaScript to override window.print
552
+ let script = WKUserScript(
553
+ source: """
554
+ window.print = function() {
555
+ window.webkit.messageHandlers.magicPrint.postMessage('magicPrint');
556
+ };
557
+ """,
558
+ injectionTime: .atDocumentStart,
559
+ forMainFrameOnly: false
560
+ )
561
+ userContentController.addUserScript(script)
562
+
563
+ webConfiguration.allowsInlineMediaPlayback = true
564
+ webConfiguration.userContentController = userContentController
565
+ webConfiguration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
566
+ webConfiguration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
567
+
568
+ // Enhanced configuration for Google Pay support (only when enabled)
569
+ if enableGooglePaySupport {
570
+ print("[InAppBrowser] Enabling Google Pay support features for iOS")
571
+
572
+ // Allow arbitrary loads in web views for Payment Request API
573
+ webConfiguration.setValue(true, forKey: "allowsArbitraryLoads")
574
+
575
+ // Enable JavaScript popup support for Google Pay
576
+ webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
577
+
578
+ // Inject Google Pay support script
579
+ let googlePayScript = WKUserScript(
580
+ source: """
581
+ console.log('[InAppBrowser] Injecting Google Pay support for iOS');
582
+
583
+ // Enhanced window.open for Google Pay
584
+ (function() {
585
+ const originalWindowOpen = window.open;
586
+ window.open = function(url, target, features) {
587
+ console.log('[InAppBrowser iOS] Enhanced window.open called:', url, target, features);
588
+
589
+ // For Google Pay URLs, handle popup properly
590
+ if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
591
+ console.log('[InAppBrowser iOS] Google Pay popup detected');
592
+ return originalWindowOpen.call(window, url, target || '_blank', features);
593
+ }
594
+
595
+ return originalWindowOpen.call(window, url, target, features);
596
+ };
597
+
598
+ // Add Cross-Origin-Opener-Policy meta tag if not present
599
+ if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
600
+ const meta = document.createElement('meta');
601
+ meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
602
+ meta.setAttribute('content', 'same-origin-allow-popups');
603
+ if (document.head) {
604
+ document.head.appendChild(meta);
605
+ console.log('[InAppBrowser iOS] Added Cross-Origin-Opener-Policy meta tag');
606
+ }
607
+ }
608
+
609
+ console.log('[InAppBrowser iOS] Google Pay support enhancements complete');
610
+ })();
611
+ """,
612
+ injectionTime: .atDocumentStart,
613
+ forMainFrameOnly: false
614
+ )
615
+ userContentController.addUserScript(googlePayScript)
616
+ }
617
+
618
+ let webView = WKWebView(frame: .zero, configuration: webConfiguration)
619
+
620
+ // if webView.responds(to: Selector(("setInspectable:"))) {
621
+ // // Fix: https://stackoverflow.com/questions/76216183/how-to-debug-wkwebview-in-ios-16-4-1-using-xcode-14-2/76603043#76603043
622
+ // webView.perform(Selector(("setInspectable:")), with: isInspectable)
623
+ // }
624
+
625
+ if #available(iOS 16.4, *) {
626
+ webView.isInspectable = true
627
+ } else {
628
+ // Fallback on earlier versions
629
+ }
630
+
631
+ // First add the webView to view hierarchy
632
+ self.view.addSubview(webView)
633
+
634
+ // Then set up constraints
635
+ webView.translatesAutoresizingMaskIntoConstraints = false
636
+ var bottomPadding = self.view.bottomAnchor
637
+
638
+ if self.enabledSafeBottomMargin {
639
+ bottomPadding = self.view.safeAreaLayoutGuide.bottomAnchor
640
+ }
641
+
642
+ NSLayoutConstraint.activate([
643
+ webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
644
+ webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
645
+ webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
646
+ webView.bottomAnchor.constraint(equalTo: bottomPadding)
647
+ ])
648
+
649
+ webView.uiDelegate = self
650
+ webView.navigationDelegate = self
651
+
652
+ webView.allowsBackForwardNavigationGestures = true
653
+ webView.isMultipleTouchEnabled = true
654
+
655
+ webView.addObserver(self, forKeyPath: estimatedProgressKeyPath, options: .new, context: nil)
656
+ if websiteTitleInNavigationBar {
657
+ webView.addObserver(self, forKeyPath: titleKeyPath, options: .new, context: nil)
658
+ }
659
+ webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
660
+
661
+ if !self.blankNavigationTab {
662
+ self.view.addSubview(webView)
663
+ // Then set up constraints
664
+ webView.translatesAutoresizingMaskIntoConstraints = false
665
+ }
666
+ self.webView = webView
667
+
668
+ self.webView?.customUserAgent = self.customUserAgent ?? self.userAgent ?? self.originalUserAgent
669
+
670
+ self.navigationItem.title = self.navigationItem.title ?? self.source?.absoluteString
671
+
672
+ if let navigation = self.navigationController {
673
+ self.previousNavigationBarState = (navigation.navigationBar.tintColor, navigation.navigationBar.isHidden)
674
+ self.previousToolbarState = (navigation.toolbar.tintColor, navigation.toolbar.isHidden)
675
+ }
676
+
677
+ if let s = self.source {
678
+ self.load(source: s)
679
+ } else {
680
+ print("[\(type(of: self))][Error] Invalid url")
681
+ }
682
+ }
683
+
684
+ open func setupViewElements() {
685
+ self.setUpProgressView()
686
+ self.setUpConstraints()
687
+ self.setUpNavigationBarAppearance()
688
+ self.addBarButtonItems()
689
+ self.updateBarButtonItems()
690
+ }
691
+
692
+ @objc func restateViewHeight() {
693
+ var bottomPadding = CGFloat(0.0)
694
+ var topPadding = CGFloat(0.0)
695
+ if #available(iOS 11.0, *) {
696
+ let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
697
+ bottomPadding = window?.safeAreaInsets.bottom ?? 0.0
698
+ topPadding = window?.safeAreaInsets.top ?? 0.0
699
+ }
700
+ if UIDevice.current.orientation.isPortrait {
701
+ // Don't force toolbar visibility
702
+ if self.viewHeightPortrait == nil {
703
+ self.viewHeightPortrait = self.view.safeAreaLayoutGuide.layoutFrame.size.height
704
+ self.viewHeightPortrait! += bottomPadding
705
+ if self.navigationController?.navigationBar.isHidden == true {
706
+ self.viewHeightPortrait! += topPadding
707
+ }
708
+ }
709
+ self.currentViewHeight = self.viewHeightPortrait
710
+ } else if UIDevice.current.orientation.isLandscape {
711
+ // Don't force toolbar visibility
712
+ if self.viewHeightLandscape == nil {
713
+ self.viewHeightLandscape = self.view.safeAreaLayoutGuide.layoutFrame.size.height
714
+ self.viewHeightLandscape! += bottomPadding
715
+ if self.navigationController?.navigationBar.isHidden == true {
716
+ self.viewHeightLandscape! += topPadding
717
+ }
718
+ }
719
+ self.currentViewHeight = self.viewHeightLandscape
720
+ }
721
+ }
722
+
723
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
724
+ // self.view.frame.size.height = self.currentViewHeight!
725
+ }
726
+
727
+ override open func viewWillLayoutSubviews() {
728
+ restateViewHeight()
729
+ // Don't override frame height when enabledSafeBottomMargin is true, as it would override our constraints
730
+ if self.currentViewHeight != nil && !self.enabledSafeBottomMargin {
731
+ self.view.frame.size.height = self.currentViewHeight!
732
+ }
733
+ }
734
+
735
+ override open func viewWillAppear(_ animated: Bool) {
736
+ super.viewWillAppear(animated)
737
+ if !self.viewWasPresented {
738
+ self.setupViewElements()
739
+ setUpState()
740
+ self.viewWasPresented = true
741
+ }
742
+
743
+ // Force update button appearances
744
+ updateButtonTintColors()
745
+
746
+ // Ensure status bar appearance is correct when view appears
747
+ // Make sure we have the latest tint color
748
+ if let tintColor = self.tintColor {
749
+ // Update the status bar background if needed
750
+ if let navController = navigationController, let backgroundColor = navController.navigationBar.backgroundColor ?? statusBarBackgroundView?.backgroundColor {
751
+ setupStatusBarBackground(color: backgroundColor)
752
+ } else {
753
+ setupStatusBarBackground(color: UIColor.white)
754
+ }
755
+ }
756
+
757
+ // Update status bar style
758
+ updateStatusBarStyle()
759
+
760
+ // Special handling for blank toolbar mode
761
+ if blankNavigationTab && statusBarBackgroundView != nil {
762
+ if let color = statusBarBackgroundView?.backgroundColor {
763
+ // Set view color to match status bar
764
+ view.backgroundColor = color
765
+ }
766
+ }
767
+ }
768
+
769
+ override open func viewDidAppear(_ animated: Bool) {
770
+ super.viewDidAppear(animated)
771
+
772
+ // Force add buttonNearDone if it's not visible yet
773
+ if buttonNearDoneIcon != nil {
774
+ // Check if button already exists in the navigation bar
775
+ let buttonExists = navigationItem.rightBarButtonItems?.contains { item in
776
+ return item.action == #selector(buttonNearDoneDidClick)
777
+ } ?? false
778
+
779
+ if !buttonExists {
780
+ // Create and add the button directly
781
+ let buttonItem = UIBarButtonItem(
782
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
783
+ style: .plain,
784
+ target: self,
785
+ action: #selector(buttonNearDoneDidClick)
786
+ )
787
+
788
+ // Apply tint color
789
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
790
+ buttonItem.tintColor = tintColor
791
+ }
792
+
793
+ // Add to right items
794
+ if navigationItem.rightBarButtonItems == nil {
795
+ navigationItem.rightBarButtonItems = [buttonItem]
796
+ } else {
797
+ var items = navigationItem.rightBarButtonItems ?? []
798
+ items.append(buttonItem)
799
+ navigationItem.rightBarButtonItems = items
800
+ }
801
+
802
+ print("[DEBUG] Force added buttonNearDone in viewDidAppear")
803
+ }
804
+ }
805
+ }
806
+
807
+ override open func viewWillDisappear(_ animated: Bool) {
808
+ super.viewWillDisappear(animated)
809
+ rollbackState()
810
+ }
811
+
812
+ override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
813
+ switch keyPath {
814
+ case estimatedProgressKeyPath?:
815
+ DispatchQueue.main.async {
816
+ guard let estimatedProgress = self.webView?.estimatedProgress else {
817
+ return
818
+ }
819
+ self.progressView?.alpha = 1
820
+ self.progressView?.setProgress(Float(estimatedProgress), animated: true)
821
+
822
+ if estimatedProgress >= 1.0 {
823
+ UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: {
824
+ self.progressView?.alpha = 0
825
+ }, completion: {
826
+ _ in
827
+ self.progressView?.setProgress(0, animated: false)
828
+ })
829
+ }
830
+ }
831
+ case titleKeyPath?:
832
+ if self.hasDynamicTitle {
833
+ self.navigationItem.title = webView?.url?.host
834
+ }
835
+ case "URL":
836
+
837
+ self.capBrowserPlugin?.notifyListeners("urlChangeEvent", data: ["url": webView?.url?.absoluteString ?? ""])
838
+ self.injectJavaScriptInterface()
839
+ default:
840
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
841
+ }
842
+ }
843
+ }
844
+
845
+ // MARK: - Public Methods
846
+ public extension WKWebViewController {
847
+
848
+ func load(source s: WKWebSource) {
849
+ switch s {
850
+ case .remote(let url):
851
+ self.load(remote: url)
852
+ case .file(let url, access: let access):
853
+ self.load(file: url, access: access)
854
+ case .string(let str, base: let base):
855
+ self.load(string: str, base: base)
856
+ }
857
+ }
858
+
859
+ func load(remote: URL) {
860
+ DispatchQueue.main.async {
861
+ self.webView?.load(self.createRequest(url: remote))
862
+ }
863
+ }
864
+
865
+ func load(file: URL, access: URL) {
866
+ webView?.loadFileURL(file, allowingReadAccessTo: access)
867
+ }
868
+
869
+ func load(string: String, base: URL? = nil) {
870
+ webView?.loadHTMLString(string, baseURL: base)
871
+ }
872
+
873
+ func goBackToFirstPage() {
874
+ if let firstPageItem = webView?.backForwardList.backList.first {
875
+ webView?.go(to: firstPageItem)
876
+ }
877
+ }
878
+ func reload() {
879
+ webView?.reload()
880
+ }
881
+
882
+ func executeScript(script: String, completion: ((Any?, Error?) -> Void)? = nil) {
883
+ DispatchQueue.main.async { [weak self] in
884
+ self?.webView?.evaluateJavaScript(script, completionHandler: completion)
885
+ }
886
+ }
887
+
888
+ func applyTextZoom(_ zoomPercent: Int) {
889
+ let script = """
890
+ document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust = '\(zoomPercent)%';
891
+ document.getElementsByTagName('body')[0].style.textSizeAdjust = '\(zoomPercent)%';
892
+ """
893
+
894
+ executeScript(script: script)
895
+ }
896
+
897
+ open func cleanupWebView() {
898
+ guard let webView = self.webView else { return }
899
+ webView.stopLoading()
900
+ // Break delegate callbacks early
901
+ webView.navigationDelegate = nil
902
+ webView.uiDelegate = nil
903
+ webView.loadHTMLString("", baseURL: nil)
904
+
905
+ webView.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
906
+ if websiteTitleInNavigationBar {
907
+ webView.removeObserver(self, forKeyPath: titleKeyPath)
908
+ }
909
+ webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
910
+
911
+ webView.configuration.userContentController.removeAllUserScripts()
912
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "messageHandler")
913
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "close")
914
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptSuccess")
915
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptError")
916
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "magicPrint")
917
+
918
+ webView.removeFromSuperview()
919
+ // Also clean progress bar view if present
920
+ progressView?.removeFromSuperview()
921
+ progressView = nil
922
+ self.webView = nil
923
+ }
924
+ }
925
+
926
+ // MARK: - Fileprivate Methods
927
+ fileprivate extension WKWebViewController {
928
+ var availableCookies: [HTTPCookie]? {
929
+ return cookies?.filter {
930
+ cookie in
931
+ var result = true
932
+ let url = self.source?.remoteURL
933
+ if let host = url?.host, !cookie.domain.hasSuffix(host) {
934
+ result = false
935
+ }
936
+ if cookie.isSecure && url?.scheme != "https" {
937
+ result = false
938
+ }
939
+
940
+ return result
941
+ }
942
+ }
943
+ func createRequest(url: URL) -> URLRequest {
944
+ var request = URLRequest(url: url)
945
+
946
+ // Set up headers
947
+ if let headers = headers {
948
+ for (field, value) in headers {
949
+ request.addValue(value, forHTTPHeaderField: field)
950
+ }
951
+ }
952
+
953
+ // Set up Cookies
954
+ if let cookies = availableCookies, let value = HTTPCookie.requestHeaderFields(with: cookies)[cookieKey] {
955
+ request.addValue(value, forHTTPHeaderField: cookieKey)
956
+ }
957
+
958
+ return request
959
+ }
960
+
961
+ func setUpProgressView() {
962
+ let progressView = UIProgressView(progressViewStyle: .default)
963
+ progressView.trackTintColor = UIColor(white: 1, alpha: 0)
964
+ self.progressView = progressView
965
+ // updateProgressViewFrame()
966
+ }
967
+
968
+ func setUpConstraints() {
969
+ if !(self.navigationController?.navigationBar.isHidden)! {
970
+ self.progressView?.frame.origin.y = CGFloat((self.navigationController?.navigationBar.frame.height)!)
971
+ self.navigationController?.navigationBar.addSubview(self.progressView!)
972
+ }
973
+ }
974
+
975
+ func addBarButtonItems() {
976
+ func barButtonItem(_ type: BarButtonItemType) -> UIBarButtonItem? {
977
+ switch type {
978
+ case .back:
979
+ return backBarButtonItem
980
+ case .forward:
981
+ return forwardBarButtonItem
982
+ case .reload:
983
+ return reloadBarButtonItem
984
+ case .stop:
985
+ return stopBarButtonItem
986
+ case .activity:
987
+ return activityBarButtonItem
988
+ case .done:
989
+ return doneBarButtonItem
990
+ case .flexibleSpace:
991
+ return flexibleSpaceBarButtonItem
992
+ case .custom(let icon, let title, let action):
993
+ let item: BlockBarButtonItem
994
+ if let icon = icon {
995
+ item = BlockBarButtonItem(image: icon, style: .plain, target: self, action: #selector(customDidClick(sender:)))
996
+ } else {
997
+ item = BlockBarButtonItem(title: title, style: .plain, target: self, action: #selector(customDidClick(sender:)))
998
+ }
999
+ item.block = action
1000
+ return item
1001
+ }
1002
+ }
1003
+
1004
+ switch doneBarButtonItemPosition {
1005
+ case .left:
1006
+ if !leftNavigationBarItemTypes.contains(where: { type in
1007
+ switch type {
1008
+ case .done:
1009
+ return true
1010
+ default:
1011
+ return false
1012
+ }
1013
+ }) {
1014
+ leftNavigationBarItemTypes.insert(.done, at: 0)
1015
+ }
1016
+ case .right:
1017
+ if !rightNavigaionBarItemTypes.contains(where: { type in
1018
+ switch type {
1019
+ case .done:
1020
+ return true
1021
+ default:
1022
+ return false
1023
+ }
1024
+ }) {
1025
+ rightNavigaionBarItemTypes.insert(.done, at: 0)
1026
+ }
1027
+ case .none:
1028
+ break
1029
+ }
1030
+
1031
+ navigationItem.leftBarButtonItems = leftNavigationBarItemTypes.map {
1032
+ barButtonItemType in
1033
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1034
+ return barButtonItem
1035
+ }
1036
+ return UIBarButtonItem()
1037
+ }
1038
+
1039
+ var rightBarButtons = rightNavigaionBarItemTypes.map {
1040
+ barButtonItemType in
1041
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1042
+ return barButtonItem
1043
+ }
1044
+ return UIBarButtonItem()
1045
+ }
1046
+
1047
+ // If we have buttonNearDoneIcon and the first (or only) right button is the done button
1048
+ if buttonNearDoneIcon != nil &&
1049
+ ((rightBarButtons.count == 1 && rightBarButtons[0] == doneBarButtonItem) ||
1050
+ (rightBarButtons.isEmpty && doneBarButtonItemPosition == .right) ||
1051
+ rightBarButtons.contains(doneBarButtonItem)) {
1052
+
1053
+ // Check if button already exists to avoid duplicates
1054
+ let buttonExists = rightBarButtons.contains { item in
1055
+ let selector = #selector(buttonNearDoneDidClick)
1056
+ return item.action == selector
1057
+ }
1058
+
1059
+ if !buttonExists {
1060
+ // Create button with proper tint and template rendering mode
1061
+ let buttonItem = UIBarButtonItem(
1062
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
1063
+ style: .plain,
1064
+ target: self,
1065
+ action: #selector(buttonNearDoneDidClick)
1066
+ )
1067
+
1068
+ // Apply tint from navigation bar or from tintColor property
1069
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
1070
+ buttonItem.tintColor = tintColor
1071
+ }
1072
+
1073
+ // Make sure the done button is there before adding this one
1074
+ if rightBarButtons.isEmpty && doneBarButtonItemPosition == .right {
1075
+ rightBarButtons.append(doneBarButtonItem)
1076
+ }
1077
+
1078
+ // Add the button
1079
+ rightBarButtons.append(buttonItem)
1080
+
1081
+ print("[DEBUG] Added buttonNearDone to right bar buttons, icon: \(String(describing: buttonNearDoneIcon))")
1082
+ } else {
1083
+ print("[DEBUG] buttonNearDone already exists in right bar buttons")
1084
+ }
1085
+ }
1086
+
1087
+ navigationItem.rightBarButtonItems = rightBarButtons
1088
+
1089
+ // After all buttons are set up, apply tint color
1090
+ updateButtonTintColors()
1091
+ }
1092
+
1093
+ func updateBarButtonItems() {
1094
+ // Update navigation buttons (completely separate from close button)
1095
+ backBarButtonItem.isEnabled = webView?.canGoBack ?? false
1096
+ forwardBarButtonItem.isEnabled = webView?.canGoForward ?? false
1097
+
1098
+ let updateReloadBarButtonItem: (UIBarButtonItem, Bool) -> UIBarButtonItem = {
1099
+ [unowned self] barButtonItem, isLoading in
1100
+ switch barButtonItem {
1101
+ case self.reloadBarButtonItem:
1102
+ fallthrough
1103
+ case self.stopBarButtonItem:
1104
+ return isLoading ? self.stopBarButtonItem : self.reloadBarButtonItem
1105
+ default:
1106
+ break
1107
+ }
1108
+ return barButtonItem
1109
+ }
1110
+
1111
+ let isLoading = webView?.isLoading ?? false
1112
+ navigationItem.leftBarButtonItems = navigationItem.leftBarButtonItems?.map {
1113
+ barButtonItem -> UIBarButtonItem in
1114
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1115
+ }
1116
+
1117
+ navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems?.map {
1118
+ barButtonItem -> UIBarButtonItem in
1119
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
1120
+ }
1121
+ }
1122
+
1123
+ func setUpState() {
1124
+ navigationController?.setNavigationBarHidden(false, animated: true)
1125
+
1126
+ // Always hide toolbar since we never want it
1127
+ navigationController?.setToolbarHidden(true, animated: true)
1128
+
1129
+ // Set tint colors but don't override specific colors
1130
+ if tintColor == nil {
1131
+ // Use system appearance if no specific tint color is set
1132
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
1133
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
1134
+
1135
+ navigationController?.navigationBar.tintColor = textColor
1136
+ progressView?.progressTintColor = textColor
1137
+ } else {
1138
+ progressView?.progressTintColor = tintColor
1139
+ navigationController?.navigationBar.tintColor = tintColor
1140
+ }
1141
+ }
1142
+
1143
+ func rollbackState() {
1144
+ progressView?.progress = 0
1145
+
1146
+ navigationController?.navigationBar.tintColor = previousNavigationBarState.tintColor
1147
+
1148
+ navigationController?.setNavigationBarHidden(previousNavigationBarState.hidden, animated: true)
1149
+ }
1150
+
1151
+ func checkRequestCookies(_ request: URLRequest, cookies: [HTTPCookie]) -> Bool {
1152
+ if cookies.count <= 0 {
1153
+ return true
1154
+ }
1155
+ guard let headerFields = request.allHTTPHeaderFields, let cookieString = headerFields[cookieKey] else {
1156
+ return false
1157
+ }
1158
+
1159
+ let requestCookies = cookieString.components(separatedBy: ";").map {
1160
+ $0.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=", maxSplits: 1).map(String.init)
1161
+ }
1162
+
1163
+ var valid = false
1164
+ for cookie in cookies {
1165
+ valid = requestCookies.filter {
1166
+ $0[0] == cookie.name && $0[1] == cookie.value
1167
+ }.count > 0
1168
+ if !valid {
1169
+ break
1170
+ }
1171
+ }
1172
+ return valid
1173
+ }
1174
+
1175
+ func openURLWithApp(_ url: URL) -> Bool {
1176
+ let application = UIApplication.shared
1177
+ if application.canOpenURL(url) {
1178
+ application.open(url, options: [:], completionHandler: nil)
1179
+ return true
1180
+ }
1181
+
1182
+ return false
1183
+ }
1184
+
1185
+ func handleURLWithApp(_ url: URL, targetFrame: WKFrameInfo?) -> Bool {
1186
+ // If preventDeeplink is true, don't try to open URLs in external apps
1187
+ if self.preventDeeplink {
1188
+ return false
1189
+ }
1190
+
1191
+ let hosts = UrlsHandledByApp.hosts
1192
+ let schemes = UrlsHandledByApp.schemes
1193
+ let blank = UrlsHandledByApp.blank
1194
+
1195
+ var tryToOpenURLWithApp = false
1196
+
1197
+ // Handle all non-http(s) schemes by default
1198
+ if let scheme = url.scheme?.lowercased(), !scheme.hasPrefix("http") && !scheme.hasPrefix("file") {
1199
+ tryToOpenURLWithApp = true
1200
+ }
1201
+
1202
+ // Also handle specific hosts and schemes from UrlsHandledByApp
1203
+ if let host = url.host, hosts.contains(host) {
1204
+ tryToOpenURLWithApp = true
1205
+ }
1206
+ if let scheme = url.scheme, schemes.contains(scheme) {
1207
+ tryToOpenURLWithApp = true
1208
+ }
1209
+ if blank && targetFrame == nil {
1210
+ tryToOpenURLWithApp = true
1211
+ }
1212
+
1213
+ return tryToOpenURLWithApp ? openURLWithApp(url) : false
1214
+ }
1215
+
1216
+ @objc func backDidClick(sender: AnyObject) {
1217
+ // Only handle back navigation, not closing
1218
+ if webView?.canGoBack ?? false {
1219
+ webView?.goBack()
1220
+ }
1221
+ }
1222
+
1223
+ @objc func forwardDidClick(sender: AnyObject) {
1224
+ webView?.goForward()
1225
+ }
1226
+
1227
+ @objc func buttonNearDoneDidClick(sender: AnyObject) {
1228
+ self.capBrowserPlugin?.notifyListeners("buttonNearDoneClick", data: [:])
1229
+ }
1230
+
1231
+ @objc func reloadDidClick(sender: AnyObject) {
1232
+ webView?.stopLoading()
1233
+ if webView?.url != nil {
1234
+ webView?.reload()
1235
+ } else if let s = self.source {
1236
+ self.load(source: s)
1237
+ }
1238
+ }
1239
+
1240
+ @objc func stopDidClick(sender: AnyObject) {
1241
+ webView?.stopLoading()
1242
+ }
1243
+
1244
+ @objc func activityDidClick(sender: AnyObject) {
1245
+ print("[DEBUG] Activity button clicked, shareSubject: \(self.shareSubject ?? "nil")")
1246
+
1247
+ guard let s = self.source else {
1248
+ print("[DEBUG] Activity button: No source available")
1249
+ return
1250
+ }
1251
+
1252
+ let items: [Any]
1253
+ switch s {
1254
+ case .remote(let u):
1255
+ items = [u]
1256
+ case .file(let u, access: _):
1257
+ items = [u]
1258
+ case .string(let str, base: _):
1259
+ items = [str]
1260
+ }
1261
+ showDisclaimer(items: items, sender: sender)
1262
+ }
1263
+
1264
+ func showDisclaimer(items: [Any], sender: AnyObject) {
1265
+ // Show disclaimer dialog before sharing if shareDisclaimer is set
1266
+ if let disclaimer = self.shareDisclaimer, !disclaimer.isEmpty {
1267
+ // Create and show the alert
1268
+ let alert = UIAlertController(
1269
+ title: disclaimer["title"] as? String ?? "Title",
1270
+ message: disclaimer["message"] as? String ?? "Message",
1271
+ preferredStyle: UIAlertController.Style.alert)
1272
+
1273
+ // Add confirm button that continues with sharing
1274
+ alert.addAction(UIAlertAction(
1275
+ title: disclaimer["confirmBtn"] as? String ?? "Confirm",
1276
+ style: UIAlertAction.Style.default,
1277
+ handler: { _ in
1278
+ // Notify that confirm was clicked
1279
+ self.capBrowserPlugin?.notifyListeners("confirmBtnClicked", data: nil)
1280
+
1281
+ // Show the share dialog
1282
+ self.showShareSheet(items: items, sender: sender)
1283
+ }
1284
+ ))
1285
+
1286
+ // Add cancel button
1287
+ alert.addAction(UIAlertAction(
1288
+ title: disclaimer["cancelBtn"] as? String ?? "Cancel",
1289
+ style: UIAlertAction.Style.cancel,
1290
+ handler: nil
1291
+ ))
1292
+
1293
+ // Present the alert
1294
+ self.present(alert, animated: true, completion: nil)
1295
+ } else {
1296
+ // No disclaimer, directly show share sheet
1297
+ showShareSheet(items: items, sender: sender)
1298
+ }
1299
+ }
1300
+
1301
+ // Separated the actual sharing functionality
1302
+ private func showShareSheet(items: [Any], sender: AnyObject) {
1303
+ let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
1304
+ activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
1305
+ activityViewController.popoverPresentationController?.barButtonItem = (sender as! UIBarButtonItem)
1306
+ self.present(activityViewController, animated: true, completion: nil)
1307
+ }
1308
+
1309
+ func closeView () {
1310
+ var canDismiss = true
1311
+ if let url = self.source?.url {
1312
+ canDismiss = delegate?.webViewController?(self, canDismiss: url) ?? true
1313
+ }
1314
+ if canDismiss {
1315
+ let currentUrl = webView?.url?.absoluteString ?? ""
1316
+ cleanupWebView()
1317
+ self.capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1318
+ dismiss(animated: true, completion: nil)
1319
+ }
1320
+ }
1321
+
1322
+ @objc func doneDidClick(sender: AnyObject) {
1323
+ // check if closeModal is true, if true display alert before close
1324
+ if self.closeModal {
1325
+ let alert = UIAlertController(title: self.closeModalTitle, message: self.closeModalDescription, preferredStyle: UIAlertController.Style.alert)
1326
+ alert.addAction(UIAlertAction(title: self.closeModalOk, style: UIAlertAction.Style.default, handler: { _ in
1327
+ self.closeView()
1328
+ }))
1329
+ alert.addAction(UIAlertAction(title: self.closeModalCancel, style: UIAlertAction.Style.default, handler: nil))
1330
+ self.present(alert, animated: true, completion: nil)
1331
+ } else {
1332
+ self.closeView()
1333
+ }
1334
+
1335
+ }
1336
+
1337
+ @objc func customDidClick(sender: BlockBarButtonItem) {
1338
+ sender.block?(self)
1339
+ }
1340
+
1341
+ func canRotate() {}
1342
+
1343
+ func close() {
1344
+ let currentUrl = webView?.url?.absoluteString ?? ""
1345
+ cleanupWebView()
1346
+ capBrowserPlugin?.notifyListeners("closeEvent", data: ["url": currentUrl])
1347
+ dismiss(animated: true, completion: nil)
1348
+ }
1349
+
1350
+ open func setUpNavigationBarAppearance() {
1351
+ // Set up basic bar appearance
1352
+ if let navBar = navigationController?.navigationBar {
1353
+ // Make navigation bar transparent
1354
+ navBar.setBackgroundImage(UIImage(), for: .default)
1355
+ navBar.shadowImage = UIImage()
1356
+ navBar.isTranslucent = true
1357
+
1358
+ // Ensure tint colors are applied properly
1359
+ if navBar.tintColor == nil {
1360
+ navBar.tintColor = tintColor ?? .black
1361
+ }
1362
+
1363
+ // Ensure text colors are set
1364
+ if navBar.titleTextAttributes == nil {
1365
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor ?? .black]
1366
+ }
1367
+
1368
+ // Ensure the navigation bar buttons are properly visible
1369
+ for item in navBar.items ?? [] {
1370
+ for barButton in (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? []) {
1371
+ barButton.tintColor = tintColor ?? navBar.tintColor ?? .black
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ // Force button colors to update
1377
+ updateButtonTintColors()
1378
+ }
1379
+ }
1380
+
1381
+ // MARK: - WKUIDelegate
1382
+ extension WKWebViewController: WKUIDelegate {
1383
+ public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
1384
+ // Create a strong reference to the completion handler to ensure it's called
1385
+ let strongCompletionHandler = completionHandler
1386
+
1387
+ // Ensure UI updates are on the main thread
1388
+ DispatchQueue.main.async { [weak self] in
1389
+ guard let self = self else {
1390
+ // View controller was deallocated
1391
+ strongCompletionHandler()
1392
+ return
1393
+ }
1394
+
1395
+ // Check if view is available and ready for presentation
1396
+ guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
1397
+ print("[InAppBrowser] Cannot present alert - view not in window hierarchy or being dismissed")
1398
+ strongCompletionHandler()
1399
+ return
1400
+ }
1401
+
1402
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
1403
+ alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
1404
+ strongCompletionHandler()
1405
+ }))
1406
+
1407
+ // Try to present the alert
1408
+ do {
1409
+ self.present(alertController, animated: true, completion: nil)
1410
+ } catch {
1411
+ // This won't typically be triggered as present doesn't throw,
1412
+ // but adding as a safeguard
1413
+ print("[InAppBrowser] Error presenting alert: \(error)")
1414
+ strongCompletionHandler()
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ // MARK: - WKNavigationDelegate
1421
+ extension WKWebViewController: WKNavigationDelegate {
1422
+ internal func injectPreShowScript() {
1423
+ if preShowSemaphore != nil {
1424
+ return
1425
+ }
1426
+
1427
+ // Safely construct script template with proper escaping
1428
+ let userScript = self.preShowScript ?? ""
1429
+
1430
+ // Build script using safe concatenation to avoid multi-line string issues
1431
+ let scriptTemplate = [
1432
+ "async function preShowFunction() {",
1433
+ userScript,
1434
+ "}",
1435
+ "preShowFunction().then(",
1436
+ " () => window.webkit.messageHandlers.preShowScriptSuccess.postMessage({})",
1437
+ ").catch(",
1438
+ " err => {",
1439
+ " console.error('Preshow error', err);",
1440
+ " window.webkit.messageHandlers.preShowScriptError.postMessage(JSON.stringify(err, Object.getOwnPropertyNames(err)));",
1441
+ " }",
1442
+ ")"
1443
+ ]
1444
+
1445
+ let script = scriptTemplate.joined(separator: "\n")
1446
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript script: \(script)")
1447
+
1448
+ self.preShowSemaphore = DispatchSemaphore(value: 0)
1449
+ self.executeScript(script: script) // this will run on the main thread
1450
+
1451
+ defer {
1452
+ self.preShowSemaphore = nil
1453
+ self.preShowError = nil
1454
+ }
1455
+
1456
+ if self.preShowSemaphore?.wait(timeout: .now() + 10) == .timedOut {
1457
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript running for over 10 seconds. The plugin will not wait any longer!")
1458
+ return
1459
+ }
1460
+
1461
+ // "async function preShowFunction() {\n" +
1462
+ // self.preShowScript + "\n" +
1463
+ // "};\n" +
1464
+ // "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
1465
+
1466
+ }
1467
+
1468
+ public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
1469
+ updateBarButtonItems()
1470
+ self.progressView?.progress = 0
1471
+ if let u = webView.url {
1472
+ self.url = u
1473
+ delegate?.webViewController?(self, didStart: u)
1474
+ }
1475
+ }
1476
+ public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
1477
+ if !didpageInit && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
1478
+ // injectPreShowScript will block, don't execute on the main thread
1479
+ if self.preShowScript != nil && !self.preShowScript!.isEmpty {
1480
+ DispatchQueue.global(qos: .userInitiated).async {
1481
+ self.injectPreShowScript()
1482
+ DispatchQueue.main.async { [weak self] in
1483
+ self?.capBrowserPlugin?.presentView()
1484
+ }
1485
+ }
1486
+ } else {
1487
+ self.capBrowserPlugin?.presentView()
1488
+ }
1489
+ } else if self.preShowScript != nil && !self.preShowScript!.isEmpty && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
1490
+ DispatchQueue.global(qos: .userInitiated).async {
1491
+ self.injectPreShowScript()
1492
+ }
1493
+ }
1494
+
1495
+ // Apply text zoom if set
1496
+ if let zoom = self.textZoom {
1497
+ applyTextZoom(zoom)
1498
+ }
1499
+
1500
+ didpageInit = true
1501
+ updateBarButtonItems()
1502
+ self.progressView?.progress = 0
1503
+ if let url = webView.url {
1504
+ self.url = url
1505
+ delegate?.webViewController?(self, didFinish: url)
1506
+ }
1507
+ self.injectJavaScriptInterface()
1508
+ self.capBrowserPlugin?.notifyListeners("browserPageLoaded", data: [:])
1509
+ }
1510
+
1511
+ public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
1512
+ updateBarButtonItems()
1513
+ self.progressView?.progress = 0
1514
+ if let url = webView.url {
1515
+ self.url = url
1516
+ delegate?.webViewController?(self, didFail: url, withError: error)
1517
+ }
1518
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1519
+ }
1520
+
1521
+ public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
1522
+ updateBarButtonItems()
1523
+ self.progressView?.progress = 0
1524
+ if let url = webView.url {
1525
+ self.url = url
1526
+ delegate?.webViewController?(self, didFail: url, withError: error)
1527
+ }
1528
+ self.capBrowserPlugin?.notifyListeners("pageLoadError", data: [:])
1529
+ }
1530
+
1531
+ public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
1532
+ if let credentials = credentials,
1533
+ challenge.protectionSpace.receivesCredentialSecurely,
1534
+ 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) {
1535
+ let urlCredential = URLCredential(user: credentials.username, password: credentials.password, persistence: .none)
1536
+ completionHandler(.useCredential, urlCredential)
1537
+ } else if let bypassedSSLHosts = bypassedSSLHosts, bypassedSSLHosts.contains(challenge.protectionSpace.host) {
1538
+ let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
1539
+ completionHandler(.useCredential, credential)
1540
+ } else {
1541
+ guard self.ignoreUntrustedSSLError else {
1542
+ completionHandler(.performDefaultHandling, nil)
1543
+ return
1544
+ }
1545
+ /* allows to open links with self-signed certificates
1546
+ Follow Apple's guidelines https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication
1547
+ */
1548
+ guard let serverTrust = challenge.protectionSpace.serverTrust else {
1549
+ completionHandler(.useCredential, nil)
1550
+ return
1551
+ }
1552
+ let credential = URLCredential(trust: serverTrust)
1553
+ completionHandler(.useCredential, credential)
1554
+ }
1555
+ self.injectJavaScriptInterface()
1556
+ }
1557
+
1558
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
1559
+ var actionPolicy: WKNavigationActionPolicy = .allow
1560
+
1561
+ if self.preventDeeplink {
1562
+ actionPolicy = .preventDeeplinkActionPolicy
1563
+ }
1564
+
1565
+ guard let u = navigationAction.request.url else {
1566
+ decisionHandler(actionPolicy)
1567
+ return
1568
+ }
1569
+
1570
+ // Check if the URL is an App Store URL
1571
+ if u.absoluteString.contains("apps.apple.com") {
1572
+ UIApplication.shared.open(u, options: [:], completionHandler: nil)
1573
+ // Cancel the navigation in the web view
1574
+ decisionHandler(.cancel)
1575
+ return
1576
+ }
1577
+
1578
+ if !self.allowsFileURL && u.isFileURL {
1579
+ print("Cannot handle file URLs")
1580
+ decisionHandler(.cancel)
1581
+ return
1582
+ }
1583
+
1584
+ if handleURLWithApp(u, targetFrame: navigationAction.targetFrame) {
1585
+ actionPolicy = .cancel
1586
+ }
1587
+
1588
+ if u.host == self.source?.url?.host, let cookies = availableCookies, !checkRequestCookies(navigationAction.request, cookies: cookies) {
1589
+ self.load(remote: u)
1590
+ actionPolicy = .cancel
1591
+ }
1592
+
1593
+ if let navigationType = NavigationType(rawValue: navigationAction.navigationType.rawValue), let result = delegate?.webViewController?(self, decidePolicy: u, navigationType: navigationType) {
1594
+ actionPolicy = result ? .allow : .cancel
1595
+ }
1596
+ self.injectJavaScriptInterface()
1597
+ decisionHandler(actionPolicy)
1598
+ }
1599
+ }
1600
+
1601
+ class BlockBarButtonItem: UIBarButtonItem {
1602
+
1603
+ var block: ((WKWebViewController) -> Void)?
1604
+ }
1605
+
1606
+ extension WKNavigationActionPolicy {
1607
+ static let preventDeeplinkActionPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!
1608
+ }