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