@capgo/capacitor-inappbrowser 8.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CapgoCapacitorInappbrowser.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +1610 -0
  5. package/android/build.gradle +64 -0
  6. package/android/src/main/AndroidManifest.xml +22 -0
  7. package/android/src/main/assets/proxy-bridge.js +586 -0
  8. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ActiveWebViewSupport.java +10 -0
  9. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CustomSchemeInterceptSupport.java +51 -0
  10. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +1820 -0
  11. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/MobileAppBridgeScript.java +122 -0
  12. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/NativeProxyRule.java +128 -0
  13. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +725 -0
  14. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyBridge.java +93 -0
  15. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyRequestSupport.java +869 -0
  16. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyResponseRouting.java +35 -0
  17. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/SafeAreaInsetsSupport.java +28 -0
  18. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +42 -0
  19. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +6022 -0
  20. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewPermissionSupport.java +58 -0
  21. package/android/src/main/res/.gitkeep +0 -0
  22. package/android/src/main/res/drawable/arrow_back_disabled.xml +9 -0
  23. package/android/src/main/res/drawable/arrow_back_enabled.xml +9 -0
  24. package/android/src/main/res/drawable/arrow_forward_disabled.xml +9 -0
  25. package/android/src/main/res/drawable/arrow_forward_enabled.xml +9 -0
  26. package/android/src/main/res/drawable/ic_clear_24px.xml +9 -0
  27. package/android/src/main/res/drawable/ic_refresh.xml +9 -0
  28. package/android/src/main/res/drawable/ic_share.xml +10 -0
  29. package/android/src/main/res/layout/activity_browser.xml +32 -0
  30. package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
  31. package/android/src/main/res/layout/content_browser.xml +17 -0
  32. package/android/src/main/res/layout/tool_bar.xml +87 -0
  33. package/android/src/main/res/values/browser_theme.xml +3 -0
  34. package/android/src/main/res/values/colors.xml +5 -0
  35. package/android/src/main/res/values/dimens.xml +3 -0
  36. package/android/src/main/res/values/strings.xml +13 -0
  37. package/android/src/main/res/values/styles.xml +4 -0
  38. package/android/src/main/res/values/themes.xml +27 -0
  39. package/android/src/main/res/xml/file_paths.xml +14 -0
  40. package/dist/docs.json +3200 -0
  41. package/dist/esm/definitions.d.ts +1507 -0
  42. package/dist/esm/definitions.js +40 -0
  43. package/dist/esm/definitions.js.map +1 -0
  44. package/dist/esm/index.d.ts +6 -0
  45. package/dist/esm/index.js +102 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/web.d.ts +47 -0
  48. package/dist/esm/web.js +120 -0
  49. package/dist/esm/web.js.map +1 -0
  50. package/dist/plugin.cjs.js +270 -0
  51. package/dist/plugin.cjs.js.map +1 -0
  52. package/dist/plugin.js +273 -0
  53. package/dist/plugin.js.map +1 -0
  54. package/ios/Sources/InAppBrowserPlugin/Enums.swift +65 -0
  55. package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +1932 -0
  56. package/ios/Sources/InAppBrowserPlugin/ProxySchemeHandler.swift +1185 -0
  57. package/ios/Sources/InAppBrowserPlugin/WKWebView+SchemeHandling.swift +43 -0
  58. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +3032 -0
  59. package/package.json +139 -0
