@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.
- package/CapgoCapacitorInappbrowser.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +1610 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +22 -0
- package/android/src/main/assets/proxy-bridge.js +586 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ActiveWebViewSupport.java +10 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CustomSchemeInterceptSupport.java +51 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +1820 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/MobileAppBridgeScript.java +122 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/NativeProxyRule.java +128 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +725 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyBridge.java +93 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyRequestSupport.java +869 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyResponseRouting.java +35 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/SafeAreaInsetsSupport.java +28 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +42 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +6022 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewPermissionSupport.java +58 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/arrow_back_disabled.xml +9 -0
- package/android/src/main/res/drawable/arrow_back_enabled.xml +9 -0
- package/android/src/main/res/drawable/arrow_forward_disabled.xml +9 -0
- package/android/src/main/res/drawable/arrow_forward_enabled.xml +9 -0
- package/android/src/main/res/drawable/ic_clear_24px.xml +9 -0
- package/android/src/main/res/drawable/ic_refresh.xml +9 -0
- package/android/src/main/res/drawable/ic_share.xml +10 -0
- package/android/src/main/res/layout/activity_browser.xml +32 -0
- package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
- package/android/src/main/res/layout/content_browser.xml +17 -0
- package/android/src/main/res/layout/tool_bar.xml +87 -0
- package/android/src/main/res/values/browser_theme.xml +3 -0
- package/android/src/main/res/values/colors.xml +5 -0
- package/android/src/main/res/values/dimens.xml +3 -0
- package/android/src/main/res/values/strings.xml +13 -0
- package/android/src/main/res/values/styles.xml +4 -0
- package/android/src/main/res/values/themes.xml +27 -0
- package/android/src/main/res/xml/file_paths.xml +14 -0
- package/dist/docs.json +3200 -0
- package/dist/esm/definitions.d.ts +1507 -0
- package/dist/esm/definitions.js +40 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +102 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +47 -0
- package/dist/esm/web.js +120 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +270 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +273 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/InAppBrowserPlugin/Enums.swift +65 -0
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +1932 -0
- package/ios/Sources/InAppBrowserPlugin/ProxySchemeHandler.swift +1185 -0
- package/ios/Sources/InAppBrowserPlugin/WKWebView+SchemeHandling.swift +43 -0
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +3032 -0
- package/package.json +139 -0
|
@@ -0,0 +1,1932 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import WebKit
|
|
4
|
+
import AuthenticationServices
|
|
5
|
+
|
|
6
|
+
enum ActiveWebViewSupport {
|
|
7
|
+
static func shouldActivateNewWebView(isHidden: Bool, hasActiveWebView: Bool) -> Bool {
|
|
8
|
+
!isHidden || !hasActiveWebView
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static func resolveVisibilityTarget(originatingWebViewId: String?, activeWebViewId: String?) -> String? {
|
|
12
|
+
originatingWebViewId ?? activeWebViewId
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protocol ProxyRequestLocating {
|
|
17
|
+
func hasPendingProxyRequest(_ requestId: String) -> Bool
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
enum ProxyResponseRoutingSupport {
|
|
21
|
+
enum Resolution<T> {
|
|
22
|
+
case matched(T)
|
|
23
|
+
case ambiguous
|
|
24
|
+
case missing
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static func resolveTargetHandler<T: ProxyRequestLocating>(
|
|
28
|
+
webviewId: String?,
|
|
29
|
+
requestId: String,
|
|
30
|
+
handlers: [String: T]
|
|
31
|
+
) -> Resolution<T> {
|
|
32
|
+
if let webviewId, !webviewId.isEmpty {
|
|
33
|
+
guard let handler = handlers[webviewId] else {
|
|
34
|
+
return .missing
|
|
35
|
+
}
|
|
36
|
+
return .matched(handler)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var matchedHandler: T?
|
|
40
|
+
for handler in handlers.values where handler.hasPendingProxyRequest(requestId) {
|
|
41
|
+
if matchedHandler != nil {
|
|
42
|
+
return .ambiguous
|
|
43
|
+
}
|
|
44
|
+
matchedHandler = handler
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
guard let matchedHandler else {
|
|
48
|
+
return .missing
|
|
49
|
+
}
|
|
50
|
+
return .matched(matchedHandler)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
extension UIColor {
|
|
55
|
+
|
|
56
|
+
convenience init(hexString: String) {
|
|
57
|
+
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
58
|
+
var int = UInt64()
|
|
59
|
+
Scanner(string: hex).scanHexInt64(&int)
|
|
60
|
+
let components = (
|
|
61
|
+
R: CGFloat((int >> 16) & 0xff) / 255,
|
|
62
|
+
G: CGFloat((int >> 08) & 0xff) / 255,
|
|
63
|
+
B: CGFloat((int >> 00) & 0xff) / 255
|
|
64
|
+
)
|
|
65
|
+
self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
72
|
+
* here: https://capacitorjs.com/docs/plugins/ios
|
|
73
|
+
*/
|
|
74
|
+
@objc(InAppBrowserPlugin)
|
|
75
|
+
public class InAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
76
|
+
enum InvisibilityMode: String {
|
|
77
|
+
case aware = "AWARE"
|
|
78
|
+
case fakeVisible = "FAKE_VISIBLE"
|
|
79
|
+
}
|
|
80
|
+
private let pluginVersion: String = "8.6.15"
|
|
81
|
+
public let identifier = "InAppBrowserPlugin"
|
|
82
|
+
public let jsName = "InAppBrowser"
|
|
83
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
84
|
+
CAPPluginMethod(name: "goBack", returnType: CAPPluginReturnPromise),
|
|
85
|
+
CAPPluginMethod(name: "open", returnType: CAPPluginReturnPromise),
|
|
86
|
+
CAPPluginMethod(name: "openWebView", returnType: CAPPluginReturnPromise),
|
|
87
|
+
CAPPluginMethod(name: "clearCookies", returnType: CAPPluginReturnPromise),
|
|
88
|
+
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
|
|
89
|
+
CAPPluginMethod(name: "clearAllCookies", returnType: CAPPluginReturnPromise),
|
|
90
|
+
CAPPluginMethod(name: "clearCache", returnType: CAPPluginReturnPromise),
|
|
91
|
+
CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
|
|
92
|
+
CAPPluginMethod(name: "setUrl", returnType: CAPPluginReturnPromise),
|
|
93
|
+
CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise),
|
|
94
|
+
CAPPluginMethod(name: "hide", returnType: CAPPluginReturnPromise),
|
|
95
|
+
CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise),
|
|
96
|
+
CAPPluginMethod(name: "executeScript", returnType: CAPPluginReturnPromise),
|
|
97
|
+
CAPPluginMethod(name: "postMessage", returnType: CAPPluginReturnPromise),
|
|
98
|
+
CAPPluginMethod(name: "takeScreenshot", returnType: CAPPluginReturnPromise),
|
|
99
|
+
CAPPluginMethod(name: "updateDimensions", returnType: CAPPluginReturnPromise),
|
|
100
|
+
CAPPluginMethod(name: "setEnabledSafeTopMargin", returnType: CAPPluginReturnPromise),
|
|
101
|
+
CAPPluginMethod(name: "setEnabledSafeBottomMargin", returnType: CAPPluginReturnPromise),
|
|
102
|
+
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
|
|
103
|
+
CAPPluginMethod(name: "openSecureWindow", returnType: CAPPluginReturnPromise),
|
|
104
|
+
CAPPluginMethod(name: "handleProxyRequest", returnType: CAPPluginReturnPromise)
|
|
105
|
+
]
|
|
106
|
+
var navigationWebViewController: UINavigationController?
|
|
107
|
+
private var navigationControllers: [String: UINavigationController] = [:]
|
|
108
|
+
private var privacyScreen: UIImageView?
|
|
109
|
+
private var isSetupDone = false
|
|
110
|
+
var currentPluginCall: CAPPluginCall?
|
|
111
|
+
var isPresentAfterPageLoad = false
|
|
112
|
+
var isHidden = false
|
|
113
|
+
var invisibilityMode: InvisibilityMode = .aware
|
|
114
|
+
var webViewController: WKWebViewController?
|
|
115
|
+
private var webViewControllers: [String: WKWebViewController] = [:]
|
|
116
|
+
private var proxySchemeHandlers: [String: ProxySchemeHandler] = [:]
|
|
117
|
+
private var webViewStack: [String] = []
|
|
118
|
+
private var activeWebViewId: String?
|
|
119
|
+
private weak var presentationContainerView: UIView?
|
|
120
|
+
private var presentationContainerWasInteractive = true
|
|
121
|
+
private var presentationContainerPreviousAlpha: CGFloat = 1
|
|
122
|
+
private var hiddenWebViewContainers: [ObjectIdentifier: UIView] = [:]
|
|
123
|
+
private var closeModalTitle: String?
|
|
124
|
+
private var closeModalDescription: String?
|
|
125
|
+
private var closeModalOk: String?
|
|
126
|
+
private var closeModalCancel: String?
|
|
127
|
+
private var openSecureWindowCall: CAPPluginCall?
|
|
128
|
+
|
|
129
|
+
private func setup() {
|
|
130
|
+
self.isSetupDone = true
|
|
131
|
+
|
|
132
|
+
#if swift(>=4.2)
|
|
133
|
+
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
134
|
+
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil)
|
|
135
|
+
#else
|
|
136
|
+
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: nil)
|
|
137
|
+
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: .UIApplicationWillResignActive, object: nil)
|
|
138
|
+
#endif
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func setActiveWebView(id: String, webView: WKWebViewController, navigationController: UINavigationController) {
|
|
142
|
+
activeWebViewId = id
|
|
143
|
+
self.webViewController = webView
|
|
144
|
+
self.navigationWebViewController = navigationController
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func registerWebView(
|
|
148
|
+
id: String,
|
|
149
|
+
webView: WKWebViewController,
|
|
150
|
+
navigationController: UINavigationController,
|
|
151
|
+
makeActive: Bool = true
|
|
152
|
+
) {
|
|
153
|
+
webViewControllers[id] = webView
|
|
154
|
+
navigationControllers[id] = navigationController
|
|
155
|
+
webViewStack.removeAll { $0 == id }
|
|
156
|
+
webViewStack.append(id)
|
|
157
|
+
if makeActive || activeWebViewId == nil || self.webViewController == nil || self.navigationWebViewController == nil {
|
|
158
|
+
setActiveWebView(id: id, webView: webView, navigationController: navigationController)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private func unregisterWebView(id: String) {
|
|
163
|
+
if let webView = webViewControllers[id]?.capableWebView {
|
|
164
|
+
cleanupHiddenWebViewContainer(for: webView)
|
|
165
|
+
}
|
|
166
|
+
proxySchemeHandlers[id]?.cancelAllPendingTasks()
|
|
167
|
+
proxySchemeHandlers[id] = nil
|
|
168
|
+
webViewControllers[id] = nil
|
|
169
|
+
navigationControllers[id] = nil
|
|
170
|
+
webViewStack.removeAll { $0 == id }
|
|
171
|
+
activeWebViewId = webViewStack.last
|
|
172
|
+
if let activeId = activeWebViewId {
|
|
173
|
+
self.webViewController = webViewControllers[activeId]
|
|
174
|
+
self.navigationWebViewController = navigationControllers[activeId]
|
|
175
|
+
} else {
|
|
176
|
+
self.webViewController = nil
|
|
177
|
+
self.navigationWebViewController = nil
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func resolveNavigationController(for id: String?) -> UINavigationController? {
|
|
182
|
+
if let id {
|
|
183
|
+
return navigationControllers[id]
|
|
184
|
+
}
|
|
185
|
+
return navigationWebViewController
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func notifyPopupWindowOpened(id: String, parentId: String?, url: String?, visible: Bool) {
|
|
189
|
+
var event: [String: Any] = [
|
|
190
|
+
"id": id,
|
|
191
|
+
"visible": visible
|
|
192
|
+
]
|
|
193
|
+
if let parentId, !parentId.isEmpty {
|
|
194
|
+
event["parentId"] = parentId
|
|
195
|
+
}
|
|
196
|
+
if let url, !url.isEmpty {
|
|
197
|
+
event["url"] = url
|
|
198
|
+
}
|
|
199
|
+
notifyListeners("popupWindowOpened", data: event)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private func cloneNavigationAppearance(from source: UINavigationController?, to target: UINavigationController) {
|
|
203
|
+
guard let source else { return }
|
|
204
|
+
target.navigationBar.isTranslucent = source.navigationBar.isTranslucent
|
|
205
|
+
target.toolbar.isTranslucent = source.toolbar.isTranslucent
|
|
206
|
+
target.navigationBar.tintColor = source.navigationBar.tintColor
|
|
207
|
+
target.navigationBar.barTintColor = source.navigationBar.barTintColor
|
|
208
|
+
target.navigationBar.titleTextAttributes = source.navigationBar.titleTextAttributes
|
|
209
|
+
target.toolbar.tintColor = source.toolbar.tintColor
|
|
210
|
+
|
|
211
|
+
if #available(iOS 13.0, *) {
|
|
212
|
+
target.navigationBar.standardAppearance = source.navigationBar.standardAppearance
|
|
213
|
+
target.navigationBar.scrollEdgeAppearance = source.navigationBar.scrollEdgeAppearance
|
|
214
|
+
target.navigationBar.compactAppearance = source.navigationBar.compactAppearance
|
|
215
|
+
target.navigationBar.compactScrollEdgeAppearance = source.navigationBar.compactScrollEdgeAppearance
|
|
216
|
+
target.toolbar.standardAppearance = source.toolbar.standardAppearance
|
|
217
|
+
target.toolbar.compactAppearance = source.toolbar.compactAppearance
|
|
218
|
+
target.toolbar.scrollEdgeAppearance = source.toolbar.scrollEdgeAppearance
|
|
219
|
+
} else {
|
|
220
|
+
target.navigationBar.setBackgroundImage(source.navigationBar.backgroundImage(for: .default), for: .default)
|
|
221
|
+
target.navigationBar.shadowImage = source.navigationBar.shadowImage
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func createManagedPopupWebView(
|
|
226
|
+
from parentController: WKWebViewController,
|
|
227
|
+
configuration: WKWebViewConfiguration,
|
|
228
|
+
navigationAction: WKNavigationAction
|
|
229
|
+
) -> WKWebView? {
|
|
230
|
+
let popupId = UUID().uuidString
|
|
231
|
+
let shouldHidePopup = parentController.hiddenPopupWindow
|
|
232
|
+
if let parentWebView = parentController.capableWebView {
|
|
233
|
+
configuration.processPool = parentWebView.configuration.processPool
|
|
234
|
+
}
|
|
235
|
+
if let dataStore = parentController.websiteDataStore() {
|
|
236
|
+
configuration.websiteDataStore = dataStore
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let proxyHandler = parentController.proxySchemeHandler?.duplicate(for: popupId)
|
|
240
|
+
if let proxyHandler {
|
|
241
|
+
proxySchemeHandlers[popupId] = proxyHandler
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let popupController = WKWebViewController()
|
|
245
|
+
guard let popupWebView = popupController.inheritPopupPresentation(
|
|
246
|
+
from: parentController,
|
|
247
|
+
request: navigationAction.request,
|
|
248
|
+
configuration: configuration,
|
|
249
|
+
instanceId: popupId,
|
|
250
|
+
proxySchemeHandler: proxyHandler
|
|
251
|
+
) else {
|
|
252
|
+
proxySchemeHandlers[popupId] = nil
|
|
253
|
+
return nil
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let navigationController = UINavigationController(rootViewController: popupController)
|
|
257
|
+
cloneNavigationAppearance(from: parentController.navigationController, to: navigationController)
|
|
258
|
+
let shouldActivatePopup = ActiveWebViewSupport.shouldActivateNewWebView(
|
|
259
|
+
isHidden: shouldHidePopup,
|
|
260
|
+
hasActiveWebView: activeWebViewId != nil
|
|
261
|
+
)
|
|
262
|
+
registerWebView(
|
|
263
|
+
id: popupId,
|
|
264
|
+
webView: popupController,
|
|
265
|
+
navigationController: navigationController,
|
|
266
|
+
makeActive: shouldActivatePopup
|
|
267
|
+
)
|
|
268
|
+
if shouldHidePopup {
|
|
269
|
+
guard attachWebViewToWindow(popupWebView) else {
|
|
270
|
+
unregisterWebView(id: popupId)
|
|
271
|
+
return nil
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
|
|
275
|
+
presenter?.present(navigationController, animated: true, completion: nil)
|
|
276
|
+
}
|
|
277
|
+
notifyPopupWindowOpened(
|
|
278
|
+
id: popupId,
|
|
279
|
+
parentId: parentController.instanceId,
|
|
280
|
+
url: navigationAction.request.url?.absoluteString,
|
|
281
|
+
visible: !shouldHidePopup
|
|
282
|
+
)
|
|
283
|
+
return popupWebView
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func resolveWebViewController(for id: String?) -> WKWebViewController? {
|
|
287
|
+
if let id {
|
|
288
|
+
return webViewControllers[id]
|
|
289
|
+
}
|
|
290
|
+
return webViewController
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func cookieStore(for id: String) -> WKHTTPCookieStore? {
|
|
294
|
+
resolveWebViewController(for: id)?.websiteDataStore()?.httpCookieStore
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func dataStores(for targetId: String?) -> [WKWebsiteDataStore] {
|
|
298
|
+
if let targetId {
|
|
299
|
+
guard let controller = webViewControllers[targetId],
|
|
300
|
+
let store = controller.websiteDataStore() else {
|
|
301
|
+
return []
|
|
302
|
+
}
|
|
303
|
+
return [store]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let controllers = Array(webViewControllers.values)
|
|
307
|
+
if controllers.isEmpty {
|
|
308
|
+
return [WKWebsiteDataStore.default()]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var seen = Set<ObjectIdentifier>()
|
|
312
|
+
var stores: [WKWebsiteDataStore] = []
|
|
313
|
+
for controller in controllers {
|
|
314
|
+
if let store = controller.websiteDataStore() {
|
|
315
|
+
let identifier = ObjectIdentifier(store)
|
|
316
|
+
if seen.insert(identifier).inserted {
|
|
317
|
+
stores.append(store)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return stores
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private func parseProxyRules(_ rawRules: [Any]) throws -> [NativeProxyRule] {
|
|
325
|
+
try rawRules.enumerated().map { index, item in
|
|
326
|
+
guard let dictionary = item as? [String: Any] else {
|
|
327
|
+
throw NSError(
|
|
328
|
+
domain: "InAppBrowserPlugin",
|
|
329
|
+
code: -1,
|
|
330
|
+
userInfo: [NSLocalizedDescriptionKey: "Proxy rule at index \(index) must be an object"]
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
return try NativeProxyRule.from(dictionary: dictionary)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
func presentView(webViewId: String? = nil, isAnimated: Bool = true) {
|
|
338
|
+
let resolvedId = webViewId ?? activeWebViewId
|
|
339
|
+
let navigationController = resolvedId.flatMap { navigationControllers[$0] } ?? self.navigationWebViewController
|
|
340
|
+
guard let navigationController else {
|
|
341
|
+
self.currentPluginCall?.reject("Navigation controller is not initialized")
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
|
|
346
|
+
presenter?.present(navigationController, animated: isAnimated, completion: {
|
|
347
|
+
self.currentPluginCall?.resolve()
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func activeWindow() -> UIWindow? {
|
|
352
|
+
return UIApplication.shared.connectedScenes
|
|
353
|
+
.compactMap { $0 as? UIWindowScene }
|
|
354
|
+
.first { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }?
|
|
355
|
+
.windows
|
|
356
|
+
.first { $0.isKeyWindow }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private func hiddenContainerFrame(in window: UIWindow) -> CGRect {
|
|
360
|
+
switch self.invisibilityMode {
|
|
361
|
+
case .aware:
|
|
362
|
+
return CGRect(
|
|
363
|
+
x: window.bounds.maxX + 2048,
|
|
364
|
+
y: window.bounds.maxY + 2048,
|
|
365
|
+
width: 1,
|
|
366
|
+
height: 1
|
|
367
|
+
)
|
|
368
|
+
case .fakeVisible:
|
|
369
|
+
return CGRect(
|
|
370
|
+
x: window.bounds.maxX + 2048,
|
|
371
|
+
y: 0,
|
|
372
|
+
width: max(window.bounds.width, 1),
|
|
373
|
+
height: max(window.bounds.height, 1)
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private func ensureHiddenWebViewContainer(for webView: WKWebView, in window: UIWindow) -> UIView {
|
|
379
|
+
let key = ObjectIdentifier(webView)
|
|
380
|
+
let frame = hiddenContainerFrame(in: window)
|
|
381
|
+
|
|
382
|
+
if let existingContainer = hiddenWebViewContainers[key] {
|
|
383
|
+
existingContainer.frame = frame
|
|
384
|
+
if existingContainer.superview !== window {
|
|
385
|
+
existingContainer.removeFromSuperview()
|
|
386
|
+
window.addSubview(existingContainer)
|
|
387
|
+
}
|
|
388
|
+
return existingContainer
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let container = UIView(frame: frame)
|
|
392
|
+
container.backgroundColor = .clear
|
|
393
|
+
container.isOpaque = false
|
|
394
|
+
container.isUserInteractionEnabled = false
|
|
395
|
+
container.clipsToBounds = true
|
|
396
|
+
container.accessibilityElementsHidden = true
|
|
397
|
+
window.addSubview(container)
|
|
398
|
+
hiddenWebViewContainers[key] = container
|
|
399
|
+
return container
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private func cleanupHiddenWebViewContainer(for webView: WKWebView?) {
|
|
403
|
+
guard let webView else {
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let key = ObjectIdentifier(webView)
|
|
408
|
+
if let container = hiddenWebViewContainers.removeValue(forKey: key) {
|
|
409
|
+
container.removeFromSuperview()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func attachWebViewToWindow(_ webView: WKWebView) -> Bool {
|
|
414
|
+
guard let window = activeWindow() else {
|
|
415
|
+
return false
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let container = ensureHiddenWebViewContainer(for: webView, in: window)
|
|
419
|
+
webView.removeFromSuperview()
|
|
420
|
+
webView.translatesAutoresizingMaskIntoConstraints = true
|
|
421
|
+
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
422
|
+
|
|
423
|
+
switch self.invisibilityMode {
|
|
424
|
+
case .aware:
|
|
425
|
+
webView.frame = container.bounds
|
|
426
|
+
webView.alpha = 1
|
|
427
|
+
webView.isOpaque = false
|
|
428
|
+
webView.backgroundColor = .clear
|
|
429
|
+
webView.scrollView.backgroundColor = .clear
|
|
430
|
+
case .fakeVisible:
|
|
431
|
+
webView.frame = container.bounds
|
|
432
|
+
webView.alpha = 1
|
|
433
|
+
webView.isOpaque = false
|
|
434
|
+
webView.backgroundColor = .clear
|
|
435
|
+
webView.scrollView.backgroundColor = .clear
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
webView.isUserInteractionEnabled = false
|
|
439
|
+
container.addSubview(webView)
|
|
440
|
+
return true
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private func attachWebViewToController(_ webViewController: WKWebViewController, webView: WKWebView) {
|
|
444
|
+
cleanupHiddenWebViewContainer(for: webView)
|
|
445
|
+
webView.removeFromSuperview()
|
|
446
|
+
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
447
|
+
webViewController.view.addSubview(webView)
|
|
448
|
+
|
|
449
|
+
let bottomAnchor = webViewController.enabledSafeBottomMargin
|
|
450
|
+
? webViewController.view.safeAreaLayoutGuide.bottomAnchor
|
|
451
|
+
: webViewController.view.bottomAnchor
|
|
452
|
+
|
|
453
|
+
let topAnchor = webViewController.enabledSafeTopMargin
|
|
454
|
+
? webViewController.view.safeAreaLayoutGuide.topAnchor
|
|
455
|
+
: webViewController.view.topAnchor
|
|
456
|
+
|
|
457
|
+
NSLayoutConstraint.activate([
|
|
458
|
+
webView.topAnchor.constraint(equalTo: topAnchor),
|
|
459
|
+
webView.leadingAnchor.constraint(equalTo: webViewController.view.leadingAnchor),
|
|
460
|
+
webView.trailingAnchor.constraint(equalTo: webViewController.view.trailingAnchor),
|
|
461
|
+
webView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
462
|
+
])
|
|
463
|
+
|
|
464
|
+
if let backgroundColor = webViewController.view.backgroundColor {
|
|
465
|
+
webView.backgroundColor = backgroundColor
|
|
466
|
+
webView.scrollView.backgroundColor = backgroundColor
|
|
467
|
+
}
|
|
468
|
+
webView.alpha = 1
|
|
469
|
+
webView.isOpaque = true
|
|
470
|
+
webView.isUserInteractionEnabled = true
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
func handleWebViewDidClose(id: String, url: String) {
|
|
474
|
+
if !id.isEmpty, webViewControllers[id] != nil {
|
|
475
|
+
self.notifyListeners("closeEvent", data: ["id": id, "url": url])
|
|
476
|
+
unregisterWebView(id: id)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
cleanupHiddenWebViewContainer(for: self.webViewController?.capableWebView)
|
|
481
|
+
self.notifyListeners("closeEvent", data: ["url": url])
|
|
482
|
+
self.webViewController = nil
|
|
483
|
+
self.navigationWebViewController = nil
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@objc func clearAllCookies(_ call: CAPPluginCall) {
|
|
487
|
+
DispatchQueue.main.async {
|
|
488
|
+
let targetId = call.getString("id")
|
|
489
|
+
if let targetId, self.webViewControllers[targetId] == nil {
|
|
490
|
+
call.reject("WebView is not initialized")
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let dataStores = self.dataStores(for: targetId)
|
|
495
|
+
if dataStores.isEmpty {
|
|
496
|
+
call.reject("WebView is not initialized")
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let dataTypes = Set([WKWebsiteDataTypeCookies])
|
|
501
|
+
let group = DispatchGroup()
|
|
502
|
+
for dataStore in dataStores {
|
|
503
|
+
group.enter()
|
|
504
|
+
dataStore.removeData(ofTypes: dataTypes,
|
|
505
|
+
modifiedSince: Date(timeIntervalSince1970: 0)) {
|
|
506
|
+
group.leave()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
group.notify(queue: .main) {
|
|
510
|
+
call.resolve()
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@objc func clearCache(_ call: CAPPluginCall) {
|
|
516
|
+
DispatchQueue.main.async {
|
|
517
|
+
let targetId = call.getString("id")
|
|
518
|
+
if let targetId, self.webViewControllers[targetId] == nil {
|
|
519
|
+
call.reject("WebView is not initialized")
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let dataStores = self.dataStores(for: targetId)
|
|
524
|
+
if dataStores.isEmpty {
|
|
525
|
+
call.reject("WebView is not initialized")
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let dataTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
|
|
530
|
+
let group = DispatchGroup()
|
|
531
|
+
for dataStore in dataStores {
|
|
532
|
+
group.enter()
|
|
533
|
+
dataStore.removeData(ofTypes: dataTypes,
|
|
534
|
+
modifiedSince: Date(timeIntervalSince1970: 0)) {
|
|
535
|
+
group.leave()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
group.notify(queue: .main) {
|
|
539
|
+
call.resolve()
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@objc func clearCookies(_ call: CAPPluginCall) {
|
|
545
|
+
guard let url = call.getString("url"),
|
|
546
|
+
let host = URL(string: url)?.host else {
|
|
547
|
+
call.reject("Invalid URL")
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
DispatchQueue.main.async {
|
|
552
|
+
let targetId = call.getString("id")
|
|
553
|
+
if let targetId, self.webViewControllers[targetId] == nil {
|
|
554
|
+
call.reject("WebView is not initialized")
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let dataStores = self.dataStores(for: targetId)
|
|
559
|
+
if dataStores.isEmpty {
|
|
560
|
+
call.reject("WebView is not initialized")
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let outerGroup = DispatchGroup()
|
|
565
|
+
for dataStore in dataStores {
|
|
566
|
+
outerGroup.enter()
|
|
567
|
+
dataStore.httpCookieStore.getAllCookies { cookies in
|
|
568
|
+
let innerGroup = DispatchGroup()
|
|
569
|
+
for cookie in cookies {
|
|
570
|
+
if cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain) {
|
|
571
|
+
innerGroup.enter()
|
|
572
|
+
dataStore.httpCookieStore.delete(cookie) {
|
|
573
|
+
innerGroup.leave()
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
innerGroup.notify(queue: .main) {
|
|
578
|
+
outerGroup.leave()
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
outerGroup.notify(queue: .main) {
|
|
584
|
+
call.resolve()
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@objc func getCookies(_ call: CAPPluginCall) {
|
|
590
|
+
let urlString = call.getString("url") ?? ""
|
|
591
|
+
let includeHttpOnly = call.getBool("includeHttpOnly") ?? true
|
|
592
|
+
|
|
593
|
+
guard let url = URL(string: urlString), let host = url.host else {
|
|
594
|
+
call.reject("Invalid URL")
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
DispatchQueue.main.async {
|
|
599
|
+
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
|
|
600
|
+
var cookieDict = [String: String]()
|
|
601
|
+
for cookie in cookies {
|
|
602
|
+
|
|
603
|
+
if (includeHttpOnly || !cookie.isHTTPOnly) && (cookie.domain == host || cookie.domain.hasSuffix(".\(host)") || host.hasSuffix(cookie.domain)) {
|
|
604
|
+
cookieDict[cookie.name] = cookie.value
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
call.resolve(cookieDict)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@objc func openWebView(_ call: CAPPluginCall) {
|
|
614
|
+
if !self.isSetupDone {
|
|
615
|
+
self.setup()
|
|
616
|
+
}
|
|
617
|
+
self.currentPluginCall = call
|
|
618
|
+
|
|
619
|
+
guard let urlString = call.getString("url") else {
|
|
620
|
+
call.reject("Must provide a URL to open")
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if urlString.isEmpty {
|
|
625
|
+
call.reject("URL must not be empty")
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let webViewId = UUID().uuidString
|
|
630
|
+
let showScreenshotButton = call.getBool("showScreenshotButton", false)
|
|
631
|
+
let buttonNearDoneSettings = call.getObject("buttonNearDone")
|
|
632
|
+
|
|
633
|
+
var buttonNearDoneIcon: UIImage?
|
|
634
|
+
if let buttonNearDoneSettings {
|
|
635
|
+
guard let iosSettingsRaw = buttonNearDoneSettings["ios"] else {
|
|
636
|
+
call.reject("IOS settings not found")
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
guard let iosSettings = iosSettingsRaw as? JSObject else {
|
|
640
|
+
call.reject("IOS settings are not an object")
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
guard let iconType = iosSettings["iconType"] as? String else {
|
|
645
|
+
call.reject("buttonNearDone.iconType is empty")
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
if iconType != "sf-symbol" && iconType != "asset" {
|
|
649
|
+
call.reject("IconType is neither 'sf-symbol' nor 'asset'")
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
guard let icon = iosSettings["icon"] as? String else {
|
|
653
|
+
call.reject("buttonNearDone.icon is empty")
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if iconType == "sf-symbol" {
|
|
658
|
+
buttonNearDoneIcon = UIImage(systemName: icon)?.withRenderingMode(.alwaysTemplate)
|
|
659
|
+
print("[DEBUG] Set buttonNearDone SF Symbol icon: \(icon)")
|
|
660
|
+
} else {
|
|
661
|
+
// Look in app's web assets/public directory
|
|
662
|
+
guard let webDir = Bundle.main.resourceURL?.appendingPathComponent("public") else {
|
|
663
|
+
call.reject("Failed to locate bundled web assets")
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Try several path combinations to find the asset
|
|
668
|
+
let paths = [
|
|
669
|
+
icon, // Just the icon name
|
|
670
|
+
"public/\(icon)", // With public/ prefix
|
|
671
|
+
icon.replacingOccurrences(of: "public/", with: "") // Without public/ prefix
|
|
672
|
+
]
|
|
673
|
+
|
|
674
|
+
var foundImage = false
|
|
675
|
+
|
|
676
|
+
for path in paths {
|
|
677
|
+
// Try as a direct path from web assets dir
|
|
678
|
+
let assetPath = path.replacingOccurrences(of: "public/", with: "")
|
|
679
|
+
let fileURL = webDir.appendingPathComponent(assetPath)
|
|
680
|
+
|
|
681
|
+
print("[DEBUG] Trying to load from: \(fileURL.path)")
|
|
682
|
+
|
|
683
|
+
if FileManager.default.fileExists(atPath: fileURL.path),
|
|
684
|
+
let data = try? Data(contentsOf: fileURL),
|
|
685
|
+
let img = UIImage(data: data) {
|
|
686
|
+
buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
|
|
687
|
+
print("[DEBUG] Successfully loaded buttonNearDone from web assets: \(fileURL.path)")
|
|
688
|
+
foundImage = true
|
|
689
|
+
break
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Try with www directory as an alternative
|
|
693
|
+
if let wwwDir = Bundle.main.resourceURL?.appendingPathComponent("www") {
|
|
694
|
+
let wwwFileURL = wwwDir.appendingPathComponent(assetPath)
|
|
695
|
+
|
|
696
|
+
print("[DEBUG] Trying to load from www dir: \(wwwFileURL.path)")
|
|
697
|
+
|
|
698
|
+
if FileManager.default.fileExists(atPath: wwwFileURL.path),
|
|
699
|
+
let data = try? Data(contentsOf: wwwFileURL),
|
|
700
|
+
let img = UIImage(data: data) {
|
|
701
|
+
buttonNearDoneIcon = img.withRenderingMode(.alwaysTemplate)
|
|
702
|
+
print("[DEBUG] Successfully loaded buttonNearDone from www dir: \(wwwFileURL.path)")
|
|
703
|
+
foundImage = true
|
|
704
|
+
break
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Try looking in app bundle assets
|
|
709
|
+
if let iconImage = UIImage(named: path) {
|
|
710
|
+
buttonNearDoneIcon = iconImage.withRenderingMode(.alwaysTemplate)
|
|
711
|
+
print("[DEBUG] Successfully loaded buttonNearDone from app bundle: \(path)")
|
|
712
|
+
foundImage = true
|
|
713
|
+
break
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if !foundImage {
|
|
718
|
+
call.reject("Failed to load buttonNearDone icon: \(icon)")
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if showScreenshotButton, buttonNearDoneSettings != nil {
|
|
725
|
+
call.reject("showScreenshotButton is not compatible with buttonNearDone")
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
|
|
730
|
+
let closeModal = call.getBool("closeModal", false)
|
|
731
|
+
let closeModalTitle = call.getString("closeModalTitle", "Close")
|
|
732
|
+
let closeModalDescription = call.getString("closeModalDescription", "Are you sure you want to close this window?")
|
|
733
|
+
let closeModalOk = call.getString("closeModalOk", "OK")
|
|
734
|
+
let closeModalCancel = call.getString("closeModalCancel", "Cancel")
|
|
735
|
+
let closeModalURLPattern = call.getString("closeModalURLPattern")
|
|
736
|
+
let isInspectable = call.getBool("isInspectable", false)
|
|
737
|
+
let preventDeeplink = call.getBool("preventDeeplink", false)
|
|
738
|
+
let openBlankTargetInWebView = call.getBool("openBlankTargetInWebView", false)
|
|
739
|
+
let isAnimated = call.getBool("isAnimated", true)
|
|
740
|
+
let enabledSafeBottomMargin = call.getBool("enabledSafeBottomMargin", false)
|
|
741
|
+
let enabledSafeTopMargin = call.getBool("enabledSafeTopMargin", true)
|
|
742
|
+
let hidden = call.getBool("hidden", false)
|
|
743
|
+
self.isHidden = hidden
|
|
744
|
+
let hiddenPopupWindow = call.getBool("hiddenPopupWindow", false)
|
|
745
|
+
let allowWebViewJsVisibilityControl = self.getConfig().getBoolean("allowWebViewJsVisibilityControl", false)
|
|
746
|
+
let allowScreenshotsFromWebPage = call.getBool("allowScreenshotsFromWebPage", false)
|
|
747
|
+
let captureConsoleLogs = call.getBool("captureConsoleLogs", false)
|
|
748
|
+
let handleDownloads = call.getBool("handleDownloads", false)
|
|
749
|
+
let invisibilityModeRaw = call.getString("invisibilityMode", "AWARE")
|
|
750
|
+
self.invisibilityMode = InvisibilityMode(rawValue: invisibilityModeRaw.uppercased()) ?? .aware
|
|
751
|
+
|
|
752
|
+
// Validate preShowScript requires isPresentAfterPageLoad
|
|
753
|
+
if call.getString("preShowScript") != nil && !call.getBool("isPresentAfterPageLoad", false) {
|
|
754
|
+
call.reject("preShowScript requires isPresentAfterPageLoad to be true")
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Validate closeModal options
|
|
759
|
+
if closeModal {
|
|
760
|
+
if call.getString("closeModalTitle") != nil ||
|
|
761
|
+
call.getString("closeModalDescription") != nil ||
|
|
762
|
+
call.getString("closeModalOk") != nil ||
|
|
763
|
+
call.getString("closeModalCancel") != nil {
|
|
764
|
+
// Store the values to be set after proper initialization
|
|
765
|
+
self.closeModalTitle = closeModalTitle
|
|
766
|
+
self.closeModalDescription = closeModalDescription
|
|
767
|
+
self.closeModalOk = closeModalOk
|
|
768
|
+
self.closeModalCancel = closeModalCancel
|
|
769
|
+
}
|
|
770
|
+
if let pattern = closeModalURLPattern {
|
|
771
|
+
if (try? NSRegularExpression(pattern: pattern)) == nil {
|
|
772
|
+
call.reject("Invalid closeModalURLPattern regex")
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
// Reject if closeModal is false but closeModal options are provided
|
|
778
|
+
if call.getString("closeModalTitle") != nil ||
|
|
779
|
+
call.getString("closeModalDescription") != nil ||
|
|
780
|
+
call.getString("closeModalOk") != nil ||
|
|
781
|
+
call.getString("closeModalCancel") != nil ||
|
|
782
|
+
call.getString("closeModalURLPattern") != nil {
|
|
783
|
+
call.reject("closeModal options require closeModal to be true")
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Validate shareDisclaimer requires shareSubject
|
|
789
|
+
if call.getString("shareSubject") == nil && call.getObject("shareDisclaimer") != nil {
|
|
790
|
+
call.reject("shareDisclaimer requires shareSubject to be provided")
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Validate buttonNearDone compatibility with toolbar type
|
|
795
|
+
if buttonNearDoneSettings != nil || showScreenshotButton {
|
|
796
|
+
let toolbarType = call.getString("toolbarType", "")
|
|
797
|
+
if toolbarType == "activity" || toolbarType == "navigation" || toolbarType == "blank" {
|
|
798
|
+
let optionName = showScreenshotButton ? "showScreenshotButton" : "buttonNearDone"
|
|
799
|
+
call.reject(optionName + " is not compatible with toolbarType: " + toolbarType)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
var disclaimerContent: JSObject?
|
|
805
|
+
if let shareDisclaimerRaw = call.getObject("shareDisclaimer"), !shareDisclaimerRaw.isEmpty {
|
|
806
|
+
disclaimerContent = shareDisclaimerRaw
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
let toolbarType = call.getString("toolbarType", "")
|
|
810
|
+
let backgroundColor = call.getString("backgroundColor", "black") == "white" ? UIColor.white : UIColor.black
|
|
811
|
+
|
|
812
|
+
// Don't null out shareDisclaimer regardless of toolbarType
|
|
813
|
+
// if toolbarType != "activity" {
|
|
814
|
+
// disclaimerContent = nil
|
|
815
|
+
// }
|
|
816
|
+
|
|
817
|
+
let ignoreUntrustedSSLError = call.getBool("ignoreUntrustedSSLError", false)
|
|
818
|
+
let enableGooglePaySupport = call.getBool("enableGooglePaySupport", false)
|
|
819
|
+
let activeNativeNavigationForWebview = call.getBool("activeNativeNavigationForWebview", true)
|
|
820
|
+
|
|
821
|
+
self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
|
|
822
|
+
let showReloadButton = call.getBool("showReloadButton", false)
|
|
823
|
+
|
|
824
|
+
let blockedHostsRaw = call.getArray("blockedHosts", [])
|
|
825
|
+
let blockedHosts = blockedHostsRaw.compactMap { $0 as? String }
|
|
826
|
+
|
|
827
|
+
let authorizedAppLinksRaw = call.getArray("authorizedAppLinks", [])
|
|
828
|
+
let authorizedAppLinks = authorizedAppLinksRaw.compactMap { $0 as? String }
|
|
829
|
+
|
|
830
|
+
let credentials = self.readCredentials(call)
|
|
831
|
+
|
|
832
|
+
// Read HTTP method and body for custom requests
|
|
833
|
+
let httpMethod = call.getString("method")
|
|
834
|
+
let httpBody = call.getString("body")
|
|
835
|
+
|
|
836
|
+
// Read dimension options
|
|
837
|
+
let width = call.getFloat("width")
|
|
838
|
+
let height = call.getFloat("height")
|
|
839
|
+
let xPos = call.getFloat("x")
|
|
840
|
+
let yPos = call.getFloat("y")
|
|
841
|
+
|
|
842
|
+
// Read disableOverscroll option (iOS only - controls WebView bounce effect)
|
|
843
|
+
let disableOverscroll = call.getBool("disableOverscroll", false)
|
|
844
|
+
|
|
845
|
+
let legacyProxyRequests = ProxySchemeRequestSupport.legacyProxyRequestsConfiguration(from: call.options["proxyRequests"])
|
|
846
|
+
let outboundProxyRulesRaw = call.getArray("outboundProxyRules", [])
|
|
847
|
+
let inboundProxyRulesRaw = call.getArray("inboundProxyRules", [])
|
|
848
|
+
let outboundProxyRules: [NativeProxyRule]
|
|
849
|
+
let inboundProxyRules: [NativeProxyRule]
|
|
850
|
+
do {
|
|
851
|
+
outboundProxyRules = try parseProxyRules(outboundProxyRulesRaw)
|
|
852
|
+
inboundProxyRules = try parseProxyRules(inboundProxyRulesRaw)
|
|
853
|
+
} catch {
|
|
854
|
+
call.reject("Invalid proxy rules: \(error.localizedDescription)")
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Validate dimension parameters
|
|
859
|
+
if width != nil && height == nil {
|
|
860
|
+
call.reject("Height must be specified when width is provided")
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
DispatchQueue.main.async {
|
|
865
|
+
guard let url = URL(string: urlString) else {
|
|
866
|
+
call.reject("Invalid URL format")
|
|
867
|
+
return
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
var proxyHandler: ProxySchemeHandler?
|
|
871
|
+
if legacyProxyRequests.isEnabled || !outboundProxyRules.isEmpty || !inboundProxyRules.isEmpty {
|
|
872
|
+
proxyHandler = ProxySchemeHandler(
|
|
873
|
+
plugin: self,
|
|
874
|
+
webviewId: webViewId,
|
|
875
|
+
legacyProxyRequests: legacyProxyRequests.isEnabled,
|
|
876
|
+
legacyProxyRequestURLRegex: legacyProxyRequests.urlRegex,
|
|
877
|
+
outboundRules: outboundProxyRules,
|
|
878
|
+
inboundRules: inboundProxyRules
|
|
879
|
+
)
|
|
880
|
+
self.proxySchemeHandlers[webViewId] = proxyHandler
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
self.webViewController = WKWebViewController.init(
|
|
884
|
+
url: url,
|
|
885
|
+
headers: headers,
|
|
886
|
+
isInspectable: isInspectable,
|
|
887
|
+
credentials: credentials,
|
|
888
|
+
preventDeeplink: preventDeeplink,
|
|
889
|
+
blankNavigationTab: toolbarType == "blank",
|
|
890
|
+
enabledSafeBottomMargin: enabledSafeBottomMargin,
|
|
891
|
+
enabledSafeTopMargin: enabledSafeTopMargin,
|
|
892
|
+
blockedHosts: blockedHosts,
|
|
893
|
+
authorizedAppLinks: authorizedAppLinks,
|
|
894
|
+
allowWebViewJsVisibilityControl: allowWebViewJsVisibilityControl,
|
|
895
|
+
allowScreenshotsFromWebPage: allowScreenshotsFromWebPage,
|
|
896
|
+
captureConsoleLogs: captureConsoleLogs,
|
|
897
|
+
proxyRequests: legacyProxyRequests.isEnabled,
|
|
898
|
+
proxySchemeHandler: proxyHandler,
|
|
899
|
+
documentStartUserScripts: self.documentStartUserScripts(
|
|
900
|
+
authorizedAppLinks: authorizedAppLinks,
|
|
901
|
+
openBlankTargetInWebView: openBlankTargetInWebView
|
|
902
|
+
),
|
|
903
|
+
openBlankTargetInWebView: openBlankTargetInWebView
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
guard let webViewController = self.webViewController else {
|
|
907
|
+
call.reject("Failed to initialize WebViewController")
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
webViewController.instanceId = webViewId
|
|
912
|
+
|
|
913
|
+
// Set HTTP method and body if provided
|
|
914
|
+
if let method = httpMethod {
|
|
915
|
+
webViewController.httpMethod = method
|
|
916
|
+
}
|
|
917
|
+
if let body = httpBody {
|
|
918
|
+
webViewController.httpBody = body
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Set dimensions if provided
|
|
922
|
+
if let width = width {
|
|
923
|
+
webViewController.customWidth = CGFloat(width)
|
|
924
|
+
}
|
|
925
|
+
if let height = height {
|
|
926
|
+
webViewController.customHeight = CGFloat(height)
|
|
927
|
+
}
|
|
928
|
+
if let xPos = xPos {
|
|
929
|
+
webViewController.customX = CGFloat(xPos)
|
|
930
|
+
}
|
|
931
|
+
if let yPos = yPos {
|
|
932
|
+
webViewController.customY = CGFloat(yPos)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Set disableOverscroll option
|
|
936
|
+
webViewController.disableOverscroll = disableOverscroll
|
|
937
|
+
webViewController.handleDownloads = handleDownloads
|
|
938
|
+
|
|
939
|
+
// Set native navigation gestures before view loads
|
|
940
|
+
webViewController.activeNativeNavigationForWebview = activeNativeNavigationForWebview
|
|
941
|
+
|
|
942
|
+
// Update the webview's gesture property (if webview already exists)
|
|
943
|
+
webViewController.updateNavigationGestures()
|
|
944
|
+
|
|
945
|
+
if self.bridge?.statusBarVisible == true {
|
|
946
|
+
let subviews = self.bridge?.webView?.superview?.subviews
|
|
947
|
+
if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
|
|
948
|
+
if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
|
|
949
|
+
webViewController.capacitorStatusBar = emptyStatusBar
|
|
950
|
+
emptyStatusBar.removeFromSuperview()
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
webViewController.source = .remote(url)
|
|
956
|
+
webViewController.leftNavigationBarItemTypes = []
|
|
957
|
+
|
|
958
|
+
// Configure close button based on showArrow
|
|
959
|
+
let showArrow = call.getBool("showArrow", false)
|
|
960
|
+
if showArrow {
|
|
961
|
+
// When showArrow is true, put arrow on left
|
|
962
|
+
webViewController.doneBarButtonItemPosition = .left
|
|
963
|
+
webViewController.showArrowAsClose = true
|
|
964
|
+
} else {
|
|
965
|
+
// Default X on right
|
|
966
|
+
webViewController.doneBarButtonItemPosition = toolbarType == "activity" ? .none : .right
|
|
967
|
+
webViewController.showArrowAsClose = false
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Configure navigation buttons based on toolbarType
|
|
971
|
+
if toolbarType == "activity" {
|
|
972
|
+
// Activity mode should ONLY have:
|
|
973
|
+
// 1. Close button (if not hidden by doneBarButtonItemPosition)
|
|
974
|
+
// 2. Share button (if shareSubject is provided)
|
|
975
|
+
webViewController.leftNavigationBarItemTypes = [] // Clear any left items
|
|
976
|
+
webViewController.rightNavigaionBarItemTypes = [] // Clear any right items
|
|
977
|
+
|
|
978
|
+
// Only add share button if subject is provided
|
|
979
|
+
if call.getString("shareSubject") != nil {
|
|
980
|
+
// Add share button to right bar
|
|
981
|
+
webViewController.rightNavigaionBarItemTypes.append(.activity)
|
|
982
|
+
print("[DEBUG] Activity mode: Added share button, shareSubject: \(call.getString("shareSubject") ?? "nil")")
|
|
983
|
+
} else {
|
|
984
|
+
// In activity mode, always make the share button visible by setting a default shareSubject
|
|
985
|
+
webViewController.shareSubject = "Share"
|
|
986
|
+
webViewController.rightNavigaionBarItemTypes.append(.activity)
|
|
987
|
+
print("[DEBUG] Activity mode: Setting default shareSubject")
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Set done button position based on showArrow
|
|
991
|
+
if showArrow {
|
|
992
|
+
webViewController.doneBarButtonItemPosition = .left
|
|
993
|
+
} else {
|
|
994
|
+
// In activity mode, keep the done button visible even when showArrow is false
|
|
995
|
+
webViewController.doneBarButtonItemPosition = .right
|
|
996
|
+
}
|
|
997
|
+
} else if toolbarType == "navigation" {
|
|
998
|
+
// Navigation mode puts back/forward on the left
|
|
999
|
+
webViewController.leftNavigationBarItemTypes = [.back, .forward]
|
|
1000
|
+
if showReloadButton {
|
|
1001
|
+
webViewController.leftNavigationBarItemTypes.append(.reload)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Only add share button if subject is provided
|
|
1005
|
+
if call.getString("shareSubject") != nil {
|
|
1006
|
+
// Add share button to right navigation bar
|
|
1007
|
+
webViewController.rightNavigaionBarItemTypes.append(.activity)
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
// Other modes may have reload button
|
|
1011
|
+
if showReloadButton {
|
|
1012
|
+
webViewController.leftNavigationBarItemTypes.append(.reload)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Only add share button if subject is provided
|
|
1016
|
+
if call.getString("shareSubject") != nil {
|
|
1017
|
+
// Add share button to right navigation bar
|
|
1018
|
+
webViewController.rightNavigaionBarItemTypes.append(.activity)
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Set buttonNearDoneIcon if provided
|
|
1023
|
+
if let buttonNearDoneIcon = buttonNearDoneIcon {
|
|
1024
|
+
webViewController.buttonNearDoneIcon = buttonNearDoneIcon
|
|
1025
|
+
print("[DEBUG] Button near done icon set: \(buttonNearDoneIcon)")
|
|
1026
|
+
} else if showScreenshotButton {
|
|
1027
|
+
webViewController.buttonNearDoneIcon = UIImage(systemName: "camera")?.withRenderingMode(.alwaysTemplate)
|
|
1028
|
+
}
|
|
1029
|
+
webViewController.showScreenshotButton = showScreenshotButton
|
|
1030
|
+
|
|
1031
|
+
webViewController.capBrowserPlugin = self
|
|
1032
|
+
webViewController.title = call.getString("title", "New Window")
|
|
1033
|
+
// Only set shareSubject if not already set for activity mode
|
|
1034
|
+
if webViewController.shareSubject == nil {
|
|
1035
|
+
webViewController.shareSubject = call.getString("shareSubject")
|
|
1036
|
+
}
|
|
1037
|
+
webViewController.shareDisclaimer = disclaimerContent
|
|
1038
|
+
|
|
1039
|
+
// Debug shareDisclaimer
|
|
1040
|
+
if let disclaimer = disclaimerContent {
|
|
1041
|
+
print("[DEBUG] Share disclaimer set: \(disclaimer)")
|
|
1042
|
+
} else {
|
|
1043
|
+
print("[DEBUG] No share disclaimer set")
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
webViewController.preShowScript = call.getString("preShowScript")
|
|
1047
|
+
webViewController.preShowScriptInjectionTime = call.getString("preShowScriptInjectionTime", "pageLoad")
|
|
1048
|
+
|
|
1049
|
+
// If script should be injected at document start, inject it now
|
|
1050
|
+
if webViewController.preShowScriptInjectionTime == "documentStart" {
|
|
1051
|
+
webViewController.injectPreShowScriptAtDocumentStart()
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
webViewController.websiteTitleInNavigationBar = call.getBool("visibleTitle", true)
|
|
1055
|
+
webViewController.ignoreUntrustedSSLError = ignoreUntrustedSSLError
|
|
1056
|
+
|
|
1057
|
+
// Set Google Pay support
|
|
1058
|
+
webViewController.enableGooglePaySupport = enableGooglePaySupport
|
|
1059
|
+
webViewController.hiddenPopupWindow = hiddenPopupWindow
|
|
1060
|
+
webViewController.opensHidden = hidden
|
|
1061
|
+
webViewController.captureConsoleLogs = captureConsoleLogs
|
|
1062
|
+
|
|
1063
|
+
// Set text zoom if specified
|
|
1064
|
+
if let textZoom = call.getInt("textZoom") {
|
|
1065
|
+
webViewController.textZoom = textZoom
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Set closeModal properties after proper initialization
|
|
1069
|
+
if closeModal {
|
|
1070
|
+
webViewController.closeModal = true
|
|
1071
|
+
webViewController.closeModalTitle = self.closeModalTitle ?? closeModalTitle
|
|
1072
|
+
webViewController.closeModalDescription = self.closeModalDescription ?? closeModalDescription
|
|
1073
|
+
webViewController.closeModalOk = self.closeModalOk ?? closeModalOk
|
|
1074
|
+
webViewController.closeModalCancel = self.closeModalCancel ?? closeModalCancel
|
|
1075
|
+
if let pattern = closeModalURLPattern {
|
|
1076
|
+
webViewController.closeModalURLPattern = pattern
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
|
|
1081
|
+
if let navigationController = self.navigationWebViewController {
|
|
1082
|
+
self.registerWebView(id: webViewId, webView: webViewController, navigationController: navigationController)
|
|
1083
|
+
}
|
|
1084
|
+
self.navigationWebViewController?.navigationBar.isTranslucent = false
|
|
1085
|
+
self.navigationWebViewController?.toolbar.isTranslucent = false
|
|
1086
|
+
|
|
1087
|
+
// Ensure no lines or borders appear by default
|
|
1088
|
+
self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
|
1089
|
+
self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
|
|
1090
|
+
self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
|
|
1091
|
+
self.navigationWebViewController?.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any)
|
|
1092
|
+
|
|
1093
|
+
// Handle web view background color
|
|
1094
|
+
webViewController.view.backgroundColor = backgroundColor
|
|
1095
|
+
|
|
1096
|
+
// Handle toolbar color
|
|
1097
|
+
if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
|
|
1098
|
+
// If specific color provided, use it
|
|
1099
|
+
let color = UIColor(hexString: toolbarColor)
|
|
1100
|
+
|
|
1101
|
+
// Apply to status bar and navigation bar area with a single colored view
|
|
1102
|
+
webViewController.setupStatusBarBackground(color: color)
|
|
1103
|
+
|
|
1104
|
+
// Set status bar style based on toolbar color
|
|
1105
|
+
let isDark = self.isDarkColor(color)
|
|
1106
|
+
webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
|
|
1107
|
+
webViewController.updateStatusBarStyle()
|
|
1108
|
+
|
|
1109
|
+
// Apply text color
|
|
1110
|
+
let textColor: UIColor
|
|
1111
|
+
if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
|
|
1112
|
+
textColor = UIColor(hexString: toolbarTextColor)
|
|
1113
|
+
} else {
|
|
1114
|
+
textColor = isDark ? UIColor.white : UIColor.black
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Apply tint color to all UI elements without changing background
|
|
1118
|
+
self.navigationWebViewController?.navigationBar.tintColor = textColor
|
|
1119
|
+
webViewController.tintColor = textColor
|
|
1120
|
+
self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
|
|
1121
|
+
} else {
|
|
1122
|
+
// Use system appearance
|
|
1123
|
+
let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
|
|
1124
|
+
let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
|
|
1125
|
+
let textColor: UIColor
|
|
1126
|
+
|
|
1127
|
+
if let toolbarTextColor = call.getString("toolbarTextColor"), self.isHexColorCode(toolbarTextColor) {
|
|
1128
|
+
textColor = UIColor(hexString: toolbarTextColor)
|
|
1129
|
+
} else {
|
|
1130
|
+
textColor = isDarkMode ? UIColor.white : UIColor.black
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Apply colors
|
|
1134
|
+
webViewController.setupStatusBarBackground(color: backgroundColor)
|
|
1135
|
+
webViewController.tintColor = textColor
|
|
1136
|
+
self.navigationWebViewController?.navigationBar.tintColor = textColor
|
|
1137
|
+
self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
|
|
1138
|
+
webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
|
|
1139
|
+
webViewController.updateStatusBarStyle()
|
|
1140
|
+
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Configure modal presentation for touch passthrough if custom dimensions are set
|
|
1144
|
+
if width != nil || height != nil {
|
|
1145
|
+
self.navigationWebViewController?.modalPresentationStyle = .overFullScreen
|
|
1146
|
+
|
|
1147
|
+
// Create a pass-through container
|
|
1148
|
+
let containerView = PassThroughView()
|
|
1149
|
+
containerView.backgroundColor = .clear
|
|
1150
|
+
|
|
1151
|
+
// Calculate dimensions - use screen width if only height is provided
|
|
1152
|
+
let finalWidth = width.map { CGFloat($0) } ?? UIScreen.main.bounds.width
|
|
1153
|
+
let finalHeight = height.map { CGFloat($0) } ?? UIScreen.main.bounds.height
|
|
1154
|
+
|
|
1155
|
+
containerView.targetFrame = CGRect(
|
|
1156
|
+
x: CGFloat(xPos ?? 0),
|
|
1157
|
+
y: CGFloat(yPos ?? 0),
|
|
1158
|
+
width: finalWidth,
|
|
1159
|
+
height: finalHeight
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
// Replace the navigation controller's view with our pass-through container
|
|
1163
|
+
if let navController = self.navigationWebViewController,
|
|
1164
|
+
let originalView = navController.view {
|
|
1165
|
+
navController.view = containerView
|
|
1166
|
+
containerView.addSubview(originalView)
|
|
1167
|
+
originalView.frame = CGRect(
|
|
1168
|
+
x: CGFloat(xPos ?? 0),
|
|
1169
|
+
y: CGFloat(yPos ?? 0),
|
|
1170
|
+
width: finalWidth,
|
|
1171
|
+
height: finalHeight
|
|
1172
|
+
)
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
|
|
1179
|
+
if toolbarType == "blank" {
|
|
1180
|
+
self.navigationWebViewController?.navigationBar.isHidden = true
|
|
1181
|
+
webViewController.blankNavigationTab = true
|
|
1182
|
+
|
|
1183
|
+
// Even with hidden navigation bar, we need to set proper status bar appearance
|
|
1184
|
+
// If toolbarColor is explicitly set, use that for status bar style
|
|
1185
|
+
if let toolbarColor = call.getString("toolbarColor"), self.isHexColorCode(toolbarColor) {
|
|
1186
|
+
let color = UIColor(hexString: toolbarColor)
|
|
1187
|
+
let isDark = self.isDarkColor(color)
|
|
1188
|
+
webViewController.statusBarStyle = isDark ? .lightContent : .darkContent
|
|
1189
|
+
webViewController.updateStatusBarStyle()
|
|
1190
|
+
|
|
1191
|
+
// Apply status bar background color via the special view
|
|
1192
|
+
webViewController.setupStatusBarBackground(color: color)
|
|
1193
|
+
|
|
1194
|
+
// Apply background color to whole view to ensure no gaps
|
|
1195
|
+
webViewController.view.backgroundColor = color
|
|
1196
|
+
self.navigationWebViewController?.view.backgroundColor = color
|
|
1197
|
+
|
|
1198
|
+
// Apply status bar background color
|
|
1199
|
+
if let navController = self.navigationWebViewController {
|
|
1200
|
+
navController.view.backgroundColor = color
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
// Follow system appearance if no specific color
|
|
1204
|
+
let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
|
|
1205
|
+
let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
|
|
1206
|
+
webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
|
|
1207
|
+
webViewController.updateStatusBarStyle()
|
|
1208
|
+
|
|
1209
|
+
// Apply status bar background color via the special view
|
|
1210
|
+
webViewController.setupStatusBarBackground(color: backgroundColor)
|
|
1211
|
+
|
|
1212
|
+
// Set appropriate background color
|
|
1213
|
+
if let navController = self.navigationWebViewController {
|
|
1214
|
+
navController.view.backgroundColor = backgroundColor
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// We don't use the toolbar anymore, always hide it
|
|
1221
|
+
self.navigationWebViewController?.setToolbarHidden(true, animated: false)
|
|
1222
|
+
|
|
1223
|
+
if hidden {
|
|
1224
|
+
guard let webView = webViewController.capableWebView else {
|
|
1225
|
+
call.reject("Failed to get webview for hidden mode")
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
// Zero-frame in window hierarchy required for WKWebView JS execution when hidden
|
|
1229
|
+
if !self.attachWebViewToWindow(webView) {
|
|
1230
|
+
call.reject("Failed to get active window for hidden webview")
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
} else if !self.isPresentAfterPageLoad {
|
|
1234
|
+
self.presentView(webViewId: webViewId, isAnimated: isAnimated)
|
|
1235
|
+
}
|
|
1236
|
+
call.resolve(["id": webViewId])
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@objc func goBack(_ call: CAPPluginCall) {
|
|
1241
|
+
DispatchQueue.main.async {
|
|
1242
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1243
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1244
|
+
call.resolve(["canGoBack": false])
|
|
1245
|
+
return
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let canGoBack = webViewController.goBack()
|
|
1249
|
+
call.resolve(["canGoBack": canGoBack])
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
@objc func reload(_ call: CAPPluginCall) {
|
|
1254
|
+
DispatchQueue.main.async {
|
|
1255
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1256
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1257
|
+
call.reject("WebView is not initialized")
|
|
1258
|
+
return
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
webViewController.reload()
|
|
1262
|
+
call.resolve()
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
@objc func setUrl(_ call: CAPPluginCall) {
|
|
1267
|
+
guard let urlString = call.getString("url") else {
|
|
1268
|
+
call.reject("Cannot get new url to set")
|
|
1269
|
+
return
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
guard let url = URL(string: urlString) else {
|
|
1273
|
+
call.reject("Invalid URL")
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
let targetId = call.getString("id")
|
|
1278
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1279
|
+
call.reject("WebView is not initialized")
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
webViewController.load(remote: url)
|
|
1284
|
+
call.resolve()
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private func setHiddenState(_ hidden: Bool, targetId: String?, call: CAPPluginCall?) {
|
|
1288
|
+
DispatchQueue.main.async {
|
|
1289
|
+
let resolvedId = targetId ?? self.activeWebViewId
|
|
1290
|
+
guard let webViewController = self.resolveWebViewController(for: resolvedId),
|
|
1291
|
+
let webView = webViewController.capableWebView else {
|
|
1292
|
+
call?.reject("WebView is not initialized")
|
|
1293
|
+
return
|
|
1294
|
+
}
|
|
1295
|
+
let navigationController = self.resolveNavigationController(for: resolvedId)
|
|
1296
|
+
|
|
1297
|
+
self.isHidden = hidden
|
|
1298
|
+
|
|
1299
|
+
if hidden {
|
|
1300
|
+
if let navController = navigationController, navController.presentingViewController != nil {
|
|
1301
|
+
navController.view.isHidden = true
|
|
1302
|
+
navController.view.isUserInteractionEnabled = false
|
|
1303
|
+
if let containerView = navController.view.superview {
|
|
1304
|
+
if self.presentationContainerView == nil || self.presentationContainerView !== containerView {
|
|
1305
|
+
self.presentationContainerView = containerView
|
|
1306
|
+
self.presentationContainerWasInteractive = containerView.isUserInteractionEnabled
|
|
1307
|
+
self.presentationContainerPreviousAlpha = containerView.alpha
|
|
1308
|
+
}
|
|
1309
|
+
containerView.isUserInteractionEnabled = false
|
|
1310
|
+
containerView.alpha = 0
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if !self.attachWebViewToWindow(webView) {
|
|
1315
|
+
call?.reject("Failed to get active window for hidden webview")
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
if webView.superview !== webViewController.view {
|
|
1320
|
+
self.attachWebViewToController(webViewController, webView: webView)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if let navController = navigationController {
|
|
1324
|
+
if let resolvedId {
|
|
1325
|
+
self.setActiveWebView(id: resolvedId, webView: webViewController, navigationController: navController)
|
|
1326
|
+
}
|
|
1327
|
+
navController.view.isHidden = false
|
|
1328
|
+
navController.view.isUserInteractionEnabled = true
|
|
1329
|
+
if let containerView = self.presentationContainerView {
|
|
1330
|
+
containerView.isUserInteractionEnabled = self.presentationContainerWasInteractive
|
|
1331
|
+
containerView.alpha = self.presentationContainerPreviousAlpha
|
|
1332
|
+
self.presentationContainerView = nil
|
|
1333
|
+
self.presentationContainerWasInteractive = true
|
|
1334
|
+
self.presentationContainerPreviousAlpha = 1
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if navController.presentingViewController == nil {
|
|
1338
|
+
let presenter = self.bridge?.viewController?.presentedViewController ?? self.bridge?.viewController
|
|
1339
|
+
presenter?.present(navController, animated: true, completion: {
|
|
1340
|
+
call?.resolve()
|
|
1341
|
+
})
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
call?.resolve()
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
func setHiddenFromJavaScript(_ hidden: Bool, sourceId: String? = nil) {
|
|
1352
|
+
self.setHiddenState(
|
|
1353
|
+
hidden,
|
|
1354
|
+
targetId: ActiveWebViewSupport.resolveVisibilityTarget(
|
|
1355
|
+
originatingWebViewId: sourceId,
|
|
1356
|
+
activeWebViewId: activeWebViewId
|
|
1357
|
+
),
|
|
1358
|
+
call: nil
|
|
1359
|
+
)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
@objc func hide(_ call: CAPPluginCall) {
|
|
1363
|
+
self.setHiddenState(true, targetId: call.getString("id"), call: call)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
@objc func show(_ call: CAPPluginCall) {
|
|
1367
|
+
self.setHiddenState(false, targetId: call.getString("id"), call: call)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
@objc func executeScript(_ call: CAPPluginCall) {
|
|
1371
|
+
guard let script = call.getString("code") else {
|
|
1372
|
+
call.reject("Cannot get script to execute")
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
DispatchQueue.main.async {
|
|
1376
|
+
if let targetId = call.getString("id") {
|
|
1377
|
+
guard let webViewController = self.webViewControllers[targetId] else {
|
|
1378
|
+
call.reject("WebView is not initialized")
|
|
1379
|
+
return
|
|
1380
|
+
}
|
|
1381
|
+
webViewController.executeScript(script: script)
|
|
1382
|
+
call.resolve()
|
|
1383
|
+
return
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if !self.webViewControllers.isEmpty {
|
|
1387
|
+
self.webViewControllers.values.forEach { $0.executeScript(script: script) }
|
|
1388
|
+
call.resolve()
|
|
1389
|
+
return
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
guard let webViewController = self.webViewController else {
|
|
1393
|
+
call.reject("WebView is not initialized")
|
|
1394
|
+
return
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
webViewController.executeScript(script: script)
|
|
1398
|
+
call.resolve()
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
@objc func postMessage(_ call: CAPPluginCall) {
|
|
1403
|
+
let eventData = call.getObject("detail", [:])
|
|
1404
|
+
// Check if eventData is empty
|
|
1405
|
+
if eventData.isEmpty {
|
|
1406
|
+
call.reject("Event data must not be empty")
|
|
1407
|
+
return
|
|
1408
|
+
}
|
|
1409
|
+
print("Event data: \(eventData)")
|
|
1410
|
+
|
|
1411
|
+
DispatchQueue.main.async {
|
|
1412
|
+
if let targetId = call.getString("id") {
|
|
1413
|
+
guard let webViewController = self.webViewControllers[targetId] else {
|
|
1414
|
+
call.reject("WebView is not initialized")
|
|
1415
|
+
return
|
|
1416
|
+
}
|
|
1417
|
+
webViewController.postMessageToJS(message: eventData)
|
|
1418
|
+
call.resolve()
|
|
1419
|
+
return
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if !self.webViewControllers.isEmpty {
|
|
1423
|
+
self.webViewControllers.values.forEach { $0.postMessageToJS(message: eventData) }
|
|
1424
|
+
call.resolve()
|
|
1425
|
+
return
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
guard let webViewController = self.webViewController else {
|
|
1429
|
+
call.reject("WebView is not initialized")
|
|
1430
|
+
return
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
webViewController.postMessageToJS(message: eventData)
|
|
1434
|
+
call.resolve()
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
@objc func takeScreenshot(_ call: CAPPluginCall) {
|
|
1439
|
+
DispatchQueue.main.async {
|
|
1440
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1441
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1442
|
+
call.reject("WebView is not initialized")
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
webViewController.takeScreenshot { result in
|
|
1447
|
+
switch result {
|
|
1448
|
+
case .success(let screenshot):
|
|
1449
|
+
call.resolve(screenshot)
|
|
1450
|
+
case .failure(let error):
|
|
1451
|
+
call.reject(error.localizedDescription)
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private func normalizedAuthorizedHosts(from links: [String]) -> [String] {
|
|
1458
|
+
links.compactMap { link in
|
|
1459
|
+
guard let host = URLComponents(string: link)?.host?.lowercased() else {
|
|
1460
|
+
return nil
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return host.hasPrefix("www.") ? String(host.dropFirst(4)) : host
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
private func blankTargetInWebViewScript(authorizedAppLinks: [String]) -> String {
|
|
1468
|
+
let authorizedHosts = normalizedAuthorizedHosts(from: authorizedAppLinks)
|
|
1469
|
+
let hostsData = try? JSONSerialization.data(withJSONObject: authorizedHosts)
|
|
1470
|
+
let hostsJSON = hostsData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
|
1471
|
+
|
|
1472
|
+
return """
|
|
1473
|
+
(function() {
|
|
1474
|
+
const authorizedHosts = new Set(\(hostsJSON));
|
|
1475
|
+
const normalizeHost = (host) => (host || '').replace(/^www\\./i, '').toLowerCase();
|
|
1476
|
+
|
|
1477
|
+
document.addEventListener('click', function(event) {
|
|
1478
|
+
const element = event.target;
|
|
1479
|
+
if (!element || typeof element.closest !== 'function') {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const anchor = element.closest('a[target="_blank"][href]');
|
|
1484
|
+
if (!anchor) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
let nextUrl;
|
|
1489
|
+
try {
|
|
1490
|
+
nextUrl = new URL(anchor.getAttribute('href'), window.location.href);
|
|
1491
|
+
} catch (_) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const protocol = nextUrl.protocol.toLowerCase();
|
|
1496
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (authorizedHosts.has(normalizeHost(nextUrl.hostname))) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
event.preventDefault();
|
|
1505
|
+
setTimeout(function() {
|
|
1506
|
+
window.location.assign(nextUrl.toString());
|
|
1507
|
+
}, 0);
|
|
1508
|
+
}, true);
|
|
1509
|
+
})();
|
|
1510
|
+
"""
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private func documentStartUserScripts(
|
|
1514
|
+
authorizedAppLinks: [String],
|
|
1515
|
+
openBlankTargetInWebView: Bool
|
|
1516
|
+
) -> [String] {
|
|
1517
|
+
guard openBlankTargetInWebView else {
|
|
1518
|
+
return []
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return [blankTargetInWebViewScript(authorizedAppLinks: authorizedAppLinks)]
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
func isHexColorCode(_ input: String) -> Bool {
|
|
1525
|
+
let hexColorRegex = "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$"
|
|
1526
|
+
|
|
1527
|
+
do {
|
|
1528
|
+
let regex = try NSRegularExpression(pattern: hexColorRegex)
|
|
1529
|
+
let range = NSRange(location: 0, length: input.utf16.count)
|
|
1530
|
+
if regex.firstMatch(in: input, options: [], range: range) != nil {
|
|
1531
|
+
return true
|
|
1532
|
+
}
|
|
1533
|
+
} catch {
|
|
1534
|
+
print("Error creating regular expression: \(error)")
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return false
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
@objc func open(_ call: CAPPluginCall) {
|
|
1541
|
+
if !self.isSetupDone {
|
|
1542
|
+
self.setup()
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
let isInspectable = call.getBool("isInspectable", false)
|
|
1546
|
+
let preventDeeplink = call.getBool("preventDeeplink", false)
|
|
1547
|
+
self.isPresentAfterPageLoad = call.getBool("isPresentAfterPageLoad", false)
|
|
1548
|
+
|
|
1549
|
+
self.currentPluginCall = call
|
|
1550
|
+
|
|
1551
|
+
guard let urlString = call.getString("url") else {
|
|
1552
|
+
call.reject("Must provide a URL to open")
|
|
1553
|
+
return
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if urlString.isEmpty {
|
|
1557
|
+
call.reject("URL must not be empty")
|
|
1558
|
+
return
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let headers = call.getObject("headers", [:]).mapValues { String(describing: $0 as Any) }
|
|
1562
|
+
let credentials = self.readCredentials(call)
|
|
1563
|
+
|
|
1564
|
+
DispatchQueue.main.async {
|
|
1565
|
+
guard let url = URL(string: urlString) else {
|
|
1566
|
+
call.reject("Invalid URL format")
|
|
1567
|
+
return
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
self.webViewController = WKWebViewController.init(url: url, headers: headers, isInspectable: isInspectable, credentials: credentials, preventDeeplink: preventDeeplink, blankNavigationTab: true, enabledSafeBottomMargin: false, enabledSafeTopMargin: true)
|
|
1571
|
+
|
|
1572
|
+
guard let webViewController = self.webViewController else {
|
|
1573
|
+
call.reject("Failed to initialize WebViewController")
|
|
1574
|
+
return
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if self.bridge?.statusBarVisible == true {
|
|
1578
|
+
let subviews = self.bridge?.webView?.superview?.subviews
|
|
1579
|
+
if let emptyStatusBarIndex = subviews?.firstIndex(where: { $0.subviews.isEmpty }) {
|
|
1580
|
+
if let emptyStatusBar = subviews?[emptyStatusBarIndex] {
|
|
1581
|
+
webViewController.capacitorStatusBar = emptyStatusBar
|
|
1582
|
+
emptyStatusBar.removeFromSuperview()
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
webViewController.source = .remote(url)
|
|
1588
|
+
webViewController.leftNavigationBarItemTypes = [.back, .forward, .reload]
|
|
1589
|
+
webViewController.capBrowserPlugin = self
|
|
1590
|
+
webViewController.hasDynamicTitle = true
|
|
1591
|
+
|
|
1592
|
+
self.navigationWebViewController = UINavigationController.init(rootViewController: webViewController)
|
|
1593
|
+
self.navigationWebViewController?.navigationBar.isTranslucent = false
|
|
1594
|
+
|
|
1595
|
+
// Ensure no lines or borders appear by default
|
|
1596
|
+
self.navigationWebViewController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
|
1597
|
+
self.navigationWebViewController?.navigationBar.shadowImage = UIImage()
|
|
1598
|
+
self.navigationWebViewController?.navigationBar.setValue(true, forKey: "hidesShadow")
|
|
1599
|
+
|
|
1600
|
+
// Use system appearance
|
|
1601
|
+
let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
|
|
1602
|
+
let backgroundColor = isDarkMode ? UIColor.black : UIColor.white
|
|
1603
|
+
let textColor = isDarkMode ? UIColor.white : UIColor.black
|
|
1604
|
+
|
|
1605
|
+
// Apply colors
|
|
1606
|
+
webViewController.setupStatusBarBackground(color: backgroundColor)
|
|
1607
|
+
webViewController.tintColor = textColor
|
|
1608
|
+
self.navigationWebViewController?.navigationBar.tintColor = textColor
|
|
1609
|
+
self.navigationWebViewController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]
|
|
1610
|
+
webViewController.statusBarStyle = isDarkMode ? .lightContent : .darkContent
|
|
1611
|
+
webViewController.updateStatusBarStyle()
|
|
1612
|
+
|
|
1613
|
+
// Always hide toolbar to ensure no bottom bar
|
|
1614
|
+
self.navigationWebViewController?.setToolbarHidden(true, animated: false)
|
|
1615
|
+
|
|
1616
|
+
self.navigationWebViewController?.modalPresentationStyle = .overCurrentContext
|
|
1617
|
+
self.navigationWebViewController?.modalTransitionStyle = .crossDissolve
|
|
1618
|
+
|
|
1619
|
+
if !self.isPresentAfterPageLoad {
|
|
1620
|
+
self.presentView()
|
|
1621
|
+
}
|
|
1622
|
+
call.resolve()
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
@objc func handleProxyRequest(_ call: CAPPluginCall) {
|
|
1627
|
+
guard let requestId = call.getString("requestId") else {
|
|
1628
|
+
call.reject("requestId is required")
|
|
1629
|
+
return
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
let webviewId = call.getString("webviewId")
|
|
1633
|
+
let phase = call.getString("phase")
|
|
1634
|
+
let decisionObj = call.getObject("decision") ?? call.getObject("response")
|
|
1635
|
+
|
|
1636
|
+
let handler: ProxySchemeHandler?
|
|
1637
|
+
switch ProxyResponseRoutingSupport.resolveTargetHandler(
|
|
1638
|
+
webviewId: webviewId,
|
|
1639
|
+
requestId: requestId,
|
|
1640
|
+
handlers: proxySchemeHandlers
|
|
1641
|
+
) {
|
|
1642
|
+
case .matched(let proxyHandler):
|
|
1643
|
+
handler = proxyHandler
|
|
1644
|
+
case .ambiguous:
|
|
1645
|
+
call.reject("webviewId is required when multiple webviews are open")
|
|
1646
|
+
return
|
|
1647
|
+
case .missing:
|
|
1648
|
+
handler = nil
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
guard let proxyHandler = handler else {
|
|
1652
|
+
if let webviewId,
|
|
1653
|
+
webViewControllers[webviewId] == nil,
|
|
1654
|
+
navigationControllers[webviewId] == nil {
|
|
1655
|
+
print("[InAppBrowser][Proxy] Ignoring late proxy response for closed webview \(webviewId)")
|
|
1656
|
+
call.resolve()
|
|
1657
|
+
return
|
|
1658
|
+
}
|
|
1659
|
+
call.reject("No proxy handler found")
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
var responseDict: [String: Any]?
|
|
1664
|
+
if let decisionObj = decisionObj {
|
|
1665
|
+
var dict: [String: Any] = [:]
|
|
1666
|
+
if let cancel = decisionObj["cancel"] as? Bool {
|
|
1667
|
+
dict["cancel"] = cancel
|
|
1668
|
+
}
|
|
1669
|
+
if let request = decisionObj["request"] as? JSObject {
|
|
1670
|
+
var requestDict: [String: Any] = [:]
|
|
1671
|
+
requestDict["url"] = request["url"]
|
|
1672
|
+
requestDict["method"] = request["method"]
|
|
1673
|
+
requestDict["body"] = request["body"]
|
|
1674
|
+
if let headers = request["headers"] as? JSObject {
|
|
1675
|
+
var headersDict: [String: String] = [:]
|
|
1676
|
+
for (key, value) in headers {
|
|
1677
|
+
if let strValue = value as? String {
|
|
1678
|
+
headersDict[key] = strValue
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
requestDict["headers"] = headersDict
|
|
1682
|
+
}
|
|
1683
|
+
dict["request"] = requestDict
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
let responsePayload = (decisionObj["response"] as? JSObject) ?? decisionObj
|
|
1687
|
+
if responsePayload["status"] != nil {
|
|
1688
|
+
var responsePayloadDict: [String: Any] = [:]
|
|
1689
|
+
responsePayloadDict["status"] = responsePayload["status"]
|
|
1690
|
+
responsePayloadDict["body"] = responsePayload["body"]
|
|
1691
|
+
if let headers = responsePayload["headers"] as? JSObject {
|
|
1692
|
+
var headersDict: [String: String] = [:]
|
|
1693
|
+
for (key, value) in headers {
|
|
1694
|
+
if let strValue = value as? String {
|
|
1695
|
+
headersDict[key] = strValue
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
responsePayloadDict["headers"] = headersDict
|
|
1699
|
+
}
|
|
1700
|
+
if decisionObj["response"] != nil {
|
|
1701
|
+
dict["response"] = responsePayloadDict
|
|
1702
|
+
} else {
|
|
1703
|
+
dict.merge(responsePayloadDict) { _, new in new }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
responseDict = dict
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
proxyHandler.handleResponse(requestId: requestId, phase: phase, responseData: responseDict)
|
|
1710
|
+
call.resolve()
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
@objc func close(_ call: CAPPluginCall) {
|
|
1714
|
+
let isAnimated = call.getBool("isAnimated", true)
|
|
1715
|
+
|
|
1716
|
+
DispatchQueue.main.async {
|
|
1717
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1718
|
+
if let targetId,
|
|
1719
|
+
let webViewController = self.webViewControllers[targetId],
|
|
1720
|
+
let navigationController = self.navigationControllers[targetId] {
|
|
1721
|
+
let currentUrl = webViewController.url?.absoluteString ?? ""
|
|
1722
|
+
webViewController.cleanupWebView()
|
|
1723
|
+
self.handleWebViewDidClose(id: targetId, url: currentUrl)
|
|
1724
|
+
navigationController.dismiss(animated: isAnimated, completion: nil)
|
|
1725
|
+
call.resolve()
|
|
1726
|
+
return
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
guard let webViewController = self.webViewController,
|
|
1730
|
+
let navigationController = self.navigationWebViewController else {
|
|
1731
|
+
call.reject("WebView is not initialized")
|
|
1732
|
+
return
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
let currentUrl = webViewController.url?.absoluteString ?? ""
|
|
1736
|
+
let isPresented = navigationController.presentingViewController != nil
|
|
1737
|
+
|
|
1738
|
+
if self.isHidden {
|
|
1739
|
+
webViewController.capableWebView?.removeFromSuperview()
|
|
1740
|
+
webViewController.cleanupWebView()
|
|
1741
|
+
if isPresented {
|
|
1742
|
+
navigationController.dismiss(animated: isAnimated) {
|
|
1743
|
+
self.handleWebViewDidClose(id: "", url: currentUrl)
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
self.handleWebViewDidClose(id: "", url: currentUrl)
|
|
1747
|
+
}
|
|
1748
|
+
self.isHidden = false
|
|
1749
|
+
call.resolve()
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
webViewController.cleanupWebView()
|
|
1754
|
+
self.handleWebViewDidClose(id: "", url: currentUrl)
|
|
1755
|
+
navigationController.dismiss(animated: isAnimated, completion: nil)
|
|
1756
|
+
call.resolve()
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
private func showPrivacyScreen() {
|
|
1761
|
+
if privacyScreen == nil {
|
|
1762
|
+
let newPrivacyScreen = UIImageView()
|
|
1763
|
+
self.privacyScreen = newPrivacyScreen
|
|
1764
|
+
if let launchImage = UIImage(named: "LaunchImage") {
|
|
1765
|
+
newPrivacyScreen.image = launchImage
|
|
1766
|
+
newPrivacyScreen.frame = UIScreen.main.bounds
|
|
1767
|
+
newPrivacyScreen.contentMode = .scaleAspectFill
|
|
1768
|
+
newPrivacyScreen.isUserInteractionEnabled = false
|
|
1769
|
+
} else if let launchImage = UIImage(named: "Splash") {
|
|
1770
|
+
newPrivacyScreen.image = launchImage
|
|
1771
|
+
newPrivacyScreen.frame = UIScreen.main.bounds
|
|
1772
|
+
newPrivacyScreen.contentMode = .scaleAspectFill
|
|
1773
|
+
newPrivacyScreen.isUserInteractionEnabled = false
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if let screen = self.privacyScreen {
|
|
1777
|
+
self.navigationWebViewController?.view.addSubview(screen)
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private func hidePrivacyScreen() {
|
|
1782
|
+
self.privacyScreen?.removeFromSuperview()
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
@objc func appDidBecomeActive(_ notification: NSNotification) {
|
|
1786
|
+
self.hidePrivacyScreen()
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
@objc func appWillResignActive(_ notification: NSNotification) {
|
|
1790
|
+
self.showPrivacyScreen()
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
private func readCredentials(_ call: CAPPluginCall) -> WKWebViewCredentials? {
|
|
1794
|
+
var credentials: WKWebViewCredentials?
|
|
1795
|
+
let credentialsDict = call.getObject("credentials", [:]).mapValues { String(describing: $0 as Any) }
|
|
1796
|
+
if !credentialsDict.isEmpty, let username = credentialsDict["username"], let password = credentialsDict["password"] {
|
|
1797
|
+
credentials = WKWebViewCredentials(username: username, password: password)
|
|
1798
|
+
}
|
|
1799
|
+
return credentials
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
private func isDarkColor(_ color: UIColor) -> Bool {
|
|
1803
|
+
let components = color.cgColor.components ?? []
|
|
1804
|
+
let red = components[0]
|
|
1805
|
+
let green = components[1]
|
|
1806
|
+
let blue = components[2]
|
|
1807
|
+
let brightness = (red * 299 + green * 587 + blue * 114) / 1000
|
|
1808
|
+
return brightness < 0.5
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
1812
|
+
call.resolve(["version": self.pluginVersion])
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
@objc func updateDimensions(_ call: CAPPluginCall) {
|
|
1816
|
+
let width = call.getFloat("width")
|
|
1817
|
+
let height = call.getFloat("height")
|
|
1818
|
+
let xPos = call.getFloat("x")
|
|
1819
|
+
let yPos = call.getFloat("y")
|
|
1820
|
+
|
|
1821
|
+
DispatchQueue.main.async {
|
|
1822
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1823
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1824
|
+
call.reject("WebView is not initialized")
|
|
1825
|
+
return
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
webViewController.updateDimensions(
|
|
1829
|
+
width: width.map { CGFloat($0) },
|
|
1830
|
+
height: height.map { CGFloat($0) },
|
|
1831
|
+
xPos: xPos.map { CGFloat($0) },
|
|
1832
|
+
yPos: yPos.map { CGFloat($0) }
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
call.resolve()
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
@objc func setEnabledSafeTopMargin(_ call: CAPPluginCall) {
|
|
1840
|
+
if call.options["enabled"] == nil {
|
|
1841
|
+
print("[InAppBrowser][Warning] setEnabledSafeTopMargin called without 'enabled'; defaulting to true")
|
|
1842
|
+
}
|
|
1843
|
+
let enabled = call.getBool("enabled", true)
|
|
1844
|
+
DispatchQueue.main.async {
|
|
1845
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1846
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1847
|
+
call.reject("WebView is not initialized")
|
|
1848
|
+
return
|
|
1849
|
+
}
|
|
1850
|
+
webViewController.updateSafeTopMargin(enabled)
|
|
1851
|
+
call.resolve()
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
@objc func setEnabledSafeBottomMargin(_ call: CAPPluginCall) {
|
|
1856
|
+
if call.options["enabled"] == nil {
|
|
1857
|
+
print("[InAppBrowser][Warning] setEnabledSafeBottomMargin called without 'enabled'; defaulting to false")
|
|
1858
|
+
}
|
|
1859
|
+
let enabled = call.getBool("enabled", false)
|
|
1860
|
+
DispatchQueue.main.async {
|
|
1861
|
+
let targetId = call.getString("id") ?? self.activeWebViewId
|
|
1862
|
+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
|
|
1863
|
+
call.reject("WebView is not initialized")
|
|
1864
|
+
return
|
|
1865
|
+
}
|
|
1866
|
+
webViewController.updateSafeBottomMargin(enabled)
|
|
1867
|
+
call.resolve()
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
@objc func openSecureWindow(_ call: CAPPluginCall) {
|
|
1872
|
+
guard let urlString = call.getString("authEndpoint") else {
|
|
1873
|
+
call.reject("authEndpoint is required")
|
|
1874
|
+
return
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
guard let url = URL(string: urlString) else {
|
|
1878
|
+
call.reject("Invalid URL")
|
|
1879
|
+
return
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
guard let redirectUri = call.getString("redirectUri") else {
|
|
1883
|
+
call.reject("Redirect URI is required")
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Store the call for later resolution
|
|
1888
|
+
self.openSecureWindowCall = call
|
|
1889
|
+
|
|
1890
|
+
let prefersEphemeral = call.getBool("prefersEphemeralWebBrowserSession") ?? false
|
|
1891
|
+
|
|
1892
|
+
// Open the URL in a secure browser window
|
|
1893
|
+
DispatchQueue.main.async {
|
|
1894
|
+
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: url.scheme) {
|
|
1895
|
+
callbackURL, error in
|
|
1896
|
+
|
|
1897
|
+
// Clean up the stored call
|
|
1898
|
+
self.openSecureWindowCall = nil
|
|
1899
|
+
|
|
1900
|
+
if let error = error {
|
|
1901
|
+
// Handle error (e.g., user cancelled)
|
|
1902
|
+
call.reject(error.localizedDescription)
|
|
1903
|
+
return
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
guard let callbackURL = callbackURL else {
|
|
1907
|
+
call.reject("No callback URL received")
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if !callbackURL.absoluteString.hasPrefix(redirectUri) {
|
|
1912
|
+
call.reject("Redirect URI does not match, expected " + redirectUri + " but got " + callbackURL.absoluteString)
|
|
1913
|
+
return
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Resolve the call with the callback URL
|
|
1917
|
+
call.resolve(["redirectedUri": callbackURL.absoluteString])
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Present the session
|
|
1921
|
+
session.prefersEphemeralWebBrowserSession = prefersEphemeral
|
|
1922
|
+
session.presentationContextProvider = self
|
|
1923
|
+
session.start()
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
extension InAppBrowserPlugin: ASWebAuthenticationPresentationContextProviding {
|
|
1929
|
+
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
1930
|
+
return self.bridge?.viewController?.view.window ?? ASPresentationAnchor()
|
|
1931
|
+
}
|
|
1932
|
+
}
|