@capgo/inappbrowser 8.2.0 → 8.3.0-alpha.0

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.
@@ -0,0 +1,257 @@
1
+ import Foundation
2
+ import WebKit
3
+ import Capacitor
4
+
5
+ public class ProxySchemeHandler: NSObject, WKURLSchemeHandler {
6
+ weak var plugin: InAppBrowserPlugin?
7
+ private var pendingTasks: [String: WKURLSchemeTask] = [:]
8
+ private var pendingBodies: [String: Data] = [:]
9
+ private var pendingNetworkTasks: [String: URLSessionDataTask] = [:]
10
+ private var stoppedRequests: Set<String> = []
11
+ private let taskLock = NSLock()
12
+ private let webviewId: String
13
+ private let proxyTimeoutSeconds: TimeInterval = 10
14
+
15
+ init(plugin: InAppBrowserPlugin, webviewId: String) {
16
+ self.plugin = plugin
17
+ self.webviewId = webviewId
18
+ super.init()
19
+ }
20
+
21
+ public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
22
+ let request = urlSchemeTask.request
23
+ guard let url = request.url else {
24
+ urlSchemeTask.didFailWithError(NSError(
25
+ domain: "ProxySchemeHandler",
26
+ code: -1,
27
+ userInfo: [NSLocalizedDescriptionKey: "No URL in request"]
28
+ ))
29
+ return
30
+ }
31
+
32
+ let requestId = UUID().uuidString
33
+
34
+ taskLock.lock()
35
+ pendingTasks[requestId] = urlSchemeTask
36
+ taskLock.unlock()
37
+
38
+ // Encode body to base64, buffering stream data for later pass-through
39
+ var base64Body: String? = nil
40
+ if let bodyData = request.httpBody {
41
+ base64Body = bodyData.base64EncodedString()
42
+ } else if let bodyStream = request.httpBodyStream {
43
+ let data = Data(reading: bodyStream)
44
+ if !data.isEmpty {
45
+ base64Body = data.base64EncodedString()
46
+ taskLock.lock()
47
+ pendingBodies[requestId] = data
48
+ taskLock.unlock()
49
+ }
50
+ }
51
+
52
+ // Build headers dict
53
+ var headers: [String: String] = [:]
54
+ if let allHeaders = request.allHTTPHeaderFields {
55
+ headers = allHeaders
56
+ }
57
+
58
+ let eventData: [String: Any] = [
59
+ "requestId": requestId,
60
+ "url": url.absoluteString,
61
+ "method": request.httpMethod ?? "GET",
62
+ "headers": headers,
63
+ "body": base64Body as Any,
64
+ "webviewId": webviewId,
65
+ ]
66
+
67
+ plugin?.notifyListeners("proxyRequest", data: eventData)
68
+
69
+ // Timeout: if JS never responds, fail the request after proxyTimeoutSeconds
70
+ DispatchQueue.global().asyncAfter(deadline: .now() + proxyTimeoutSeconds) { [weak self] in
71
+ guard let self = self else { return }
72
+ self.taskLock.lock()
73
+ guard let task = self.pendingTasks.removeValue(forKey: requestId) else {
74
+ self.taskLock.unlock()
75
+ return
76
+ }
77
+ self.pendingBodies.removeValue(forKey: requestId)
78
+ self.taskLock.unlock()
79
+
80
+ print("[InAppBrowser] Proxy request timed out after \(Int(self.proxyTimeoutSeconds))s: \(requestId)")
81
+ task.didFailWithError(NSError(
82
+ domain: "ProxySchemeHandler",
83
+ code: NSURLErrorTimedOut,
84
+ userInfo: [NSLocalizedDescriptionKey: "Proxy handler did not respond within \(Int(self.proxyTimeoutSeconds)) seconds"]
85
+ ))
86
+ }
87
+ }
88
+
89
+ public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
90
+ taskLock.lock()
91
+ let requestIdToRemove = pendingTasks.first(where: { $0.value === urlSchemeTask })?.key
92
+ var networkTask: URLSessionDataTask?
93
+ if let key = requestIdToRemove {
94
+ pendingTasks.removeValue(forKey: key)
95
+ pendingBodies.removeValue(forKey: key)
96
+ networkTask = pendingNetworkTasks.removeValue(forKey: key)
97
+ stoppedRequests.insert(key)
98
+ }
99
+ taskLock.unlock()
100
+ networkTask?.cancel()
101
+ }
102
+
103
+ /// Called from handleProxyRequest plugin method with the JS response
104
+ func handleResponse(requestId: String, responseData: [String: Any]?) {
105
+ taskLock.lock()
106
+ let isStopped = stoppedRequests.remove(requestId) != nil
107
+ guard let urlSchemeTask = pendingTasks[requestId] else {
108
+ pendingTasks.removeValue(forKey: requestId)
109
+ pendingBodies.removeValue(forKey: requestId)
110
+ taskLock.unlock()
111
+ return
112
+ }
113
+ let bufferedBody = pendingBodies.removeValue(forKey: requestId)
114
+
115
+ if isStopped {
116
+ pendingTasks.removeValue(forKey: requestId)
117
+ taskLock.unlock()
118
+ return
119
+ }
120
+
121
+ if responseData != nil {
122
+ // JS provided a response — remove from pending now
123
+ pendingTasks.removeValue(forKey: requestId)
124
+ }
125
+ // For pass-through (nil), keep pendingTasks entry so stop can find it
126
+ taskLock.unlock()
127
+
128
+ if let responseData = responseData {
129
+ let statusCode = responseData["status"] as? Int ?? 200
130
+ let headersDict = responseData["headers"] as? [String: String] ?? [:]
131
+ let base64Body = responseData["body"] as? String ?? ""
132
+
133
+ let bodyData = Data(base64Encoded: base64Body) ?? Data()
134
+
135
+ guard let url = urlSchemeTask.request.url else {
136
+ urlSchemeTask.didFailWithError(NSError(
137
+ domain: "ProxySchemeHandler",
138
+ code: -1,
139
+ userInfo: [NSLocalizedDescriptionKey: "No URL"]
140
+ ))
141
+ return
142
+ }
143
+
144
+ guard let httpResponse = HTTPURLResponse(
145
+ url: url,
146
+ statusCode: statusCode,
147
+ httpVersion: "HTTP/1.1",
148
+ headerFields: headersDict
149
+ ) else {
150
+ urlSchemeTask.didFailWithError(NSError(
151
+ domain: "ProxySchemeHandler",
152
+ code: -2,
153
+ userInfo: [NSLocalizedDescriptionKey: "Failed to create response"]
154
+ ))
155
+ return
156
+ }
157
+
158
+ urlSchemeTask.didReceive(httpResponse)
159
+ urlSchemeTask.didReceive(bodyData)
160
+ urlSchemeTask.didFinish()
161
+ } else {
162
+ // Null response = pass-through via URLSession
163
+ executePassThrough(requestId: requestId, urlSchemeTask: urlSchemeTask, bufferedBody: bufferedBody)
164
+ }
165
+ }
166
+
167
+ private func executePassThrough(requestId: String, urlSchemeTask: WKURLSchemeTask, bufferedBody: Data?) {
168
+ var request = urlSchemeTask.request
169
+ // Restore body if it was consumed from httpBodyStream during interception
170
+ if request.httpBody == nil, let body = bufferedBody {
171
+ request.httpBody = body
172
+ }
173
+ let session = URLSession.shared
174
+ let task = session.dataTask(with: request) { [weak self] data, response, error in
175
+ guard let self = self else { return }
176
+
177
+ // Clean up and check if this request was stopped while in-flight
178
+ self.taskLock.lock()
179
+ self.pendingTasks.removeValue(forKey: requestId)
180
+ self.pendingNetworkTasks.removeValue(forKey: requestId)
181
+ let wasStopped = self.stoppedRequests.remove(requestId) != nil
182
+ self.taskLock.unlock()
183
+
184
+ if wasStopped {
185
+ return
186
+ }
187
+
188
+ if let error = error {
189
+ if (error as NSError).code == NSURLErrorCancelled {
190
+ return
191
+ }
192
+ urlSchemeTask.didFailWithError(error)
193
+ return
194
+ }
195
+
196
+ guard let httpResponse = response as? HTTPURLResponse else {
197
+ urlSchemeTask.didFailWithError(NSError(
198
+ domain: "ProxySchemeHandler",
199
+ code: -3,
200
+ userInfo: [NSLocalizedDescriptionKey: "Non-HTTP response"]
201
+ ))
202
+ return
203
+ }
204
+
205
+ urlSchemeTask.didReceive(httpResponse)
206
+ if let data = data {
207
+ urlSchemeTask.didReceive(data)
208
+ }
209
+ urlSchemeTask.didFinish()
210
+ }
211
+
212
+ taskLock.lock()
213
+ pendingNetworkTasks[requestId] = task
214
+ taskLock.unlock()
215
+ task.resume()
216
+ }
217
+
218
+ func cancelAllPendingTasks() {
219
+ taskLock.lock()
220
+ let tasks = pendingTasks
221
+ let networkTasks = pendingNetworkTasks
222
+ pendingTasks.removeAll()
223
+ pendingBodies.removeAll()
224
+ pendingNetworkTasks.removeAll()
225
+ stoppedRequests.removeAll()
226
+ taskLock.unlock()
227
+
228
+ for (_, networkTask) in networkTasks {
229
+ networkTask.cancel()
230
+ }
231
+ for (_, task) in tasks {
232
+ task.didFailWithError(NSError(
233
+ domain: "ProxySchemeHandler",
234
+ code: NSURLErrorCancelled,
235
+ userInfo: [NSLocalizedDescriptionKey: "WebView closed"]
236
+ ))
237
+ }
238
+ }
239
+ }
240
+
241
+ extension Data {
242
+ init(reading input: InputStream) {
243
+ self.init()
244
+ input.open()
245
+ let bufferSize = 1024
246
+ let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
247
+ while input.hasBytesAvailable {
248
+ let read = input.read(buffer, maxLength: bufferSize)
249
+ if read < 0 {
250
+ break
251
+ }
252
+ self.append(buffer, count: read)
253
+ }
254
+ buffer.deallocate()
255
+ input.close()
256
+ }
257
+ }
@@ -0,0 +1,53 @@
1
+ import Foundation
2
+ import ObjectiveC
3
+ import WebKit
4
+
5
+ /// Enables registering a `WKURLSchemeHandler` for built-in schemes like `http` and `https`.
6
+ ///
7
+ /// Apple's public `setURLSchemeHandler(_:forURLScheme:)` API throws for http/https because
8
+ /// `WKWebView.handlesURLScheme(_:)` returns `true` for them. This extension swizzles that
9
+ /// class method to return `false` for specified schemes, allowing the public API to work.
10
+ ///
11
+ /// This is the industry-standard approach used by DuckDuckGo Browser, TON Proxy, and others.
12
+ extension WKWebView {
13
+
14
+ /// Tracks which schemes have been overridden to allow custom handling
15
+ private static var _overriddenSchemes = Set<String>()
16
+
17
+ /// One-time swizzle — matches the pattern used by DuckDuckGo and ton-proxy-swift
18
+ private static let _swizzleOnce: Void = {
19
+ let original = class_getClassMethod(WKWebView.self, #selector(WKWebView.handlesURLScheme(_:)))
20
+ let swizzled = class_getClassMethod(
21
+ WKWebView.self,
22
+ #selector(WKWebView._capgo_handlesURLScheme(_:))
23
+ )
24
+
25
+ guard let original, let swizzled else {
26
+ print("[InAppBrowser][Proxy] WARNING: Could not get methods for swizzle")
27
+ return
28
+ }
29
+
30
+ method_exchangeImplementations(original, swizzled)
31
+ }()
32
+
33
+ /// Swizzled replacement — returns false for overridden schemes, calls original for all others
34
+ @objc(capgo_handlesURLScheme:)
35
+ private static func _capgo_handlesURLScheme(_ urlScheme: String) -> Bool {
36
+ if _overriddenSchemes.contains(urlScheme.lowercased()) {
37
+ return false
38
+ }
39
+ // After swizzle, this actually calls the ORIGINAL handlesURLScheme
40
+ return _capgo_handlesURLScheme(urlScheme)
41
+ }
42
+
43
+ /// Call this before `setURLSchemeHandler(_:forURLScheme:)` to allow registering for http/https.
44
+ static func enableCustomSchemeHandling(for schemes: [String]) {
45
+ // Add schemes BEFORE triggering the swizzle
46
+ for scheme in schemes {
47
+ _overriddenSchemes.insert(scheme.lowercased())
48
+ }
49
+
50
+ // Trigger the one-time swizzle
51
+ _ = _swizzleOnce
52
+ }
53
+ }
@@ -101,11 +101,13 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
101
101
  self.initWebview(isInspectable: isInspectable)
102
102
  }