@@ -0,0 +1,3032 @@
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 QuickLook
11
+ import WebKit
12
+
13
+ private let estimatedProgressKeyPath = "estimatedProgress"
14
+ private let titleKeyPath = "title"
15
+ private let cookieKey = "Cookie"
16
+
17
+ enum CustomSchemeInterceptSupport {
18
+ static let standardHandledSchemes = ["tel", "mailto", "sms"]
19
+ private static let webSchemes = ["http", "https", "file"]
20
+
21
+ static func shouldEmitInterceptEvent(for url: URL) -> Bool {
22
+ guard let scheme = url.scheme?.lowercased(), !scheme.isEmpty else {
23
+ return false
24
+ }
25
+ return !webSchemes.contains(scheme) && !standardHandledSchemes.contains(scheme)
26
+ }
27
+ }
28
+
29
+ private struct UrlsHandledByApp {
30
+ static var hosts = ["itunes.apple.com"]
31
+ static var schemes = CustomSchemeInterceptSupport.standardHandledSchemes
32
+ }
33
+
34
+ private struct WKDownloadState {
35
+ let destinationURL: URL?
36
+ let mimeType: String?
37
+ let sourceURL: String?
38
+ }
39
+
40
+ private enum DownloadReservationStore {
41
+ static var paths: Set<String> = []
42
+ static let lock = NSLock()
43
+ }
44
+
45
+ public struct WKWebViewCredentials {
46
+ var username: String
47
+ var password: String
48
+ }
49
+
50
+ @objc public protocol WKWebViewControllerDelegate {
51
+ @objc optional func webViewController(_ controller: WKWebViewController, canDismiss url: URL) -> Bool
52
+
53
+ @objc optional func webViewController(_ controller: WKWebViewController, didStart url: URL)
54
+ @objc optional func webViewController(_ controller: WKWebViewController, didFinish url: URL)
55
+ @objc optional func webViewController(_ controller: WKWebViewController, didFail url: URL, withError error: Error)
56
+ @objc optional func webViewController(_ controller: WKWebViewController, decidePolicy url: URL, navigationType: NavigationType) -> Bool
57
+ }
58
+
59
+ extension Dictionary {
60
+ func mapKeys<T>(_ transform: (Key) throws -> T) rethrows -> [T: Value] {
61
+ var dictionary = [T: Value]()
62
+ for (key, value) in self {
63
+ dictionary[try transform(key)] = value
64
+ }
65
+ return dictionary
66
+ }
67
+ }
68
+
69
+ enum ConsoleMessageSupport {
70
+ static func normalizePayload(from data: [String: Any]) -> [String: Any] {
71
+ var payload: [String: Any] = [
72
+ "level": normalizedLevel(data["level"] as? String),
73
+ "message": normalizedMessage(data["message"])
74
+ ]
75
+
76
+ if let source = data["source"] as? String, !source.isEmpty {
77
+ payload["source"] = source
78
+ }
79
+
80
+ if let line = integerValue(from: data["line"]) {
81
+ payload["line"] = line
82
+ }
83
+
84
+ if let column = integerValue(from: data["column"]) {
85
+ payload["column"] = column
86
+ }
87
+
88
+ if let kind = data["kind"] as? String, !kind.isEmpty {
89
+ payload["kind"] = kind
90
+ }
91
+
92
+ return payload
93
+ }
94
+
95
+ static func captureScriptSource() -> String {
96
+ return """
97
+ (function() {
98
+ if (window.__capgoInAppBrowserConsoleCaptureInstalled) {
99
+ return;
100
+ }
101
+ window.__capgoInAppBrowserConsoleCaptureInstalled = true;
102
+
103
+ var nativeHandler =
104
+ window.webkit &&
105
+ window.webkit.messageHandlers &&
106
+ window.webkit.messageHandlers.consoleMessageHandler;
107
+
108
+ if (!nativeHandler) {
109
+ return;
110
+ }
111
+
112
+ var serialize = function(value) {
113
+ if (value instanceof Error) {
114
+ return value.stack || value.message || String(value);
115
+ }
116
+ if (typeof value === 'string') {
117
+ return value;
118
+ }
119
+ try {
120
+ return JSON.stringify(value);
121
+ } catch (_error) {
122
+ try {
123
+ return String(value);
124
+ } catch (_error2) {
125
+ return '[unserializable]';
126
+ }
127
+ }
128
+ };
129
+
130
+ var post = function(level, values, metadata) {
131
+ try {
132
+ var parts = Array.prototype.slice.call(values || []).map(serialize);
133
+ nativeHandler.postMessage({
134
+ level: level,
135
+ message: parts.join(' '),
136
+ source: metadata && metadata.source ? metadata.source : window.location.href,
137
+ line: metadata && typeof metadata.line === 'number' ? metadata.line : null,
138
+ column: metadata && typeof metadata.column === 'number' ? metadata.column : null,
139
+ kind: metadata && metadata.kind ? metadata.kind : 'console',
140
+ });
141
+ } catch (_error) {}
142
+ };
143
+
144
+ ['log', 'info', 'warn', 'error', 'debug'].forEach(function(level) {
145
+ var original = console[level] ? console[level].bind(console) : null;
146
+ console[level] = function() {
147
+ post(level, arguments, null);
148
+ if (original) {
149
+ return original.apply(console, arguments);
150
+ }
151
+ };
152
+ });
153
+
154
+ var originalAssert = console.assert ? console.assert.bind(console) : null;
155
+ console.assert = function(condition) {
156
+ if (!condition) {
157
+ var args = Array.prototype.slice.call(arguments, 1);
158
+ post('assert', args.length ? args : ['console.assert'], null);
159
+ }
160
+ if (originalAssert) {
161
+ return originalAssert.apply(console, arguments);
162
+ }
163
+ };
164
+
165
+ window.addEventListener('error', function(event) {
166
+ post(
167
+ 'error',
168
+ [event && event.message ? event.message : 'Uncaught error'],
169
+ {
170
+ source: event && event.filename ? event.filename : window.location.href,
171
+ line: event && typeof event.lineno === 'number' ? event.lineno : null,
172
+ column: event && typeof event.colno === 'number' ? event.colno : null,
173
+ kind: 'error',
174
+ }
175
+ );
176
+ });
177
+
178
+ window.addEventListener('unhandledrejection', function(event) {
179
+ var reason = event ? event.reason : null;
180
+ var message =
181
+ reason && reason.message
182
+ ? reason.message
183
+ : reason
184
+ ? serialize(reason)
185
+ : 'Unhandled promise rejection';
186
+ post('error', [message], { source: window.location.href, kind: 'unhandledrejection' });
187
+ });
188
+ })();
189
+ """
190
+ }
191
+
192
+ private static func normalizedLevel(_ value: String?) -> String {
193
+ let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
194
+ switch normalized {
195
+ case "log", "info", "warn", "error", "debug", "assert":
196
+ return normalized ?? "log"
197
+ default:
198
+ return "log"
199
+ }
200
+ }
201
+
202
+ private static func normalizedMessage(_ value: Any?) -> String {
203
+ switch value {
204
+ case let text as String:
205
+ return text
206
+ case let number as NSNumber:
207
+ return number.stringValue
208
+ case nil:
209
+ return ""
210
+ default:
211
+ return String(describing: value!)
212
+ }
213
+ }
214
+
215
+ private static func integerValue(from value: Any?) -> Int? {
216
+ switch value {
217
+ case let int as Int:
218
+ return int
219
+ case let number as NSNumber:
220
+ return number.intValue
221
+ case let text as String:
222
+ return Int(text)
223
+ default:
224
+ return nil
225
+ }
226
+ }
227
+ }
228
+
229
+ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
230
+
231
+ public init() {
232
+ super.init(nibName: nil, bundle: nil)
233
+ }
234
+
235
+ public required init?(coder aDecoder: NSCoder) {
236
+ super.init(coder: aDecoder)
237
+ }
238
+
239
+ public init(source: WKWebSource?, credentials: WKWebViewCredentials? = nil, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
240
+ super.init(nibName: nil, bundle: nil)
241
+ self.source = source
242
+ self.credentials = credentials
243
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
244
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
245
+ self.captureConsoleLogs = captureConsoleLogs
246
+ self.initWebview()
247
+ }
248
+
249
+ public init(url: URL, credentials: WKWebViewCredentials? = nil, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
250
+ super.init(nibName: nil, bundle: nil)
251
+ self.source = .remote(url)
252
+ self.credentials = credentials
253
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
254
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
255
+ self.captureConsoleLogs = captureConsoleLogs
256
+ self.initWebview()
257
+ }
258
+
259
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
260
+ super.init(nibName: nil, bundle: nil)
261
+ self.source = .remote(url)
262
+ self.credentials = credentials
263
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
264
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
265
+ self.captureConsoleLogs = captureConsoleLogs
266
+ self.setHeaders(headers: headers)
267
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
268
+ self.initWebview(isInspectable: isInspectable)
269
+ }
270
+
271
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, openBlankTargetInWebView: Bool = false) {
272
+ super.init(nibName: nil, bundle: nil)
273
+ self.blankNavigationTab = blankNavigationTab
274
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
275
+ self.enabledSafeTopMargin = enabledSafeTopMargin
276
+ self.source = .remote(url)
277
+ self.credentials = credentials
278
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
279
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
280
+ self.captureConsoleLogs = captureConsoleLogs
281
+ self.openBlankTargetInWebView = openBlankTargetInWebView
282
+ self.setHeaders(headers: headers)
283
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
284
+ self.initWebview(isInspectable: isInspectable)
285
+ }
286
+
287
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, openBlankTargetInWebView: Bool = false) {
288
+ super.init(nibName: nil, bundle: nil)
289
+ self.blankNavigationTab = blankNavigationTab
290
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
291
+ self.enabledSafeTopMargin = enabledSafeTopMargin
292
+ self.source = .remote(url)
293
+ self.credentials = credentials
294
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
295
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
296
+ self.captureConsoleLogs = captureConsoleLogs
297
+ self.openBlankTargetInWebView = openBlankTargetInWebView
298
+ self.setHeaders(headers: headers)
299
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
300
+ self.setBlockedHosts(blockedHosts: blockedHosts)
301
+ self.initWebview(isInspectable: isInspectable)
302
+ }
303
+
304
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String], allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, proxyRequests: Bool = false, proxySchemeHandler: ProxySchemeHandler? = nil, documentStartUserScripts: [String] = [], openBlankTargetInWebView: Bool = false) {
305
+ super.init(nibName: nil, bundle: nil)
306
+ self.blankNavigationTab = blankNavigationTab
307
+ self.enabledSafeBottomMargin = enabledSafeBottomMargin
308
+ self.enabledSafeTopMargin = enabledSafeTopMargin
309
+ self.source = .remote(url)
310
+ self.credentials = credentials
311
+ self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
312
+ self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
313
+ self.captureConsoleLogs = captureConsoleLogs
314
+ self.proxyRequests = proxyRequests
315
+ self.proxySchemeHandler = proxySchemeHandler
316
+ self.openBlankTargetInWebView = openBlankTargetInWebView
317
+ self.setHeaders(headers: headers)
318
+ self.setPreventDeeplink(preventDeeplink: preventDeeplink)
319
+ self.setBlockedHosts(blockedHosts: blockedHosts)
320
+ self.setAuthorizedAppLinks(authorizedAppLinks: authorizedAppLinks)
321
+ self.documentStartUserScripts = documentStartUserScripts
322
+ self.initWebview(isInspectable: isInspectable)
323
+ }
324
+
325
+ open var hasDynamicTitle = false
326
+ open var source: WKWebSource?
327
+ /// use `source` instead
328
+ open internal(set) var url: URL?
329
+ open var tintColor: UIColor?
330
+ open var allowsFileURL = true
331
+ open var allowWebViewJsVisibilityControl = false
332
+ open var allowScreenshotsFromWebPage = false
333
+ open var captureConsoleLogs = false
334
+ open var handleDownloads = false
335
+ open var delegate: WKWebViewControllerDelegate?
336
+ open var bypassedSSLHosts: [String]?
337
+ open var cookies: [HTTPCookie]?
338
+ open var headers: [String: String]?
339
+ open var httpMethod: String?
340
+ open var httpBody: String?
341
+ open var capBrowserPlugin: InAppBrowserPlugin?
342
+ var instanceId: String = ""
343
+ var shareDisclaimer: [String: Any]?
344
+ var shareSubject: String?
345
+ var didpageInit = false
346
+ var viewHeightLandscape: CGFloat?
347
+ var viewHeightPortrait: CGFloat?
348
+ var currentViewHeight: CGFloat?
349
+ open var closeModal = false
350
+ open var closeModalTitle = ""
351
+ open var closeModalDescription = ""
352
+ open var closeModalOk = ""
353
+ open var closeModalCancel = ""
354
+ open var closeModalURLPattern = "" {
355
+ didSet {
356
+ closeModalURLRegex = closeModalURLPattern.isEmpty ? nil : (try? NSRegularExpression(pattern: closeModalURLPattern))
357
+ }
358
+ }
359
+ private var closeModalURLRegex: NSRegularExpression?
360
+ open var ignoreUntrustedSSLError = false
361
+ open var enableGooglePaySupport = false
362
+ var viewWasPresented = false
363
+ var preventDeeplink: Bool = false
364
+ var openBlankTargetInWebView: Bool = false
365
+ var blankNavigationTab: Bool = false
366
+ var capacitorStatusBar: UIView?
367
+ var enabledSafeBottomMargin: Bool = false
368
+ var enabledSafeTopMargin: Bool = true
369
+ var blockedHosts: [String] = []
370
+ var authorizedAppLinks: [String] = []
371
+ var activeNativeNavigationForWebview: Bool = true
372
+ var disableOverscroll: Bool = false
373
+ var proxyRequests: Bool = false
374
+ var proxySchemeHandler: ProxySchemeHandler?
375
+ var initialWebConfiguration: WKWebViewConfiguration?
376
+ var waitsForPopupNavigation = false
377
+ var hiddenPopupWindow = false
378
+ var opensHidden = false
379
+
380
+ // Dimension properties
381
+ var customWidth: CGFloat?
382
+ var customHeight: CGFloat?
383
+ var customX: CGFloat?
384
+ var customY: CGFloat?
385
+
386
+ internal var preShowSemaphore: DispatchSemaphore?
387
+ internal var preShowError: String?
388
+ private var isWebViewInitialized = false
389
+ private var downloadStates: [ObjectIdentifier: WKDownloadState] = [:]
390
+ private var previewItemURL: URL?
391
+
392
+ func setHeaders(headers: [String: String]) {
393
+ self.headers = headers
394
+ let lowercasedHeaders = headers.mapKeys { $0.lowercased() }
395
+ let userAgent = lowercasedHeaders["user-agent"]
396
+ self.headers?.removeValue(forKey: "User-Agent")
397
+ self.headers?.removeValue(forKey: "user-agent")
398
+
399
+ if let userAgent = userAgent {
400
+ self.customUserAgent = userAgent
401
+ }
402
+ }
403
+
404
+ func setPreventDeeplink(preventDeeplink: Bool) {
405
+ self.preventDeeplink = preventDeeplink
406
+ }
407
+
408
+ func setBlockedHosts(blockedHosts: [String]) {
409
+ self.blockedHosts = blockedHosts
410
+ }
411
+
412
+ func setAuthorizedAppLinks(authorizedAppLinks: [String]) {
413
+ self.authorizedAppLinks = authorizedAppLinks
414
+ }
415
+
416
+ private func register(download: WKDownload, response: URLResponse?, sourceURL: String? = nil) {
417
+ download.delegate = self
418
+ downloadStates[ObjectIdentifier(download)] = WKDownloadState(
419
+ destinationURL: nil,
420
+ mimeType: response?.mimeType,
421
+ sourceURL: sourceURL ?? response?.url?.absoluteString
422
+ )
423
+ }
424
+
425
+ private func attachmentDisposition(_ response: URLResponse) -> String? {
426
+ guard let httpResponse = response as? HTTPURLResponse else {
427
+ return nil
428
+ }
429
+
430
+ return httpResponse.value(forHTTPHeaderField: "Content-Disposition")
431
+ }
432
+
433
+ private func attachmentDispositionType(_ response: URLResponse) -> String? {
434
+ guard let disposition = attachmentDisposition(response)?
435
+ .split(separator: ";", maxSplits: 1)
436
+ .first?
437
+ .trimmingCharacters(in: .whitespacesAndNewlines)
438
+ .lowercased(),
439
+ !disposition.isEmpty else {
440
+ return nil
441
+ }
442
+
443
+ return disposition
444
+ }
445
+
446
+ private func shouldInterceptDownload(for response: WKNavigationResponse) -> Bool {
447
+ guard handleDownloads else {
448
+ return false
449
+ }
450
+
451
+ if !response.canShowMIMEType {
452
+ return true
453
+ }
454
+
455
+ guard let disposition = attachmentDispositionType(response.response) else {
456
+ return false
457
+ }
458
+
459
+ return disposition == "attachment"
460
+ }
461
+
462
+ private func sanitizeDownloadFilename(_ suggestedFilename: String) -> String {
463
+ let invalidCharacters = CharacterSet(charactersIn: "/:\\?%*|\"<>")
464
+ let sanitized = suggestedFilename
465
+ .components(separatedBy: invalidCharacters)
466
+ .joined(separator: "_")
467
+ .trimmingCharacters(in: .whitespacesAndNewlines)
468
+
469
+ return sanitized.isEmpty ? "download" : sanitized
470
+ }
471
+
472
+ private func uniqueDownloadDestination(for suggestedFilename: String) throws -> URL {
473
+ let downloadsDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("InAppBrowserDownloads", isDirectory: true)
474
+ try FileManager.default.createDirectory(at: downloadsDirectory, withIntermediateDirectories: true)
475
+
476
+ let sanitizedFilename = sanitizeDownloadFilename(suggestedFilename)
477
+ let baseName = (sanitizedFilename as NSString).deletingPathExtension
478
+ let fileExtension = (sanitizedFilename as NSString).pathExtension
479
+
480
+ var candidateURL = downloadsDirectory.appendingPathComponent(sanitizedFilename)
481
+ var duplicateIndex = 1
482
+
483
+ while FileManager.default.fileExists(atPath: candidateURL.path) || !reserveDownloadDestination(candidateURL) {
484
+ let duplicateSuffix = "-\(duplicateIndex)"
485
+ let duplicateFilename = fileExtension.isEmpty
486
+ ? baseName + duplicateSuffix
487
+ : baseName + duplicateSuffix + "." + fileExtension
488
+ candidateURL = downloadsDirectory.appendingPathComponent(duplicateFilename)
489
+ duplicateIndex += 1
490
+ }
491
+
492
+ return candidateURL
493
+ }
494
+
495
+ private func reserveDownloadDestination(_ candidateURL: URL) -> Bool {
496
+ DownloadReservationStore.lock.lock()
497
+ defer { DownloadReservationStore.lock.unlock() }
498
+
499
+ let candidatePath = candidateURL.path
500
+ guard !DownloadReservationStore.paths.contains(candidatePath) else {
501
+ return false
502
+ }
503
+ DownloadReservationStore.paths.insert(candidatePath)
504
+ return true
505
+ }
506
+
507
+ private func releaseDownloadDestination(_ destinationURL: URL?) {
508
+ guard let destinationURL else {
509
+ return
510
+ }
511
+
512
+ DownloadReservationStore.lock.lock()
513
+ DownloadReservationStore.paths.remove(destinationURL.path)
514
+ DownloadReservationStore.lock.unlock()
515
+ }
516
+
517
+ private func normalizedMimeType(_ mimeType: String?, fileURL: URL) -> String? {
518
+ let candidate = mimeType?
519
+ .components(separatedBy: ";")
520
+ .first?
521
+ .trimmingCharacters(in: .whitespacesAndNewlines)
522
+ .lowercased()
523
+
524
+ let genericMimeTypes: Set<String> = [
525
+ "application/octet-stream",
526
+ "binary/octet-stream",
527
+ "application/download",
528
+ "application/x-download",
529
+ "application/binary",
530
+ "application/x-binary",
531
+ ]
532
+
533
+ if let candidate, !candidate.isEmpty, !genericMimeTypes.contains(candidate) {
534
+ return candidate
535
+ }
536
+
537
+ switch fileURL.pathExtension.lowercased() {
538
+ case "pdf":
539
+ return "application/pdf"
540
+ case "json":
541
+ return "application/json"
542
+ case "txt":
543
+ return "text/plain"
544
+ case "csv":
545
+ return "text/csv"
546
+ case "html", "htm":
547
+ return "text/html"
548
+ case "xhtml":
549
+ return "application/xhtml+xml"
550
+ case "png":
551
+ return "image/png"
552
+ case "jpg", "jpeg":
553
+ return "image/jpeg"
554
+ case "gif":
555
+ return "image/gif"
556
+ case "webp":
557
+ return "image/webp"
558
+ case "svg":
559
+ return "image/svg+xml"
560
+ default:
561
+ return nil
562
+ }
563
+ }
564
+
565
+ private func shouldPreviewDownloadedFile(_ fileURL: URL, mimeType: String?) -> Bool {
566
+ let activeExtensions: Set<String> = ["html", "htm", "xhtml", "svg"]
567
+ if activeExtensions.contains(fileURL.pathExtension.lowercased()) {
568
+ return false
569
+ }
570
+
571
+ guard let normalizedMimeType = normalizedMimeType(mimeType, fileURL: fileURL) else {
572
+ return false
573
+ }
574
+
575
+ switch normalizedMimeType {
576
+ case "text/html", "application/xhtml+xml", "image/svg+xml":
577
+ return false
578
+ default:
579
+ break
580
+ }
581
+
582
+ return normalizedMimeType == "application/pdf" ||
583
+ normalizedMimeType == "application/json" ||
584
+ normalizedMimeType.starts(with: "text/") ||
585
+ normalizedMimeType.starts(with: "image/")
586
+ }
587
+
588
+ private func emitDownloadCompleted(_ fileURL: URL, mimeType: String?, sourceURL: String?, handledBy: String) {
589
+ var data: [String: Any] = [
590
+ "fileName": fileURL.lastPathComponent,
591
+ "path": fileURL.path,
592
+ "localUrl": fileURL.absoluteString,
593
+ "handledBy": handledBy,
594
+ ]
595
+ if let sourceURL {
596
+ data["sourceUrl"] = sourceURL
597
+ }
598
+ if let mimeType {
599
+ data["mimeType"] = mimeType
600
+ }
601
+ emit("downloadCompleted", data: data)
602
+ }
603
+
604
+ private func emitDownloadFailed(sourceURL: String?, fileName: String? = nil, mimeType: String? = nil, error: String) {
605
+ var data: [String: Any] = ["error": error]
606
+ if let sourceURL {
607
+ data["sourceUrl"] = sourceURL
608
+ }
609
+ if let fileName {
610
+ data["fileName"] = fileName
611
+ }
612
+ if let mimeType {
613
+ data["mimeType"] = mimeType
614
+ }
615
+ emit("downloadFailed", data: data)
616
+ }
617
+
618
+ private func previewDownloadedFile(_ fileURL: URL, mimeType: String?, sourceURL: String?) {
619
+ DispatchQueue.main.async {
620
+ if self.allowsFileURL && self.shouldPreviewDownloadedFile(fileURL, mimeType: mimeType) {
621
+ let accessURL = fileURL.deletingLastPathComponent()
622
+ self.source = .file(fileURL, access: accessURL)
623
+ self.load(file: fileURL, access: accessURL)
624
+ self.emitDownloadCompleted(fileURL, mimeType: mimeType, sourceURL: sourceURL, handledBy: "inAppBrowser")
625
+ return
626
+ }
627
+
628
+ self.previewItemURL = fileURL
629
+ let previewController = QLPreviewController()
630
+ previewController.dataSource = self
631
+ previewController.delegate = self
632
+ self.present(previewController, animated: true)
633
+ self.emitDownloadCompleted(fileURL, mimeType: mimeType, sourceURL: sourceURL, handledBy: "systemPreview")
634
+ }
635
+ }
636
+
637
+ internal var customUserAgent: String? {
638
+ didSet {
639
+ guard let agent = userAgent else {
640
+ return
641
+ }
642
+ webView?.customUserAgent = agent
643
+ }
644
+ }
645
+
646
+ open var userAgent: String? {
647
+ didSet {
648
+ guard let originalUserAgent = originalUserAgent, let userAgent = userAgent else {
649
+ return
650
+ }
651
+ webView?.customUserAgent = [originalUserAgent, userAgent].joined(separator: " ")
652
+ }
653
+ }
654
+
655
+ open var pureUserAgent: String? {
656
+ didSet {
657
+ guard let agent = pureUserAgent else {
658
+ return
659
+ }
660
+ webView?.customUserAgent = agent
661
+ }
662
+ }
663
+
664
+ open var websiteTitleInNavigationBar = true
665
+ open var doneBarButtonItemPosition: NavigationBarPosition = .right
666
+ open var showArrowAsClose = false
667
+ open var documentStartUserScripts: [String] = []
668
+ open var preShowScript: String?
669
+ open var preShowScriptInjectionTime: String = "pageLoad" // "documentStart" or "pageLoad"
670
+ open var leftNavigationBarItemTypes: [BarButtonItemType] = []
671
+ open var rightNavigaionBarItemTypes: [BarButtonItemType] = []
672
+
673
+ // Status bar style to be applied
674
+ open var statusBarStyle: UIStatusBarStyle = .default
675
+
676
+ // Status bar background view
677
+ private var statusBarBackgroundView: UIView?
678
+
679
+ // Status bar height
680
+ private var statusBarHeight: CGFloat {
681
+ return UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
682
+ }
683
+
684
+ // Make status bar background with colored view underneath
685
+ open func setupStatusBarBackground(color: UIColor) {
686
+ // Remove any existing status bar view
687
+ statusBarBackgroundView?.removeFromSuperview()
688
+
689
+ // Create a new view to cover both status bar and navigation bar
690
+ statusBarBackgroundView = UIView()
691
+
692
+ if let navView = navigationController?.view {
693
+ // Add to back of view hierarchy
694
+ navView.insertSubview(statusBarBackgroundView!, at: 0)
695
+ statusBarBackgroundView?.translatesAutoresizingMaskIntoConstraints = false
696
+
697
+ // Calculate total height - status bar + navigation bar
698
+ let navBarHeight = navigationController?.navigationBar.frame.height ?? 44
699
+ let totalHeight = (navigationController?.view.safeAreaInsets.top ?? CGFloat(0)) + navBarHeight
700
+
701
+ // Position from top of screen to bottom of navigation bar
702
+ NSLayoutConstraint.activate([
703
+ statusBarBackgroundView!.topAnchor.constraint(equalTo: navView.topAnchor),
704
+ statusBarBackgroundView!.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
705
+ statusBarBackgroundView!.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
706
+ statusBarBackgroundView!.heightAnchor.constraint(equalToConstant: totalHeight)
707
+ ])
708
+
709
+ // Set background color
710
+ statusBarBackgroundView?.backgroundColor = color
711
+
712
+ // Make navigation bar transparent to show our view underneath
713
+ navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
714
+ navigationController?.navigationBar.shadowImage = UIImage()
715
+ navigationController?.navigationBar.isTranslucent = true
716
+ navigationController?.navigationBar.isTranslucent = true
717
+ }
718
+ }
719
+
720
+ // Override to use our custom status bar style
721
+ override open var preferredStatusBarStyle: UIStatusBarStyle {
722
+ return statusBarStyle
723
+ }
724
+
725
+ // Force status bar style update when needed
726
+ open func updateStatusBarStyle() {
727
+ setNeedsStatusBarAppearanceUpdate()
728
+ }
729
+
730
+ open var backBarButtonItemImage: UIImage?
731
+ open var forwardBarButtonItemImage: UIImage?
732
+ open var reloadBarButtonItemImage: UIImage?
733
+ open var stopBarButtonItemImage: UIImage?
734
+ open var activityBarButtonItemImage: UIImage?
735
+
736
+ open var buttonNearDoneIcon: UIImage?
737
+ open var showScreenshotButton = false
738
+
739
+ fileprivate var webView: WKWebView?
740
+ fileprivate var progressView: UIProgressView?
741
+
742
+ fileprivate var previousNavigationBarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
743
+ fileprivate var previousToolbarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
744
+
745
+ fileprivate var originalUserAgent: String?
746
+
747
+ fileprivate lazy var backBarButtonItem: UIBarButtonItem = {
748
+ let navBackImage = UIImage(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
749
+ let barButtonItem = UIBarButtonItem(image: navBackImage, style: .plain, target: self, action: #selector(backDidClick(sender:)))
750
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
751
+ barButtonItem.tintColor = tintColor
752
+ }
753
+ return barButtonItem
754
+ }()
755
+
756
+ fileprivate lazy var forwardBarButtonItem: UIBarButtonItem = {
757
+ let forwardImage = UIImage(systemName: "chevron.forward")?.withRenderingMode(.alwaysTemplate)
758
+ let barButtonItem = UIBarButtonItem(image: forwardImage, style: .plain, target: self, action: #selector(forwardDidClick(sender:)))
759
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
760
+ barButtonItem.tintColor = tintColor
761
+ }
762
+ return barButtonItem
763
+ }()
764
+
765
+ fileprivate lazy var reloadBarButtonItem: UIBarButtonItem = {
766
+ if let image = reloadBarButtonItemImage {
767
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(reloadDidClick(sender:)))
768
+ } else {
769
+ return UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reloadDidClick(sender:)))
770
+ }
771
+ }()
772
+
773
+ fileprivate lazy var stopBarButtonItem: UIBarButtonItem = {
774
+ if let image = stopBarButtonItemImage {
775
+ return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(stopDidClick(sender:)))
776
+ } else {
777
+ return UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(stopDidClick(sender:)))
778
+ }
779
+ }()
780
+
781
+ fileprivate lazy var activityBarButtonItem: UIBarButtonItem = {
782
+ // Check if custom image is provided
783
+ if let image = activityBarButtonItemImage {
784
+ let button = UIBarButtonItem(image: image.withRenderingMode(.alwaysTemplate),
785
+ style: .plain,
786
+ target: self,
787
+ action: #selector(activityDidClick(sender:)))
788
+
789
+ // Apply tint from navigation bar or from tintColor property
790
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
791
+ button.tintColor = tintColor
792
+ }
793
+
794
+ print("[DEBUG] Created activity button with custom image")
795
+ return button
796
+ } else {
797
+ // Use system share icon
798
+ let button = UIBarButtonItem(barButtonSystemItem: .action,
799
+ target: self,
800
+ action: #selector(activityDidClick(sender:)))
801
+
802
+ // Apply tint from navigation bar or from tintColor property
803
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
804
+ button.tintColor = tintColor
805
+ }
806
+
807
+ print("[DEBUG] Created activity button with system action icon")
808
+ return button
809
+ }
810
+ }()
811
+
812
+ fileprivate lazy var doneBarButtonItem: UIBarButtonItem = {
813
+ if showArrowAsClose {
814
+ // Show chevron icon when showArrowAsClose is true (originally was arrow.left)
815
+ let chevronImage = UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate)
816
+ let barButtonItem = UIBarButtonItem(image: chevronImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
817
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
818
+ barButtonItem.tintColor = tintColor
819
+ }
820
+ return barButtonItem
821
+ } else {
822
+ // Show X icon by default
823
+ let xImage = UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate)
824
+ let barButtonItem = UIBarButtonItem(image: xImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
825
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
826
+ barButtonItem.tintColor = tintColor
827
+ }
828
+ return barButtonItem
829
+ }
830
+ }()
831
+
832
+ fileprivate lazy var flexibleSpaceBarButtonItem: UIBarButtonItem = {
833
+ return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
834
+ }()
835
+
836
+ fileprivate var credentials: WKWebViewCredentials?
837
+
838
+ var textZoom: Int?
839
+
840
+ var capableWebView: WKWebView? {
841
+ return webView
842
+ }
843
+
844
+ deinit {
845
+ webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
846
+ if websiteTitleInNavigationBar {
847
+ webView?.removeObserver(self, forKeyPath: titleKeyPath)
848
+ }
849
+ webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
850
+ }
851
+
852
+ override open func viewDidDisappear(_ animated: Bool) {
853
+ super.viewDidDisappear(animated)
854
+
855
+ if self.isBeingDismissed || self.isMovingFromParent {
856
+ self.cleanupWebView()
857
+ }
858
+
859
+ if let capacitorStatusBar = capacitorStatusBar {
860
+ self.capBrowserPlugin?.bridge?.webView?.superview?.addSubview(capacitorStatusBar)
861
+ self.capBrowserPlugin?.bridge?.webView?.frame.origin.y = capacitorStatusBar.frame.height
862
+ }
863
+ }
864
+
865
+ override open func viewDidLoad() {
866
+ super.viewDidLoad()
867
+ if self.webView == nil {
868
+ self.initWebview()
869
+ }
870
+
871
+ // Apply navigation gestures setting
872
+ updateNavigationGestures()
873
+
874
+ // Force all buttons to use tint color
875
+ updateButtonTintColors()
876
+
877
+ // Extra call to ensure buttonNearDone is visible
878
+ if buttonNearDoneIcon != nil {
879
+ // Delay slightly to ensure navigation items are set up
880
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
881
+ self?.updateButtonTintColors()
882
+
883
+ // Force update UI if needed
884
+ self?.navigationController?.navigationBar.setNeedsLayout()
885
+ }
886
+ }
887
+ }
888
+
889
+ func updateButtonTintColors() {
890
+ // Ensure all button items use the navigation bar's tint color
891
+ if let tintColor = navigationController?.navigationBar.tintColor {
892
+ backBarButtonItem.tintColor = tintColor
893
+ forwardBarButtonItem.tintColor = tintColor
894
+ reloadBarButtonItem.tintColor = tintColor
895
+ stopBarButtonItem.tintColor = tintColor
896
+ activityBarButtonItem.tintColor = tintColor
897
+ doneBarButtonItem.tintColor = tintColor
898
+
899
+ // Update navigation items
900
+ if let leftItems = navigationItem.leftBarButtonItems {
901
+ for item in leftItems {
902
+ item.tintColor = tintColor
903
+ }
904
+ }
905
+
906
+ if let rightItems = navigationItem.rightBarButtonItems {
907
+ for item in rightItems {
908
+ item.tintColor = tintColor
909
+ }
910
+ }
911
+
912
+ // Create buttonNearDone button with the correct tint color if it doesn't already exist
913
+ if buttonNearDoneIcon != nil &&
914
+ navigationItem.rightBarButtonItems?.count == 1 &&
915
+ navigationItem.rightBarButtonItems?.first == doneBarButtonItem {
916
+
917
+ // Create a properly tinted button
918
+ let buttonItem = UIBarButtonItem(image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
919
+ style: .plain,
920
+ target: self,
921
+ action: #selector(buttonNearDoneDidClick))
922
+ buttonItem.tintColor = tintColor
923
+
924
+ // Add it to right items
925
+ navigationItem.rightBarButtonItems?.append(buttonItem)
926
+ }
927
+ }
928
+ }
929
+
930
+ override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
931
+ super.traitCollectionDidChange(previousTraitCollection)
932
+
933
+ // Update colors when appearance changes
934
+ if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
935
+ // Update tint colors
936
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
937
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
938
+
939
+ if let navBar = navigationController?.navigationBar {
940
+ if navBar.backgroundColor == UIColor.black || navBar.backgroundColor == UIColor.white {
941
+ navBar.backgroundColor = isDarkMode ? UIColor.black : UIColor.white
942
+ navBar.tintColor = textColor
943
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
944
+
945
+ // Update all buttons
946
+ updateButtonTintColors()
947
+ }
948
+ }
949
+ }
950
+ }
951
+
952
+ open func setCredentials(credentials: WKWebViewCredentials?) {
953
+ self.credentials = credentials
954
+ }
955
+
956
+ // Method to send a message from Swift to JavaScript
957
+ open func postMessageToJS(message: [String: Any]) {
958
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: message, options: []),
959
+ let jsonString = String(data: jsonData, encoding: .utf8) else {
960
+ print("[InAppBrowser] Failed to serialize message to JSON")
961
+ return
962
+ }
963
+
964
+ // Safely build the script to avoid any potential issues
965
+ let script = "window.dispatchEvent(new CustomEvent('messageFromNative', { detail: \(jsonString) }));"
966
+
967
+ DispatchQueue.main.async {
968
+ self.webView?.evaluateJavaScript(script) { _, error in
969
+ if let error = error {
970
+ print("[InAppBrowser] JavaScript evaluation error in postMessageToJS: \(error)")
971
+ }
972
+ }
973
+ }
974
+ }
975
+
976
+ private func payload(with data: [String: Any] = [:]) -> [String: Any] {
977
+ guard !instanceId.isEmpty else {
978
+ return data
979
+ }
980
+ var payload = data
981
+ payload["id"] = instanceId
982
+ return payload
983
+ }
984
+
985
+ private func emit(_ eventName: String, data: [String: Any] = [:]) {
986
+ capBrowserPlugin?.notifyListeners(eventName, data: payload(with: data))
987
+ }
988
+
989
+ private func jsonString(from object: Any) -> String? {
990
+ guard JSONSerialization.isValidJSONObject(object),
991
+ let data = try? JSONSerialization.data(withJSONObject: object),
992
+ let string = String(data: data, encoding: .utf8) else {
993
+ return nil
994
+ }
995
+ return string
996
+ }
997
+
998
+ private func resolveJavaScriptScreenshot(requestId: String, result: [String: Any]) {
999
+ guard let payload = jsonString(from: ["requestId": requestId, "result": result]) else {
1000
+ return
1001
+ }
1002
+ let script = "window.__capgoInAppBrowserResolveScreenshot(\(payload));"
1003
+ DispatchQueue.main.async {
1004
+ self.webView?.evaluateJavaScript(script, completionHandler: nil)
1005
+ }
1006
+ }
1007
+
1008
+ private func rejectJavaScriptScreenshot(requestId: String, message: String) {
1009
+ guard let payload = jsonString(from: ["requestId": requestId, "message": message]) else {
1010
+ return
1011
+ }
1012
+ let script = "window.__capgoInAppBrowserRejectScreenshot(\(payload));"
1013
+ DispatchQueue.main.async {
1014
+ self.webView?.evaluateJavaScript(script, completionHandler: nil)
1015
+ }
1016
+ }
1017
+
1018
+ func takeScreenshot(completion: @escaping (Result<[String: Any], Error>) -> Void) {
1019
+ DispatchQueue.main.async {
1020
+ guard let webView = self.webView else {
1021
+ completion(.failure(NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "WebView is not initialized"])))
1022
+ return
1023
+ }
1024
+
1025
+ let targetSize = webView.bounds.size
1026
+ guard targetSize.width > 0, targetSize.height > 0 else {
1027
+ completion(.failure(NSError(domain: "InAppBrowser", code: 2, userInfo: [NSLocalizedDescriptionKey: "WebView is not ready to capture a screenshot"])))
1028
+ return
1029
+ }
1030
+
1031
+ let configuration = WKSnapshotConfiguration()
1032
+ configuration.rect = webView.bounds
1033
+ configuration.afterScreenUpdates = false
1034
+
1035
+ webView.takeSnapshot(with: configuration) { image, error in
1036
+ if let error {
1037
+ completion(.failure(error))
1038
+ return
1039
+ }
1040
+
1041
+ guard let image,
1042
+ let imageData = image.pngData() else {
1043
+ completion(.failure(NSError(domain: "InAppBrowser", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to encode screenshot"])))
1044
+ return
1045
+ }
1046
+
1047
+ let base64 = imageData.base64EncodedString()
1048
+ let result: [String: Any] = [
1049
+ "format": "png",
1050
+ "mimeType": "image/png",
1051
+ "base64": base64,
1052
+ "dataUrl": "data:image/png;base64,\(base64)",
1053
+ "width": Int(image.size.width * image.scale),
1054
+ "height": Int(image.size.height * image.scale)
1055
+ ]
1056
+ self.emit("screenshotTaken", data: result)
1057
+ completion(.success(result))
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ // Method to receive messages from JavaScript
1063
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
1064
+ if message.name == "messageHandler" {
1065
+ if let messageBody = message.body as? [String: Any] {
1066
+ print("Received message from JavaScript:", messageBody)
1067
+ emit("messageFromWebview", data: messageBody)
1068
+ } else {
1069
+ print("Received non-dictionary message from JavaScript:", message.body)
1070
+ emit("messageFromWebview", data: ["rawMessage": String(describing: message.body)])
1071
+ }
1072
+ } else if message.name == "consoleMessageHandler" {
1073
+ if let messageBody = message.body as? [String: Any] {
1074
+ emit("consoleMessage", data: ConsoleMessageSupport.normalizePayload(from: messageBody))
1075
+ } else {
1076
+ emit(
1077
+ "consoleMessage",
1078
+ data: ConsoleMessageSupport.normalizePayload(
1079
+ from: [
1080
+ "level": "log",
1081
+ "message": String(describing: message.body)
1082
+ ]
1083
+ )
1084
+ )
1085
+ }
1086
+ } else if message.name == "preShowScriptSuccess" {
1087
+ guard let semaphore = preShowSemaphore else {
1088
+ print("[InAppBrowser - preShowScriptSuccess]: Semaphore not found")
1089
+ return
1090
+ }
1091
+
1092
+ semaphore.signal()
1093
+ } else if message.name == "preShowScriptError" {
1094
+ guard let semaphore = preShowSemaphore else {
1095
+ print("[InAppBrowser - preShowScriptError]: Semaphore not found")
1096
+ return
1097
+ }
1098
+ print("[InAppBrowser - preShowScriptError]: Error!!!!")
1099
+ semaphore.signal()
1100
+ } else if message.name == "close" {
1101
+ closeView()
1102
+ } else if message.name == "hide" {
1103
+ guard allowWebViewJsVisibilityControl else {
1104
+ return
1105
+ }
1106
+ capBrowserPlugin?.setHiddenFromJavaScript(true, sourceId: instanceId)
1107
+ } else if message.name == "show" {
1108
+ guard allowWebViewJsVisibilityControl else {
1109
+ return
1110
+ }
1111
+ capBrowserPlugin?.setHiddenFromJavaScript(false, sourceId: instanceId)
1112
+ } else if message.name == "takeScreenshot" {
1113
+ guard let body = message.body as? [String: Any],
1114
+ let requestId = body["requestId"] as? String,
1115
+ !requestId.isEmpty else {
1116
+ return
1117
+ }
1118
+
1119
+ guard allowScreenshotsFromWebPage else {
1120
+ self.rejectJavaScriptScreenshot(requestId: requestId, message: "Screenshot bridge is not enabled for this page")
1121
+ return
1122
+ }
1123
+
1124
+ takeScreenshot { result in
1125
+ switch result {
1126
+ case .success(let screenshot):
1127
+ self.resolveJavaScriptScreenshot(requestId: requestId, result: screenshot)
1128
+ case .failure(let error):
1129
+ self.rejectJavaScriptScreenshot(requestId: requestId, message: error.localizedDescription)
1130
+ }
1131
+ }
1132
+ } else if message.name == "magicPrint" {
1133
+ if let webView = self.webView {
1134
+ let printController = UIPrintInteractionController.shared
1135
+
1136
+ let printInfo = UIPrintInfo(dictionary: nil)
1137
+ printInfo.outputType = .general
1138
+ printInfo.jobName = "Print Job"
1139
+
1140
+ printController.printInfo = printInfo
1141
+ printController.printFormatter = webView.viewPrintFormatter()
1142
+
1143
+ printController.present(animated: true, completionHandler: nil)
1144
+ }
1145
+ }
1146
+ }
1147
+
1148
+ private func mobileAppScriptSource() -> String {
1149
+ let screenshotControls = allowScreenshotsFromWebPage ? """
1150
+ ,
1151
+ takeScreenshot: function() {
1152
+ return new Promise(function(resolve, reject) {
1153
+ if (!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.takeScreenshot)) {
1154
+ reject(new Error('Screenshot bridge is not available'));
1155
+ return;
1156
+ }
1157
+ var requestId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).slice(2);
1158
+ window.__capgoInAppBrowserPendingScreenshots[requestId] = { resolve: resolve, reject: reject };
1159
+ window.webkit.messageHandlers.takeScreenshot.postMessage({ requestId: requestId });
1160
+ });
1161
+ }
1162
+ """ : ""
1163
+ let extraControls = allowWebViewJsVisibilityControl ? """
1164
+ ,
1165
+ hide: function() {
1166
+ window.webkit.messageHandlers.hide.postMessage(null);
1167
+ },
1168
+ show: function() {
1169
+ window.webkit.messageHandlers.show.postMessage(null);
1170
+ }
1171
+ """ : ""
1172
+ return """
1173
+ window.__capgoInAppBrowserPendingScreenshots = window.__capgoInAppBrowserPendingScreenshots || {};
1174
+ window.__capgoInAppBrowserResolveScreenshot = window.__capgoInAppBrowserResolveScreenshot || function(payload) {
1175
+ var pending = window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
1176
+ if (!pending) {
1177
+ return;
1178
+ }
1179
+ delete window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
1180
+ pending.resolve(payload.result);
1181
+ };
1182
+ window.__capgoInAppBrowserRejectScreenshot = window.__capgoInAppBrowserRejectScreenshot || function(payload) {
1183
+ var pending = window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
1184
+ if (!pending) {
1185
+ return;
1186
+ }
1187
+ delete window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
1188
+ pending.reject(new Error(payload.message));
1189
+ };
1190
+ window.mobileApp = Object.assign({}, window.mobileApp || {}, {
1191
+ postMessage: function(message) {
1192
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.messageHandler) {
1193
+ window.webkit.messageHandlers.messageHandler.postMessage(message);
1194
+ }
1195
+ },
1196
+ close: function() {
1197
+ window.webkit.messageHandlers.close.postMessage(null);
1198
+ }\(extraControls)\(screenshotControls)
1199
+ });
1200
+ if (!window.__capgoInAppBrowserWindowCloseInstalled) {
1201
+ window.__capgoInAppBrowserWindowCloseInstalled = true;
1202
+ window.__capgoInAppBrowserOriginalClose = window.close;
1203
+ window.close = function() {
1204
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.close) {
1205
+ window.webkit.messageHandlers.close.postMessage(null);
1206
+ return;
1207
+ }
1208
+ if (typeof window.__capgoInAppBrowserOriginalClose === 'function') {
1209
+ return window.__capgoInAppBrowserOriginalClose.apply(window, arguments);
1210
+ }
1211
+ };
1212
+ }
1213
+ """
1214
+ }
1215
+
1216
+ private func addMobileAppUserScript(to userContentController: WKUserContentController) {
1217
+ let script = WKUserScript(
1218
+ source: mobileAppScriptSource(),
1219
+ injectionTime: .atDocumentStart,
1220
+ forMainFrameOnly: true
1221
+ )
1222
+ userContentController.addUserScript(script)
1223
+ }
1224
+
1225
+ private func addConsoleCaptureUserScript(to userContentController: WKUserContentController) {
1226
+ let script = WKUserScript(
1227
+ source: ConsoleMessageSupport.captureScriptSource(),
1228
+ injectionTime: .atDocumentStart,
1229
+ forMainFrameOnly: false
1230
+ )
1231
+ userContentController.addUserScript(script)
1232
+ }
1233
+
1234
+ func injectJavaScriptInterface() {
1235
+ let script = mobileAppScriptSource()
1236
+ DispatchQueue.main.async {
1237
+ self.webView?.evaluateJavaScript(script) { result, error in
1238
+ if let error = error {
1239
+ print("JavaScript evaluation error: \(error)")
1240
+ } else if let result = result {
1241
+ print("JavaScript result: \(result)")
1242
+ } else {
1243
+ print("JavaScript executed with no result")
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ open func initWebview(isInspectable: Bool = true) {
1250
+ if self.isWebViewInitialized {
1251
+ return
1252
+ }
1253
+ self.isWebViewInitialized = true
1254
+ self.view.backgroundColor = UIColor.white
1255
+
1256
+ self.extendedLayoutIncludesOpaqueBars = true
1257
+ self.edgesForExtendedLayout = [.bottom]
1258
+
1259
+ let webConfiguration = initialWebConfiguration ?? WKWebViewConfiguration()
1260
+ let userContentController = webConfiguration.userContentController
1261
+ userContentController.removeAllUserScripts()
1262
+ userContentController.removeScriptMessageHandler(forName: "messageHandler")
1263
+ userContentController.removeScriptMessageHandler(forName: "preShowScriptError")
1264
+ userContentController.removeScriptMessageHandler(forName: "preShowScriptSuccess")
1265
+ userContentController.removeScriptMessageHandler(forName: "close")
1266
+ userContentController.removeScriptMessageHandler(forName: "hide")
1267
+ userContentController.removeScriptMessageHandler(forName: "show")
1268
+ userContentController.removeScriptMessageHandler(forName: "takeScreenshot")
1269
+ userContentController.removeScriptMessageHandler(forName: "consoleMessageHandler")
1270
+ userContentController.removeScriptMessageHandler(forName: "magicPrint")
1271
+
1272
+ if proxyRequests || proxySchemeHandler != nil, let handler = proxySchemeHandler {
1273
+ WKWebView.enableCustomSchemeHandling(for: ["https", "http"])
1274
+ if !WKWebView.handlesURLScheme("https") && !WKWebView.handlesURLScheme("http") {
1275
+ webConfiguration.setURLSchemeHandler(handler, forURLScheme: "https")
1276
+ webConfiguration.setURLSchemeHandler(handler, forURLScheme: "http")
1277
+ } else {
1278
+ print("[InAppBrowser][Proxy] WARNING: handlesURLScheme swizzle failed; proxy scheme handler not registered")
1279
+ }
1280
+ }
1281
+
1282
+ let weakHandler = WeakScriptMessageHandler(self)
1283
+ userContentController.add(weakHandler, name: "messageHandler")
1284
+ userContentController.add(weakHandler, name: "preShowScriptError")
1285
+ userContentController.add(weakHandler, name: "preShowScriptSuccess")
1286
+ userContentController.add(weakHandler, name: "close")
1287
+ userContentController.add(weakHandler, name: "hide")
1288
+ userContentController.add(weakHandler, name: "show")
1289
+ if allowScreenshotsFromWebPage {
1290
+ userContentController.add(weakHandler, name: "takeScreenshot")
1291
+ }
1292
+ if captureConsoleLogs {
1293
+ userContentController.add(weakHandler, name: "consoleMessageHandler")
1294
+ addConsoleCaptureUserScript(to: userContentController)
1295
+ }
1296
+ if let preShowScript = self.preShowScript,
1297
+ !preShowScript.isEmpty,
1298
+ preShowScriptInjectionTime == "documentStart" {
1299
+ userContentController.addUserScript(
1300
+ WKUserScript(
1301
+ source: preShowScript,
1302
+ injectionTime: .atDocumentStart,
1303
+ forMainFrameOnly: false
1304
+ )
1305
+ )
1306
+ print("[InAppBrowser] Injected preShowScript at document start")
1307
+ }
1308
+ for scriptSource in documentStartUserScripts where !scriptSource.isEmpty {
1309
+ userContentController.addUserScript(
1310
+ WKUserScript(
1311
+ source: scriptSource,
1312
+ injectionTime: .atDocumentStart,
1313
+ forMainFrameOnly: false
1314
+ )
1315
+ )
1316
+ }
1317
+ userContentController.add(weakHandler, name: "magicPrint")
1318
+
1319
+ // Inject JavaScript to override window.print
1320
+ let script = WKUserScript(
1321
+ source: """
1322
+ window.print = function() {
1323
+ window.webkit.messageHandlers.magicPrint.postMessage('magicPrint');
1324
+ };
1325
+ """,
1326
+ injectionTime: .atDocumentStart,
1327
+ forMainFrameOnly: false
1328
+ )
1329
+ userContentController.addUserScript(script)
1330
+ addMobileAppUserScript(to: userContentController)
1331
+
1332
+ webConfiguration.allowsInlineMediaPlayback = true
1333
+ webConfiguration.userContentController = userContentController
1334
+ // Enable background task processing
1335
+ if initialWebConfiguration == nil {
1336
+ webConfiguration.processPool = WKProcessPool()
1337
+ }
1338
+
1339
+ // Enable JavaScript to run automatically (needed for preShowScript and Firebase polyfill)
1340
+ webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
1341
+
1342
+ // Enhanced configuration for Google Pay support (only when enabled)
1343
+ if enableGooglePaySupport {
1344
+ print("[InAppBrowser] Enabling Google Pay support features for iOS")
1345
+
1346
+ // Inject Google Pay support script
1347
+ let googlePayScript = WKUserScript(
1348
+ source: """
1349
+ console.log('[InAppBrowser] Injecting Google Pay support for iOS');
1350
+
1351
+ // Enhanced window.open for Google Pay
1352
+ (function() {
1353
+ const originalWindowOpen = window.open;
1354
+ window.open = function(url, target, features) {
1355
+ console.log('[InAppBrowser iOS] Enhanced window.open called:', url, target, features);
1356
+
1357
+ // For Google Pay URLs, handle popup properly
1358
+ if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
1359
+ console.log('[InAppBrowser iOS] Google Pay popup detected');
1360
+ return originalWindowOpen.call(window, url, target || '_blank', features);
1361
+ }
1362
+
1363
+ return originalWindowOpen.call(window, url, target, features);
1364
+ };
1365
+
1366
+ // Add Cross-Origin-Opener-Policy meta tag if not present
1367
+ if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
1368
+ const meta = document.createElement('meta');
1369
+ meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
1370
+ meta.setAttribute('content', 'same-origin-allow-popups');
1371
+ if (document.head) {
1372
+ document.head.appendChild(meta);
1373
+ console.log('[InAppBrowser iOS] Added Cross-Origin-Opener-Policy meta tag');
1374
+ }
1375
+ }
1376
+
1377
+ console.log('[InAppBrowser iOS] Google Pay support enhancements complete');
1378
+ })();
1379
+ """,
1380
+ injectionTime: .atDocumentStart,
1381
+ forMainFrameOnly: false
1382
+ )
1383
+ userContentController.addUserScript(googlePayScript)
1384
+ }
1385
+
1386
+ let webView = WKWebView(frame: .zero, configuration: webConfiguration)
1387
+
1388
+ // if webView.responds(to: Selector(("setInspectable:"))) {
1389
+ // // Fix: https://stackoverflow.com/questions/76216183/how-to-debug-wkwebview-in-ios-16-4-1-using-xcode-14-2/76603043#76603043
1390
+ // webView.perform(Selector(("setInspectable:")), with: isInspectable)
1391
+ // }
1392
+
1393
+ if #available(iOS 16.4, *) {
1394
+ webView.isInspectable = true
1395
+ } else {
1396
+ // Fallback on earlier versions
1397
+ }
1398
+
1399
+ // First add the webView to view hierarchy
1400
+ self.view.addSubview(webView)
1401
+
1402
+ // Then set up constraints
1403
+ webView.translatesAutoresizingMaskIntoConstraints = false
1404
+ var bottomAnchor = self.view.bottomAnchor
1405
+ var topAnchor = self.view.safeAreaLayoutGuide.topAnchor
1406
+
1407
+ if self.enabledSafeBottomMargin {
1408
+ bottomAnchor = self.view.safeAreaLayoutGuide.bottomAnchor
1409
+ }
1410
+
1411
+ if !self.enabledSafeTopMargin {
1412
+ topAnchor = self.view.topAnchor
1413
+ }
1414
+
1415
+ NSLayoutConstraint.activate([
1416
+ webView.topAnchor.constraint(equalTo: topAnchor),
1417
+ webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
1418
+ webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
1419
+ webView.bottomAnchor.constraint(equalTo: bottomAnchor)
1420
+ ])
1421
+
1422
+ webView.uiDelegate = self
1423
+ webView.navigationDelegate = self
1424
+
1425
+ webView.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
1426
+ webView.isMultipleTouchEnabled = true
1427
+
1428
+ // Disable bounce effect by setting scrollView.bounces to false when disableOverscroll is true
1429
+ webView.scrollView.bounces = !self.disableOverscroll
1430
+
1431
+ webView.addObserver(self, forKeyPath: estimatedProgressKeyPath, options: .new, context: nil)
1432
+ if websiteTitleInNavigationBar {
1433
+ webView.addObserver(self, forKeyPath: titleKeyPath, options: .new, context: nil)
1434
+ }
1435
+ webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
1436
+
1437
+ if !self.blankNavigationTab {
1438
+ self.view.addSubview(webView)
1439
+ // Then set up constraints
1440
+ webView.translatesAutoresizingMaskIntoConstraints = false
1441
+ }
1442
+ self.webView = webView
1443
+
1444
+ self.webView?.customUserAgent = self.customUserAgent ?? self.userAgent ?? self.originalUserAgent
1445
+
1446
+ self.navigationItem.title = self.navigationItem.title ?? self.source?.absoluteString
1447
+
1448
+ if let navigation = self.navigationController {
1449
+ self.previousNavigationBarState = (navigation.navigationBar.tintColor, navigation.navigationBar.isHidden)
1450
+ self.previousToolbarState = (navigation.toolbar.tintColor, navigation.toolbar.isHidden)
1451
+ }
1452
+
1453
+ if let sourceValue = self.source, !waitsForPopupNavigation {
1454
+ self.load(source: sourceValue)
1455
+ } else if self.source == nil {
1456
+ print("[\(type(of: self))][Error] Invalid url")
1457
+ }
1458
+ }
1459
+
1460
+ open func setupViewElements() {
1461
+ self.setUpProgressView()
1462
+ self.setUpConstraints()
1463
+ self.setUpNavigationBarAppearance()
1464
+ self.addBarButtonItems()
1465
+ self.updateBarButtonItems()
1466
+ }
1467
+
1468
+ @discardableResult
1469
+ func inheritPopupPresentation(
1470
+ from parent: WKWebViewController,
1471
+ request: URLRequest,
1472
+ configuration: WKWebViewConfiguration,
1473
+ instanceId: String,
1474
+ proxySchemeHandler: ProxySchemeHandler?
1475
+ ) -> WKWebView? {
1476
+ self.instanceId = instanceId
1477
+ self.source = request.url.map { .remote($0) }
1478
+ self.url = request.url
1479
+ self.credentials = parent.credentials
1480
+ self.headers = parent.headers
1481
+ self.customUserAgent = parent.customUserAgent
1482
+ self.userAgent = parent.userAgent
1483
+ self.pureUserAgent = parent.pureUserAgent
1484
+ self.capBrowserPlugin = parent.capBrowserPlugin
1485
+ self.shareDisclaimer = parent.shareDisclaimer
1486
+ self.shareSubject = parent.shareSubject
1487
+ self.closeModal = parent.closeModal
1488
+ self.closeModalTitle = parent.closeModalTitle
1489
+ self.closeModalDescription = parent.closeModalDescription
1490
+ self.closeModalOk = parent.closeModalOk
1491
+ self.closeModalCancel = parent.closeModalCancel
1492
+ self.closeModalURLPattern = parent.closeModalURLPattern
1493
+ self.ignoreUntrustedSSLError = parent.ignoreUntrustedSSLError
1494
+ self.enableGooglePaySupport = parent.enableGooglePaySupport
1495
+ self.preventDeeplink = parent.preventDeeplink
1496
+ self.openBlankTargetInWebView = parent.openBlankTargetInWebView
1497
+ self.blankNavigationTab = false
1498
+ self.enabledSafeBottomMargin = parent.enabledSafeBottomMargin
1499
+ self.enabledSafeTopMargin = parent.enabledSafeTopMargin
1500
+ self.blockedHosts = parent.blockedHosts
1501
+ self.authorizedAppLinks = parent.authorizedAppLinks
1502
+ self.activeNativeNavigationForWebview = parent.activeNativeNavigationForWebview
1503
+ self.disableOverscroll = parent.disableOverscroll
1504
+ self.proxyRequests = parent.proxyRequests
1505
+ self.proxySchemeHandler = proxySchemeHandler
1506
+ self.initialWebConfiguration = configuration
1507
+ self.waitsForPopupNavigation = true
1508
+ self.hiddenPopupWindow = parent.hiddenPopupWindow
1509
+ self.opensHidden = parent.hiddenPopupWindow
1510
+ self.captureConsoleLogs = parent.captureConsoleLogs
1511
+ self.allowWebViewJsVisibilityControl = parent.allowWebViewJsVisibilityControl
1512
+ self.allowScreenshotsFromWebPage = parent.allowScreenshotsFromWebPage
1513
+ self.handleDownloads = parent.handleDownloads
1514
+ self.websiteTitleInNavigationBar = parent.websiteTitleInNavigationBar
1515
+ self.doneBarButtonItemPosition = parent.doneBarButtonItemPosition
1516
+ self.showArrowAsClose = parent.showArrowAsClose
1517
+ self.documentStartUserScripts = parent.documentStartUserScripts
1518
+ self.preShowScript = parent.preShowScript
1519
+ self.preShowScriptInjectionTime = parent.preShowScriptInjectionTime
1520
+ self.leftNavigationBarItemTypes = parent.leftNavigationBarItemTypes
1521
+ self.rightNavigaionBarItemTypes = parent.rightNavigaionBarItemTypes
1522
+ self.statusBarStyle = parent.statusBarStyle
1523
+ self.tintColor = parent.tintColor
1524
+ self.backBarButtonItemImage = parent.backBarButtonItemImage
1525
+ self.forwardBarButtonItemImage = parent.forwardBarButtonItemImage
1526
+ self.reloadBarButtonItemImage = parent.reloadBarButtonItemImage
1527
+ self.stopBarButtonItemImage = parent.stopBarButtonItemImage
1528
+ self.activityBarButtonItemImage = parent.activityBarButtonItemImage
1529
+ self.buttonNearDoneIcon = parent.buttonNearDoneIcon
1530
+ self.showScreenshotButton = parent.showScreenshotButton
1531
+ self.textZoom = parent.textZoom
1532
+ self.customWidth = parent.customWidth
1533
+ self.customHeight = parent.customHeight
1534
+ self.customX = parent.customX
1535
+ self.customY = parent.customY
1536
+ self.view.backgroundColor = parent.view.backgroundColor
1537
+ self.title = parent.title ?? request.url?.host ?? "Popup Window"
1538
+ self.navigationItem.title = self.title
1539
+ self.initWebview()
1540
+ return self.capableWebView
1541
+ }
1542
+
1543
+ @objc func restateViewHeight() {
1544
+ var bottomPadding = CGFloat(0.0)
1545
+ var topPadding = CGFloat(0.0)
1546
+ let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
1547
+ bottomPadding = window?.safeAreaInsets.bottom ?? 0.0
1548
+ topPadding = window?.safeAreaInsets.top ?? 0.0
1549
+ if UIDevice.current.orientation.isPortrait {
1550
+ // Don't force toolbar visibility
1551
+ if self.viewHeightPortrait == nil {
1552
+ self.viewHeightPortrait = self.view.safeAreaLayoutGuide.layoutFrame.size.height
1553
+ self.viewHeightPortrait! += bottomPadding
1554
+ if self.navigationController?.navigationBar.isHidden == true {
1555
+ self.viewHeightPortrait! += topPadding
1556
+ }
1557
+ }
1558
+ self.currentViewHeight = self.viewHeightPortrait
1559
+ } else if UIDevice.current.orientation.isLandscape {
1560
+ // Don't force toolbar visibility
1561
+ if self.viewHeightLandscape == nil {
1562
+ self.viewHeightLandscape = self.view.safeAreaLayoutGuide.layoutFrame.size.height
1563
+ self.viewHeightLandscape! += bottomPadding
1564
+ if self.navigationController?.navigationBar.isHidden == true {
1565
+ self.viewHeightLandscape! += topPadding
1566
+ }
1567
+ }
1568
+ self.currentViewHeight = self.viewHeightLandscape
1569
+ }
1570
+ }
1571
+
1572
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
1573
+ // self.view.frame.size.height = self.currentViewHeight!
1574
+ }
1575
+
1576
+ override open func viewWillLayoutSubviews() {
1577
+ restateViewHeight()
1578
+ // Don't override frame height when enabledSafeBottomMargin is true, as it would override our constraints
1579
+ if self.currentViewHeight != nil && !self.enabledSafeBottomMargin {
1580
+ self.view.frame.size.height = self.currentViewHeight!
1581
+ }
1582
+ }
1583
+
1584
+ override open func viewWillAppear(_ animated: Bool) {
1585
+ super.viewWillAppear(animated)
1586
+ if !self.viewWasPresented {
1587
+ self.setupViewElements()
1588
+ setUpState()
1589
+ self.viewWasPresented = true
1590
+
1591
+ // Apply custom dimensions if specified
1592
+ applyCustomDimensions()
1593
+ }
1594
+
1595
+ // Force update button appearances
1596
+ updateButtonTintColors()
1597
+
1598
+ // Ensure status bar appearance is correct when view appears
1599
+ // Make sure we have the latest tint color
1600
+ if let tintColor = self.tintColor {
1601
+ // Update the status bar background if needed
1602
+ if let navController = navigationController, let backgroundColor = navController.navigationBar.backgroundColor ?? statusBarBackgroundView?.backgroundColor {
1603
+ setupStatusBarBackground(color: backgroundColor)
1604
+ } else {
1605
+ setupStatusBarBackground(color: UIColor.white)
1606
+ }
1607
+ }
1608
+
1609
+ // Update status bar style
1610
+ updateStatusBarStyle()
1611
+
1612
+ // Special handling for blank toolbar mode
1613
+ if blankNavigationTab && statusBarBackgroundView != nil {
1614
+ if let color = statusBarBackgroundView?.backgroundColor {
1615
+ // Set view color to match status bar
1616
+ view.backgroundColor = color
1617
+ }
1618
+ }
1619
+ }
1620
+
1621
+ override open func viewDidAppear(_ animated: Bool) {
1622
+ super.viewDidAppear(animated)
1623
+
1624
+ // Force add buttonNearDone if it's not visible yet
1625
+ if buttonNearDoneIcon != nil {
1626
+ // Check if button already exists in the navigation bar
1627
+ let buttonExists = navigationItem.rightBarButtonItems?.contains { item in
1628
+ return item.action == #selector(buttonNearDoneDidClick)
1629
+ } ?? false
1630
+
1631
+ if !buttonExists {
1632
+ // Create and add the button directly
1633
+ let buttonItem = UIBarButtonItem(
1634
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
1635
+ style: .plain,
1636
+ target: self,
1637
+ action: #selector(buttonNearDoneDidClick)
1638
+ )
1639
+
1640
+ // Apply tint color
1641
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
1642
+ buttonItem.tintColor = tintColor
1643
+ }
1644
+
1645
+ // Add to right items
1646
+ if navigationItem.rightBarButtonItems == nil {
1647
+ navigationItem.rightBarButtonItems = [buttonItem]
1648
+ } else {
1649
+ var items = navigationItem.rightBarButtonItems ?? []
1650
+ items.append(buttonItem)
1651
+ navigationItem.rightBarButtonItems = items
1652
+ }
1653
+
1654
+ print("[DEBUG] Force added buttonNearDone in viewDidAppear")
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ override open func viewWillDisappear(_ animated: Bool) {
1660
+ super.viewWillDisappear(animated)
1661
+ rollbackState()
1662
+ }
1663
+
1664
+ override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
1665
+ switch keyPath {
1666
+ case estimatedProgressKeyPath?:
1667
+ DispatchQueue.main.async {
1668
+ guard let estimatedProgress = self.webView?.estimatedProgress else {
1669
+ return
1670
+ }
1671
+ self.progressView?.alpha = 1
1672
+ self.progressView?.setProgress(Float(estimatedProgress), animated: true)
1673
+
1674
+ if estimatedProgress >= 1.0 {
1675
+ UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: {
1676
+ self.progressView?.alpha = 0
1677
+ }, completion: {
1678
+ _ in
1679
+ self.progressView?.setProgress(0, animated: false)
1680
+ })
1681
+ }
1682
+ }
1683
+ case titleKeyPath?:
1684
+ if self.hasDynamicTitle {
1685
+ self.navigationItem.title = webView?.url?.host
1686
+ }
1687
+ case "URL":
1688
+ // Guard against notifications during cleanup when webView is being torn down
1689
+ guard self.webView != nil else { return }
1690
+ emit("urlChangeEvent", data: ["url": webView?.url?.absoluteString ?? ""])
1691
+ DispatchQueue.main.async { [weak self] in
1692
+ self?.updateBarButtonItems()
1693
+ }
1694
+ self.injectJavaScriptInterface()
1695
+ default:
1696
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ // MARK: - Public Methods
1702
+ public extension WKWebViewController {
1703
+
1704
+ func load(source sourceValue: WKWebSource) {
1705
+ switch sourceValue {
1706
+ case .remote(let url):
1707
+ self.load(remote: url)
1708
+ case .file(let url, access: let access):
1709
+ self.load(file: url, access: access)
1710
+ case .string(let str, base: let base):
1711
+ self.load(string: str, base: base)
1712
+ }
1713
+ }
1714
+
1715
+ func load(remote: URL) {
1716
+ DispatchQueue.main.async {
1717
+ self.webView?.load(self.createRequest(url: remote))
1718
+ }
1719
+ }
1720
+
1721
+ func load(file: URL, access: URL) {
1722
+ webView?.loadFileURL(file, allowingReadAccessTo: access)
1723
+ }
1724
+
1725
+ func load(string: String, base: URL? = nil) {
1726
+ webView?.loadHTMLString(string, baseURL: base)
1727
+ }
1728
+
1729
+ func goBackToFirstPage() {
1730
+ if let firstPageItem = webView?.backForwardList.backList.first {
1731
+ webView?.go(to: firstPageItem)
1732
+ }
1733
+ }
1734
+ func reload() {
1735
+ webView?.reload()
1736
+ }
1737
+
1738
+ func websiteDataStore() -> WKWebsiteDataStore? {
1739
+ return webView?.configuration.websiteDataStore
1740
+ }
1741
+
1742
+ func executeScript(script: String, completion: ((Any?, Error?) -> Void)? = nil) {
1743
+ DispatchQueue.main.async { [weak self] in
1744
+ self?.webView?.evaluateJavaScript(script, completionHandler: completion)
1745
+ }
1746
+ }
1747
+
1748
+ func applyTextZoom(_ zoomPercent: Int) {
1749
+ let script = """
1750
+ document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust = '\(zoomPercent)%';
1751
+ document.getElementsByTagName('body')[0].style.textSizeAdjust = '\(zoomPercent)%';
1752
+ """
1753
+
1754
+ executeScript(script: script)
1755
+ }
1756
+
1757
+ func injectPreShowScriptAtDocumentStart() {
1758
+ guard let preShowScript = self.preShowScript,
1759
+ !preShowScript.isEmpty,
1760
+ self.preShowScriptInjectionTime == "documentStart",
1761
+ let webView = self.webView else {
1762
+ return
1763
+ }
1764
+
1765
+ let userScript = WKUserScript(
1766
+ source: preShowScript,
1767
+ injectionTime: .atDocumentStart,
1768
+ forMainFrameOnly: false
1769
+ )
1770
+ webView.configuration.userContentController.addUserScript(userScript)
1771
+ print("[InAppBrowser] Injected preShowScript at document start")
1772
+
1773
+ // Reload the webview so the script executes at document start
1774
+ if let currentURL = webView.url {
1775
+ load(remote: currentURL)
1776
+ } else if let source = self.source {
1777
+ load(source: source)
1778
+ }
1779
+ }
1780
+
1781
+ func updateNavigationGestures() {
1782
+ self.webView?.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
1783
+ }
1784
+
1785
+ open func cleanupWebView() {
1786
+ guard let webView = self.webView else { return }
1787
+ webView.stopLoading()
1788
+ previewItemURL = nil
1789
+
1790
+ // Remove KVO observers FIRST, before any operation that could trigger them
1791
+ webView.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
1792
+ if websiteTitleInNavigationBar {
1793
+ webView.removeObserver(self, forKeyPath: titleKeyPath)
1794
+ }
1795
+ webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
1796
+
1797
+ // Now safe to tear down
1798
+ webView.navigationDelegate = nil
1799
+ webView.uiDelegate = nil
1800
+ webView.loadHTMLString("", baseURL: nil)
1801
+
1802
+ webView.configuration.userContentController.removeAllUserScripts()
1803
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "messageHandler")
1804
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "close")
1805
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "hide")
1806
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "show")
1807
+ if allowScreenshotsFromWebPage {
1808
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "takeScreenshot")
1809
+ }
1810
+ if captureConsoleLogs {
1811
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "consoleMessageHandler")
1812
+ }
1813
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptSuccess")
1814
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "preShowScriptError")
1815
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "magicPrint")
1816
+
1817
+ webView.removeFromSuperview()
1818
+ // Also clean progress bar view if present
1819
+ progressView?.removeFromSuperview()
1820
+ progressView = nil
1821
+ self.webView = nil
1822
+ }
1823
+ }
1824
+
1825
+ // MARK: - Fileprivate Methods
1826
+ fileprivate extension WKWebViewController {
1827
+ var availableCookies: [HTTPCookie]? {
1828
+ return cookies?.filter {
1829
+ cookie in
1830
+ var result = true
1831
+ let url = self.source?.remoteURL
1832
+ if let host = url?.host, !cookie.domain.hasSuffix(host) {
1833
+ result = false
1834
+ }
1835
+ if cookie.isSecure && url?.scheme != "https" {
1836
+ result = false
1837
+ }
1838
+
1839
+ return result
1840
+ }
1841
+ }
1842
+ func createRequest(url: URL) -> URLRequest {
1843
+ var request = URLRequest(url: url)
1844
+
1845
+ // Set up HTTP method if specified
1846
+ if let method = httpMethod {
1847
+ request.httpMethod = method.uppercased()
1848
+ }
1849
+
1850
+ // Set up HTTP body if specified
1851
+ if let body = httpBody, let data = body.data(using: .utf8) {
1852
+ request.httpBody = data
1853
+ }
1854
+
1855
+ // Set up headers
1856
+ if let headers = headers {
1857
+ for (field, value) in headers {
1858
+ request.addValue(value, forHTTPHeaderField: field)
1859
+ }
1860
+ }
1861
+
1862
+ // Set up Cookies
1863
+ if let cookies = availableCookies, let value = HTTPCookie.requestHeaderFields(with: cookies)[cookieKey] {
1864
+ request.addValue(value, forHTTPHeaderField: cookieKey)
1865
+ }
1866
+
1867
+ return request
1868
+ }
1869
+
1870
+ func setUpProgressView() {
1871
+ let progressView = UIProgressView(progressViewStyle: .default)
1872
+ progressView.trackTintColor = UIColor(white: 1, alpha: 0)
1873
+ self.progressView = progressView
1874
+ // updateProgressViewFrame()
1875
+ }
1876
+
1877
+ func setUpConstraints() {
1878
+ if !(self.navigationController?.navigationBar.isHidden)! {
1879
+ self.progressView?.frame.origin.y = CGFloat((self.navigationController?.navigationBar.frame.height)!)
1880
+ self.navigationController?.navigationBar.addSubview(self.progressView!)
1881
+ }
1882
+ }
1883
+
1884
+ func addBarButtonItems() {
1885
+ func barButtonItem(_ type: BarButtonItemType) -> UIBarButtonItem? {
1886
+ switch type {
1887
+ case .back:
1888
+ return backBarButtonItem
1889
+ case .forward:
1890
+ return forwardBarButtonItem
1891
+ case .reload:
1892
+ return reloadBarButtonItem
1893
+ case .stop:
1894
+ return stopBarButtonItem
1895
+ case .activity:
1896
+ return activityBarButtonItem
1897
+ case .done:
1898
+ return doneBarButtonItem
1899
+ case .flexibleSpace:
1900
+ return flexibleSpaceBarButtonItem
1901
+ case .custom(let icon, let title, let action):
1902
+ let item: BlockBarButtonItem
1903
+ if let icon = icon {
1904
+ item = BlockBarButtonItem(image: icon, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1905
+ } else {
1906
+ item = BlockBarButtonItem(title: title, style: .plain, target: self, action: #selector(customDidClick(sender:)))
1907
+ }
1908
+ item.block = action
1909
+ return item
1910
+ }
1911
+ }
1912
+
1913
+ switch doneBarButtonItemPosition {
1914
+ case .left:
1915
+ if !leftNavigationBarItemTypes.contains(where: { type in
1916
+ switch type {
1917
+ case .done:
1918
+ return true
1919
+ default:
1920
+ return false
1921
+ }
1922
+ }) {
1923
+ leftNavigationBarItemTypes.insert(.done, at: 0)
1924
+ }
1925
+ case .right:
1926
+ if !rightNavigaionBarItemTypes.contains(where: { type in
1927
+ switch type {
1928
+ case .done:
1929
+ return true
1930
+ default:
1931
+ return false
1932
+ }
1933
+ }) {
1934
+ rightNavigaionBarItemTypes.insert(.done, at: 0)
1935
+ }
1936
+ case .none:
1937
+ break
1938
+ }
1939
+
1940
+ navigationItem.leftBarButtonItems = leftNavigationBarItemTypes.map {
1941
+ barButtonItemType in
1942
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1943
+ return barButtonItem
1944
+ }
1945
+ return UIBarButtonItem()
1946
+ }
1947
+
1948
+ var rightBarButtons = rightNavigaionBarItemTypes.map {
1949
+ barButtonItemType in
1950
+ if let barButtonItem = barButtonItem(barButtonItemType) {
1951
+ return barButtonItem
1952
+ }
1953
+ return UIBarButtonItem()
1954
+ }
1955
+
1956
+ // If we have buttonNearDoneIcon and the first (or only) right button is the done button
1957
+ if buttonNearDoneIcon != nil &&
1958
+ ((rightBarButtons.count == 1 && rightBarButtons[0] == doneBarButtonItem) ||
1959
+ (rightBarButtons.isEmpty && doneBarButtonItemPosition == .right) ||
1960
+ rightBarButtons.contains(doneBarButtonItem)) {
1961
+
1962
+ // Check if button already exists to avoid duplicates
1963
+ let buttonExists = rightBarButtons.contains { item in
1964
+ let selector = #selector(buttonNearDoneDidClick)
1965
+ return item.action == selector
1966
+ }
1967
+
1968
+ if !buttonExists {
1969
+ // Create button with proper tint and template rendering mode
1970
+ let buttonItem = UIBarButtonItem(
1971
+ image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
1972
+ style: .plain,
1973
+ target: self,
1974
+ action: #selector(buttonNearDoneDidClick)
1975
+ )
1976
+
1977
+ // Apply tint from navigation bar or from tintColor property
1978
+ if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
1979
+ buttonItem.tintColor = tintColor
1980
+ }
1981
+
1982
+ // Make sure the done button is there before adding this one
1983
+ if rightBarButtons.isEmpty && doneBarButtonItemPosition == .right {
1984
+ rightBarButtons.append(doneBarButtonItem)
1985
+ }
1986
+
1987
+ // Add the button
1988
+ rightBarButtons.append(buttonItem)
1989
+
1990
+ print("[DEBUG] Added buttonNearDone to right bar buttons, icon: \(String(describing: buttonNearDoneIcon))")
1991
+ } else {
1992
+ print("[DEBUG] buttonNearDone already exists in right bar buttons")
1993
+ }
1994
+ }
1995
+
1996
+ navigationItem.rightBarButtonItems = rightBarButtons
1997
+
1998
+ // After all buttons are set up, apply tint color
1999
+ updateButtonTintColors()
2000
+ }
2001
+
2002
+ func updateBarButtonItems() {
2003
+ // Update navigation buttons (completely separate from close button)
2004
+ backBarButtonItem.isEnabled = webView?.canGoBack ?? false
2005
+ forwardBarButtonItem.isEnabled = webView?.canGoForward ?? false
2006
+
2007
+ let updateReloadBarButtonItem: (UIBarButtonItem, Bool) -> UIBarButtonItem = {
2008
+ [weak self] barButtonItem, isLoading in
2009
+ guard let self = self else { return barButtonItem }
2010
+ switch barButtonItem {
2011
+ case self.reloadBarButtonItem, self.stopBarButtonItem:
2012
+ return isLoading ? self.stopBarButtonItem : self.reloadBarButtonItem
2013
+ default:
2014
+ return barButtonItem
2015
+ }
2016
+ }
2017
+
2018
+ let isLoading = webView?.isLoading ?? false
2019
+ navigationItem.leftBarButtonItems = navigationItem.leftBarButtonItems?.map {
2020
+ barButtonItem -> UIBarButtonItem in
2021
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
2022
+ }
2023
+
2024
+ navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems?.map {
2025
+ barButtonItem -> UIBarButtonItem in
2026
+ return updateReloadBarButtonItem(barButtonItem, isLoading)
2027
+ }
2028
+ }
2029
+
2030
+ func setUpState() {
2031
+ navigationController?.setNavigationBarHidden(false, animated: true)
2032
+
2033
+ // Always hide toolbar since we never want it
2034
+ navigationController?.setToolbarHidden(true, animated: true)
2035
+
2036
+ // Set tint colors but don't override specific colors
2037
+ if tintColor == nil {
2038
+ // Use system appearance if no specific tint color is set
2039
+ let isDarkMode = traitCollection.userInterfaceStyle == .dark
2040
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
2041
+
2042
+ navigationController?.navigationBar.tintColor = textColor
2043
+ progressView?.progressTintColor = textColor
2044
+ } else {
2045
+ progressView?.progressTintColor = tintColor
2046
+ navigationController?.navigationBar.tintColor = tintColor
2047
+ }
2048
+ }
2049
+
2050
+ func rollbackState() {
2051
+ progressView?.progress = 0
2052
+
2053
+ navigationController?.navigationBar.tintColor = previousNavigationBarState.tintColor
2054
+
2055
+ navigationController?.setNavigationBarHidden(previousNavigationBarState.hidden, animated: true)
2056
+ }
2057
+
2058
+ func checkRequestCookies(_ request: URLRequest, cookies: [HTTPCookie]) -> Bool {
2059
+ if cookies.count <= 0 {
2060
+ return true
2061
+ }
2062
+ guard let headerFields = request.allHTTPHeaderFields, let cookieString = headerFields[cookieKey] else {
2063
+ return false
2064
+ }
2065
+
2066
+ let requestCookies = cookieString.components(separatedBy: ";").map {
2067
+ $0.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=", maxSplits: 1).map(String.init)
2068
+ }
2069
+
2070
+ var valid = false
2071
+ for cookie in cookies {
2072
+ valid = requestCookies.filter {
2073
+ $0[0] == cookie.name && $0[1] == cookie.value
2074
+ }.count > 0
2075
+ if !valid {
2076
+ break
2077
+ }
2078
+ }
2079
+ return valid
2080
+ }
2081
+
2082
+ private func tryOpenCustomScheme(_ url: URL) -> Bool {
2083
+ let app = UIApplication.shared
2084
+ let shouldEmitCustomSchemeEvent = CustomSchemeInterceptSupport.shouldEmitInterceptEvent(for: url)
2085
+
2086
+ if app.canOpenURL(url) {
2087
+ app.open(url, options: [:], completionHandler: nil)
2088
+ if shouldEmitCustomSchemeEvent {
2089
+ emit("customSchemeIntercepted", data: ["url": url.absoluteString, "opened": true])
2090
+ }
2091
+ return true // external app opened -> cancel WebView load
2092
+ }
2093
+
2094
+ // Cannot open scheme: notify and still block WebView (avoid rendering garbage / errors)
2095
+ if shouldEmitCustomSchemeEvent {
2096
+ emit("customSchemeIntercepted", data: ["url": url.absoluteString, "opened": false])
2097
+ }
2098
+ emit("pageLoadError")
2099
+ return true
2100
+ }
2101
+
2102
+ private func tryOpenUniversalLink(_ url: URL, completion: @escaping (Bool) -> Void) {
2103
+ // Only for http(s):// and authorized hosts
2104
+ UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in
2105
+ completion(opened) // true => app opened, false => no associated app
2106
+ }
2107
+ }
2108
+
2109
+ func openURLWithApp(_ url: URL) -> Bool {
2110
+ let application = UIApplication.shared
2111
+ if application.canOpenURL(url) {
2112
+ application.open(url, options: [:], completionHandler: nil)
2113
+ return true
2114
+ }
2115
+
2116
+ return false
2117
+ }
2118
+
2119
+ private func normalizeHost(_ host: String?) -> String? {
2120
+ guard var hostValue = host?.lowercased() else { return nil }
2121
+ if hostValue.hasPrefix("www.") { hostValue.removeFirst(4) }
2122
+ return hostValue
2123
+ }
2124
+
2125
+ func isUrlAuthorized(_ url: URL, authorizedLinks: [String]) -> Bool {
2126
+ guard !authorizedLinks.isEmpty else { return false }
2127
+
2128
+ let urlHostNorm = normalizeHost(url.host)
2129
+ for auth in authorizedLinks {
2130
+ guard let comp = URLComponents(string: auth) else { continue }
2131
+ let authHostNorm = normalizeHost(comp.host)
2132
+ if urlHostNorm == authHostNorm {
2133
+ return true
2134
+ }
2135
+ }
2136
+
2137
+ return false
2138
+ }
2139
+
2140
+ private func isHttpOrHttps(_ url: URL) -> Bool {
2141
+ guard let scheme = url.scheme?.lowercased() else {
2142
+ return false
2143
+ }
2144
+ return scheme == "http" || scheme == "https"
2145
+ }
2146
+
2147
+ private func shouldLoadBlankTargetInCurrentWebView(_ url: URL, targetFrame: WKFrameInfo?) -> Bool {
2148
+ guard targetFrame == nil else {
2149
+ return false
2150
+ }
2151
+ guard isHttpOrHttps(url) else {
2152
+ return false
2153
+ }
2154
+
2155
+ // Preserve native handling for authorized App/Universal Links unless deeplinks are blocked
2156
+ if isUrlAuthorized(url, authorizedLinks: authorizedAppLinks) && !preventDeeplink {
2157
+ return false
2158
+ }
2159
+
2160
+ return openBlankTargetInWebView || preventDeeplink
2161
+ }
2162
+
2163
+ /// Attempts to open URL in an external app if it's a custom scheme OR an authorized universal link.
2164
+ /// Returns via completion whether an external app was opened (true) or not (false).
2165
+ private func handleURLWithApp(_ url: URL, targetFrame _: WKFrameInfo?, completion: @escaping (Bool) -> Void) {
2166
+
2167
+ // If preventDeeplink is true, don't try to open URLs in external apps
2168
+ if preventDeeplink {
2169
+ print("[InAppBrowser] preventDeeplink is true, won't try to open URLs in external apps")
2170
+ completion(false)
2171
+ return
2172
+ }
2173
+
2174
+ let scheme = url.scheme?.lowercased() ?? ""
2175
+ let host = url.host?.lowercased() ?? ""
2176
+
2177
+ print("[InAppBrowser] scheme \(scheme), host \(host)")
2178
+
2179
+ // Don't try to open internal WebKit URLs externally (about:, data:, blob:, etc.)
2180
+ let internalSchemes = ["about", "data", "blob", "javascript"]
2181
+ if internalSchemes.contains(scheme) {
2182
+ print("[InAppBrowser] internal WebKit scheme detected, allowing navigation")
2183
+ completion(false)
2184
+ return
2185
+ }
2186
+
2187
+ // Handle all non-http(s) schemes by default
2188
+ if scheme != "http" && scheme != "https" && scheme != "file" {
2189
+ print("[InAppBrowser] not http(s) scheme, try to open URLs in external apps")
2190
+ completion(tryOpenCustomScheme(url))
2191
+ return
2192
+ }
2193
+
2194
+ // Also handle specific hosts and schemes from UrlsHandledByApp
2195
+ let hosts = UrlsHandledByApp.hosts
2196
+ let schemes = UrlsHandledByApp.schemes
2197
+
2198
+ if hosts.contains(host) {
2199
+ print("[InAppBrowser] host \(host) matches one in UrlsHandledByApp, try to open URLs in external apps")
2200
+ completion(tryOpenCustomScheme(url))
2201
+ return
2202
+ }
2203
+ if schemes.contains(scheme) {
2204
+ print("[InAppBrowser] scheme \(scheme) matches one in UrlsHandledByApp, try to open URLs in external apps")
2205
+ completion(tryOpenCustomScheme(url))
2206
+ return
2207
+ }
2208
+
2209
+ // Authorized Universal Link hosts: prefer app via universalLinksOnly
2210
+ print("[InAppBrowser] Authorized App Links: \(self.authorizedAppLinks)")
2211
+ if isUrlAuthorized(url, authorizedLinks: self.authorizedAppLinks) {
2212
+ print("[InAppBrowser] Authorized Universal Link detected \(scheme + host), try to open URLs in external apps")
2213
+ tryOpenUniversalLink(url) { opened in
2214
+ print("[InAppBrowser] Handle as Universal Link: \(opened)")
2215
+ completion(opened) // opened => cancel navigation; not opened => allow WebView
2216
+ }
2217
+ return
2218
+ }
2219
+
2220
+ // Default: let WebView load
2221
+ print("[InAppBrowser] default completion handler: false")
2222
+ completion(false)
2223
+ }
2224
+
2225
+ @objc func backDidClick(sender: AnyObject) {
2226
+ // Only handle back navigation, not closing
2227
+ if webView?.canGoBack ?? false {
2228
+ webView?.goBack()
2229
+ }
2230
+ }
2231
+
2232
+ // Public method for safe back navigation
2233
+ public func goBack() -> Bool {
2234
+ if webView?.canGoBack ?? false {
2235
+ webView?.goBack()
2236
+ return true
2237
+ }
2238
+ return false
2239
+ }
2240
+
2241
+ @objc func forwardDidClick(sender: AnyObject) {
2242
+ webView?.goForward()
2243
+ }
2244
+
2245
+ @objc func buttonNearDoneDidClick(sender: AnyObject) {
2246
+ if showScreenshotButton {
2247
+ takeScreenshot { result in
2248
+ if case .failure(let error) = result {
2249
+ print("[InAppBrowser] Failed to capture screenshot from toolbar button: \(error.localizedDescription)")
2250
+ }
2251
+ }
2252
+ return
2253
+ }
2254
+ emit("buttonNearDoneClick")
2255
+ }
2256
+
2257
+ @objc func reloadDidClick(sender: AnyObject) {
2258
+ webView?.stopLoading()
2259
+ if webView?.url != nil {
2260
+ webView?.reload()
2261
+ } else if let sourceValue = self.source {
2262
+ self.load(source: sourceValue)
2263
+ }
2264
+ }
2265
+
2266
+ @objc func stopDidClick(sender: AnyObject) {
2267
+ webView?.stopLoading()
2268
+ }
2269
+
2270
+ @objc func activityDidClick(sender: AnyObject) {
2271
+ print("[DEBUG] Activity button clicked, shareSubject: \(self.shareSubject ?? "nil")")
2272
+
2273
+ guard let sourceValue = self.source else {
2274
+ print("[DEBUG] Activity button: No source available")
2275
+ return
2276
+ }
2277
+
2278
+ let items: [Any]
2279
+ switch sourceValue {
2280
+ case .remote(let urlValue):
2281
+ items = [urlValue]
2282
+ case .file(let urlValue, access: _):
2283
+ items = [urlValue]
2284
+ case .string(let str, base: _):
2285
+ items = [str]
2286
+ }
2287
+ showDisclaimer(items: items, sender: sender)
2288
+ }
2289
+
2290
+ func showDisclaimer(items: [Any], sender: AnyObject) {
2291
+ // Show disclaimer dialog before sharing if shareDisclaimer is set
2292
+ if let disclaimer = self.shareDisclaimer, !disclaimer.isEmpty {
2293
+ // Create and show the alert
2294
+ let alert = UIAlertController(
2295
+ title: disclaimer["title"] as? String ?? "Title",
2296
+ message: disclaimer["message"] as? String ?? "Message",
2297
+ preferredStyle: UIAlertController.Style.alert)
2298
+ let currentUrl = self.webView?.url?.absoluteString ?? ""
2299
+
2300
+ // Add confirm button that continues with sharing
2301
+ alert.addAction(UIAlertAction(
2302
+ title: disclaimer["confirmBtn"] as? String ?? "Confirm",
2303
+ style: UIAlertAction.Style.default,
2304
+ handler: { _ in
2305
+ // Notify that confirm was clicked
2306
+ self.emit("confirmBtnClicked", data: ["url": currentUrl])
2307
+
2308
+ // Show the share dialog
2309
+ self.showShareSheet(items: items, sender: sender)
2310
+ }
2311
+ ))
2312
+
2313
+ // Add cancel button
2314
+ alert.addAction(UIAlertAction(
2315
+ title: disclaimer["cancelBtn"] as? String ?? "Cancel",
2316
+ style: UIAlertAction.Style.cancel,
2317
+ handler: nil
2318
+ ))
2319
+
2320
+ // Present the alert
2321
+ self.present(alert, animated: true, completion: nil)
2322
+ } else {
2323
+ // No disclaimer, directly show share sheet
2324
+ showShareSheet(items: items, sender: sender)
2325
+ }
2326
+ }
2327
+
2328
+ // Separated the actual sharing functionality
2329
+ private func showShareSheet(items: [Any], sender: AnyObject) {
2330
+ let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
2331
+ activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
2332
+ if let barButtonItem = sender as? UIBarButtonItem {
2333
+ activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
2334
+ }
2335
+ self.present(activityViewController, animated: true, completion: nil)
2336
+ }
2337
+
2338
+ func closeView() {
2339
+ var canDismiss = true
2340
+ if let url = self.source?.url {
2341
+ canDismiss = delegate?.webViewController?(self, canDismiss: url) ?? true
2342
+ }
2343
+ if canDismiss {
2344
+ let currentUrl = webView?.url?.absoluteString ?? ""
2345
+ cleanupWebView()
2346
+ self.capBrowserPlugin?.handleWebViewDidClose(id: instanceId, url: currentUrl)
2347
+ dismiss(animated: true, completion: nil)
2348
+ }
2349
+ }
2350
+
2351
+ @objc func doneDidClick(sender: AnyObject) {
2352
+ // check if closeModal is true, if true display alert before close
2353
+ if self.closeModal {
2354
+ let currentUrl = webView?.url?.absoluteString ?? ""
2355
+ var shouldShowModal = true
2356
+ if let regex = self.closeModalURLRegex {
2357
+ let matched = regex.firstMatch(in: currentUrl, range: NSRange(currentUrl.startIndex..., in: currentUrl)) != nil
2358
+ shouldShowModal = matched
2359
+ }
2360
+ if shouldShowModal {
2361
+ let alert = UIAlertController(title: self.closeModalTitle, message: self.closeModalDescription, preferredStyle: UIAlertController.Style.alert)
2362
+ alert.addAction(UIAlertAction(title: self.closeModalOk, style: UIAlertAction.Style.default, handler: { _ in
2363
+ // Notify that confirm was clicked
2364
+ self.emit("confirmBtnClicked", data: ["url": currentUrl])
2365
+ self.closeView()
2366
+ }))
2367
+ alert.addAction(UIAlertAction(title: self.closeModalCancel, style: UIAlertAction.Style.default, handler: nil))
2368
+ self.present(alert, animated: true, completion: nil)
2369
+ } else {
2370
+ self.closeView()
2371
+ }
2372
+ } else {
2373
+ self.closeView()
2374
+ }
2375
+
2376
+ }
2377
+
2378
+ @objc func customDidClick(sender: BlockBarButtonItem) {
2379
+ sender.block?(self)
2380
+ }
2381
+
2382
+ func canRotate() {}
2383
+
2384
+ func close() {
2385
+ let currentUrl = webView?.url?.absoluteString ?? ""
2386
+ cleanupWebView()
2387
+ capBrowserPlugin?.handleWebViewDidClose(id: instanceId, url: currentUrl)
2388
+ dismiss(animated: true, completion: nil)
2389
+ }
2390
+
2391
+ open func setUpNavigationBarAppearance() {
2392
+ // Set up basic bar appearance
2393
+ if let navBar = navigationController?.navigationBar {
2394
+ // Make navigation bar transparent
2395
+ navBar.setBackgroundImage(UIImage(), for: .default)
2396
+ navBar.shadowImage = UIImage()
2397
+ navBar.isTranslucent = true
2398
+
2399
+ // Ensure tint colors are applied properly
2400
+ if navBar.tintColor == nil {
2401
+ navBar.tintColor = tintColor ?? .black
2402
+ }
2403
+
2404
+ // Ensure text colors are set
2405
+ if navBar.titleTextAttributes == nil {
2406
+ navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor ?? .black]
2407
+ }
2408
+
2409
+ // Ensure the navigation bar buttons are properly visible
2410
+ for item in navBar.items ?? [] {
2411
+ for barButton in (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? []) {
2412
+ barButton.tintColor = tintColor ?? navBar.tintColor ?? .black
2413
+ }
2414
+ }
2415
+ }
2416
+
2417
+ // Force button colors to update
2418
+ updateButtonTintColors()
2419
+ }
2420
+ }
2421
+
2422
+ // MARK: - WKUIDelegate
2423
+ extension WKWebViewController: WKUIDelegate {
2424
+ public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
2425
+ // Create a strong reference to the completion handler to ensure it's called
2426
+ let strongCompletionHandler = completionHandler
2427
+
2428
+ // Ensure UI updates are on the main thread
2429
+ DispatchQueue.main.async { [weak self] in
2430
+ guard let self = self else {
2431
+ // View controller was deallocated
2432
+ strongCompletionHandler()
2433
+ return
2434
+ }
2435
+
2436
+ // Check if view is available and ready for presentation
2437
+ guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
2438
+ print("[InAppBrowser] Cannot present alert - view not in window hierarchy or being dismissed")
2439
+ strongCompletionHandler()
2440
+ return
2441
+ }
2442
+
2443
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
2444
+ alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
2445
+ strongCompletionHandler()
2446
+ }))
2447
+
2448
+ // Try to present the alert
2449
+ do {
2450
+ self.present(alertController, animated: true, completion: nil)
2451
+ } catch {
2452
+ // This won't typically be triggered as present doesn't throw,
2453
+ // but adding as a safeguard
2454
+ print("[InAppBrowser] Error presenting alert: \(error)")
2455
+ strongCompletionHandler()
2456
+ }
2457
+ }
2458
+ }
2459
+
2460
+ public func webView(_: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
2461
+ DispatchQueue.main.async { [weak self] in
2462
+ guard let self = self else {
2463
+ completionHandler(false)
2464
+ return
2465
+ }
2466
+
2467
+ guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
2468
+ print("[InAppBrowser] Cannot present confirm - view not in window hierarchy or being dismissed")
2469
+ completionHandler(false)
2470
+ return
2471
+ }
2472
+
2473
+ guard self.presentedViewController == nil else {
2474
+ print("[InAppBrowser] Cannot present confirm - another controller is already presented")
2475
+ completionHandler(false)
2476
+ return
2477
+ }
2478
+
2479
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
2480
+ alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in
2481
+ completionHandler(false)
2482
+ }))
2483
+ alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
2484
+ completionHandler(true)
2485
+ }))
2486
+
2487
+ self.present(alertController, animated: true, completion: nil)
2488
+ }
2489
+ }
2490
+
2491
+ public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
2492
+ guard let url = navigationAction.request.url else {
2493
+ return nil
2494
+ }
2495
+
2496
+ print("[InAppBrowser] Handling popup/new window request for URL: \(url.absoluteString)")
2497
+ return capBrowserPlugin?.createManagedPopupWebView(
2498
+ from: self,
2499
+ configuration: configuration,
2500
+ navigationAction: navigationAction
2501
+ )
2502
+ }
2503
+
2504
+ public func webViewDidClose(_ webView: WKWebView) {
2505
+ if webView == self.webView {
2506
+ closeView()
2507
+ }
2508
+ }
2509
+
2510
+ @available(iOS 15.0, *)
2511
+ public func webView(_ webView: WKWebView, requestGeolocationPermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
2512
+ print("[InAppBrowser] Geolocation permission requested for origin: \(origin.host)")
2513
+
2514
+ // Grant geolocation permission automatically for openWebView
2515
+ // This allows websites to access location when opened with openWebView
2516
+ decisionHandler(.grant)
2517
+ }
2518
+ }
2519
+
2520
+ // MARK: - Host Blocking Utilities
2521
+ extension WKWebViewController {
2522
+
2523
+ /// Checks if a host should be blocked based on the configured blocked hosts patterns
2524
+ /// - Parameter host: The host to check
2525
+ /// - Returns: true if the host should be blocked, false otherwise
2526
+ private func shouldBlockHost(_ host: String) -> Bool {
2527
+ guard !host.isEmpty else { return false }
2528
+
2529
+ let normalizedHost = host.lowercased()
2530
+
2531
+ return blockedHosts.contains { blockPattern in
2532
+ return matchesBlockPattern(host: normalizedHost, pattern: blockPattern.lowercased())
2533
+ }
2534
+ }
2535
+
2536
+ /// Matches a host against a blocking pattern (supports wildcards)
2537
+ /// - Parameters:
2538
+ /// - host: The normalized host to check
2539
+ /// - pattern: The normalized blocking pattern
2540
+ /// - Returns: true if the host matches the pattern
2541
+ private func matchesBlockPattern(host: String, pattern: String) -> Bool {
2542
+ guard !pattern.isEmpty else { return false }
2543
+
2544
+ // Exact match - fastest check first
2545
+ if host == pattern {
2546
+ return true
2547
+ }
2548
+
2549
+ // No wildcards - already checked exact match above
2550
+ guard pattern.contains("*") else {
2551
+ return false
2552
+ }
2553
+
2554
+ // Handle wildcard patterns
2555
+ if pattern.hasPrefix("*.") {
2556
+ return matchesWildcardDomain(host: host, pattern: pattern)
2557
+ } else if pattern.contains("*") {
2558
+ return matchesRegexPattern(host: host, pattern: pattern)
2559
+ }
2560
+
2561
+ return false
2562
+ }
2563
+
2564
+ /// Handles simple subdomain wildcard patterns like "*.example.com"
2565
+ /// - Parameters:
2566
+ /// - host: The host to check
2567
+ /// - pattern: The wildcard pattern starting with "*."
2568
+ /// - Returns: true if the host matches the wildcard domain
2569
+ private func matchesWildcardDomain(host: String, pattern: String) -> Bool {
2570
+ let domain = String(pattern.dropFirst(2)) // Remove "*."
2571
+
2572
+ guard !domain.isEmpty else { return false }
2573
+
2574
+ // Match exact domain or any subdomain
2575
+ return host == domain || host.hasSuffix("." + domain)
2576
+ }
2577
+
2578
+ /// Handles complex regex patterns with multiple wildcards
2579
+ /// - Parameters:
2580
+ /// - host: The host to check
2581
+ /// - pattern: The pattern with wildcards to convert to regex
2582
+ /// - Returns: true if the host matches the regex pattern
2583
+ private func matchesRegexPattern(host: String, pattern: String) -> Bool {
2584
+ // Escape everything, then re-enable '*' as a wildcard
2585
+ let escaped = NSRegularExpression.escapedPattern(for: pattern)
2586
+ let wildcardEnabled = escaped.replacingOccurrences(of: "\\*", with: ".*")
2587
+ let regexPattern = "^\(wildcardEnabled)$"
2588
+
2589
+ do {
2590
+ let regex = try NSRegularExpression(pattern: regexPattern, options: [])
2591
+ let range = NSRange(location: 0, length: host.utf16.count)
2592
+ return regex.firstMatch(in: host, options: [], range: range) != nil
2593
+ } catch {
2594
+ print("[InAppBrowser] Invalid regex pattern '\(regexPattern)': \(error)")
2595
+ return false
2596
+ }
2597
+ }
2598
+ }
2599
+
2600
+ // MARK: - WKNavigationDelegate
2601
+ extension WKWebViewController: WKNavigationDelegate {
2602
+ internal func injectPreShowScript() {
2603
+ if preShowSemaphore != nil {
2604
+ return
2605
+ }
2606
+
2607
+ // Safely construct script template with proper escaping
2608
+ let userScript = self.preShowScript ?? ""
2609
+
2610
+ // Build script using safe concatenation to avoid multi-line string issues
2611
+ let scriptTemplate = [
2612
+ "async function preShowFunction() {",
2613
+ userScript,
2614
+ "}",
2615
+ "preShowFunction().then(",
2616
+ " () => window.webkit.messageHandlers.preShowScriptSuccess.postMessage({})",
2617
+ ").catch(",
2618
+ " err => {",
2619
+ " console.error('Preshow error', err);",
2620
+ " window.webkit.messageHandlers.preShowScriptError.postMessage(JSON.stringify(err, Object.getOwnPropertyNames(err)));",
2621
+ " }",
2622
+ ")"
2623
+ ]
2624
+
2625
+ let script = scriptTemplate.joined(separator: "\n")
2626
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript script: \(script)")
2627
+
2628
+ self.preShowSemaphore = DispatchSemaphore(value: 0)
2629
+ self.executeScript(script: script) // this will run on the main thread
2630
+
2631
+ defer {
2632
+ self.preShowSemaphore = nil
2633
+ self.preShowError = nil
2634
+ }
2635
+
2636
+ if self.preShowSemaphore?.wait(timeout: .now() + 10) == .timedOut {
2637
+ print("[InAppBrowser - InjectPreShowScript] PreShowScript running for over 10 seconds. The plugin will not wait any longer!")
2638
+ return
2639
+ }
2640
+
2641
+ // "async function preShowFunction() {\n" +
2642
+ // self.preShowScript + "\n" +
2643
+ // "};\n" +
2644
+ // "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
2645
+
2646
+ }
2647
+
2648
+ public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
2649
+ updateBarButtonItems()
2650
+ self.progressView?.progress = 0
2651
+ if let urlValue = webView.url {
2652
+ self.url = urlValue
2653
+ delegate?.webViewController?(self, didStart: urlValue)
2654
+ }
2655
+ }
2656
+ public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
2657
+ if !didpageInit && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
2658
+ // Only inject preShowScript if it wasn't already injected at document start
2659
+ let shouldInjectScript = self.preShowScript.map { !$0.isEmpty } ?? false &&
2660
+ self.preShowScriptInjectionTime != "documentStart"
2661
+
2662
+ if shouldInjectScript {
2663
+ if self.opensHidden {
2664
+ DispatchQueue.global(qos: .userInitiated).async {
2665
+ self.injectPreShowScript()
2666
+ }
2667
+ } else {
2668
+ // injectPreShowScript will block, don't execute on the main thread
2669
+ DispatchQueue.global(qos: .userInitiated).async {
2670
+ self.injectPreShowScript()
2671
+ DispatchQueue.main.async { [weak self] in
2672
+ self?.capBrowserPlugin?.presentView(webViewId: self?.instanceId)
2673
+ }
2674
+ }
2675
+ }
2676
+ } else if !self.opensHidden {
2677
+ self.capBrowserPlugin?.presentView(webViewId: instanceId)
2678
+ }
2679
+ } else if self.preShowScript != nil &&
2680
+ !self.preShowScript!.isEmpty &&
2681
+ self.capBrowserPlugin?.isPresentAfterPageLoad == true &&
2682
+ self.preShowScriptInjectionTime != "documentStart" {
2683
+ // Only inject if not already injected at document start
2684
+ DispatchQueue.global(qos: .userInitiated).async {
2685
+ self.injectPreShowScript()
2686
+ }
2687
+ }
2688
+
2689
+ // Apply text zoom if set
2690
+ if let zoom = self.textZoom {
2691
+ applyTextZoom(zoom)
2692
+ }
2693
+
2694
+ didpageInit = true
2695
+ updateBarButtonItems()
2696
+ self.progressView?.progress = 0
2697
+ if let url = webView.url {
2698
+ self.url = url
2699
+ delegate?.webViewController?(self, didFinish: url)
2700
+ }
2701
+ self.injectJavaScriptInterface()
2702
+ emit("browserPageLoaded")
2703
+ }
2704
+
2705
+ public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
2706
+ updateBarButtonItems()
2707
+ self.progressView?.progress = 0
2708
+ if let url = webView.url {
2709
+ self.url = url
2710
+ delegate?.webViewController?(self, didFail: url, withError: error)
2711
+ }
2712
+ emit("pageLoadError")
2713
+ }
2714
+
2715
+ public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
2716
+ updateBarButtonItems()
2717
+ self.progressView?.progress = 0
2718
+ if let url = webView.url {
2719
+ self.url = url
2720
+ delegate?.webViewController?(self, didFail: url, withError: error)
2721
+ }
2722
+ emit("pageLoadError")
2723
+ }
2724
+
2725
+ public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
2726
+ if let credentials = credentials,
2727
+ challenge.protectionSpace.receivesCredentialSecurely,
2728
+ 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) {
2729
+ let urlCredential = URLCredential(user: credentials.username, password: credentials.password, persistence: .none)
2730
+ completionHandler(.useCredential, urlCredential)
2731
+ } else if let bypassedSSLHosts = bypassedSSLHosts, bypassedSSLHosts.contains(challenge.protectionSpace.host) {
2732
+ let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
2733
+ completionHandler(.useCredential, credential)
2734
+ } else {
2735
+ guard self.ignoreUntrustedSSLError else {
2736
+ completionHandler(.performDefaultHandling, nil)
2737
+ return
2738
+ }
2739
+ /* allows to open links with self-signed certificates
2740
+ Follow Apple's guidelines https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication
2741
+ */
2742
+ guard let serverTrust = challenge.protectionSpace.serverTrust else {
2743
+ completionHandler(.useCredential, nil)
2744
+ return
2745
+ }
2746
+ let credential = URLCredential(trust: serverTrust)
2747
+ completionHandler(.useCredential, credential)
2748
+ }
2749
+ self.injectJavaScriptInterface()
2750
+ }
2751
+
2752
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
2753
+ let shouldForceDownload = handleDownloads && navigationAction.shouldPerformDownload
2754
+
2755
+ var actionPolicy: WKNavigationActionPolicy = self.preventDeeplink ? .preventDeeplinkActionPolicy : .allow
2756
+
2757
+ guard let url = navigationAction.request.url else {
2758
+ print("[InAppBrowser] Cannot determine URL from navigationAction")
2759
+ decisionHandler(actionPolicy)
2760
+ return
2761
+ }
2762
+
2763
+ if url.absoluteString.contains("apps.apple.com") {
2764
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
2765
+ decisionHandler(.cancel)
2766
+ return
2767
+ }
2768
+
2769
+ if !self.allowsFileURL, url.isFileURL {
2770
+ print("[InAppBrowser] Cannot handle file URLs")
2771
+ decisionHandler(.cancel)
2772
+ return
2773
+ }
2774
+
2775
+ // Defer the rest of the logic until the async external-app handling checks completes.
2776
+ handleURLWithApp(url, targetFrame: navigationAction.targetFrame) { [weak self] openedExternally in
2777
+ guard let self else {
2778
+ decisionHandler(.cancel)
2779
+ return
2780
+ }
2781
+
2782
+ if openedExternally {
2783
+ decisionHandler(.cancel)
2784
+ return
2785
+ }
2786
+
2787
+ if self.shouldLoadBlankTargetInCurrentWebView(url, targetFrame: navigationAction.targetFrame) {
2788
+ if let webView = self.webView {
2789
+ webView.load(navigationAction.request)
2790
+ } else {
2791
+ self.load(remote: url)
2792
+ }
2793
+ decisionHandler(.cancel)
2794
+ return
2795
+ }
2796
+
2797
+ let host = url.host ?? ""
2798
+
2799
+ if host == self.source?.url?.host,
2800
+ let cookies = self.availableCookies,
2801
+ !self.checkRequestCookies(navigationAction.request, cookies: cookies) {
2802
+ self.load(remote: url)
2803
+ decisionHandler(.cancel)
2804
+ return
2805
+ }
2806
+
2807
+ if self.shouldBlockHost(host) {
2808
+ print("[InAppBrowser] Blocked host detected: \(host)")
2809
+ emit("urlChangeEvent", data: ["url": url.absoluteString])
2810
+ decisionHandler(.cancel)
2811
+ return
2812
+ }
2813
+
2814
+ if let navigationType = NavigationType(rawValue: navigationAction.navigationType.rawValue),
2815
+ let result = self.delegate?.webViewController?(self, decidePolicy: url, navigationType: navigationType) {
2816
+ actionPolicy = result ? .allow : .cancel
2817
+ }
2818
+
2819
+ if shouldForceDownload, actionPolicy != .cancel {
2820
+ decisionHandler(.download)
2821
+ return
2822
+ }
2823
+
2824
+ self.injectJavaScriptInterface()
2825
+ decisionHandler(actionPolicy)
2826
+ }
2827
+ }
2828
+
2829
+ public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
2830
+ if shouldInterceptDownload(for: navigationResponse) {
2831
+ decisionHandler(.download)
2832
+ return
2833
+ }
2834
+
2835
+ decisionHandler(.allow)
2836
+ }
2837
+
2838
+ public func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
2839
+ register(download: download, response: nil, sourceURL: navigationAction.request.url?.absoluteString)
2840
+ }
2841
+
2842
+ public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
2843
+ register(download: download, response: navigationResponse.response)
2844
+ }
2845
+
2846
+ // MARK: - Dimension Management
2847
+
2848
+ /// Apply custom dimensions to the view if specified
2849
+ open func applyCustomDimensions() {
2850
+ guard let navigationController = navigationController else { return }
2851
+
2852
+ // Apply custom dimensions if both width and height are specified
2853
+ if let width = customWidth, let height = customHeight {
2854
+ let xPos = customX ?? 0
2855
+ let yPos = customY ?? 0
2856
+
2857
+ // Set the frame for the navigation controller's view
2858
+ navigationController.view.frame = CGRect(x: xPos, y: yPos, width: width, height: height)
2859
+ }
2860
+ // If only height is specified, use fullscreen width
2861
+ else if let height = customHeight, customWidth == nil {
2862
+ let xPos = customX ?? 0
2863
+ let yPos = customY ?? 0
2864
+ let screenWidth = UIScreen.main.bounds.width
2865
+
2866
+ // Set the frame with fullscreen width and custom height
2867
+ navigationController.view.frame = CGRect(x: xPos, y: yPos, width: screenWidth, height: height)
2868
+ }
2869
+ // Otherwise, use default fullscreen behavior (no action needed)
2870
+ }
2871
+
2872
+ /// Update dimensions at runtime
2873
+ open func updateDimensions(width: CGFloat?, height: CGFloat?, xPos: CGFloat?, yPos: CGFloat?) {
2874
+ // Update stored dimensions
2875
+ if let width = width {
2876
+ customWidth = width
2877
+ }
2878
+ if let height = height {
2879
+ customHeight = height
2880
+ }
2881
+ if let xPos = xPos {
2882
+ customX = xPos
2883
+ }
2884
+ if let yPos = yPos {
2885
+ customY = yPos
2886
+ }
2887
+
2888
+ // Apply the new dimensions
2889
+ applyCustomDimensions()
2890
+ }
2891
+
2892
+ open func updateSafeTopMargin(_ enabled: Bool) {
2893
+ guard enabled != self.enabledSafeTopMargin else { return }
2894
+ self.enabledSafeTopMargin = enabled
2895
+ guard let webView = self.webView else { return }
2896
+ guard webView.superview === self.view else { return }
2897
+
2898
+ // Find and deactivate the existing top constraint
2899
+ let existingTopConstraints = self.view.constraints.filter {
2900
+ ($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .top
2901
+ }
2902
+ NSLayoutConstraint.deactivate(existingTopConstraints)
2903
+
2904
+ // Create new top constraint based on enabled value
2905
+ let topAnchor = enabled ? self.view.safeAreaLayoutGuide.topAnchor : self.view.topAnchor
2906
+ NSLayoutConstraint.activate([
2907
+ webView.topAnchor.constraint(equalTo: topAnchor)
2908
+ ])
2909
+ self.view.layoutIfNeeded()
2910
+ }
2911
+
2912
+ open func updateSafeBottomMargin(_ enabled: Bool) {
2913
+ guard enabled != self.enabledSafeBottomMargin else { return }
2914
+ self.enabledSafeBottomMargin = enabled
2915
+ guard let webView = self.webView else { return }
2916
+ guard webView.superview === self.view else { return }
2917
+
2918
+ // Find and deactivate the existing bottom constraint
2919
+ let existingBottomConstraints = self.view.constraints.filter {
2920
+ ($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .bottom
2921
+ }
2922
+ NSLayoutConstraint.deactivate(existingBottomConstraints)
2923
+
2924
+ // Create new bottom constraint based on enabled value
2925
+ let bottomAnchor = enabled ? self.view.safeAreaLayoutGuide.bottomAnchor : self.view.bottomAnchor
2926
+ NSLayoutConstraint.activate([
2927
+ webView.bottomAnchor.constraint(equalTo: bottomAnchor)
2928
+ ])
2929
+ self.view.layoutIfNeeded()
2930
+ }
2931
+ }
2932
+
2933
+ extension WKWebViewController: WKDownloadDelegate {
2934
+ public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
2935
+ let identifier = ObjectIdentifier(download)
2936
+ let sourceURL = downloadStates[identifier]?.sourceURL ?? response.url?.absoluteString
2937
+ do {
2938
+ let destinationURL = try uniqueDownloadDestination(for: suggestedFilename)
2939
+ downloadStates[identifier] = WKDownloadState(destinationURL: destinationURL, mimeType: response.mimeType, sourceURL: sourceURL)
2940
+ completionHandler(destinationURL)
2941
+ } catch {
2942
+ print("[InAppBrowser] Failed to prepare download destination: \(error)")
2943
+ downloadStates.removeValue(forKey: identifier)
2944
+ emitDownloadFailed(
2945
+ sourceURL: sourceURL,
2946
+ fileName: suggestedFilename,
2947
+ mimeType: response.mimeType,
2948
+ error: "Failed to prepare download destination: \(error.localizedDescription)"
2949
+ )
2950
+ completionHandler(nil)
2951
+ }
2952
+ }
2953
+
2954
+ public func downloadDidFinish(_ download: WKDownload) {
2955
+ let identifier = ObjectIdentifier(download)
2956
+ guard let state = downloadStates.removeValue(forKey: identifier) else {
2957
+ return
2958
+ }
2959
+ releaseDownloadDestination(state.destinationURL)
2960
+ guard let destinationURL = state.destinationURL else {
2961
+ emitDownloadFailed(sourceURL: state.sourceURL, mimeType: state.mimeType, error: "Download finished without a destination URL")
2962
+ return
2963
+ }
2964
+
2965
+ previewDownloadedFile(destinationURL, mimeType: state.mimeType, sourceURL: state.sourceURL)
2966
+ }
2967
+
2968
+ public func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
2969
+ let state = downloadStates.removeValue(forKey: ObjectIdentifier(download))
2970
+ releaseDownloadDestination(state?.destinationURL)
2971
+ emitDownloadFailed(
2972
+ sourceURL: state?.sourceURL,
2973
+ fileName: state?.destinationURL?.lastPathComponent,
2974
+ mimeType: state?.mimeType,
2975
+ error: "Download failed: \(error.localizedDescription)"
2976
+ )
2977
+ print("[InAppBrowser] Download failed: \(error.localizedDescription)")
2978
+ }
2979
+ }
2980
+
2981
+ extension WKWebViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate {
2982
+ public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
2983
+ previewItemURL == nil ? 0 : 1
2984
+ }
2985
+
2986
+ public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
2987
+ return previewItemURL! as NSURL
2988
+ }
2989
+
2990
+ public func previewControllerDidDismiss(_ controller: QLPreviewController) {
2991
+ previewItemURL = nil
2992
+ }
2993
+ }
2994
+
2995
+ class BlockBarButtonItem: UIBarButtonItem {
2996
+
2997
+ var block: ((WKWebViewController) -> Void)?
2998
+ }
2999
+
3000
+ /// Custom view that passes touches outside a target frame to the underlying view
3001
+ class PassThroughView: UIView {
3002
+ var targetFrame: CGRect?
3003
+
3004
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
3005
+ // If we have a target frame and the touch is outside it, pass through
3006
+ if let frame = targetFrame {
3007
+ if !frame.contains(point) {
3008
+ return nil // Pass through to underlying views
3009
+ }
3010
+ }
3011
+
3012
+ // Otherwise, handle normally
3013
+ return super.hitTest(point, with: event)
3014
+ }
3015
+ }
3016
+
3017
+ extension WKNavigationActionPolicy {
3018
+ static let preventDeeplinkActionPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!
3019
+ }
3020
+
3021
+ class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
3022
+ weak var delegate: WKScriptMessageHandler?
3023
+
3024
+ init(_ delegate: WKScriptMessageHandler) {
3025
+ self.delegate = delegate
3026
+ super.init()
3027
+ }
3028
+
3029
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
3030
+ self.delegate?.userContentController(userContentController, didReceive: message)
3031
+ }
3032
+ }