expo-modules-core 3.0.21 → 3.0.23
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/CHANGELOG.md +22 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/kotlin/edgeToEdge/EdgeToEdgePackage.kt +69 -0
- package/ios/AppDelegates/ExpoAppDelegateSubscriberManager.swift +512 -0
- package/ios/Core/DynamicTypes/DynamicEncodableType.swift +47 -0
- package/ios/Core/DynamicTypes/DynamicType.swift +10 -42
- package/ios/Core/JSValueEncoder.swift +201 -0
- package/ios/JSI/EXJSIConversions.mm +2 -1
- package/ios/JSI/EXJavaScriptValue.h +1 -0
- package/ios/JSI/EXJavaScriptValue.mm +5 -1
- package/ios/Tests/DynamicTypeSpec.swift +70 -0
- package/ios/Tests/FunctionSpec.swift +17 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,28 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 3.0.23 — 2025-10-28
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add `applicationDidReceiveMemoryWarning` subscribing to ExpoAppDelegateSubscriberManager ([#40504](https://github.com/expo/expo/pull/40504) by [@szydlovsky](https://github.com/szydlovsky))
|
|
18
|
+
- [iOS] Swift's `Encodable` types can now be converted to JavaScript values. ([#40621](https://github.com/expo/expo/pull/40621) by [@tsapeta](https://github.com/tsapeta))
|
|
19
|
+
|
|
20
|
+
### 🐛 Bug fixes
|
|
21
|
+
|
|
22
|
+
- [core] [iOS] Addresses a potential crash where `[NSString UTF8String]` could return `nil` for certain string inputs (non-English), leading to an attempt to dereference a null pointer during `jsi::String` creation. ([#40639](https://github.com/expo/expo/pull/40639) by [@mohammadamin16](https://github.com/mohammadamin16))
|
|
23
|
+
- [Android] Fix `androidNavigationBar.enforceContrast` app config property not working. ([#40263](https://github.com/expo/expo/pull/40263) by [@behenate](https://github.com/behenate))
|
|
24
|
+
|
|
25
|
+
### 💡 Others
|
|
26
|
+
|
|
27
|
+
- [iOS] Removed some runtime type checks for dynamic types. ([#40611](https://github.com/expo/expo/pull/40611) by [@tsapeta](https://github.com/tsapeta))
|
|
28
|
+
|
|
29
|
+
## 3.0.22 — 2025-10-20
|
|
30
|
+
|
|
31
|
+
### 🎉 New features
|
|
32
|
+
|
|
33
|
+
- [iOS] Introduce ExpoAppDelegateSubscriberManager class ([#40008](https://github.com/expo/expo/pull/40008) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
34
|
+
|
|
13
35
|
## 3.0.21 — 2025-10-09
|
|
14
36
|
|
|
15
37
|
### 💡 Others
|
package/android/build.gradle
CHANGED
|
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
group = 'host.exp.exponent'
|
|
32
|
-
version = '3.0.
|
|
32
|
+
version = '3.0.23'
|
|
33
33
|
|
|
34
34
|
def isExpoModulesCoreTests = {
|
|
35
35
|
Gradle gradle = getGradle()
|
|
@@ -79,7 +79,7 @@ android {
|
|
|
79
79
|
defaultConfig {
|
|
80
80
|
consumerProguardFiles 'proguard-rules.pro'
|
|
81
81
|
versionCode 1
|
|
82
|
-
versionName "3.0.
|
|
82
|
+
versionName "3.0.23"
|
|
83
83
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
84
84
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
|
|
85
85
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package expo.modules.kotlin.edgeToEdge
|
|
2
|
+
|
|
3
|
+
import android.R
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.res.TypedArray
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import android.view.Window
|
|
11
|
+
import androidx.annotation.RequiresApi
|
|
12
|
+
import expo.modules.core.BasePackage
|
|
13
|
+
import expo.modules.core.interfaces.ReactActivityLifecycleListener
|
|
14
|
+
import kotlin.Unit
|
|
15
|
+
|
|
16
|
+
class EdgeToEdgePackage : BasePackage() {
|
|
17
|
+
override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener?>? {
|
|
18
|
+
return listOf(object : ReactActivityLifecycleListener {
|
|
19
|
+
override fun onCreate(activity: Activity?, savedInstanceState: Bundle?) {
|
|
20
|
+
val edgeToEdgeEnabled = invokeWindowUtilKtMethod<Boolean>("isEdgeToEdgeFeatureFlagOn") ?: true
|
|
21
|
+
|
|
22
|
+
if (edgeToEdgeEnabled) {
|
|
23
|
+
invokeWindowUtilKtMethod<Unit>("enableEdgeToEdge", Pair(Window::class.java, activity?.window))
|
|
24
|
+
|
|
25
|
+
// React-native sets `window.isNavigationBarContrastEnforced` to `true` in `WindowUtilKt.enableEdgeToEdge`.
|
|
26
|
+
// We have to set it back to the value defined in the app styles, which comes from our config plugin.
|
|
27
|
+
activity?.enforceNavigationBarContrastFromTheme()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@RequiresApi(Build.VERSION_CODES.Q)
|
|
35
|
+
private fun Activity.getEnforceContrastFromTheme(): Boolean {
|
|
36
|
+
val attrs = intArrayOf(R.attr.enforceNavigationBarContrast)
|
|
37
|
+
val typedArray: TypedArray = theme.obtainStyledAttributes(attrs)
|
|
38
|
+
|
|
39
|
+
return try {
|
|
40
|
+
typedArray.getBoolean(0, true)
|
|
41
|
+
} finally {
|
|
42
|
+
typedArray.recycle()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun Activity.enforceNavigationBarContrastFromTheme() {
|
|
47
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
48
|
+
window.isNavigationBarContrastEnforced = getEnforceContrastFromTheme()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private inline fun <reified T> invokeWindowUtilKtMethod(
|
|
53
|
+
methodName: String,
|
|
54
|
+
vararg args: Pair<Class<*>, Any?>
|
|
55
|
+
): T? {
|
|
56
|
+
val windowUtilClassName = "com.facebook.react.views.view.WindowUtilKt"
|
|
57
|
+
|
|
58
|
+
return runCatching {
|
|
59
|
+
val windowUtilKtClass = Class.forName(windowUtilClassName)
|
|
60
|
+
val parameterTypes = args.map { it.first }.toTypedArray()
|
|
61
|
+
val parameterValues = args.map { it.second }.toTypedArray()
|
|
62
|
+
val method = windowUtilKtClass.getDeclaredMethod(methodName, *parameterTypes)
|
|
63
|
+
|
|
64
|
+
method.isAccessible = true
|
|
65
|
+
method.invoke(null, *parameterValues) as? T
|
|
66
|
+
}.onFailure {
|
|
67
|
+
Log.e("EdgeToEdgePackage", "Failed to invoke '$methodName' on $windowUtilClassName", it)
|
|
68
|
+
}.getOrNull()
|
|
69
|
+
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import Dispatch
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
public class ExpoAppDelegateSubscriberManager: NSObject {
|
|
5
|
+
#if os(iOS) || os(tvOS)
|
|
6
|
+
|
|
7
|
+
@objc
|
|
8
|
+
public static func application(
|
|
9
|
+
_ application: UIApplication,
|
|
10
|
+
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
11
|
+
) -> Bool {
|
|
12
|
+
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
|
|
13
|
+
$0.responds(to: #selector(application(_:willFinishLaunchingWithOptions:)))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If we can't find a subscriber that implements `willFinishLaunchingWithOptions`, we will delegate the decision if we can handel the passed URL to
|
|
17
|
+
// the `didFinishLaunchingWithOptions` method by returning `true` here.
|
|
18
|
+
// You can read more about how iOS handles deep links here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application#discussion
|
|
19
|
+
if parsedSubscribers.isEmpty {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return parsedSubscribers.reduce(false) { result, subscriber in
|
|
24
|
+
return subscriber.application?(application, willFinishLaunchingWithOptions: launchOptions) ?? false || result
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc
|
|
29
|
+
public static func application(
|
|
30
|
+
_ application: UIApplication,
|
|
31
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
32
|
+
) -> Bool {
|
|
33
|
+
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
|
|
34
|
+
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL – we always return `true` anyway.
|
|
35
|
+
_ = subscriber.application?(application, didFinishLaunchingWithOptions: launchOptions)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#elseif os(macOS)
|
|
42
|
+
@objc
|
|
43
|
+
public static func applicationWillFinishLaunching(_ notification: Notification) {
|
|
44
|
+
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
|
|
45
|
+
$0.responds(to: #selector(applicationWillFinishLaunching(_:)))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parsedSubscribers.forEach { subscriber in
|
|
49
|
+
subscriber.applicationWillFinishLaunching?(notification)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@objc
|
|
54
|
+
public static func applicationDidFinishLaunching(_ notification: Notification) {
|
|
55
|
+
ExpoAppDelegateSubscriberRepository
|
|
56
|
+
.subscribers
|
|
57
|
+
.forEach { subscriber in
|
|
58
|
+
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL – we always return `true` anyway.
|
|
59
|
+
_ = subscriber.applicationDidFinishLaunching?(notification)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// TODO: - Configuring and Discarding Scenes
|
|
64
|
+
#endif
|
|
65
|
+
|
|
66
|
+
// MARK: - Responding to App Life-Cycle Events
|
|
67
|
+
|
|
68
|
+
#if os(iOS) || os(tvOS)
|
|
69
|
+
|
|
70
|
+
@objc
|
|
71
|
+
public static func applicationDidBecomeActive(_ application: UIApplication) {
|
|
72
|
+
ExpoAppDelegateSubscriberRepository
|
|
73
|
+
.subscribers
|
|
74
|
+
.forEach { $0.applicationDidBecomeActive?(application) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc
|
|
78
|
+
public static func applicationWillResignActive(_ application: UIApplication) {
|
|
79
|
+
ExpoAppDelegateSubscriberRepository
|
|
80
|
+
.subscribers
|
|
81
|
+
.forEach { $0.applicationWillResignActive?(application) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@objc
|
|
85
|
+
public static func applicationDidEnterBackground(_ application: UIApplication) {
|
|
86
|
+
ExpoAppDelegateSubscriberRepository
|
|
87
|
+
.subscribers
|
|
88
|
+
.forEach { $0.applicationDidEnterBackground?(application) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc
|
|
92
|
+
public static func applicationWillEnterForeground(_ application: UIApplication) {
|
|
93
|
+
ExpoAppDelegateSubscriberRepository
|
|
94
|
+
.subscribers
|
|
95
|
+
.forEach { $0.applicationWillEnterForeground?(application) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@objc
|
|
99
|
+
public static func applicationWillTerminate(_ application: UIApplication) {
|
|
100
|
+
ExpoAppDelegateSubscriberRepository
|
|
101
|
+
.subscribers
|
|
102
|
+
.forEach { $0.applicationWillTerminate?(application) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#elseif os(macOS)
|
|
106
|
+
@objc
|
|
107
|
+
public static func applicationDidBecomeActive(_ notification: Notification) {
|
|
108
|
+
ExpoAppDelegateSubscriberRepository
|
|
109
|
+
.subscribers
|
|
110
|
+
.forEach { $0.applicationDidBecomeActive?(notification) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@objc
|
|
114
|
+
public static func applicationWillResignActive(_ notification: Notification) {
|
|
115
|
+
ExpoAppDelegateSubscriberRepository
|
|
116
|
+
.subscribers
|
|
117
|
+
.forEach { $0.applicationWillResignActive?(notification) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@objc
|
|
121
|
+
public static func applicationDidHide(_ notification: Notification) {
|
|
122
|
+
ExpoAppDelegateSubscriberRepository
|
|
123
|
+
.subscribers
|
|
124
|
+
.forEach { $0.applicationDidHide?(notification) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@objc
|
|
128
|
+
public static func applicationWillUnhide(_ notification: Notification) {
|
|
129
|
+
ExpoAppDelegateSubscriberRepository
|
|
130
|
+
.subscribers
|
|
131
|
+
.forEach { $0.applicationWillUnhide?(notification) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@objc
|
|
135
|
+
public static func applicationWillTerminate(_ notification: Notification) {
|
|
136
|
+
ExpoAppDelegateSubscriberRepository
|
|
137
|
+
.subscribers
|
|
138
|
+
.forEach { $0.applicationWillTerminate?(notification) }
|
|
139
|
+
}
|
|
140
|
+
#endif
|
|
141
|
+
|
|
142
|
+
// MARK: - Responding to Environment Changes
|
|
143
|
+
|
|
144
|
+
#if os(iOS) || os(tvOS)
|
|
145
|
+
|
|
146
|
+
@objc
|
|
147
|
+
public static func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
|
148
|
+
ExpoAppDelegateSubscriberRepository
|
|
149
|
+
.subscribers
|
|
150
|
+
.forEach { $0.applicationDidReceiveMemoryWarning?(application) }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#endif
|
|
154
|
+
|
|
155
|
+
// TODO: - Managing App State Restoration
|
|
156
|
+
|
|
157
|
+
// MARK: - Downloading Data in the Background
|
|
158
|
+
|
|
159
|
+
#if os(iOS) || os(tvOS)
|
|
160
|
+
@objc
|
|
161
|
+
public static func application(
|
|
162
|
+
_ application: UIApplication,
|
|
163
|
+
handleEventsForBackgroundURLSession identifier: String,
|
|
164
|
+
completionHandler: @escaping () -> Void
|
|
165
|
+
) {
|
|
166
|
+
let selector = #selector(application(_:handleEventsForBackgroundURLSession:completionHandler:))
|
|
167
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
168
|
+
var subscribersLeft = subs.count
|
|
169
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.handleBackgroundEvents")
|
|
170
|
+
|
|
171
|
+
let handler = {
|
|
172
|
+
dispatchQueue.sync {
|
|
173
|
+
subscribersLeft -= 1
|
|
174
|
+
|
|
175
|
+
if subscribersLeft == 0 {
|
|
176
|
+
completionHandler()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if subs.isEmpty {
|
|
182
|
+
completionHandler()
|
|
183
|
+
} else {
|
|
184
|
+
subs.forEach {
|
|
185
|
+
$0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: handler)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#endif
|
|
191
|
+
|
|
192
|
+
// MARK: - Handling Remote Notification Registration
|
|
193
|
+
|
|
194
|
+
@objc
|
|
195
|
+
public static func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
196
|
+
ExpoAppDelegateSubscriberRepository
|
|
197
|
+
.subscribers
|
|
198
|
+
.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@objc
|
|
202
|
+
public static func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
203
|
+
ExpoAppDelegateSubscriberRepository
|
|
204
|
+
.subscribers
|
|
205
|
+
.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#if os(iOS) || os(tvOS)
|
|
209
|
+
@objc
|
|
210
|
+
public static func application(
|
|
211
|
+
_ application: UIApplication,
|
|
212
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
213
|
+
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
214
|
+
) {
|
|
215
|
+
let selector = #selector(application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
|
|
216
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
217
|
+
var subscribersLeft = subs.count
|
|
218
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.remoteNotification", qos: .userInteractive)
|
|
219
|
+
var failedCount = 0
|
|
220
|
+
var newDataCount = 0
|
|
221
|
+
|
|
222
|
+
let handler = { (result: UIBackgroundFetchResult) in
|
|
223
|
+
dispatchQueue.sync {
|
|
224
|
+
if result == .failed {
|
|
225
|
+
failedCount += 1
|
|
226
|
+
} else if result == .newData {
|
|
227
|
+
newDataCount += 1
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
subscribersLeft -= 1
|
|
231
|
+
|
|
232
|
+
if subscribersLeft == 0 {
|
|
233
|
+
if newDataCount > 0 {
|
|
234
|
+
completionHandler(.newData)
|
|
235
|
+
} else if failedCount > 0 {
|
|
236
|
+
completionHandler(.failed)
|
|
237
|
+
} else {
|
|
238
|
+
completionHandler(.noData)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if subs.isEmpty {
|
|
245
|
+
completionHandler(.noData)
|
|
246
|
+
} else {
|
|
247
|
+
subs.forEach { subscriber in
|
|
248
|
+
subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: handler)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#elseif os(macOS)
|
|
254
|
+
@objc
|
|
255
|
+
public static func application(
|
|
256
|
+
_ application: NSApplication,
|
|
257
|
+
didReceiveRemoteNotification userInfo: [String: Any]
|
|
258
|
+
) {
|
|
259
|
+
let selector = #selector(application(_:didReceiveRemoteNotification:))
|
|
260
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
261
|
+
|
|
262
|
+
subs.forEach { subscriber in
|
|
263
|
+
subscriber.application?(application, didReceiveRemoteNotification: userInfo)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
#endif
|
|
267
|
+
|
|
268
|
+
// MARK: - Continuing User Activity and Handling Quick Actions
|
|
269
|
+
|
|
270
|
+
@objc
|
|
271
|
+
public static func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
|
|
272
|
+
return ExpoAppDelegateSubscriberRepository
|
|
273
|
+
.subscribers
|
|
274
|
+
.reduce(false) { result, subscriber in
|
|
275
|
+
return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#if os(iOS) || os(tvOS)
|
|
280
|
+
@objc
|
|
281
|
+
public static func application(
|
|
282
|
+
_ application: UIApplication,
|
|
283
|
+
continue userActivity: NSUserActivity,
|
|
284
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
285
|
+
) -> Bool {
|
|
286
|
+
let selector = #selector(application(_:continue:restorationHandler:))
|
|
287
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
288
|
+
var subscribersLeft = subs.count
|
|
289
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
|
|
290
|
+
var allRestorableObjects = [UIUserActivityRestoring]()
|
|
291
|
+
|
|
292
|
+
let handler = { (restorableObjects: [UIUserActivityRestoring]?) in
|
|
293
|
+
dispatchQueue.sync {
|
|
294
|
+
if let restorableObjects = restorableObjects {
|
|
295
|
+
allRestorableObjects.append(contentsOf: restorableObjects)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
subscribersLeft -= 1
|
|
299
|
+
|
|
300
|
+
if subscribersLeft == 0 {
|
|
301
|
+
restorationHandler(allRestorableObjects)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return subs.reduce(false) { result, subscriber in
|
|
307
|
+
return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
#elseif os(macOS)
|
|
311
|
+
@objc
|
|
312
|
+
public static func application(
|
|
313
|
+
_ application: NSApplication,
|
|
314
|
+
continue userActivity: NSUserActivity,
|
|
315
|
+
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
|
|
316
|
+
) -> Bool {
|
|
317
|
+
let selector = #selector(application(_:continue:restorationHandler:))
|
|
318
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
319
|
+
var subscribersLeft = subs.count
|
|
320
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
|
|
321
|
+
var allRestorableObjects = [NSUserActivityRestoring]()
|
|
322
|
+
|
|
323
|
+
let handler = { (restorableObjects: [NSUserActivityRestoring]?) in
|
|
324
|
+
dispatchQueue.sync {
|
|
325
|
+
if let restorableObjects = restorableObjects {
|
|
326
|
+
allRestorableObjects.append(contentsOf: restorableObjects)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
subscribersLeft -= 1
|
|
330
|
+
|
|
331
|
+
if subscribersLeft == 0 {
|
|
332
|
+
restorationHandler(allRestorableObjects)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return subs.reduce(false) { result, subscriber in
|
|
338
|
+
return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
#endif
|
|
342
|
+
|
|
343
|
+
@objc
|
|
344
|
+
public static func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
|
|
345
|
+
return ExpoAppDelegateSubscriberRepository
|
|
346
|
+
.subscribers
|
|
347
|
+
.forEach { $0.application?(application, didUpdate: userActivity) }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@objc
|
|
351
|
+
public static func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
|
|
352
|
+
return ExpoAppDelegateSubscriberRepository
|
|
353
|
+
.subscribers
|
|
354
|
+
.forEach {
|
|
355
|
+
$0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#if os(iOS)
|
|
360
|
+
@objc
|
|
361
|
+
public static func application(
|
|
362
|
+
_ application: UIApplication,
|
|
363
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
364
|
+
completionHandler: @escaping (Bool) -> Void
|
|
365
|
+
) {
|
|
366
|
+
let selector = #selector(application(_:performActionFor:completionHandler:))
|
|
367
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
368
|
+
var subscribersLeft = subs.count
|
|
369
|
+
var result: Bool = false
|
|
370
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.performAction", qos: .userInteractive)
|
|
371
|
+
|
|
372
|
+
let handler = { (succeeded: Bool) in
|
|
373
|
+
dispatchQueue.sync {
|
|
374
|
+
result = result || succeeded
|
|
375
|
+
subscribersLeft -= 1
|
|
376
|
+
|
|
377
|
+
if subscribersLeft == 0 {
|
|
378
|
+
completionHandler(result)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if subs.isEmpty {
|
|
384
|
+
completionHandler(result)
|
|
385
|
+
} else {
|
|
386
|
+
subs.forEach { subscriber in
|
|
387
|
+
subscriber.application?(application, performActionFor: shortcutItem, completionHandler: handler)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
#endif
|
|
392
|
+
|
|
393
|
+
// MARK: - Background Fetch
|
|
394
|
+
|
|
395
|
+
#if os(iOS) || os(tvOS)
|
|
396
|
+
@objc
|
|
397
|
+
public static func application(
|
|
398
|
+
_ application: UIApplication,
|
|
399
|
+
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
400
|
+
) {
|
|
401
|
+
let selector = #selector(application(_:performFetchWithCompletionHandler:))
|
|
402
|
+
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
|
|
403
|
+
var subscribersLeft = subs.count
|
|
404
|
+
let dispatchQueue = DispatchQueue(label: "expo.application.performFetch", qos: .userInteractive)
|
|
405
|
+
var failedCount = 0
|
|
406
|
+
var newDataCount = 0
|
|
407
|
+
|
|
408
|
+
let handler = { (result: UIBackgroundFetchResult) in
|
|
409
|
+
dispatchQueue.sync {
|
|
410
|
+
if result == .failed {
|
|
411
|
+
failedCount += 1
|
|
412
|
+
} else if result == .newData {
|
|
413
|
+
newDataCount += 1
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
subscribersLeft -= 1
|
|
417
|
+
|
|
418
|
+
if subscribersLeft == 0 {
|
|
419
|
+
if newDataCount > 0 {
|
|
420
|
+
completionHandler(.newData)
|
|
421
|
+
} else if failedCount > 0 {
|
|
422
|
+
completionHandler(.failed)
|
|
423
|
+
} else {
|
|
424
|
+
completionHandler(.noData)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if subs.isEmpty {
|
|
431
|
+
completionHandler(.noData)
|
|
432
|
+
} else {
|
|
433
|
+
subs.forEach { subscriber in
|
|
434
|
+
subscriber.application?(application, performFetchWithCompletionHandler: handler)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#endif
|
|
440
|
+
|
|
441
|
+
// MARK: - Opening a URL-Specified Resource
|
|
442
|
+
#if os(iOS) || os(tvOS)
|
|
443
|
+
|
|
444
|
+
@objc
|
|
445
|
+
public static func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
446
|
+
return ExpoAppDelegateSubscriberRepository.subscribers.reduce(false) { result, subscriber in
|
|
447
|
+
return subscriber.application?(app, open: url, options: options) ?? false || result
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
#elseif os(macOS)
|
|
451
|
+
@objc
|
|
452
|
+
public static func application(_ app: NSApplication, open urls: [URL]) {
|
|
453
|
+
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
|
|
454
|
+
subscriber.application?(app, open: urls)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
#endif
|
|
458
|
+
|
|
459
|
+
#if os(iOS)
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
|
|
463
|
+
* a different orientation.
|
|
464
|
+
*/
|
|
465
|
+
@objc
|
|
466
|
+
public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
|
467
|
+
let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
|
|
468
|
+
let universalOrientationMask = allowedOrientations(for: .unspecified)
|
|
469
|
+
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
|
|
470
|
+
|
|
471
|
+
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
|
|
472
|
+
$0.responds(to: #selector(application(_:supportedInterfaceOrientationsFor:)))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// We want to create an intersection of all orientations set by subscribers.
|
|
476
|
+
let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
|
|
477
|
+
guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
|
|
478
|
+
return result
|
|
479
|
+
}
|
|
480
|
+
return requestedOrientation.intersection(result)
|
|
481
|
+
}
|
|
482
|
+
return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
|
|
483
|
+
}
|
|
484
|
+
#endif
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#if os(iOS)
|
|
488
|
+
private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
|
|
489
|
+
// For now only iPad-specific orientations are supported
|
|
490
|
+
let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
|
|
491
|
+
var mask: UIInterfaceOrientationMask = []
|
|
492
|
+
guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
|
|
493
|
+
return mask
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for orientation in orientations {
|
|
497
|
+
switch orientation {
|
|
498
|
+
case "UIInterfaceOrientationPortrait":
|
|
499
|
+
mask.insert(.portrait)
|
|
500
|
+
case "UIInterfaceOrientationLandscapeLeft":
|
|
501
|
+
mask.insert(.landscapeLeft)
|
|
502
|
+
case "UIInterfaceOrientationLandscapeRight":
|
|
503
|
+
mask.insert(.landscapeRight)
|
|
504
|
+
case "UIInterfaceOrientationPortraitUpsideDown":
|
|
505
|
+
mask.insert(.portraitUpsideDown)
|
|
506
|
+
default:
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return mask
|
|
511
|
+
}
|
|
512
|
+
#endif // os(iOS)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Dynamic type for values conforming to `Encodable` protocol.
|
|
5
|
+
Note that currently it can only encode from native to JavaScript values, thus cannot be used for arguments.
|
|
6
|
+
*/
|
|
7
|
+
internal struct DynamicEncodableType: AnyDynamicType {
|
|
8
|
+
static let shared = DynamicEncodableType()
|
|
9
|
+
|
|
10
|
+
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
|
|
11
|
+
return type is Encodable
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func equals(_ type: any AnyDynamicType) -> Bool {
|
|
15
|
+
// Just mocking it here as we don't really need this function and we rather want to keep it a singleton
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
|
|
20
|
+
// TODO: Create DynamicDecodableType and reuse it here – that would work perfectly with Codable types
|
|
21
|
+
fatalError("DynamicEncodableType can only cast to JavaScript, not from")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
|
|
25
|
+
if let value = value as? JavaScriptValue {
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
if let value = value as? Encodable {
|
|
29
|
+
let runtime = try appContext.runtime
|
|
30
|
+
let encoder = JSValueEncoder(runtime: runtime)
|
|
31
|
+
|
|
32
|
+
try value.encode(to: encoder)
|
|
33
|
+
|
|
34
|
+
return encoder.value
|
|
35
|
+
}
|
|
36
|
+
throw Conversions.ConversionToJSFailedException((kind: .object, nativeType: ValueType.self))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
|
|
40
|
+
// TODO: We should get rid of this function, but it seems it's still used in some places
|
|
41
|
+
return try castToJS(result, appContext: appContext)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var description: String {
|
|
45
|
+
"Encodable"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -7,54 +7,22 @@
|
|
|
7
7
|
/**
|
|
8
8
|
Factory creating an instance of the dynamic type wrapper conforming to `AnyDynamicType`.
|
|
9
9
|
Depending on the given type, it may return one of `DynamicArrayType`, `DynamicOptionalType`, `DynamicConvertibleType`, etc.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
It does some type checks in runtime when the type's conformance/inheritance is unknown for the compiler.
|
|
11
|
+
See the `~` prefix operator overloads that are used for types known for the compiler.
|
|
12
|
+
You can add more type checks for types that don't conform to `AnyArgument`, but are allowed to be used as return types.
|
|
13
|
+
`Void` is a good example as it cannot conform to anything or language protocols that cannot be extended to implement `AnyArgument`.
|
|
12
14
|
*/
|
|
13
15
|
private func DynamicType<T>(_ type: T.Type) -> AnyDynamicType {
|
|
14
|
-
if
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
if type is String.Type {
|
|
18
|
-
return DynamicStringType.shared
|
|
19
|
-
}
|
|
20
|
-
if let ArrayType = T.self as? AnyArray.Type {
|
|
21
|
-
return DynamicArrayType(elementType: ArrayType.getElementDynamicType())
|
|
22
|
-
}
|
|
23
|
-
if let DictionaryType = T.self as? AnyDictionary.Type {
|
|
24
|
-
return DynamicDictionaryType(valueType: DictionaryType.getValueDynamicType())
|
|
25
|
-
}
|
|
26
|
-
if let OptionalType = T.self as? any AnyOptional.Type {
|
|
27
|
-
return DynamicOptionalType(wrappedType: OptionalType.getWrappedDynamicType())
|
|
28
|
-
}
|
|
29
|
-
if let ConvertibleType = T.self as? Convertible.Type {
|
|
30
|
-
return DynamicConvertibleType(innerType: ConvertibleType)
|
|
31
|
-
}
|
|
32
|
-
if let EnumType = T.self as? any Enumerable.Type {
|
|
33
|
-
return DynamicEnumType(innerType: EnumType)
|
|
34
|
-
}
|
|
35
|
-
if let ViewType = T.self as? UIView.Type {
|
|
36
|
-
return DynamicViewType(innerType: ViewType)
|
|
37
|
-
}
|
|
38
|
-
if let SharedObjectType = T.self as? SharedObject.Type {
|
|
39
|
-
return DynamicSharedObjectType(innerType: SharedObjectType)
|
|
40
|
-
}
|
|
41
|
-
if let TypedArrayType = T.self as? AnyTypedArray.Type {
|
|
42
|
-
return DynamicTypedArrayType(innerType: TypedArrayType)
|
|
43
|
-
}
|
|
44
|
-
if T.self is Data.Type {
|
|
45
|
-
return DynamicDataType.shared
|
|
46
|
-
}
|
|
47
|
-
if let JavaScriptValueType = T.self as? any AnyJavaScriptValue.Type {
|
|
48
|
-
return DynamicJavaScriptType(innerType: JavaScriptValueType)
|
|
16
|
+
if let AnyArgumentType = T.self as? AnyArgument.Type {
|
|
17
|
+
return AnyArgumentType.getDynamicType()
|
|
49
18
|
}
|
|
50
19
|
if T.self == Void.self {
|
|
51
20
|
return DynamicVoidType.shared
|
|
52
21
|
}
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return AnyValueOrUndefinedType.getDynamicType()
|
|
22
|
+
if T.self is Encodable.Type {
|
|
23
|
+
// There is no dedicated `~` operator overload for encodables to avoid ambiguity
|
|
24
|
+
// when the type is both `AnyArgument` and `Encodable` (e.g. strings, numeric types).
|
|
25
|
+
return DynamicEncodableType.shared
|
|
58
26
|
}
|
|
59
27
|
return DynamicRawType(innerType: T.self)
|
|
60
28
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Encodes `Encodable` objects or values to `JavaScriptValue`. This implementation is incomplete,
|
|
5
|
+
but it supports basic use cases with structs defined by the user and when the default `Encodable` implementation is used.
|
|
6
|
+
*/
|
|
7
|
+
internal final class JSValueEncoder: Encoder {
|
|
8
|
+
private let runtime: JavaScriptRuntime
|
|
9
|
+
private let valueHolder = JSValueHolder()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
The result of encoding to `JavaScriptValue`. Use this property after running `encode(to:)` on the encodable.
|
|
13
|
+
*/
|
|
14
|
+
var value: JavaScriptValue {
|
|
15
|
+
return valueHolder.value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
Initializes the encoder with the given runtime in which the value will be created.
|
|
20
|
+
*/
|
|
21
|
+
init(runtime: JavaScriptRuntime) {
|
|
22
|
+
self.runtime = runtime
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - Encoder
|
|
26
|
+
|
|
27
|
+
// We don't use `codingPath` and `userInfo`, but they are required by the protocol.
|
|
28
|
+
let codingPath: [any CodingKey] = []
|
|
29
|
+
let userInfo: [CodingUserInfoKey: Any] = [:]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Returns an encoding container appropriate for holding multiple values keyed by the given key type.
|
|
33
|
+
*/
|
|
34
|
+
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
|
|
35
|
+
let container = JSObjectEncodingContainer<Key>(to: valueHolder, runtime: runtime)
|
|
36
|
+
return KeyedEncodingContainer(container)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
Returns an encoding container appropriate for holding multiple unkeyed values.
|
|
41
|
+
*/
|
|
42
|
+
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
|
43
|
+
return JSArrayEncodingContainer(to: valueHolder, runtime: runtime)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Returns an encoding container appropriate for holding a single primitive value, including optionals.
|
|
48
|
+
*/
|
|
49
|
+
func singleValueContainer() -> any SingleValueEncodingContainer {
|
|
50
|
+
return JSValueEncodingContainer(to: valueHolder, runtime: runtime)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
An object that holds a JS value that could be overriden by the encoding container.
|
|
56
|
+
*/
|
|
57
|
+
private final class JSValueHolder {
|
|
58
|
+
var value: JavaScriptValue = .undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
Single value container used to encode primitive values, including optionals.
|
|
63
|
+
*/
|
|
64
|
+
private struct JSValueEncodingContainer: SingleValueEncodingContainer {
|
|
65
|
+
private weak var runtime: JavaScriptRuntime?
|
|
66
|
+
private let valueHolder: JSValueHolder
|
|
67
|
+
|
|
68
|
+
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime?) {
|
|
69
|
+
self.runtime = runtime
|
|
70
|
+
self.valueHolder = valueHolder
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// MARK: - SingleValueEncodingContainer
|
|
74
|
+
|
|
75
|
+
// Unused, but required by the protocol.
|
|
76
|
+
let codingPath: [any CodingKey] = []
|
|
77
|
+
|
|
78
|
+
mutating func encodeNil() throws {
|
|
79
|
+
self.valueHolder.value = .null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
mutating func encode<ValueType: Encodable>(_ value: ValueType) throws {
|
|
83
|
+
guard let runtime else {
|
|
84
|
+
// Do nothing when the runtime is already deallocated
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
let jsValue = JavaScriptValue.from(value, runtime: runtime)
|
|
88
|
+
|
|
89
|
+
// If the given value couldn't be converted to JavaScriptValue, try to encode it farther.
|
|
90
|
+
// It might be the case when the default implementation of `Encodable` has chosen the single value container
|
|
91
|
+
// for an optional type that should rather use keyed or unkeyed container when unwrapped.
|
|
92
|
+
if jsValue.isUndefined() {
|
|
93
|
+
let encoder = JSValueEncoder(runtime: runtime)
|
|
94
|
+
try value.encode(to: encoder)
|
|
95
|
+
self.valueHolder.value = encoder.value
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
self.valueHolder.value = jsValue
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
Keyed container that encodes to a JavaScript object.
|
|
104
|
+
*/
|
|
105
|
+
private struct JSObjectEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
|
106
|
+
private weak var runtime: JavaScriptRuntime?
|
|
107
|
+
private let valueHolder: JSValueHolder
|
|
108
|
+
private var object: JavaScriptObject
|
|
109
|
+
|
|
110
|
+
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime) {
|
|
111
|
+
let object = runtime.createObject()
|
|
112
|
+
valueHolder.value = JavaScriptValue.from(object, runtime: runtime)
|
|
113
|
+
|
|
114
|
+
self.runtime = runtime
|
|
115
|
+
self.object = object
|
|
116
|
+
self.valueHolder = valueHolder
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - KeyedEncodingContainerProtocol
|
|
120
|
+
|
|
121
|
+
// Unused, but required by the protocol.
|
|
122
|
+
var codingPath: [any CodingKey] = []
|
|
123
|
+
|
|
124
|
+
mutating func encodeNil(forKey key: Key) throws {
|
|
125
|
+
object.setProperty(key.stringValue, value: JavaScriptValue.null)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
mutating func encode<ValueType: Encodable>(_ value: ValueType, forKey key: Key) throws {
|
|
129
|
+
guard let runtime else {
|
|
130
|
+
// Do nothing when the runtime is already deallocated
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
let encoder = JSValueEncoder(runtime: runtime)
|
|
134
|
+
try value.encode(to: encoder)
|
|
135
|
+
object.setProperty(key.stringValue, value: encoder.value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
|
|
139
|
+
fatalError("JSValueEncoder does not support nested containers")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
|
|
143
|
+
fatalError("JSValueEncoder does not support nested containers")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
mutating func superEncoder() -> any Encoder {
|
|
147
|
+
fatalError("superEncoder() is not implemented in JSValueEncoder")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
mutating func superEncoder(forKey key: Key) -> any Encoder {
|
|
151
|
+
return self.superEncoder()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
Unkeyed container that encodes values to a JavaScript array.
|
|
157
|
+
*/
|
|
158
|
+
private struct JSArrayEncodingContainer: UnkeyedEncodingContainer {
|
|
159
|
+
private weak var runtime: JavaScriptRuntime?
|
|
160
|
+
private let valueHolder: JSValueHolder
|
|
161
|
+
private var items: [JavaScriptValue] = []
|
|
162
|
+
|
|
163
|
+
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime) {
|
|
164
|
+
self.runtime = runtime
|
|
165
|
+
self.valueHolder = valueHolder
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MARK: - UnkeyedEncodingContainer
|
|
169
|
+
|
|
170
|
+
// Unused, but required by the protocol.
|
|
171
|
+
var codingPath: [any CodingKey] = []
|
|
172
|
+
var count: Int = 0
|
|
173
|
+
|
|
174
|
+
mutating func encodeNil() throws {
|
|
175
|
+
items.append(.null)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
mutating func encode<ValueType: Encodable>(_ value: ValueType) throws {
|
|
179
|
+
guard let runtime else {
|
|
180
|
+
// Do nothing when the runtime is already deallocated
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
let encoder = JSValueEncoder(runtime: runtime)
|
|
184
|
+
try value.encode(to: encoder)
|
|
185
|
+
|
|
186
|
+
items.append(encoder.value)
|
|
187
|
+
valueHolder.value = .from(items, runtime: runtime)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
|
191
|
+
fatalError("JSValueEncoder does not support nested containers")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
|
|
195
|
+
fatalError("JSValueEncoder does not support nested containers")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
mutating func superEncoder() -> any Encoder {
|
|
199
|
+
fatalError("superEncoder() is not implemented in JSValueEncoder")
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -31,7 +31,8 @@ jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value)
|
|
|
31
31
|
#if !TARGET_OS_OSX
|
|
32
32
|
const uint8_t *utf8 = (const uint8_t *)[value UTF8String];
|
|
33
33
|
const size_t length = [value length];
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
if (utf8 != nullptr && expo::isAllASCIIAndNotNull(utf8, utf8 + length)) {
|
|
35
36
|
return jsi::String::createFromAscii(runtime, (const char *)utf8, length);
|
|
36
37
|
}
|
|
37
38
|
// Using cStringUsingEncoding should be fine as long as we provide the length.
|
|
@@ -60,6 +60,7 @@ NS_SWIFT_NAME(JavaScriptValue)
|
|
|
60
60
|
#pragma mark - Statics
|
|
61
61
|
|
|
62
62
|
@property (class, nonatomic, assign, readonly, nonnull) EXJavaScriptValue *undefined;
|
|
63
|
+
@property (class, nonatomic, assign, readonly, nonnull) EXJavaScriptValue *null;
|
|
63
64
|
|
|
64
65
|
+ (nonnull EXJavaScriptValue *)number:(double)value;
|
|
65
66
|
|
|
@@ -170,10 +170,14 @@
|
|
|
170
170
|
|
|
171
171
|
+ (nonnull EXJavaScriptValue *)undefined
|
|
172
172
|
{
|
|
173
|
-
auto undefined = std::make_shared<jsi::Value>();
|
|
174
173
|
return [[EXJavaScriptValue alloc] initWithRuntime:nil value:jsi::Value::undefined()];
|
|
175
174
|
}
|
|
176
175
|
|
|
176
|
+
+ (nonnull EXJavaScriptValue *)null
|
|
177
|
+
{
|
|
178
|
+
return [[EXJavaScriptValue alloc] initWithRuntime:nil value:jsi::Value::null()];
|
|
179
|
+
}
|
|
180
|
+
|
|
177
181
|
+ (nonnull EXJavaScriptValue *)number:(double)value
|
|
178
182
|
{
|
|
179
183
|
return [[EXJavaScriptValue alloc] initWithRuntime:nil value:jsi::Value(value)];
|
|
@@ -405,5 +405,75 @@ final class DynamicTypeSpec: ExpoSpec {
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
+
|
|
409
|
+
// MARK: - DynamicEncodableType
|
|
410
|
+
|
|
411
|
+
describe("DynamicEncodableType") {
|
|
412
|
+
struct TestEncodable: Encodable {
|
|
413
|
+
let string: String
|
|
414
|
+
let number: Int
|
|
415
|
+
let bool: Bool
|
|
416
|
+
var object: TestEncodableChild? = nil
|
|
417
|
+
var array: [Int]? = nil
|
|
418
|
+
}
|
|
419
|
+
struct TestEncodableChild: Encodable {
|
|
420
|
+
let name: String
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
it("is created") {
|
|
424
|
+
expect(~TestEncodable.self).to(beAKindOf(DynamicEncodableType.self))
|
|
425
|
+
}
|
|
426
|
+
it("casts to JS object") {
|
|
427
|
+
let encodable = TestEncodable(string: "test", number: -5, bool: true)
|
|
428
|
+
let result = try (~TestEncodable.self).castToJS(encodable, appContext: appContext)
|
|
429
|
+
|
|
430
|
+
expect(result.kind) == .object
|
|
431
|
+
}
|
|
432
|
+
it("has proper property names") {
|
|
433
|
+
let encodable = TestEncodable(string: "test", number: -5, bool: true)
|
|
434
|
+
let result = try (~TestEncodable.self).castToJS(encodable, appContext: appContext)
|
|
435
|
+
let propertyNames = result.getObject().getPropertyNames()
|
|
436
|
+
|
|
437
|
+
expect(propertyNames.count) == 3
|
|
438
|
+
expect(propertyNames).to(contain(["string", "number", "bool"]))
|
|
439
|
+
}
|
|
440
|
+
it("has correct values") {
|
|
441
|
+
let encodable = TestEncodable(string: "test", number: -5, bool: true)
|
|
442
|
+
let result = try (~TestEncodable.self).castToJS(encodable, appContext: appContext)
|
|
443
|
+
let object = result.getObject()
|
|
444
|
+
|
|
445
|
+
expect(try object.getProperty("string").asString()) == encodable.string
|
|
446
|
+
expect(try object.getProperty("number").asInt()) == encodable.number
|
|
447
|
+
expect(try object.getProperty("bool").asBool()) == encodable.bool
|
|
448
|
+
expect(object.getProperty("object").isUndefined()) == true
|
|
449
|
+
expect(object.getProperty("array").isUndefined()) == true
|
|
450
|
+
}
|
|
451
|
+
it("casts nested objects") {
|
|
452
|
+
let encodable = TestEncodable(
|
|
453
|
+
string: "test",
|
|
454
|
+
number: -5,
|
|
455
|
+
bool: true,
|
|
456
|
+
object: TestEncodableChild(name: "expo")
|
|
457
|
+
)
|
|
458
|
+
let result = try (~TestEncodable.self).castToJS(encodable, appContext: appContext)
|
|
459
|
+
let nestedValue = result.getObject().getProperty("object")
|
|
460
|
+
|
|
461
|
+
expect(nestedValue.kind) == .object
|
|
462
|
+
expect(try nestedValue.getObject().getProperty("name").asString()) == encodable.object?.name
|
|
463
|
+
}
|
|
464
|
+
it("casts arrays") {
|
|
465
|
+
let encodable = TestEncodable(
|
|
466
|
+
string: "test",
|
|
467
|
+
number: -5,
|
|
468
|
+
bool: true,
|
|
469
|
+
array: [1, 2, 3]
|
|
470
|
+
)
|
|
471
|
+
let result = try (~TestEncodable.self).castToJS(encodable, appContext: appContext)
|
|
472
|
+
let array = result.getObject().getProperty("array").getArray()
|
|
473
|
+
|
|
474
|
+
expect(array.count) == encodable.array?.count
|
|
475
|
+
expect(array.map({ $0.getInt() })) == encodable.array
|
|
476
|
+
}
|
|
477
|
+
}
|
|
408
478
|
}
|
|
409
479
|
}
|
|
@@ -252,6 +252,11 @@ class FunctionSpec: ExpoSpec {
|
|
|
252
252
|
@Field var url: URL = defaultURL
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
struct TestEncodable: Encodable {
|
|
256
|
+
let name: String
|
|
257
|
+
let version: Int
|
|
258
|
+
}
|
|
259
|
+
|
|
255
260
|
afterEach {
|
|
256
261
|
try runtime.eval("globalThis.result = undefined")
|
|
257
262
|
}
|
|
@@ -309,6 +314,10 @@ class FunctionSpec: ExpoSpec {
|
|
|
309
314
|
return "\(f?.property ?? "no value")"
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
Function("returnEncodable") {
|
|
318
|
+
return TestEncodable(name: "Expo SDK", version: 55)
|
|
319
|
+
}
|
|
320
|
+
|
|
312
321
|
Function("withSharedObject") {
|
|
313
322
|
return SharedString("Test")
|
|
314
323
|
}
|
|
@@ -388,6 +397,14 @@ class FunctionSpec: ExpoSpec {
|
|
|
388
397
|
expect(try runtime.eval("expo.modules.TestModule.withOptionalRecord({property: \"123\"})").asString()) == "123"
|
|
389
398
|
}
|
|
390
399
|
|
|
400
|
+
it("returns encodable struct") {
|
|
401
|
+
let result = try runtime.eval("expo.modules.TestModule.returnEncodable()")
|
|
402
|
+
expect(result.kind) == .object
|
|
403
|
+
expect(result.getObject().getPropertyNames()).to(contain(["name", "version"]))
|
|
404
|
+
expect(try result.getObject().getProperty("name").asString()) == "Expo SDK"
|
|
405
|
+
expect(try result.getObject().getProperty("version").asInt()) == 55
|
|
406
|
+
}
|
|
407
|
+
|
|
391
408
|
it("returns URL (sync)") {
|
|
392
409
|
let result = try runtime.eval("globalThis.result = expo.modules.TestModule.withURL()")
|
|
393
410
|
expect(result.kind) == .string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.23",
|
|
4
4
|
"description": "The core of Expo Modules architecture",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"@testing-library/react-native": "^13.2.0",
|
|
66
66
|
"expo-module-scripts": "^5.0.7"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "f1475b7bd0e8fdec0c0027be89c4c8d650d10805"
|
|
69
69
|
}
|