103
103
 
104
- public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String]) {
104
+ public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String], proxyRequests: Bool = false, proxySchemeHandler: ProxySchemeHandler? = nil) {
105
105
  super.init(nibName: nil, bundle: nil)
106
106
  self.blankNavigationTab = blankNavigationTab
107
107
  self.enabledSafeBottomMargin = enabledSafeBottomMargin
108
108
  self.enabledSafeTopMargin = enabledSafeTopMargin
109
+ self.proxyRequests = proxyRequests
110
+ self.proxySchemeHandler = proxySchemeHandler
109
111
  self.source = .remote(url)
110
112
  self.credentials = credentials
111
113
  self.setHeaders(headers: headers)
@@ -153,6 +155,8 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
153
155
  var authorizedAppLinks: [String] = []
154
156
  var activeNativeNavigationForWebview: Bool = true
155
157
  var disableOverscroll: Bool = false
158
+ var proxyRequests: Bool = false
159
+ var proxySchemeHandler: ProxySchemeHandler?
156
160
 
157
161
  // Dimension properties
158
162
  var customWidth: CGFloat?
@@ -669,6 +673,19 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
669
673
  // Enable background task processing
670
674
  webConfiguration.processPool = WKProcessPool()
671
675
 
676
+ // Register proxy scheme handler if enabled
677
+ // Swizzles WKWebView.handlesURLScheme to allow registering for http/https via the public API
678
+ // This is the industry-standard approach used by DuckDuckGo Browser and others
679
+ if proxyRequests, let handler = proxySchemeHandler {
680
+ WKWebView.enableCustomSchemeHandling(for: ["https", "http"])
681
+ if !WKWebView.handlesURLScheme("https") && !WKWebView.handlesURLScheme("http") {
682
+ webConfiguration.setURLSchemeHandler(handler, forURLScheme: "https")
683
+ webConfiguration.setURLSchemeHandler(handler, forURLScheme: "http")
684
+ } else {
685
+ print("[InAppBrowser][Proxy] WARNING: handlesURLScheme swizzle failed; proxy scheme handler not registered")
686
+ }
687
+ }
688
+
672
689
  // Enable JavaScript to run automatically (needed for preShowScript and Firebase polyfill)
673
690
  webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
674
691
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/inappbrowser",
3
- "version": "8.2.0",
3
+ "version": "8.3.0-alpha.0",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -41,7 +41,8 @@
41
41
  "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
42
42
  "swiftlint": "node-swiftlint",
43
43
  "docgen": "docgen --api InAppBrowserPlugin --output-readme README.md --output-json dist/docs.json",
44
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
44
+ "build:proxy-bridge": "esbuild src/proxy-bridge.ts --bundle --outfile=android/src/main/assets/proxy-bridge.js --format=iife --target=es2015",
45
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs && npm run build:proxy-bridge",
45
46
  "clean": "rimraf ./dist",
46
47
  "watch": "tsc --watch",
47
48
  "prepublishOnly": "npm run build"
@@ -56,6 +57,7 @@
56
57
  "@ionic/prettier-config": "^4.0.0",
57
58
  "@ionic/swiftlint-config": "^2.0.0",
58
59
  "@types/node": "^24.10.1",
60
+ "esbuild": "^0.27.3",
59
61
  "eslint": "^8.57.1",
60
62
  "eslint-plugin-import": "^2.31.0",
61
63
  "husky": "^9.1.7",