@capacitor/ios 6.0.0-beta.2 → 6.0.0-dev-8046d907.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.
- package/Capacitor/Capacitor/AppUUID.swift +2 -4
- package/Capacitor/Capacitor/CAPBridgeViewController.swift +12 -13
- package/Capacitor/Capacitor/CAPPluginCall.swift +17 -10
- package/Capacitor/Capacitor/CapacitorBridge.swift +30 -20
- package/Capacitor/Capacitor/Codable/JSValueDecoder.swift +2 -2
- package/Capacitor/Capacitor/Codable/JSValueEncoder.swift +3 -3
- package/Capacitor/Capacitor/KeyValueStore.swift +296 -0
- package/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift +25 -22
- package/Capacitor/Capacitor/Plugins/DefaultPlugins.m +1 -0
- package/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift +20 -1
- package/Capacitor/Capacitor/Plugins/WebView.swift +8 -2
- package/Capacitor/Capacitor/PrivacyInfo.xcprivacy +14 -0
- package/Capacitor/Capacitor/WebViewAssetHandler.swift +79 -3
- package/Capacitor/Capacitor/assets/native-bridge.js +61 -20
- package/Capacitor.podspec +2 -3
- package/CapacitorCordova/CapacitorCordova/PrivacyInfo.xcprivacy +14 -0
- package/CapacitorCordova.podspec +1 -0
- package/package.json +4 -3
|
@@ -41,13 +41,11 @@ public class AppUUID {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
private static func readUUID() -> String {
|
|
44
|
-
|
|
45
|
-
return defaults.string(forKey: key) ?? ""
|
|
44
|
+
KeyValueStore.standard[key] ?? ""
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
private static func writeUUID(_ uuid: String) {
|
|
49
|
-
|
|
50
|
-
defaults.set(uuid, forKey: key)
|
|
48
|
+
KeyValueStore.standard[key] = uuid
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
}
|
|
@@ -18,9 +18,9 @@ import Cordova
|
|
|
18
18
|
public lazy final var isNewBinary: Bool = {
|
|
19
19
|
if let curVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
|
|
20
20
|
let curVersionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
|
21
|
-
if let lastVersionCode =
|
|
22
|
-
let lastVersionName =
|
|
23
|
-
return
|
|
21
|
+
if let lastVersionCode = KeyValueStore.standard["lastBinaryVersionCode", as: String.self],
|
|
22
|
+
let lastVersionName = KeyValueStore.standard["lastBinaryVersionName", as: String.self] {
|
|
23
|
+
return curVersionCode != lastVersionCode || curVersionName != lastVersionName
|
|
24
24
|
}
|
|
25
25
|
return true
|
|
26
26
|
}
|
|
@@ -34,10 +34,6 @@ import Cordova
|
|
|
34
34
|
CAPLog.enableLogging = configuration.loggingEnabled
|
|
35
35
|
logWarnings(for: configDescriptor)
|
|
36
36
|
|
|
37
|
-
if configDescriptor.instanceType == .fixed {
|
|
38
|
-
updateBinaryVersion()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
setStatusBarDefaults()
|
|
42
38
|
setScreenOrientationDefaults()
|
|
43
39
|
|
|
@@ -55,6 +51,10 @@ import Cordova
|
|
|
55
51
|
assetHandler: assetHandler,
|
|
56
52
|
delegationHandler: delegationHandler)
|
|
57
53
|
capacitorDidLoad()
|
|
54
|
+
|
|
55
|
+
if configDescriptor.instanceType == .fixed {
|
|
56
|
+
updateBinaryVersion()
|
|
57
|
+
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
override open func viewDidLoad() {
|
|
@@ -79,7 +79,7 @@ import Cordova
|
|
|
79
79
|
open func instanceDescriptor() -> InstanceDescriptor {
|
|
80
80
|
let descriptor = InstanceDescriptor.init()
|
|
81
81
|
if !isNewBinary && !descriptor.cordovaDeployDisabled {
|
|
82
|
-
if let persistedPath =
|
|
82
|
+
if let persistedPath = KeyValueStore.standard["serverBasePath", as: String.self], !persistedPath.isEmpty {
|
|
83
83
|
if let libPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first {
|
|
84
84
|
descriptor.appLocation = URL(fileURLWithPath: libPath, isDirectory: true)
|
|
85
85
|
.appendingPathComponent("NoCloud")
|
|
@@ -319,11 +319,10 @@ extension CAPBridgeViewController {
|
|
|
319
319
|
let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
|
|
320
320
|
return
|
|
321
321
|
}
|
|
322
|
-
let
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
prefs.synchronize()
|
|
322
|
+
let store = KeyValueStore.standard
|
|
323
|
+
store["lastBinaryVersionCode"] = versionCode
|
|
324
|
+
store["lastBinaryVersionName"] = versionName
|
|
325
|
+
store["serverBasePath"] = nil as String?
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
private func logWarnings(for descriptor: InstanceDescriptor) {
|
|
@@ -80,17 +80,24 @@ extension CAPPluginCall: JSValueContainer {
|
|
|
80
80
|
|
|
81
81
|
// MARK: Codable Support
|
|
82
82
|
public extension CAPPluginCall {
|
|
83
|
-
/// Encodes the given value to a ``JSObject`` and resolves the call.
|
|
83
|
+
/// Encodes the given value to a ``JSObject`` and resolves the call. If an error is thrown during encoding, ``reject(_:_:_:_:)`` is called.
|
|
84
84
|
/// - Parameters:
|
|
85
|
-
///
|
|
86
|
-
///
|
|
87
|
-
///
|
|
88
|
-
///
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
/// - data: The value to encode
|
|
86
|
+
/// - encoder: The encoder to use. Defaults to `JSValueEncoder()`
|
|
87
|
+
/// - messageForRejectionFromError: A closure that takes the error thrown from ``JSValueEncoder/encodeJSObject(_:)``
|
|
88
|
+
/// and returns a string to be provided to ``reject(_:_:_:_:)``. Defaults to a function that returns "Failed encoding response".
|
|
89
|
+
func resolve<T: Encodable>(
|
|
90
|
+
with data: T,
|
|
91
|
+
encoder: JSValueEncoder = JSValueEncoder(),
|
|
92
|
+
messageForRejectionFromError: (Error) -> String = { _ in "Failed encoding response" }
|
|
93
|
+
) {
|
|
94
|
+
do {
|
|
95
|
+
let encoded = try encoder.encodeJSObject(data)
|
|
96
|
+
resolve(encoded)
|
|
97
|
+
} catch {
|
|
98
|
+
let message = messageForRejectionFromError(error)
|
|
99
|
+
reject(message, nil, error)
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/// Decodes the options to the given type.
|
|
@@ -5,6 +5,10 @@ import Cordova
|
|
|
5
5
|
|
|
6
6
|
internal typealias CapacitorPlugin = CAPPlugin & CAPBridgedPlugin
|
|
7
7
|
|
|
8
|
+
struct RegistrationList: Codable {
|
|
9
|
+
let packageClassList: Set<String>
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
An internal class adopting a public protocol means that we have a lot of `public` methods
|
|
10
14
|
but that is by design not a mistake. And since the bridge is the center of the whole project
|
|
@@ -90,6 +94,8 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol {
|
|
|
90
94
|
static let tmpVCAppeared = Notification(name: Notification.Name(rawValue: "tmpViewControllerAppeared"))
|
|
91
95
|
public static let capacitorSite = "https://capacitorjs.com/"
|
|
92
96
|
public static let fileStartIdentifier = "/_capacitor_file_"
|
|
97
|
+
public static let httpInterceptorStartIdentifier = "/_capacitor_http_interceptor_"
|
|
98
|
+
public static let httpsInterceptorStartIdentifier = "/_capacitor_https_interceptor_"
|
|
93
99
|
public static let defaultScheme = "capacitor"
|
|
94
100
|
|
|
95
101
|
public private(set) var webViewAssetHandler: WebViewAssetHandler
|
|
@@ -277,30 +283,34 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol {
|
|
|
277
283
|
Register all plugins that have been declared
|
|
278
284
|
*/
|
|
279
285
|
func registerPlugins() {
|
|
280
|
-
|
|
281
|
-
let classCount = objc_getClassList(nil, 0)
|
|
282
|
-
let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))
|
|
286
|
+
var pluginList: [AnyClass] = [CAPHttpPlugin.self, CAPConsolePlugin.self, CAPWebViewPlugin.self, CAPCookiesPlugin.self]
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
288
|
+
if autoRegisterPlugins {
|
|
289
|
+
do {
|
|
290
|
+
if let pluginJSON = Bundle.main.url(forResource: "capacitor.config", withExtension: "json") {
|
|
291
|
+
let pluginData = try Data(contentsOf: pluginJSON)
|
|
292
|
+
let registrationList = try JSONDecoder().decode(RegistrationList.self, from: pluginData)
|
|
293
|
+
|
|
294
|
+
for plugin in registrationList.packageClassList {
|
|
295
|
+
if let pluginClass = NSClassFromString(plugin) {
|
|
296
|
+
if pluginClass == CDVPlugin.self {
|
|
297
|
+
injectCordovaFiles = true
|
|
298
|
+
} else {
|
|
299
|
+
pluginList.append(pluginClass)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
296
302
|
}
|
|
297
303
|
}
|
|
304
|
+
} catch {
|
|
305
|
+
CAPLog.print("Error registering plugins: \(error)")
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for plugin in pluginList {
|
|
310
|
+
if plugin is CAPInstancePlugin.Type { continue }
|
|
311
|
+
if let capPlugin = plugin as? CapacitorPlugin.Type {
|
|
312
|
+
registerPlugin(capPlugin)
|
|
298
313
|
}
|
|
299
|
-
classes.deallocate()
|
|
300
|
-
} else {
|
|
301
|
-
// register core plugins only
|
|
302
|
-
[CAPHttpPlugin.self, CAPConsolePlugin.self, CAPWebViewPlugin.self, CAPCookiesPlugin.self]
|
|
303
|
-
.forEach { registerPluginType($0) }
|
|
304
314
|
}
|
|
305
315
|
}
|
|
306
316
|
|
|
@@ -31,8 +31,8 @@ public final class JSValueDecoder: TopLevelDecoder {
|
|
|
31
31
|
typealias CodingUserInfo = [CodingUserInfoKey: Any]
|
|
32
32
|
|
|
33
33
|
private final class _JSValueDecoder {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
var codingPath: [CodingKey] = []
|
|
35
|
+
var userInfo: CodingUserInfo = [:]
|
|
36
36
|
fileprivate var data: JSValue
|
|
37
37
|
|
|
38
38
|
init(data: JSValue) {
|
|
@@ -123,7 +123,7 @@ extension Array: JSValueEncodingContainer where Element == EncodingContainer {
|
|
|
123
123
|
var data: JSValue? {
|
|
124
124
|
guard count != 0 else { return nil }
|
|
125
125
|
guard count != 1 else { return self[0].data }
|
|
126
|
-
var data: (any JSValue)?
|
|
126
|
+
var data: (any JSValue)?
|
|
127
127
|
|
|
128
128
|
for container in self {
|
|
129
129
|
if data == nil {
|
|
@@ -189,7 +189,7 @@ extension _JSValueEncoder: Encoder {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
private final class KeyedContainer<Key> where Key: CodingKey {
|
|
193
193
|
var object: JSObject? {
|
|
194
194
|
encodedKeyedValue?.reduce(into: [:]) { obj, next in
|
|
195
195
|
let (key, value) = next
|
|
@@ -239,7 +239,7 @@ extension KeyedContainer: KeyedEncodingContainerProtocol {
|
|
|
239
239
|
|
|
240
240
|
// This is a perectly valid name for this method. The underscore is to avoid a conflict with the
|
|
241
241
|
// protocol requirement.
|
|
242
|
-
//swiftlint:disable:next identifier_name
|
|
242
|
+
// swiftlint:disable:next identifier_name
|
|
243
243
|
func _encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Encodable {
|
|
244
244
|
switch optionalEncodingStrategy {
|
|
245
245
|
case .explicitNulls:
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyValueStore.swift
|
|
3
|
+
// Capacitor
|
|
4
|
+
//
|
|
5
|
+
// Created by Steven Sherry on 1/5/24.
|
|
6
|
+
// Copyright © 2024 Drifty Co. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
/// A generic KeyValueStore that allows storing and retrieving values associated with string keys.
|
|
12
|
+
/// The store supports both ephemeral (in-memory) storage and persistent (file-based) storage backends
|
|
13
|
+
/// by default, however it can also take anything that conforms to ``KeyValueStoreBackend`` as
|
|
14
|
+
/// a backend.
|
|
15
|
+
///
|
|
16
|
+
/// This class provides methods to get, set and delete key-value pairs for any type of value, provided the
|
|
17
|
+
/// types conform to `Codable`. The default ``Backend/ephemeral`` and ``Backend/persistent(suiteName:)``
|
|
18
|
+
/// backends are thread-safe.
|
|
19
|
+
///
|
|
20
|
+
/// ## Usage Examples
|
|
21
|
+
///
|
|
22
|
+
/// ### Non-throwing API
|
|
23
|
+
/// ```swift
|
|
24
|
+
/// let store = KeyValueStore.standard
|
|
25
|
+
/// // Set
|
|
26
|
+
/// store["key"] = "value"
|
|
27
|
+
///
|
|
28
|
+
/// // Get
|
|
29
|
+
/// if let value = store["key", as: String.self] {
|
|
30
|
+
/// // Do something with value
|
|
31
|
+
/// }
|
|
32
|
+
///
|
|
33
|
+
/// // Delete
|
|
34
|
+
/// // The type here is a required argument because
|
|
35
|
+
/// // it is unable to be inferred
|
|
36
|
+
/// store["key", as: String.self] = nil
|
|
37
|
+
/// // or
|
|
38
|
+
/// store["key"] = nil as String?
|
|
39
|
+
/// ```
|
|
40
|
+
///
|
|
41
|
+
///
|
|
42
|
+
/// ### Throwing API
|
|
43
|
+
///
|
|
44
|
+
/// ```swift
|
|
45
|
+
/// let store = KeyValueStore.standard
|
|
46
|
+
/// do {
|
|
47
|
+
/// // Set
|
|
48
|
+
/// try store.set("key", value: "value")
|
|
49
|
+
///
|
|
50
|
+
/// // Get
|
|
51
|
+
/// if let value = try store.get("key", as: String.self) {
|
|
52
|
+
/// // Do something with value
|
|
53
|
+
/// }
|
|
54
|
+
///
|
|
55
|
+
/// // Delete
|
|
56
|
+
/// try store.delete("key")
|
|
57
|
+
/// } catch {
|
|
58
|
+
/// print(error.localizedDescription)
|
|
59
|
+
/// }
|
|
60
|
+
/// ```
|
|
61
|
+
///
|
|
62
|
+
/// ### Throwing vs Non-throwing
|
|
63
|
+
///
|
|
64
|
+
/// Of the built-in backends, both ``Backend/ephemeral`` and ``Backend/persistent(suiteName:)`` will throw in the following cases:
|
|
65
|
+
/// * The data read from the file retrieved during ``get(_:as:)`` is unable to be decoded as the type provided.
|
|
66
|
+
/// * The value provided to ``set(_:value:)`` encounters an error during encoding.
|
|
67
|
+
/// * This is more likely to happen with types that have custom `Encodable` implementations
|
|
68
|
+
///
|
|
69
|
+
/// ``Backend/persistent(suiteName:)`` will throw for the following additional cases:
|
|
70
|
+
/// * A file is unable to be read from disk during ``get(_:as:)``
|
|
71
|
+
/// * The existence of the file on disk is checked before attempting to read the file, so out of the
|
|
72
|
+
/// [possible file reading errors](https://developer.apple.com/documentation/foundation/1448136-nserror_codes#file-reading-errors),
|
|
73
|
+
/// the only likely candidate would be
|
|
74
|
+
/// [NSFileReadCorruptFileError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilereadcorruptfileerror).
|
|
75
|
+
/// In practice, this should never happen since writes happen atomically.
|
|
76
|
+
/// * The data from the value encoded in ``set(_:value:)`` is unable to be written to disk
|
|
77
|
+
/// * Of the [possible file writing errors](https://developer.apple.com/documentation/foundation/1448136-nserror_codes#file-writing-errors),
|
|
78
|
+
/// the only likely candidates are
|
|
79
|
+
/// [NSFileWriteInvalidFileNameError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilewriteinvalidfilenameerror)
|
|
80
|
+
/// if the key provided makes for an invalid file name and
|
|
81
|
+
/// [NSFileWriteOutOfSpaceError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilewriteoutofspaceerror)
|
|
82
|
+
/// if the user has no space left on disk
|
|
83
|
+
///
|
|
84
|
+
/// The throwing API should be used in cases where detailed error information is needed for logging or diagnostics. The non-throwing API should be used
|
|
85
|
+
/// in cases where silent failure is preferred.
|
|
86
|
+
public class KeyValueStore {
|
|
87
|
+
|
|
88
|
+
/// The built-in storage backends
|
|
89
|
+
public enum Backend {
|
|
90
|
+
/// An in-memory backing store
|
|
91
|
+
case ephemeral
|
|
92
|
+
/// A persistent file-based backing store using the
|
|
93
|
+
/// `suiteName` as an identifier for the collection of files
|
|
94
|
+
case persistent(suiteName: String)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private let backend: any KeyValueStoreBackend
|
|
98
|
+
|
|
99
|
+
/// Creates an instance of ``KeyValueStore`` with a custom backend
|
|
100
|
+
/// - Parameter backend: The custom backend implementation
|
|
101
|
+
public init(backend: any KeyValueStoreBackend) {
|
|
102
|
+
self.backend = backend
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Creates an instance of ``KeyValueStore`` with the provided built-in ``Backend``
|
|
106
|
+
/// - Parameter type: The type of ``Backend`` to use
|
|
107
|
+
public init(type: Backend) {
|
|
108
|
+
switch type {
|
|
109
|
+
case .ephemeral:
|
|
110
|
+
backend = InMemoryStore()
|
|
111
|
+
case .persistent(suiteName: let name):
|
|
112
|
+
backend = FileStore.with(name: name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Creates an instance of ``KeyValueStore`` with ``Backend/persistent(suiteName:)``
|
|
117
|
+
/// - Parameter suiteName: The suite name to provide ``Backend/persistent(suiteName:)``
|
|
118
|
+
public convenience init(suiteName: String) {
|
|
119
|
+
self.init(type: .persistent(suiteName: suiteName))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Retrieves a value of the specified type and key
|
|
123
|
+
/// - Parameters:
|
|
124
|
+
/// - key: The unique identifier for the value
|
|
125
|
+
/// - type: The expected type of the value being retried
|
|
126
|
+
/// - Returns: A decoded value of the given type or `nil` if there is no such value
|
|
127
|
+
public func `get`<T>(_ key: String, as type: T.Type = T.self) throws -> T? where T: Decodable {
|
|
128
|
+
try backend.get(key, as: type)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Stores the value under the specified key
|
|
132
|
+
/// - Parameters:
|
|
133
|
+
/// - key: The unique identifier
|
|
134
|
+
/// - value: The value to be stored
|
|
135
|
+
public func `set`<T>(_ key: String, value: T) throws where T: Encodable {
|
|
136
|
+
try backend.set(key, value: value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Deletes the value for the specified key
|
|
140
|
+
public func `delete`(_ key: String) throws {
|
|
141
|
+
try backend.delete(key)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Convenience for accessing and modifying values in the store without calling ``get(_:as:)``, ``set(_:value:)``, or ``delete(_:)``
|
|
145
|
+
/// - Parameters:
|
|
146
|
+
/// - key: The unique identifier for the value to access or modify
|
|
147
|
+
/// - type: The type the value is stored as
|
|
148
|
+
///
|
|
149
|
+
/// This method is only really necessary when accessing a key and the type cannot be inferred from it's context.
|
|
150
|
+
/// ```swift
|
|
151
|
+
/// let store = KeyValueStore.standard
|
|
152
|
+
///
|
|
153
|
+
/// // Get
|
|
154
|
+
/// let value = store["key", as: String.self]
|
|
155
|
+
///
|
|
156
|
+
/// // If the type can be inferred then it may be omitted
|
|
157
|
+
/// let value: String? = store["key"]
|
|
158
|
+
/// let value = store["key"] as String?
|
|
159
|
+
/// let value = store["key"] ?? "default"
|
|
160
|
+
///
|
|
161
|
+
/// // Delete
|
|
162
|
+
/// store["key", as: String.self] = nil
|
|
163
|
+
/// store["key"] = nil as String?
|
|
164
|
+
/// ```
|
|
165
|
+
public subscript<T> (_ key: String, as type: T.Type = T.self) -> T? where T: Codable {
|
|
166
|
+
get { try? self.get(key) }
|
|
167
|
+
set {
|
|
168
|
+
if let newValue {
|
|
169
|
+
try? self.set(key, value: newValue)
|
|
170
|
+
} else {
|
|
171
|
+
try? self.delete(key)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// A shared persistent instance of ``KeyValueStore``
|
|
177
|
+
public static let standard = KeyValueStore(type: .persistent(suiteName: "standard"))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public protocol KeyValueStoreBackend {
|
|
181
|
+
func `get`<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable
|
|
182
|
+
func `set`<T>(_ key: String, value: T) throws where T: Encodable
|
|
183
|
+
func `delete`(_ key: String) throws
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private class FileStore: KeyValueStoreBackend {
|
|
187
|
+
private let cache = ConcurrentDictionary<Data>()
|
|
188
|
+
private let decoder = JSONDecoder()
|
|
189
|
+
private let encoder = JSONEncoder()
|
|
190
|
+
private let baseUrl: URL
|
|
191
|
+
|
|
192
|
+
private init(baseUrl: URL) {
|
|
193
|
+
self.baseUrl = baseUrl
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func get<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable {
|
|
197
|
+
if let cached = cache[key],
|
|
198
|
+
let value = try? decoder.decode(type, from: cached) {
|
|
199
|
+
return value
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let fileCacheLocation = baseUrl.appendingPathComponent(key)
|
|
203
|
+
guard FileManager.default.fileExists(atPath: fileCacheLocation.path) else { return nil }
|
|
204
|
+
|
|
205
|
+
let data = try Data(contentsOf: fileCacheLocation)
|
|
206
|
+
let decoded = try decoder.decode(type, from: data)
|
|
207
|
+
|
|
208
|
+
cache[key] = data
|
|
209
|
+
return decoded
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func set<T>(_ key: String, value: T) throws where T: Encodable {
|
|
213
|
+
let encoded = try encoder.encode(value)
|
|
214
|
+
try encoded.write(to: url(for: key), options: .atomic)
|
|
215
|
+
cache[key] = encoded
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func delete(_ key: String) throws {
|
|
219
|
+
cache[key] = nil
|
|
220
|
+
try FileManager.default.removeItem(at: url(for: key))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private func url(for key: String) -> URL {
|
|
224
|
+
baseUrl.appendingPathComponent(key)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static let instances = ConcurrentDictionary<FileStore>()
|
|
228
|
+
|
|
229
|
+
// This ensures we essentially have singletons for accessing file based resources
|
|
230
|
+
// so we don't have a scenario where two separate instances may be writing to
|
|
231
|
+
// the same files.
|
|
232
|
+
static func with(name: String) -> FileStore {
|
|
233
|
+
if let existing = instances[name] { return existing }
|
|
234
|
+
guard let library = try? FileManager
|
|
235
|
+
.default
|
|
236
|
+
.url(
|
|
237
|
+
for: .libraryDirectory,
|
|
238
|
+
in: .userDomainMask,
|
|
239
|
+
appropriateFor: nil,
|
|
240
|
+
create: true
|
|
241
|
+
)
|
|
242
|
+
else { fatalError("⚡️ ❌ Library URL unable to be accessed or created by the current application. This is an impossible state.") }
|
|
243
|
+
|
|
244
|
+
let url = library.appendingPathComponent("kvstore").appendingPathComponent(name)
|
|
245
|
+
|
|
246
|
+
// Create the folder if it doesn't exist. This should never throw for the current base directory, so we ignore the exception.
|
|
247
|
+
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
|
248
|
+
|
|
249
|
+
let new = FileStore(baseUrl: url)
|
|
250
|
+
instances[name] = new
|
|
251
|
+
return new
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private class InMemoryStore: KeyValueStoreBackend {
|
|
256
|
+
private let storage = ConcurrentDictionary<Data>()
|
|
257
|
+
private let decoder = JSONDecoder()
|
|
258
|
+
private let encoder = JSONEncoder()
|
|
259
|
+
|
|
260
|
+
func get<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable {
|
|
261
|
+
guard let data = storage[key] else { return nil }
|
|
262
|
+
return try decoder.decode(type, from: data)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func set<T>(_ key: String, value: T) throws where T: Encodable {
|
|
266
|
+
let data = try encoder.encode(value)
|
|
267
|
+
storage[key] = data
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func delete(_ key: String) {
|
|
271
|
+
storage[key] = nil
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private class ConcurrentDictionary<Value> {
|
|
276
|
+
private var storage: [String: Value]
|
|
277
|
+
private let lock = NSLock()
|
|
278
|
+
|
|
279
|
+
init(_ initial: [String: Value] = [:]) {
|
|
280
|
+
storage = initial
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
subscript (_ key: String) -> Value? {
|
|
284
|
+
get {
|
|
285
|
+
lock.withLock {
|
|
286
|
+
storage[key]
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
set {
|
|
291
|
+
lock.withLock {
|
|
292
|
+
storage[key] = newValue
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -51,7 +51,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
51
51
|
return nil
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
public func getRequestDataAsMultipartFormData(_ data: JSValue) throws -> Data {
|
|
54
|
+
public func getRequestDataAsMultipartFormData(_ data: JSValue, _ contentType: String) throws -> Data {
|
|
55
55
|
guard let obj = data as? JSObject else {
|
|
56
56
|
// Throw, other data types explicitly not supported.
|
|
57
57
|
throw CapacitorUrlRequestError.serializationError("[ data ] argument for request with content-type [ application/x-www-form-urlencoded ] may only be a plain javascript object")
|
|
@@ -62,11 +62,12 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
var data = Data()
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
var boundary = UUID().uuidString
|
|
66
|
+
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
|
|
67
|
+
boundary = contentBoundary
|
|
68
|
+
} else {
|
|
69
|
+
overrideContentType(boundary)
|
|
70
|
+
}
|
|
70
71
|
strings.forEach { key, value in
|
|
71
72
|
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
|
|
72
73
|
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
|
@@ -77,6 +78,12 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
77
78
|
return data
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
private func overrideContentType(_ boundary: String) {
|
|
82
|
+
let contentType = "multipart/form-data; boundary=\(boundary)"
|
|
83
|
+
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
|
|
84
|
+
headers["Content-Type"] = contentType
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
public func getRequestDataAsString(_ data: JSValue) throws -> Data {
|
|
81
88
|
guard let stringData = data as? String else {
|
|
82
89
|
throw CapacitorUrlRequestError.serializationError("[ data ] argument could not be parsed as string")
|
|
@@ -93,20 +100,18 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
93
100
|
return normalized[index.lowercased()]
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
func getRequestDataFromFormData(_ data: JSValue) throws -> Data? {
|
|
103
|
+
public func getRequestDataFromFormData(_ data: JSValue, _ contentType: String) throws -> Data? {
|
|
97
104
|
guard let list = data as? JSArray else {
|
|
98
105
|
// Throw, other data types explicitly not supported.
|
|
99
106
|
throw CapacitorUrlRequestError.serializationError("Data must be an array for FormData")
|
|
100
107
|
}
|
|
101
|
-
|
|
102
108
|
var data = Data()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
var boundary = UUID().uuidString
|
|
110
|
+
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
|
|
111
|
+
boundary = contentBoundary
|
|
112
|
+
} else {
|
|
113
|
+
overrideContentType(boundary)
|
|
114
|
+
}
|
|
110
115
|
for entry in list {
|
|
111
116
|
guard let item = entry as? [String: String] else {
|
|
112
117
|
throw CapacitorUrlRequestError.serializationError("Data must be an array for FormData")
|
|
@@ -120,7 +125,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
120
125
|
let fileName = item["fileName"]
|
|
121
126
|
let fileContentType = item["contentType"]
|
|
122
127
|
|
|
123
|
-
data.append("
|
|
128
|
+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
|
|
124
129
|
data.append("Content-Disposition: form-data; name=\"\(key!)\"; filename=\"\(fileName!)\"\r\n".data(using: .utf8)!)
|
|
125
130
|
data.append("Content-Type: \(fileContentType!)\r\n".data(using: .utf8)!)
|
|
126
131
|
data.append("Content-Transfer-Encoding: binary\r\n".data(using: .utf8)!)
|
|
@@ -130,16 +135,14 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
130
135
|
|
|
131
136
|
data.append("\r\n".data(using: .utf8)!)
|
|
132
137
|
} else if type == "string" {
|
|
133
|
-
data.append("
|
|
138
|
+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
|
|
134
139
|
data.append("Content-Disposition: form-data; name=\"\(key!)\"\r\n".data(using: .utf8)!)
|
|
135
140
|
data.append("\r\n".data(using: .utf8)!)
|
|
136
141
|
data.append(value.data(using: .utf8)!)
|
|
137
142
|
data.append("\r\n".data(using: .utf8)!)
|
|
138
143
|
}
|
|
139
|
-
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
|
145
|
+
data.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
|
143
146
|
|
|
144
147
|
return data
|
|
145
148
|
}
|
|
@@ -151,7 +154,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
151
154
|
}
|
|
152
155
|
return Data(base64Encoded: stringData)
|
|
153
156
|
} else if dataType == "formData" {
|
|
154
|
-
return try getRequestDataFromFormData(body)
|
|
157
|
+
return try getRequestDataFromFormData(body, contentType)
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
// If data can be parsed directly as a string, return that without processing.
|
|
@@ -162,7 +165,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
|
|
|
162
165
|
} else if contentType.contains("application/x-www-form-urlencoded") {
|
|
163
166
|
return try getRequestDataAsFormUrlEncoded(body)
|
|
164
167
|
} else if contentType.contains("multipart/form-data") {
|
|
165
|
-
return try getRequestDataAsMultipartFormData(body)
|
|
168
|
+
return try getRequestDataAsMultipartFormData(body, contentType)
|
|
166
169
|
} else {
|
|
167
170
|
throw CapacitorUrlRequestError.serializationError("[ data ] argument could not be parsed for content type [ \(contentType) ]")
|
|
168
171
|
}
|
|
@@ -24,6 +24,7 @@ CAP_PLUGIN(CAPConsolePlugin, "Console",
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
CAP_PLUGIN(CAPWebViewPlugin, "WebView",
|
|
27
|
+
CAP_PLUGIN_METHOD(setServerAssetPath, CAPPluginReturnPromise);
|
|
27
28
|
CAP_PLUGIN_METHOD(setServerBasePath, CAPPluginReturnPromise);
|
|
28
29
|
CAP_PLUGIN_METHOD(getServerBasePath, CAPPluginReturnPromise);
|
|
29
30
|
CAP_PLUGIN_METHOD(persistServerBasePath, CAPPluginReturnPromise);
|
|
@@ -37,6 +37,18 @@ func tryParseJson(_ data: Data) -> Any {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/// Helper to convert the headers dictionary to lower case keys. This allows case-insensitive querying in the bridge javascript.
|
|
41
|
+
/// - Parameters:
|
|
42
|
+
/// - headers: The headers as dictionary. The type is unspecific because the incoming headers are coming from the
|
|
43
|
+
/// allHeaderFields property of the HttpResponse.
|
|
44
|
+
/// - Returns: The modified headers dictionary with lowercase keys
|
|
45
|
+
private func lowerCaseHeaderDictionary(_ headers: [AnyHashable: Any]) -> [String: Any] {
|
|
46
|
+
// Lowercases the key of the headers dictionary.
|
|
47
|
+
return Dictionary(uniqueKeysWithValues: headers.map({ (key: AnyHashable, value: Any) in
|
|
48
|
+
return (String(describing: key).lowercased(), value)
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
open class HttpRequestHandler {
|
|
41
53
|
open class CapacitorHttpRequestBuilder {
|
|
42
54
|
public var url: URL?
|
|
@@ -121,7 +133,14 @@ open class HttpRequestHandler {
|
|
|
121
133
|
var output = [:] as [String: Any]
|
|
122
134
|
|
|
123
135
|
output["status"] = response.statusCode
|
|
124
|
-
|
|
136
|
+
|
|
137
|
+
// HTTP Headers are case insensitive. The allHeaderFields dictionary returned by Apple Foundation Code has its keys capitalized.
|
|
138
|
+
// According to the documentation at https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
|
|
139
|
+
// "HTTP headers are case insensitive. To simplify your code, URL Loading System canonicalizes certain header field names into
|
|
140
|
+
// their standard form. For example, if the server sends a content-length header, it’s automatically adjusted to be Content-Length."
|
|
141
|
+
// To handle the case insevitivy, we are converting the header keys to lower case here. When querying for headers in the native bridge,
|
|
142
|
+
// we are lowercasing the key as well.
|
|
143
|
+
output["headers"] = lowerCaseHeaderDictionary(response.allHeaderFields)
|
|
125
144
|
output["url"] = response.url?.absoluteString
|
|
126
145
|
|
|
127
146
|
guard let data = data else {
|
|
@@ -3,6 +3,13 @@ import Foundation
|
|
|
3
3
|
@objc(CAPWebViewPlugin)
|
|
4
4
|
public class CAPWebViewPlugin: CAPPlugin {
|
|
5
5
|
|
|
6
|
+
@objc func setServerAssetPath(_ call: CAPPluginCall) {
|
|
7
|
+
if let path = call.getString("path"), let viewController = bridge?.viewController as? CAPBridgeViewController {
|
|
8
|
+
viewController.setServerBasePath(path: Bundle.main.url(forResource: path, withExtension: nil)?.path ?? path)
|
|
9
|
+
call.resolve()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
@objc func setServerBasePath(_ call: CAPPluginCall) {
|
|
7
14
|
if let path = call.getString("path"), let viewController = bridge?.viewController as? CAPBridgeViewController {
|
|
8
15
|
viewController.setServerBasePath(path: path)
|
|
@@ -22,8 +29,7 @@ public class CAPWebViewPlugin: CAPPlugin {
|
|
|
22
29
|
@objc func persistServerBasePath(_ call: CAPPluginCall) {
|
|
23
30
|
if let viewController = bridge?.viewController as? CAPBridgeViewController {
|
|
24
31
|
let path = viewController.getServerBasePath()
|
|
25
|
-
|
|
26
|
-
defaults.set(path, forKey: "serverBasePath")
|
|
32
|
+
KeyValueStore.standard["serverBasePath"] = path
|
|
27
33
|
call.resolve()
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
6
|
+
<array/>
|
|
7
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
10
|
+
<array/>
|
|
11
|
+
<key>NSPrivacyTracking</key>
|
|
12
|
+
<false/>
|
|
13
|
+
</dict>
|
|
14
|
+
</plist>
|
|
@@ -20,10 +20,25 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
|
|
|
20
20
|
self.serverUrl = serverUrl
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
private func isUsingLiveReload(_ localUrl: URL) -> Bool {
|
|
24
|
+
return self.serverUrl != nil && self.serverUrl?.scheme != localUrl.scheme
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
open func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
|
24
28
|
let startPath: String
|
|
25
29
|
let url = urlSchemeTask.request.url!
|
|
26
30
|
let stringToLoad = url.path
|
|
31
|
+
let localUrl = URL.init(string: url.absoluteString)!
|
|
32
|
+
|
|
33
|
+
if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
|
|
34
|
+
handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if url.path.starts(with: CapacitorBridge.httpsInterceptorStartIdentifier) {
|
|
39
|
+
handleCapacitorHttpRequest(urlSchemeTask, localUrl, true)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
27
42
|
|
|
28
43
|
if stringToLoad.starts(with: CapacitorBridge.fileStartIdentifier) {
|
|
29
44
|
startPath = stringToLoad.replacingOccurrences(of: CapacitorBridge.fileStartIdentifier, with: "")
|
|
@@ -31,7 +46,6 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
|
|
|
31
46
|
startPath = router.route(for: stringToLoad)
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
let localUrl = URL.init(string: url.absoluteString)!
|
|
35
49
|
let fileUrl = URL.init(fileURLWithPath: startPath)
|
|
36
50
|
|
|
37
51
|
do {
|
|
@@ -43,9 +57,9 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
|
|
|
43
57
|
]
|
|
44
58
|
|
|
45
59
|
// if using live reload, then set CORS headers
|
|
46
|
-
if
|
|
60
|
+
if isUsingLiveReload(localUrl) {
|
|
47
61
|
headers["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString
|
|
48
|
-
headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
|
62
|
+
headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE"
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
if let rangeString = urlSchemeTask.request.value(forHTTPHeaderField: "Range"),
|
|
@@ -121,6 +135,68 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
|
|
|
121
135
|
return false
|
|
122
136
|
}
|
|
123
137
|
|
|
138
|
+
func handleCapacitorHttpRequest(_ urlSchemeTask: WKURLSchemeTask, _ localUrl: URL, _ isHttpsRequest: Bool) {
|
|
139
|
+
var urlRequest = urlSchemeTask.request
|
|
140
|
+
guard let url = urlRequest.url else { return }
|
|
141
|
+
var targetUrl = url.absoluteString
|
|
142
|
+
.replacingOccurrences(of: CapacitorBridge.httpInterceptorStartIdentifier, with: "")
|
|
143
|
+
.replacingOccurrences(of: CapacitorBridge.httpsInterceptorStartIdentifier, with: "")
|
|
144
|
+
// Only replace first occurrence of the scheme
|
|
145
|
+
if let range = targetUrl.range(of: localUrl.scheme ?? InstanceDescriptorDefaults.scheme) {
|
|
146
|
+
targetUrl = targetUrl.replacingCharacters(in: range, with: isHttpsRequest ? "https" : "http")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Only replace first occurrence of the hostname
|
|
150
|
+
if let range = targetUrl.range(of: (localUrl.host ?? InstanceDescriptorDefaults.hostname) + "/") {
|
|
151
|
+
targetUrl = targetUrl.replacingCharacters(in: range, with: "")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
urlRequest.url = URL(string: targetUrl.removingPercentEncoding ?? targetUrl)
|
|
155
|
+
|
|
156
|
+
let urlSession = URLSession.shared
|
|
157
|
+
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
|
|
158
|
+
if let error = error {
|
|
159
|
+
urlSchemeTask.didFailWithError(error)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if let response = response as? HTTPURLResponse {
|
|
164
|
+
let existingHeaders = response.allHeaderFields
|
|
165
|
+
var newHeaders: [AnyHashable: Any] = [:]
|
|
166
|
+
|
|
167
|
+
// if using live reload, then set CORS headers
|
|
168
|
+
if self.isUsingLiveReload(url) {
|
|
169
|
+
newHeaders = [
|
|
170
|
+
"Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
|
|
171
|
+
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] {
|
|
176
|
+
|
|
177
|
+
if let responseUrl = response.url {
|
|
178
|
+
if let modifiedResponse = HTTPURLResponse(
|
|
179
|
+
url: responseUrl,
|
|
180
|
+
statusCode: response.statusCode,
|
|
181
|
+
httpVersion: nil,
|
|
182
|
+
headerFields: mergedHeaders
|
|
183
|
+
) {
|
|
184
|
+
urlSchemeTask.didReceive(modifiedResponse)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if let data = data {
|
|
189
|
+
urlSchemeTask.didReceive(data)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
urlSchemeTask.didFinish()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
task.resume()
|
|
198
|
+
}
|
|
199
|
+
|
|
124
200
|
public let mimeTypes = [
|
|
125
201
|
"aaf": "application/octet-stream",
|
|
126
202
|
"aca": "application/octet-stream",
|
|
@@ -111,13 +111,9 @@ var nativeBridge = (function (exports) {
|
|
|
111
111
|
}
|
|
112
112
|
else if (body instanceof FormData) {
|
|
113
113
|
const formData = await convertFormData(body);
|
|
114
|
-
const boundary = `${Date.now()}`;
|
|
115
114
|
return {
|
|
116
115
|
data: formData,
|
|
117
116
|
type: 'formData',
|
|
118
|
-
headers: {
|
|
119
|
-
'Content-Type': `multipart/form-data; boundary=--${boundary}`,
|
|
120
|
-
},
|
|
121
117
|
};
|
|
122
118
|
}
|
|
123
119
|
else if (body instanceof File) {
|
|
@@ -130,6 +126,26 @@ var nativeBridge = (function (exports) {
|
|
|
130
126
|
}
|
|
131
127
|
return { data: body, type: 'json' };
|
|
132
128
|
};
|
|
129
|
+
const CAPACITOR_HTTP_INTERCEPTOR = '/_capacitor_http_interceptor_';
|
|
130
|
+
const CAPACITOR_HTTPS_INTERCEPTOR = '/_capacitor_https_interceptor_';
|
|
131
|
+
// TODO: export as Cap function
|
|
132
|
+
const isRelativeOrProxyUrl = (url) => !url ||
|
|
133
|
+
!(url.startsWith('http:') || url.startsWith('https:')) ||
|
|
134
|
+
url.indexOf(CAPACITOR_HTTP_INTERCEPTOR) > -1 ||
|
|
135
|
+
url.indexOf(CAPACITOR_HTTPS_INTERCEPTOR) > -1;
|
|
136
|
+
// TODO: export as Cap function
|
|
137
|
+
const createProxyUrl = (url, win) => {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
if (isRelativeOrProxyUrl(url))
|
|
140
|
+
return url;
|
|
141
|
+
const proxyUrl = new URL(url);
|
|
142
|
+
const bridgeUrl = new URL((_b = (_a = win.Capacitor) === null || _a === void 0 ? void 0 : _a.getServerUrl()) !== null && _b !== void 0 ? _b : '');
|
|
143
|
+
const isHttps = proxyUrl.protocol === 'https:';
|
|
144
|
+
bridgeUrl.search = proxyUrl.search;
|
|
145
|
+
bridgeUrl.hash = proxyUrl.hash;
|
|
146
|
+
bridgeUrl.pathname = `${isHttps ? CAPACITOR_HTTPS_INTERCEPTOR : CAPACITOR_HTTP_INTERCEPTOR}/${encodeURIComponent(proxyUrl.host)}${proxyUrl.pathname}`;
|
|
147
|
+
return bridgeUrl.toString();
|
|
148
|
+
};
|
|
133
149
|
const initBridge = (w) => {
|
|
134
150
|
const getPlatformId = (win) => {
|
|
135
151
|
var _a, _b;
|
|
@@ -279,6 +295,10 @@ var nativeBridge = (function (exports) {
|
|
|
279
295
|
callback(result.path);
|
|
280
296
|
});
|
|
281
297
|
};
|
|
298
|
+
IonicWebView.setServerAssetPath = (path) => {
|
|
299
|
+
var _a;
|
|
300
|
+
(_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerAssetPath({ path });
|
|
301
|
+
};
|
|
282
302
|
IonicWebView.setServerBasePath = (path) => {
|
|
283
303
|
var _a;
|
|
284
304
|
(_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerBasePath({ path });
|
|
@@ -355,15 +375,15 @@ var nativeBridge = (function (exports) {
|
|
|
355
375
|
typeof c.dir === 'function');
|
|
356
376
|
};
|
|
357
377
|
const serializeConsoleMessage = (msg) => {
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
try {
|
|
379
|
+
if (typeof msg === 'object') {
|
|
360
380
|
msg = JSON.stringify(msg);
|
|
361
381
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
382
|
+
return String(msg);
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
return '';
|
|
365
386
|
}
|
|
366
|
-
return String(msg);
|
|
367
387
|
};
|
|
368
388
|
const platform = getPlatformId(win);
|
|
369
389
|
if (platform == 'android' || platform == 'ios') {
|
|
@@ -472,6 +492,19 @@ var nativeBridge = (function (exports) {
|
|
|
472
492
|
if (request.url.startsWith(`${cap.getServerUrl()}/`)) {
|
|
473
493
|
return win.CapacitorWebFetch(resource, options);
|
|
474
494
|
}
|
|
495
|
+
if (!(options === null || options === void 0 ? void 0 : options.method) ||
|
|
496
|
+
options.method.toLocaleUpperCase() === 'GET' ||
|
|
497
|
+
options.method.toLocaleUpperCase() === 'HEAD' ||
|
|
498
|
+
options.method.toLocaleUpperCase() === 'OPTIONS' ||
|
|
499
|
+
options.method.toLocaleUpperCase() === 'TRACE') {
|
|
500
|
+
if (typeof resource === 'string') {
|
|
501
|
+
return await win.CapacitorWebFetch(createProxyUrl(resource, win), options);
|
|
502
|
+
}
|
|
503
|
+
else if (resource instanceof Request) {
|
|
504
|
+
const modifiedRequest = new Request(createProxyUrl(resource.url, win), resource);
|
|
505
|
+
return await win.CapacitorWebFetch(modifiedRequest, options);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
475
508
|
const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
|
|
476
509
|
console.time(tag);
|
|
477
510
|
try {
|
|
@@ -541,12 +574,11 @@ var nativeBridge = (function (exports) {
|
|
|
541
574
|
});
|
|
542
575
|
xhr.readyState = 0;
|
|
543
576
|
const prototype = win.CapacitorWebXMLHttpRequest.prototype;
|
|
544
|
-
const isRelativeURL = (url) => !url || !(url.startsWith('http:') || url.startsWith('https:'));
|
|
545
577
|
const isProgressEventAvailable = () => typeof ProgressEvent !== 'undefined' &&
|
|
546
578
|
ProgressEvent.prototype instanceof Event;
|
|
547
579
|
// XHR patch abort
|
|
548
580
|
prototype.abort = function () {
|
|
549
|
-
if (
|
|
581
|
+
if (isRelativeOrProxyUrl(this._url)) {
|
|
550
582
|
return win.CapacitorWebXMLHttpRequest.abort.call(this);
|
|
551
583
|
}
|
|
552
584
|
this.readyState = 0;
|
|
@@ -557,10 +589,18 @@ var nativeBridge = (function (exports) {
|
|
|
557
589
|
};
|
|
558
590
|
// XHR patch open
|
|
559
591
|
prototype.open = function (method, url) {
|
|
592
|
+
this._method = method.toLocaleUpperCase();
|
|
560
593
|
this._url = url;
|
|
561
|
-
this._method
|
|
562
|
-
|
|
563
|
-
|
|
594
|
+
if (!this._method ||
|
|
595
|
+
this._method === 'GET' ||
|
|
596
|
+
this._method === 'HEAD' ||
|
|
597
|
+
this._method === 'OPTIONS' ||
|
|
598
|
+
this._method === 'TRACE') {
|
|
599
|
+
if (isRelativeOrProxyUrl(url)) {
|
|
600
|
+
return win.CapacitorWebXMLHttpRequest.open.call(this, method, url);
|
|
601
|
+
}
|
|
602
|
+
this._url = createProxyUrl(this._url, win);
|
|
603
|
+
return win.CapacitorWebXMLHttpRequest.open.call(this, method, this._url);
|
|
564
604
|
}
|
|
565
605
|
setTimeout(() => {
|
|
566
606
|
this.dispatchEvent(new Event('loadstart'));
|
|
@@ -569,14 +609,14 @@ var nativeBridge = (function (exports) {
|
|
|
569
609
|
};
|
|
570
610
|
// XHR patch set request header
|
|
571
611
|
prototype.setRequestHeader = function (header, value) {
|
|
572
|
-
if (
|
|
612
|
+
if (isRelativeOrProxyUrl(this._url)) {
|
|
573
613
|
return win.CapacitorWebXMLHttpRequest.setRequestHeader.call(this, header, value);
|
|
574
614
|
}
|
|
575
615
|
this._headers[header] = value;
|
|
576
616
|
};
|
|
577
617
|
// XHR patch send
|
|
578
618
|
prototype.send = function (body) {
|
|
579
|
-
if (
|
|
619
|
+
if (isRelativeOrProxyUrl(this._url)) {
|
|
580
620
|
return win.CapacitorWebXMLHttpRequest.send.call(this, body);
|
|
581
621
|
}
|
|
582
622
|
const tag = `CapacitorHttp XMLHttpRequest ${Date.now()} ${this._url}`;
|
|
@@ -638,7 +678,8 @@ var nativeBridge = (function (exports) {
|
|
|
638
678
|
else {
|
|
639
679
|
this.response = nativeResponse.data;
|
|
640
680
|
}
|
|
641
|
-
this.responseText = ((_a = nativeResponse.headers['Content-Type']
|
|
681
|
+
this.responseText = ((_a = (nativeResponse.headers['Content-Type'] ||
|
|
682
|
+
nativeResponse.headers['content-type'])) === null || _a === void 0 ? void 0 : _a.startsWith('application/json'))
|
|
642
683
|
? JSON.stringify(nativeResponse.data)
|
|
643
684
|
: nativeResponse.data;
|
|
644
685
|
this.responseURL = nativeResponse.url;
|
|
@@ -695,7 +736,7 @@ var nativeBridge = (function (exports) {
|
|
|
695
736
|
};
|
|
696
737
|
// XHR patch getAllResponseHeaders
|
|
697
738
|
prototype.getAllResponseHeaders = function () {
|
|
698
|
-
if (
|
|
739
|
+
if (isRelativeOrProxyUrl(this._url)) {
|
|
699
740
|
return win.CapacitorWebXMLHttpRequest.getAllResponseHeaders.call(this);
|
|
700
741
|
}
|
|
701
742
|
let returnString = '';
|
|
@@ -708,7 +749,7 @@ var nativeBridge = (function (exports) {
|
|
|
708
749
|
};
|
|
709
750
|
// XHR patch getResponseHeader
|
|
710
751
|
prototype.getResponseHeader = function (name) {
|
|
711
|
-
if (
|
|
752
|
+
if (isRelativeOrProxyUrl(this._url)) {
|
|
712
753
|
return win.CapacitorWebXMLHttpRequest.getResponseHeader.call(this, name);
|
|
713
754
|
}
|
|
714
755
|
return this._headers[name];
|
package/Capacitor.podspec
CHANGED
|
@@ -16,10 +16,9 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.ios.deployment_target = '13.0'
|
|
17
17
|
s.authors = { 'Ionic Team' => 'hi@ionicframework.com' }
|
|
18
18
|
s.source = { git: 'https://github.com/ionic-team/capacitor.git', tag: package['version'] }
|
|
19
|
-
s.source_files = "#{prefix}Capacitor/Capacitor
|
|
20
|
-
"#{prefix}Capacitor/Capacitor/Plugins/*.{swift,h,m}", "#{prefix}Capacitor/Capacitor/Plugins/**/*.{swift,h,m}"
|
|
19
|
+
s.source_files = "#{prefix}Capacitor/Capacitor/**/*.{swift,h,m}"
|
|
21
20
|
s.module_map = "#{prefix}Capacitor/Capacitor/Capacitor.modulemap"
|
|
22
|
-
s.resources = ["#{prefix}Capacitor/Capacitor/assets/native-bridge.js"]
|
|
21
|
+
s.resources = ["#{prefix}Capacitor/Capacitor/assets/native-bridge.js", "#{prefix}Capacitor/Capacitor/PrivacyInfo.xcprivacy"]
|
|
23
22
|
s.dependency 'CapacitorCordova'
|
|
24
23
|
s.swift_version = '5.1'
|
|
25
24
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
6
|
+
<array/>
|
|
7
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
10
|
+
<array/>
|
|
11
|
+
<key>NSPrivacyTracking</key>
|
|
12
|
+
<false/>
|
|
13
|
+
</dict>
|
|
14
|
+
</plist>
|
package/CapacitorCordova.podspec
CHANGED
|
@@ -20,6 +20,7 @@ Pod::Spec.new do |s|
|
|
|
20
20
|
s.public_header_files = "#{prefix}CapacitorCordova/CapacitorCordova/Classes/Public/*.h",
|
|
21
21
|
"#{prefix}CapacitorCordova/CapacitorCordova/CapacitorCordova.h"
|
|
22
22
|
s.module_map = "#{prefix}CapacitorCordova/CapacitorCordova/CapacitorCordova.modulemap"
|
|
23
|
+
s.resources = ["#{prefix}CapacitorCordova/CapacitorCordova/PrivacyInfo.xcprivacy"]
|
|
23
24
|
s.requires_arc = true
|
|
24
25
|
s.framework = 'WebKit'
|
|
25
26
|
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capacitor/ios",
|
|
3
|
-
"version": "6.0.0-
|
|
3
|
+
"version": "6.0.0-dev-8046d907.0",
|
|
4
4
|
"description": "Capacitor: Cross-platform apps with JavaScript and the web",
|
|
5
5
|
"homepage": "https://capacitorjs.com",
|
|
6
6
|
"author": "Ionic Team <hi@ionic.io> (https://ionic.io)",
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"xc:build:xcframework": "scripts/build.sh xcframework"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@capacitor/core": "^6.0.0-
|
|
29
|
+
"@capacitor/core": "^6.0.0-rc.2"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
|
+
"gitHead": "8046d907d2d2c0779316adbcabda426acee2dfc3"
|
|
34
35
|
}
|