@capgo/inappbrowser 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CapgoInappbrowser.podspec +2 -2
  2. package/LICENSE +373 -21
  3. package/Package.swift +28 -0
  4. package/README.md +600 -74
  5. package/android/build.gradle +17 -16
  6. package/android/src/main/AndroidManifest.xml +14 -2
  7. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +952 -204
  8. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +478 -81
  9. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +10 -4
  10. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +3021 -226
  11. package/android/src/main/res/drawable/ic_refresh.xml +9 -0
  12. package/android/src/main/res/drawable/ic_share.xml +10 -0
  13. package/android/src/main/res/layout/activity_browser.xml +10 -0
  14. package/android/src/main/res/layout/content_browser.xml +3 -2
  15. package/android/src/main/res/layout/tool_bar.xml +44 -7
  16. package/android/src/main/res/values/strings.xml +4 -0
  17. package/android/src/main/res/values/themes.xml +27 -0
  18. package/android/src/main/res/xml/file_paths.xml +14 -0
  19. package/dist/docs.json +1289 -149
  20. package/dist/esm/definitions.d.ts +614 -25
  21. package/dist/esm/definitions.js +17 -1
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/index.d.ts +2 -2
  24. package/dist/esm/index.js +4 -4
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/web.d.ts +16 -3
  27. package/dist/esm/web.js +43 -7
  28. package/dist/esm/web.js.map +1 -1
  29. package/dist/plugin.cjs.js +60 -8
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/plugin.js +60 -8
  32. package/dist/plugin.js.map +1 -1
  33. package/ios/{Plugin → Sources/InAppBrowserPlugin}/Enums.swift +5 -5
  34. package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +954 -0
  35. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +2003 -0
  36. package/package.json +32 -30
  37. package/ios/Plugin/Assets.xcassets/Back.imageset/Back.png +0 -0
  38. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@2x.png +0 -0
  39. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@3x.png +0 -0
  40. package/ios/Plugin/Assets.xcassets/Back.imageset/Contents.json +0 -26
  41. package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
  42. package/ios/Plugin/Assets.xcassets/Forward.imageset/Contents.json +0 -26
  43. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward.png +0 -0
  44. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@2x.png +0 -0
  45. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@3x.png +0 -0
  46. package/ios/Plugin/InAppBrowserPlugin.h +0 -10
  47. package/ios/Plugin/InAppBrowserPlugin.m +0 -17
  48. package/ios/Plugin/InAppBrowserPlugin.swift +0 -203
  49. package/ios/Plugin/Info.plist +0 -24
  50. package/ios/Plugin/WKWebViewController.swift +0 -784
