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 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
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
29
29
  }
30
30
 
31
31
  group = 'host.exp.exponent'
32
- version = '3.0.21'
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.21"
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
- Note that this goes through many type checks, thus it might be a bit more expensive than using generic type constraints,
11
- see the `~` prefix operator below that handles types conforming to `AnyArgument` in a faster way.
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 type is any Numeric.Type {
15
- return DynamicNumberType(numberType: T.self)
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 let AnyEitherType = T.self as? AnyEither.Type {
54
- return AnyEitherType.getDynamicType()
55
- }
56
- if let AnyValueOrUndefinedType = T.self as? AnyValueOrUndefined.Type {
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
- if (expo::isAllASCIIAndNotNull(utf8, utf8 + length)) {
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.21",
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": "a2c8477a3fc5744980494805ae46f20dda94c852"
68
+ "gitHead": "f1475b7bd0e8fdec0c0027be89c4c8d650d10805"
69
69
  }