@capgo/inappbrowser 7.16.17 → 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
|
+
}
|