@@ -0,0 +1,954 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import WebKit
4
+
5
+ extension UIColor {
6
+
7
+ convenience init(hexString: String) {
8
+ let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
9
+ var int = UInt64()
10
+ Scanner(string: hex).scanHexInt64(&int)
11
+ let components = (
12
+ R: CGFloat((int >> 16) & 0xff) / 255,
13
+ G: CGFloat((int >> 08) & 0xff) / 255,
14
+ B: CGFloat((int >> 00) & 0xff) / 255
15
+ )
16
+ self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
17
+ }
18
+
19
+ }
20
+
21
+ /**
22
+ * Please read the Capacitor iOS Plugin Development Guide
23
+ * here: https://capacitorjs.com/docs/plugins/ios
24
+ */
25
+ @objc(InAppBrowserPlugin)
26
+ public class InAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
27
+ private let pluginVersion: String = "8.0.2"
28
+ public let identifier = "InAppBrowserPlugin"
29
+ public let jsName = "InAppBrowser"
30
+ public let pluginMethods: [CAPPluginMethod] = [
31
+ CAPPluginMethod(name: "goBack", returnType: CAPPluginReturnPromise),
32
+ CAPPluginMethod(name: "open", returnType: CAPPluginReturnPromise),
33
+ CAPPluginMethod(name: "openWebView", returnType: CAPPluginReturnPromise),
34
+ CAPPluginMethod(name: "clearCookies", returnType: CAPPluginReturnPromise),
35
+ CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
36
+ CAPPluginMethod(name: "clearAllCookies", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "clearCache", returnType: CAPPluginReturnPromise),
38
+ CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
39
+ CAPPluginMethod(name: "setUrl", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise),
41
+ CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise),
42
+ CAPPluginMethod(name: "executeScript", returnType: CAPPluginReturnPromise),
43
+ CAPPluginMethod(name: "postMessage", returnType: CAPPluginReturnPromise),
44
+ CAPPluginMethod(name: "updateDimensions", returnType: CAPPluginReturnPromise),
45
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
46
+ ]
47
+ var navigationWebViewController: UINavigationController?
48
+ private var privacyScreen: UIImageView?
49
+ private var isSetupDone = false
50
+ var currentPluginCall: CAPPluginCall?
51
+ var isPresentAfterPageLoad = false
52
+ var webViewController: WKWebViewController?
53
+ private var closeModalTitle: String?
54
+ private var closeModalDescription: String?
55
+ private var closeModalOk: String?
56
+ private var closeModalCancel: String?
57
+
58
+ private func setup() {
59
+ self.isSetupDone = true
60
+
61
+ #if swift(>=4.2)
62
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
63
+ NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil)
64
+ #else
65
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: nil)
66
+ NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: .UIApplicationWillResignActive, object: nil)
67
+ #endif
68
+ }
69
+
70
+ func presentView(isAnimated: Bool = true) {
71
+ guard let navigationController = self.navigationWebViewController else {
72
+ self.currentPluginCall?.reject("Navigation controller is not initialized")
73
+ return
74
+ }
75
+
76
+ self.bridge?.viewController?.present(navigationController, animated: isAnimated, completion: {
77
+ self.currentPluginCall?.resolve()
78
+ })
79
+ }
80
+
81
+ @objc func clearAllCookies(_ call: CAPPluginCall) {
82
+ DispatchQueue.main.async {
83
+ let dataStore = WKWebsiteDataStore.default()
84
+ let dataTypes = Set([WKWebsiteDataTypeCookies])
85
+
86
+ dataStore.removeData(ofTypes: dataTypes,
87
+ modifiedSince: Date(timeIntervalSince1970: 0)) {
88
+ call.resolve()
89
+ }
90
+ }
91
+ }
92
+
93
+ @objc func clearCache(_ call: CAPPluginCall) {
94
+ DispatchQueue.main.async {
95
+ let dataStore = WKWebsiteDataStore.default()
96
+ let dataTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
97
+
98
+ dataStore.removeData(ofTypes: dataTypes,
99
+ modifiedSince: Date(timeIntervalSince1970: 0)) {
100
+ call.resolve()
101
+ }
102
+ }
103
+ }
104
+
105
+ @objc func clearCookies(_ call: CAPPluginCall) {
106
+ guard let url = call.getString("url"),
107
+ let host = URL(string: url)?.host else {
108
+ call.reject("Invalid URL")
109
+ return
110
+ }
111
+
112
+ DispatchQueue.main.async {
113
+ WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
114
+ let group = DispatchGroup()
115
+ for cookie in cookies {
116
+ if cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain) {
117
+ group.enter()
118
+ WKWebsiteDataStore.default().httpCookieStore.delete(cookie) {
119
+ group.leave()
120
+ }
121
+ }
122
+ }
123
+
124
+ group.notify(queue: .main) {
125
+ call.resolve()
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ @objc func getCookies(_ call: CAPPluginCall) {
132
+ let urlString = call.getString("url") ?? ""
133
+ let includeHttpOnly = call.getBool("includeHttpOnly") ?? true
134
+
135
+ guard let url = URL(string: urlString), let host = url.host else {
136
+ call.reject("Invalid URL")
137
+ return
138
+ }
139
+
140
+ DispatchQueue.main.async {
141
+ WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
142
+ var cookieDict = [String: String]()
143
+ for cookie in cookies {
144
+
145
+ if (includeHttpOnly || !cookie.isHTTPOnly) && (cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain)) {
146
+ cookieDict[cookie.name] = cookie.value
147
+ }
148
+ }
149
+ call.resolve(cookieDict)
150
+ }
151
+ }
152
+
153
+ }
154
+
155
+ @objc func openWebView(_ call: CAPPluginCall) {
156
+ if !self.isSetupDone {
157
+ self.setup()
158
+ }
159
+ self.currentPluginCall = call
160
+
161
+ guard let urlString = call.getString("url") else {
162
+ call.reject("Must provide a URL to open")
163
+ return
164
+ }
165
+
166
+ if urlString.isEmpty {
167
+ call.reject("URL must not be empty")
168
+ return
169
+ }
170
+
171
+ var buttonNearDoneIcon: UIImage?
172
+ if let buttonNearDoneSettings = call.getObject("buttonNearDone") {
173
+ guard let iosSettingsRaw = buttonNearDoneSettings["ios"] else {
174
+ call.reject("IOS settings not found")
175
+ return
176
+ }
177
+ guard let iosSettings = iosSettingsRaw as? JSObject else {
178
+ call.reject("IOS settings are not an object")
179
+ return
180
+ }
181
+
182
+ guard let iconType = iosSettings["iconType"] as? String else {
183
+ call.reject("buttonNearDone.iconType is empty")
184
+ return
185
+ }
186
+ if iconType != "sf-symbol" && iconType != "asset" {
187
+ call.reject("IconType is neither 'sf-symbol' nor 'asset'")
188
+ return
189
+ }
190
+ guard let icon = iosSettings["icon"] as? String else {
191
+ call.reject("buttonNearDone.icon is empty")
192
+ return
193
+ }
194
+
195
+ if iconType == "sf-symbol" {
196
+ buttonNearDoneIcon = UIImage(systemName: icon)?.withRenderingMode(.alwaysTemplate)
197
+ print("[DEBUG] Set buttonNearDone SF Symbol icon: \(icon)")
198
+ } else {
199
+ // Look in app's web assets/public directory
200
+ guard let webDir = Bundle.main.resourceURL?.appendingPathComponent("public") else {
201
+ print("[DEBUG] Failed to locate web assets directory")
202
+ return
203
+ }
204
+
205
+ // Try several path combinations to find the asset
206
+ let paths = [
207
+ icon, // Just the icon name
208
+ "public/\(icon)", // With public/ prefix
209
+ icon.replacingOccurrences(of: "public/", with: "") // Without public/ prefix
210
+ ]
211
+
212
+ var foundImage = false
213
+
214
+ for path in paths {
215
+ // Try as a direct path from web assets dir
216
+ let assetPath = path.replacingOccurrences(of: "public/", with: "")
217
+ let fileURL = webDir.appendingPathComponent(assetPath)
218
+
219
+ print("[DEBUG] Trying to load from: \(fileURL.path)")
220
+
221
+ if FileManager.default.fileExists(atPath: fileURL.path),
222
+ let data = try? Data(contentsOf: fileURL),
223
+ let img = UIImage(data: data) {
224
+ buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
225
+ print("[DEBUG] Successfully loaded buttonNearDone from web assets: \(fileURL.path)")
226
+ foundImage = true
227
+ break
228
+ }
229
+
230
+ // Try with www directory as an alternative
231
+ if let wwwDir = Bundle.main.resourceURL?.appendingPathComponent("www") {
232
+ let wwwFileURL = wwwDir.appendingPathComponent(assetPath)
233
+
234
+ print("[DEBUG] Trying to load from www dir: \(wwwFileURL.path)")
235
+
236
+ if FileManager.default.fileExists(atPath: wwwFileURL.path),
237
+ let data = try? Data(contentsOf: wwwFileURL),
238
+ let img = UIImage(data: data) {
239
+ buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
240
+ print("[DEBUG] Successfully loaded buttonNearDone from www dir: \(wwwFileURL.path)")
241
+ foundImage = true
242
+ break
243
+ }
244
+ }
245
+
246
+ // Try looking in app bundle assets
247
+ if let iconImage = UIImage(named: path) {
248
+ buttonNearDoneIcon = iconImage.withRenderingMode(.alwaysTemplate)
249
+ print("[DEBUG] Successfully loaded buttonNearDone from app bundle: \(path)")
250
+ foundImage = true
251
+ break
252
+ }
253
+ }
254
+
255
+ if !foundImage {
256
+ print("[DEBUG] Failed to load buttonNearDone icon: \(icon)")
257
+
258
+ // Debug info
259
+ if let resourceURL = Bundle.main.resourceURL {
260
+ print("[DEBUG] Resource URL: \(resourceURL.path)")
261
+
262
+ // List directories to help debugging
263
+ do {
264
+ let contents = try FileManager.default.contentsOfDirectory(atPath: resourceURL.path)
265
+ print("[DEBUG] Root bundle contents: \(contents)")
266
+
267
+ // Check if public or www directories exist
268
+ if contents.contains("public") {
269
+ let publicContents = try FileManager.default.contentsOfDirectory(
270
+ atPath: resourceURL.appendingPathComponent("public").path)
271
+ print("[DEBUG] Public dir contents: \(publicContents)")
272
+ }
273
+
274
+ if contents.contains("www") {
275
+ let wwwContents = try FileManager.default.contentsOfDirectory(
276
+ atPath: resourceURL.appendingPathComponent("www").path)
277
+ print("[DEBUG] WWW dir contents: \(wwwContents)")
278
+ }
279
+ } catch {
280
+ print("[DEBUG] Error listing directories: \(error)")
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
288
+ let closeModal = call.getBool("closeModal", false)
289
+ let closeModalTitle = call.getString("closeModalTitle", "Close")
290
+ let closeModalDescription = call.getString("closeModalDescription", "Are you sure you want to close this window?")
291
+ let closeModalOk = call.getString("closeModalOk", "OK")
292
+ let closeModalCancel = call.getString("closeModalCancel", "Cancel")
293
+ let isInspectable = call.getBool("isInspectable", false)
294
+ let preventDeeplink = call.getBool("preventDeeplink", false)
295
+ let isAnimated = call.getBool("isAnimated", true)
296
+ let enabledSafeBottomMargin = call.getBool("enabledSafeBottomMargin", false)
297
+
298
+ // Validate preShowScript requires isPresentAfterPageLoad
299
+ if call.getString("preShowScript") != nil && !call.getBool("isPresentAfterPageLoad", false) {
300
+ call.reject("preShowScript requires isPresentAfterPageLoad to be true")
301
+ return
302
+ }
303
+
304
+ // Validate closeModal options
305
+ if closeModal {
306
+ if call.getString("closeModalTitle") != nil ||
307
+ call.getString("closeModalDescription") != nil ||
308
+ call.getString("closeModalOk") != nil ||
309
+ call.getString("closeModalCancel") != nil {
310
+ // Store the values to be set after proper initialization
311
+ self.closeModalTitle = closeModalTitle
312
+ self.closeModalDescription = closeModalDescription
313
+ self.closeModalOk = closeModalOk
314
+ self.closeModalCancel = closeModalCancel
315
+ }
316
+ } else {
317
+ // Reject if closeModal is false but closeModal options are provided
318
+ if call.getString("closeModalTitle") != nil ||
319
+ call.getString("closeModalDescription") != nil ||
320
+ call.getString("closeModalOk") != nil ||
321
+ call.getString("closeModalCancel") != nil {
322
+ call.reject("closeModal options require closeModal to be true")
323
+ return
324
+ }
325
+ }
326
+
327
+ // Validate shareDisclaimer requires shareSubject
328
+ if call.getString("shareSubject") == nil && call.getObject("shareDisclaimer") != nil {
329
+ call.reject("shareDisclaimer requires shareSubject to be provided")
330
+ return
331
+ }
332
+
333
+ // Validate buttonNearDone compatibility with toolbar type
334
+ if call.getString("buttonNearDone") != nil {
335
+ let toolbarType = call.getString("toolbarType", "")
336
+ if toolbarType == "activity" || toolbarType == "navigation" || toolbarType == "blank" {
337
+ call.reject("buttonNearDone is not compatible with toolbarType: " + toolbarType)
338
+ return
339
+ }
340
+ }
341
+
342
+ var disclaimerContent: JSObject?
343
+ if let shareDisclaimerRaw = call.getObject("shareDisclaimer"), !shareDisclaimerRaw.isEmpty {
344
+ disclaimerContent = shareDisclaimerRaw
345
+ }
346
+
347
+ let toolbarType = call.getString("toolbarType", "")
348
+ let backgroundColor = call.getString("backgroundColor", "black") == "white" ? UIColor.white : UIColor.black
349
+
350
+ // Don't null out shareDisclaimer regardless of toolbarType
351
+ // if toolbarType != "activity" {
352
+ // disclaimerContent = nil
353
+ // }
354
+
355
+ let ignoreUntrustedSSLError = call.getBool("ignoreUntrustedSSLError", false)
356
+ let enableGooglePaySupport = call.getBool("enableGooglePaySupport", false)
357
+ let activeNativeNavigationForWebview = call.getBool("activeNativeNavigationForWebview", true)
358
+
359
+ self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
360
+ let showReloadButton = call.getBool("showReloadButton", false)
361
+
362
+ let blockedHostsRaw = call.getArray("blockedHosts", [])
363
+ let blockedHosts = blockedHostsRaw.compactMap { $0 as? String }
364
+
365
+ let authorizedAppLinksRaw = call.getArray("authorizedAppLinks", [])
366
+ let authorizedAppLinks = authorizedAppLinksRaw.compactMap { $0 as? String }
367
+
368
+ let credentials = self.readCredentials(call)
369
+
370
+ // Read dimension options
371
+ let width = call.getFloat("width")
372
+ let height = call.getFloat("height")
373
+ let xPos = call.getFloat("x")
374
+ let yPos = call.getFloat("y")
375
+
376
+ // Validate dimension parameters
377
+ if width != nil && height == nil {
378
+ call.reject("Height must be specified when width is provided")
379
+ return
380
+ }
381
+
382
+ DispatchQueue.main.async {
383
+ guard let url = URL(string: urlString) else {
384
+ call.reject("Invalid URL format")
385
+ return
386
+ }
387
+
388
+ self.webViewController = WKWebViewController.init(
389
+ url: url,
390
+ headers: headers,
391
+ isInspectable: isInspectable,
392
+ credentials: credentials,
393
+ preventDeeplink: preventDeeplink,
394
+ blankNavigationTab: toolbarType == "blank",
395
+ enabledSafeBottomMargin: enabledSafeBottomMargin,
396
+ blockedHosts: blockedHosts,
397
+ authorizedAppLinks: authorizedAppLinks,
398
+ )
399
+
400
+ guard let webViewController = self.webViewController else {
401
+ call.reject("Failed to initialize WebViewController")
402
+ return
403
+ }
404
+
405
+ // Set dimensions if provided
406
+ if let width = width {
407
+ webViewController.customWidth = CGFloat(width)
408
+ }
409
+ if let height = height {
410
+ webViewController.customHeight = CGFloat(height)
411
+ }
412
+ if let xPos = xPos {
413
+ webViewController.customX = CGFloat(xPos)
414
+ }
415
+ if let yPos = yPos {
416
+ webViewController.customY = CGFloat(yPos)
417
+ }
418
+
419
+ // Set native navigation gestures before view loads
420
+ webViewController.activeNativeNavigationForWebview = activeNativeNavigationForWebview
421
+
422
+ // Update the webview's gesture property (if webview already exists)
423
+ webViewController.updateNavigationGestures()
424
+
425
+ if self.bridge?.statusBarVisible == true {
426
+ let subviews = self.bridge?.webView?.superview?.subviews
427
+ if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
428
+ if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
429
+ webViewController.capacitorStatusBar = emptyStatusBar
430
+ emptyStatusBar.removeFromSuperview()
431
+ }
432
+ }
433
+ }
434
+
435
+ webViewController.source = .remote(url)
436
+ webViewController.leftNavigationBarItemTypes = []
437
+
438
+ // Configure close button based on showArrow
439
+ let showArrow = call.getBool("showArrow", false)
440
+ if showArrow {
441
+ // When showArrow is true, put arrow on left
442
+ webViewController.doneBarButtonItemPosition = .left
443
+ webViewController.showArrowAsClose = true
444
+ } else {
445
+ // Default X on right
446
+ webViewController.doneBarButtonItemPosition = toolbarType == "activity" ? .none : .right
447
+ webViewController.showArrowAsClose = false
448
+ }
449
+
450
+ // Configure navigation buttons based on toolbarType
451
+ if toolbarType == "activity" {
452
+ // Activity mode should ONLY have:
453
+ // 1. Close button (if not hidden by doneBarButtonItemPosition)
454
+ // 2. Share button (if shareSubject is provided)
455
+ webViewController.leftNavigationBarItemTypes = [] // Clear any left items
456
+ webViewController.rightNavigaionBarItemTypes = [] // Clear any right items
457
+
458
+ // Only add share button if subject is provided
459
+ if call.getString("shareSubject") != nil {
460
+ // Add share button to right bar
461
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
462
+ print("[DEBUG] Activity mode: Added share button, shareSubject: \(call.getString("shareSubject") ?? "nil")")
463
+ } else {
464
+ // In activity mode, always make the share button visible by setting a default shareSubject
465
+ webViewController.shareSubject = "Share"
466
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
467
+ print("[DEBUG] Activity mode: Setting default shareSubject")
468
+ }
469
+
470
+ // Set done button position based on showArrow
471
+ if showArrow {
472
+ webViewController.doneBarButtonItemPosition = .left
473
+ } else {
474
+ // In activity mode, keep the done button visible even when showArrow is false
475
+ webViewController.doneBarButtonItemPosition = .right
476
+ }
477
+ } else if toolbarType == "navigation" {
478
+ // Navigation mode puts back/forward on the left
479
+ webViewController.leftNavigationBarItemTypes = [.back, .forward]
480
+ if showReloadButton {
481
+ webViewController.leftNavigationBarItemTypes.append(.reload)
482
+ }
483
+
484
+ // Only add share button if subject is provided
485
+ if call.getString("shareSubject") != nil {
486
+ // Add share button to right navigation bar
487
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
488
+ }
489
+ } else {
490
+ // Other modes may have reload button
491
+ if showReloadButton {
492
+ webViewController.leftNavigationBarItemTypes.append(.reload)
493
+ }
494
+
495
+ // Only add share button if subject is provided
496
+ if call.getString("shareSubject") != nil {
497
+ // Add share button to right navigation bar
498
+ webViewController.rightNavigaionBarItemTypes.append(.activity)
499
+ }
500
+ }
501
+
502
+ // Set buttonNearDoneIcon if provided
503
+ if let buttonNearDoneIcon = buttonNearDoneIcon {
504
+ webViewController.buttonNearDoneIcon = buttonNearDoneIcon
505
+ print("[DEBUG] Button near done icon set: \(buttonNearDoneIcon)")
506
+ }
507
+
508
+ webViewController.capBrowserPlugin = self
509
+ webViewController.title = call.getString("title", "New Window")
510
+ // Only set shareSubject if not already set for activity mode
511
+ if webViewController.shareSubject == nil {
512
+ webViewController.shareSubject = call.getString("shareSubject")
513
+ }
514
+ webViewController.shareDisclaimer = disclaimerContent
515
+
516
+ // Debug shareDisclaimer
517
+ if let disclaimer = disclaimerContent {
518
+ print("[DEBUG] Share disclaimer set: \(disclaimer)")
519
+ } else {
520
+ print("[DEBUG] No share disclaimer set")
521
+ }
522
+
523
+ webViewController.preShowScript = call.getString("preShowScript")
524
+ webViewController.preShowScriptInjectionTime = call.getString("preShowScriptInjectionTime", "pageLoad")
525
+
526
+ // If script should be injected at document start, inject it now
527
+ if webViewController.preShowScriptInjectionTime == "documentStart" {
528
+ webViewController.injectPreShowScriptAtDocumentStart()
529
+ }
530
+
531
+ webViewController.websiteTitleInNavigationBar = call.getBool("visibleTitle", true)
532
+ webViewController.ignoreUntrustedSSLError = ignoreUntrustedSSLError
533
+
534
+ // Set Google Pay support
535
+ webViewController.enableGooglePaySupport = enableGooglePaySupport
536
+
537
+ // Set text zoom if specified
538
+ if let textZoom = call.getInt("textZoom") {
539
+ webViewController.textZoom = textZoom
540
+ }
541
+
542
+ // Set closeModal properties after proper initialization
543
+ if closeModal {
544
+ webViewController.closeModal = true
545
+ webViewController.closeModalTitle = self.closeModalTitle ?? closeModalTitle
546
+ webViewController.closeModalDescription = self.closeModalDescription ?? closeModalDescription
547
+ webViewController.closeModalOk = self.closeModalOk ?? closeModalOk
548
+ webViewController.closeModalCancel = self.closeModalCancel ?? closeModalCancel
549
+ }
550
+
551
+ self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
552
+ self.navigationWebViewController?.navigationBar.isTranslucent = false
553
+ self.navigationWebViewController?.toolbar.isTranslucent = false
554
+
555
+ // Ensure no lines or borders appear by default
556
+ self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
557
+ self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
558
+ self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
559
+ self.navigationWebViewController?.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any)
560
+
561
+ // Handle web view background color
562
+ webViewController.view.backgroundColor = backgroundColor
563
+
564
+ // Handle toolbar color
565
+ if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
566
+ // If specific color provided, use it
567
+ let color = UIColor(hexString: toolbarColor)
568
+
569
+ // Apply to status bar and navigation bar area with a single colored view
570
+ webViewController.setupStatusBarBackground(color: color)
571
+
572
+ // Set status bar style based on toolbar color
573
+ let isDark = self.isDarkColor(color)
574
+ webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
575
+ webViewController.updateStatusBarStyle()
576
+
577
+ // Apply text color
578
+ let textColor: UIColor
579
+ if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
580
+ textColor = UIColor(hexString: toolbarTextColor)
581
+ } else {
582
+ textColor = isDark ? UIColor.white : UIColor.black
583
+ }
584
+
585
+ // Apply tint color to all UI elements without changing background
586
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
587
+ webViewController.tintColor = textColor
588
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
589
+ } else {
590
+ // Use system appearance
591
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
592
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
593
+ let textColor: UIColor
594
+
595
+ if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
596
+ textColor = UIColor(hexString: toolbarTextColor)
597
+ } else {
598
+ textColor = isDarkMode ? UIColor.white : UIColor.black
599
+ }
600
+
601
+ // Apply colors
602
+ webViewController.setupStatusBarBackground(color: backgroundColor)
603
+ webViewController.tintColor = textColor
604
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
605
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
606
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
607
+ webViewController.updateStatusBarStyle()
608
+
609
+ }
610
+
611
+ // Configure modal presentation for touch passthrough if custom dimensions are set
612
+ if width != nil || height != nil {
613
+ self.navigationWebViewController?.modalPresentationStyle = .overFullScreen
614
+
615
+ // Create a pass-through container
616
+ let containerView = PassThroughView()
617
+ containerView.backgroundColor = .clear
618
+
619
+ // Calculate dimensions - use screen width if only height is provided
620
+ let finalWidth = width.map { CGFloat($0) } ?? UIScreen.main.bounds.width
621
+ let finalHeight = height.map { CGFloat($0) } ?? UIScreen.main.bounds.height
622
+
623
+ containerView.targetFrame = CGRect(
624
+ x: CGFloat(xPos ?? 0),
625
+ y: CGFloat(yPos ?? 0),
626
+ width: finalWidth,
627
+ height: finalHeight
628
+ )
629
+
630
+ // Replace the navigation controller's view with our pass-through container
631
+ if let navController = self.navigationWebViewController,
632
+ let originalView = navController.view {
633
+ navController.view = containerView
634
+ containerView.addSubview(originalView)
635
+ originalView.frame = CGRect(
636
+ x: CGFloat(xPos ?? 0),
637
+ y: CGFloat(yPos ?? 0),
638
+ width: finalWidth,
639
+ height: finalHeight
640
+ )
641
+ }
642
+ } else {
643
+ self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
644
+ }
645
+
646
+ self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
647
+ if toolbarType == "blank" {
648
+ self.navigationWebViewController?.navigationBar.isHidden = true
649
+ webViewController.blankNavigationTab = true
650
+
651
+ // Even with hidden navigation bar, we need to set proper status bar appearance
652
+ // If toolbarColor is explicitly set, use that for status bar style
653
+ if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
654
+ let color = UIColor(hexString: toolbarColor)
655
+ let isDark = self.isDarkColor(color)
656
+ webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
657
+ webViewController.updateStatusBarStyle()
658
+
659
+ // Apply status bar background color via the special view
660
+ webViewController.setupStatusBarBackground(color: color)
661
+
662
+ // Apply background color to whole view to ensure no gaps
663
+ webViewController.view.backgroundColor = color
664
+ self.navigationWebViewController?.view.backgroundColor = color
665
+
666
+ // Apply status bar background color
667
+ if let navController = self.navigationWebViewController {
668
+ navController.view.backgroundColor = color
669
+ }
670
+ } else {
671
+ // Follow system appearance if no specific color
672
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
673
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
674
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
675
+ webViewController.updateStatusBarStyle()
676
+
677
+ // Apply status bar background color via the special view
678
+ webViewController.setupStatusBarBackground(color: backgroundColor)
679
+
680
+ // Set appropriate background color
681
+ if let navController = self.navigationWebViewController {
682
+ navController.view.backgroundColor = backgroundColor
683
+ }
684
+ }
685
+
686
+ }
687
+
688
+ // We don't use the toolbar anymore, always hide it
689
+ self.navigationWebViewController?.setToolbarHidden(true, animated: false)
690
+
691
+ if !self.isPresentAfterPageLoad {
692
+ self.presentView(isAnimated: isAnimated)
693
+ }
694
+ call.resolve()
695
+ }
696
+ }
697
+
698
+ @objc func goBack(_ call: CAPPluginCall) {
699
+ DispatchQueue.main.async {
700
+ guard let webViewController = self.webViewController else {
701
+ call.resolve(["canGoBack": false])
702
+ return
703
+ }
704
+
705
+ let canGoBack = webViewController.goBack()
706
+ call.resolve(["canGoBack": canGoBack])
707
+ }
708
+ }
709
+
710
+ @objc func reload(_ call: CAPPluginCall) {
711
+ self.webViewController?.reload()
712
+ call.resolve()
713
+ }
714
+
715
+ @objc func setUrl(_ call: CAPPluginCall) {
716
+ guard let urlString = call.getString("url") else {
717
+ call.reject("Cannot get new url to set")
718
+ return
719
+ }
720
+
721
+ guard let url = URL(string: urlString) else {
722
+ call.reject("Invalid URL")
723
+ return
724
+ }
725
+
726
+ self.webViewController?.load(remote: url)
727
+ call.resolve()
728
+ }
729
+
730
+ @objc func executeScript(_ call: CAPPluginCall) {
731
+ guard let script = call.getString("code") else {
732
+ call.reject("Cannot get script to execute")
733
+ return
734
+ }
735
+ DispatchQueue.main.async {
736
+ self.webViewController?.executeScript(script: script)
737
+ call.resolve()
738
+ }
739
+ }
740
+
741
+ @objc func postMessage(_ call: CAPPluginCall) {
742
+ let eventData = call.getObject("detail", [:])
743
+ // Check if eventData is empty
744
+ if eventData.isEmpty {
745
+ call.reject("Event data must not be empty")
746
+ return
747
+ }
748
+ print("Event data: \(eventData)")
749
+
750
+ DispatchQueue.main.async {
751
+ self.webViewController?.postMessageToJS(message: eventData)
752
+ }
753
+ call.resolve()
754
+ }
755
+
756
+ func isHexColorCode(_ input: String) -> Bool {
757
+ let hexColorRegex = "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$"
758
+
759
+ do {
760
+ let regex = try NSRegularExpression(pattern: hexColorRegex)
761
+ let range = NSRange(location: 0, length: input.utf16.count)
762
+ if regex.firstMatch(in: input, options: [], range: range) != nil {
763
+ return true
764
+ }
765
+ } catch {
766
+ print("Error creating regular expression: \(error)")
767
+ }
768
+
769
+ return false
770
+ }
771
+
772
+ @objc func open(_ call: CAPPluginCall) {
773
+ if !self.isSetupDone {
774
+ self.setup()
775
+ }
776
+
777
+ let isInspectable = call.getBool("isInspectable", false)
778
+ let preventDeeplink = call.getBool("preventDeeplink", false)
779
+ self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
780
+
781
+ self.currentPluginCall = call
782
+
783
+ guard let urlString = call.getString("url") else {
784
+ call.reject("Must provide a URL to open")
785
+ return
786
+ }
787
+
788
+ if urlString.isEmpty {
789
+ call.reject("URL must not be empty")
790
+ return
791
+ }
792
+
793
+ let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
794
+ let credentials = self.readCredentials(call)
795
+
796
+ DispatchQueue.main.async {
797
+ guard let url = URL(string: urlString) else {
798
+ call.reject("Invalid URL format")
799
+ return
800
+ }
801
+
802
+ self.webViewController = WKWebViewController.init(url: url, headers: headers, isInspectable: isInspectable, credentials: credentials, preventDeeplink: preventDeeplink, blankNavigationTab: true, enabledSafeBottomMargin: false)
803
+
804
+ guard let webViewController = self.webViewController else {
805
+ call.reject("Failed to initialize WebViewController")
806
+ return
807
+ }
808
+
809
+ if self.bridge?.statusBarVisible == true {
810
+ let subviews = self.bridge?.webView?.superview?.subviews
811
+ if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
812
+ if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
813
+ webViewController.capacitorStatusBar = emptyStatusBar
814
+ emptyStatusBar.removeFromSuperview()
815
+ }
816
+ }
817
+ }
818
+
819
+ webViewController.source = .remote(url)
820
+ webViewController.leftNavigationBarItemTypes = [.back, .forward, .reload]
821
+ webViewController.capBrowserPlugin = self
822
+ webViewController.hasDynamicTitle = true
823
+
824
+ self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
825
+ self.navigationWebViewController?.navigationBar.isTranslucent = false
826
+
827
+ // Ensure no lines or borders appear by default
828
+ self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
829
+ self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
830
+ self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
831
+
832
+ // Use system appearance
833
+ let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
834
+ let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
835
+ let textColor = isDarkMode ? UIColor.white : UIColor.black
836
+
837
+ // Apply colors
838
+ webViewController.setupStatusBarBackground(color: backgroundColor)
839
+ webViewController.tintColor = textColor
840
+ self.navigationWebViewController?.navigationBar.tintColor = textColor
841
+ self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
842
+ webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
843
+ webViewController.updateStatusBarStyle()
844
+
845
+ // Always hide toolbar to ensure no bottom bar
846
+ self.navigationWebViewController?.setToolbarHidden(true, animated: false)
847
+
848
+ self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
849
+ self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
850
+
851
+ if !self.isPresentAfterPageLoad {
852
+ self.presentView()
853
+ }
854
+ call.resolve()
855
+ }
856
+ }
857
+
858
+ @objc func close(_ call: CAPPluginCall) {
859
+ let isAnimated = call.getBool("isAnimated", true)
860
+
861
+ DispatchQueue.main.async {
862
+ let currentUrl = self.webViewController?.url?.absoluteString ?? ""
863
+
864
+ self.webViewController?.cleanupWebView()
865
+
866
+ self.navigationWebViewController?.dismiss(animated: isAnimated) {
867
+ self.webViewController = nil
868
+ self.navigationWebViewController = nil
869
+ }
870
+
871
+ self.notifyListeners("closeEvent", data: ["url": currentUrl])
872
+ call.resolve()
873
+ }
874
+ }
875
+
876
+ private func showPrivacyScreen() {
877
+ if privacyScreen == nil {
878
+ let newPrivacyScreen = UIImageView()
879
+ self.privacyScreen = newPrivacyScreen
880
+ if let launchImage = UIImage(named: "LaunchImage") {
881
+ newPrivacyScreen.image = launchImage
882
+ newPrivacyScreen.frame = UIScreen.main.bounds
883
+ newPrivacyScreen.contentMode = .scaleAspectFill
884
+ newPrivacyScreen.isUserInteractionEnabled = false
885
+ } else if let launchImage = UIImage(named: "Splash") {
886
+ newPrivacyScreen.image = launchImage
887
+ newPrivacyScreen.frame = UIScreen.main.bounds
888
+ newPrivacyScreen.contentMode = .scaleAspectFill
889
+ newPrivacyScreen.isUserInteractionEnabled = false
890
+ }
891
+ }
892
+ if let screen = self.privacyScreen {
893
+ self.navigationWebViewController?.view.addSubview(screen)
894
+ }
895
+ }
896
+
897
+ private func hidePrivacyScreen() {
898
+ self.privacyScreen?.removeFromSuperview()
899
+ }
900
+
901
+ @objc func appDidBecomeActive(_ notification: NSNotification) {
902
+ self.hidePrivacyScreen()
903
+ }
904
+
905
+ @objc func appWillResignActive(_ notification: NSNotification) {
906
+ self.showPrivacyScreen()
907
+ }
908
+
909
+ private func readCredentials(_ call: CAPPluginCall) -> WKWebViewCredentials? {
910
+ var credentials: WKWebViewCredentials?
911
+ let credentialsDict = call.getObject("credentials", [:]).mapValues { String(describing: $0 as Any) }
912
+ if !credentialsDict.isEmpty, let username = credentialsDict["username"], let password = credentialsDict["password"] {
913
+ credentials = WKWebViewCredentials(username: username, password: password)
914
+ }
915
+ return credentials
916
+ }
917
+
918
+ private func isDarkColor(_ color: UIColor) -> Bool {
919
+ let components = color.cgColor.components ?? []
920
+ let red = components[0]
921
+ let green = components[1]
922
+ let blue = components[2]
923
+ let brightness = (red * 299 + green * 587 + blue * 114) / 1000
924
+ return brightness < 0.5
925
+ }
926
+
927
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
928
+ call.resolve(["version": self.pluginVersion])
929
+ }
930
+
931
+ @objc func updateDimensions(_ call: CAPPluginCall) {
932
+ let width = call.getFloat("width")
933
+ let height = call.getFloat("height")
934
+ let xPos = call.getFloat("x")
935
+ let yPos = call.getFloat("y")
936
+
937
+ DispatchQueue.main.async {
938
+ guard let webViewController = self.webViewController else {
939
+ call.reject("WebView is not initialized")
940
+ return
941
+ }
942
+
943
+ webViewController.updateDimensions(
944
+ width: width.map { CGFloat($0) },
945
+ height: height.map { CGFloat($0) },
946
+ xPos: xPos.map { CGFloat($0) },
947
+ yPos: yPos.map { CGFloat($0) }
948
+ )
949
+
950
+ call.resolve()
951
+ }
952
+ }
953
+
954
+ }