@capgo/inappbrowser 8.0.0 → 8.0.1

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