expo-modules-core 3.0.20 → 3.0.22

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,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 3.0.22 — 2025-10-20
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS] Introduce ExpoAppDelegateSubscriberManager class ([#40008](https://github.com/expo/expo/pull/40008) by [@gabrieldonadel](https://github.com/gabrieldonadel))
18
+
19
+ ## 3.0.21 — 2025-10-09
20
+
21
+ ### 💡 Others
22
+
23
+ - [iOS] Add `invalidate` callback to SwiftUIVirtualView. ([#40237](https://github.com/expo/expo/pull/40237) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
24
+
13
25
  ## 3.0.20 — 2025-10-01
14
26
 
15
27
  ### 🐛 Bug fixes
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
29
29
  }
30
30
 
31
31
  group = 'host.exp.exponent'
32
- version = '3.0.20'
32
+ version = '3.0.22'
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.20"
82
+ versionName "3.0.22"
83
83
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
84
84
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
85
85
 
@@ -0,0 +1,501 @@
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
+ // TODO: - Responding to Environment Changes
143
+
144
+ // TODO: - Managing App State Restoration
145
+
146
+ // MARK: - Downloading Data in the Background
147
+
148
+ #if os(iOS) || os(tvOS)
149
+ @objc
150
+ public static func application(
151
+ _ application: UIApplication,
152
+ handleEventsForBackgroundURLSession identifier: String,
153
+ completionHandler: @escaping () -> Void
154
+ ) {
155
+ let selector = #selector(application(_:handleEventsForBackgroundURLSession:completionHandler:))
156
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
157
+ var subscribersLeft = subs.count
158
+ let dispatchQueue = DispatchQueue(label: "expo.application.handleBackgroundEvents")
159
+
160
+ let handler = {
161
+ dispatchQueue.sync {
162
+ subscribersLeft -= 1
163
+
164
+ if subscribersLeft == 0 {
165
+ completionHandler()
166
+ }
167
+ }
168
+ }
169
+
170
+ if subs.isEmpty {
171
+ completionHandler()
172
+ } else {
173
+ subs.forEach {
174
+ $0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: handler)
175
+ }
176
+ }
177
+ }
178
+
179
+ #endif
180
+
181
+ // MARK: - Handling Remote Notification Registration
182
+
183
+ @objc
184
+ public static func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
185
+ ExpoAppDelegateSubscriberRepository
186
+ .subscribers
187
+ .forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
188
+ }
189
+
190
+ @objc
191
+ public static func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
192
+ ExpoAppDelegateSubscriberRepository
193
+ .subscribers
194
+ .forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
195
+ }
196
+
197
+ #if os(iOS) || os(tvOS)
198
+ @objc
199
+ public static func application(
200
+ _ application: UIApplication,
201
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
202
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
203
+ ) {
204
+ let selector = #selector(application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
205
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
206
+ var subscribersLeft = subs.count
207
+ let dispatchQueue = DispatchQueue(label: "expo.application.remoteNotification", qos: .userInteractive)
208
+ var failedCount = 0
209
+ var newDataCount = 0
210
+
211
+ let handler = { (result: UIBackgroundFetchResult) in
212
+ dispatchQueue.sync {
213
+ if result == .failed {
214
+ failedCount += 1
215
+ } else if result == .newData {
216
+ newDataCount += 1
217
+ }
218
+
219
+ subscribersLeft -= 1
220
+
221
+ if subscribersLeft == 0 {
222
+ if newDataCount > 0 {
223
+ completionHandler(.newData)
224
+ } else if failedCount > 0 {
225
+ completionHandler(.failed)
226
+ } else {
227
+ completionHandler(.noData)
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ if subs.isEmpty {
234
+ completionHandler(.noData)
235
+ } else {
236
+ subs.forEach { subscriber in
237
+ subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: handler)
238
+ }
239
+ }
240
+ }
241
+
242
+ #elseif os(macOS)
243
+ @objc
244
+ public static func application(
245
+ _ application: NSApplication,
246
+ didReceiveRemoteNotification userInfo: [String: Any]
247
+ ) {
248
+ let selector = #selector(application(_:didReceiveRemoteNotification:))
249
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
250
+
251
+ subs.forEach { subscriber in
252
+ subscriber.application?(application, didReceiveRemoteNotification: userInfo)
253
+ }
254
+ }
255
+ #endif
256
+
257
+ // MARK: - Continuing User Activity and Handling Quick Actions
258
+
259
+ @objc
260
+ public static func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
261
+ return ExpoAppDelegateSubscriberRepository
262
+ .subscribers
263
+ .reduce(false) { result, subscriber in
264
+ return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
265
+ }
266
+ }
267
+
268
+ #if os(iOS) || os(tvOS)
269
+ @objc
270
+ public static func application(
271
+ _ application: UIApplication,
272
+ continue userActivity: NSUserActivity,
273
+ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
274
+ ) -> Bool {
275
+ let selector = #selector(application(_:continue:restorationHandler:))
276
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
277
+ var subscribersLeft = subs.count
278
+ let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
279
+ var allRestorableObjects = [UIUserActivityRestoring]()
280
+
281
+ let handler = { (restorableObjects: [UIUserActivityRestoring]?) in
282
+ dispatchQueue.sync {
283
+ if let restorableObjects = restorableObjects {
284
+ allRestorableObjects.append(contentsOf: restorableObjects)
285
+ }
286
+
287
+ subscribersLeft -= 1
288
+
289
+ if subscribersLeft == 0 {
290
+ restorationHandler(allRestorableObjects)
291
+ }
292
+ }
293
+ }
294
+
295
+ return subs.reduce(false) { result, subscriber in
296
+ return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
297
+ }
298
+ }
299
+ #elseif os(macOS)
300
+ @objc
301
+ public static func application(
302
+ _ application: NSApplication,
303
+ continue userActivity: NSUserActivity,
304
+ restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
305
+ ) -> Bool {
306
+ let selector = #selector(application(_:continue:restorationHandler:))
307
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
308
+ var subscribersLeft = subs.count
309
+ let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
310
+ var allRestorableObjects = [NSUserActivityRestoring]()
311
+
312
+ let handler = { (restorableObjects: [NSUserActivityRestoring]?) in
313
+ dispatchQueue.sync {
314
+ if let restorableObjects = restorableObjects {
315
+ allRestorableObjects.append(contentsOf: restorableObjects)
316
+ }
317
+
318
+ subscribersLeft -= 1
319
+
320
+ if subscribersLeft == 0 {
321
+ restorationHandler(allRestorableObjects)
322
+ }
323
+ }
324
+ }
325
+
326
+ return subs.reduce(false) { result, subscriber in
327
+ return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
328
+ }
329
+ }
330
+ #endif
331
+
332
+ @objc
333
+ public static func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
334
+ return ExpoAppDelegateSubscriberRepository
335
+ .subscribers
336
+ .forEach { $0.application?(application, didUpdate: userActivity) }
337
+ }
338
+
339
+ @objc
340
+ public static func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
341
+ return ExpoAppDelegateSubscriberRepository
342
+ .subscribers
343
+ .forEach {
344
+ $0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
345
+ }
346
+ }
347
+
348
+ #if os(iOS)
349
+ @objc
350
+ public static func application(
351
+ _ application: UIApplication,
352
+ performActionFor shortcutItem: UIApplicationShortcutItem,
353
+ completionHandler: @escaping (Bool) -> Void
354
+ ) {
355
+ let selector = #selector(application(_:performActionFor:completionHandler:))
356
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
357
+ var subscribersLeft = subs.count
358
+ var result: Bool = false
359
+ let dispatchQueue = DispatchQueue(label: "expo.application.performAction", qos: .userInteractive)
360
+
361
+ let handler = { (succeeded: Bool) in
362
+ dispatchQueue.sync {
363
+ result = result || succeeded
364
+ subscribersLeft -= 1
365
+
366
+ if subscribersLeft == 0 {
367
+ completionHandler(result)
368
+ }
369
+ }
370
+ }
371
+
372
+ if subs.isEmpty {
373
+ completionHandler(result)
374
+ } else {
375
+ subs.forEach { subscriber in
376
+ subscriber.application?(application, performActionFor: shortcutItem, completionHandler: handler)
377
+ }
378
+ }
379
+ }
380
+ #endif
381
+
382
+ // MARK: - Background Fetch
383
+
384
+ #if os(iOS) || os(tvOS)
385
+ @objc
386
+ public static func application(
387
+ _ application: UIApplication,
388
+ performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
389
+ ) {
390
+ let selector = #selector(application(_:performFetchWithCompletionHandler:))
391
+ let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
392
+ var subscribersLeft = subs.count
393
+ let dispatchQueue = DispatchQueue(label: "expo.application.performFetch", qos: .userInteractive)
394
+ var failedCount = 0
395
+ var newDataCount = 0
396
+
397
+ let handler = { (result: UIBackgroundFetchResult) in
398
+ dispatchQueue.sync {
399
+ if result == .failed {
400
+ failedCount += 1
401
+ } else if result == .newData {
402
+ newDataCount += 1
403
+ }
404
+
405
+ subscribersLeft -= 1
406
+
407
+ if subscribersLeft == 0 {
408
+ if newDataCount > 0 {
409
+ completionHandler(.newData)
410
+ } else if failedCount > 0 {
411
+ completionHandler(.failed)
412
+ } else {
413
+ completionHandler(.noData)
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ if subs.isEmpty {
420
+ completionHandler(.noData)
421
+ } else {
422
+ subs.forEach { subscriber in
423
+ subscriber.application?(application, performFetchWithCompletionHandler: handler)
424
+ }
425
+ }
426
+ }
427
+
428
+ #endif
429
+
430
+ // MARK: - Opening a URL-Specified Resource
431
+ #if os(iOS) || os(tvOS)
432
+
433
+ @objc
434
+ public static func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
435
+ return ExpoAppDelegateSubscriberRepository.subscribers.reduce(false) { result, subscriber in
436
+ return subscriber.application?(app, open: url, options: options) ?? false || result
437
+ }
438
+ }
439
+ #elseif os(macOS)
440
+ @objc
441
+ public static func application(_ app: NSApplication, open urls: [URL]) {
442
+ ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
443
+ subscriber.application?(app, open: urls)
444
+ }
445
+ }
446
+ #endif
447
+
448
+ #if os(iOS)
449
+
450
+ /**
451
+ * Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
452
+ * a different orientation.
453
+ */
454
+ @objc
455
+ public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
456
+ let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
457
+ let universalOrientationMask = allowedOrientations(for: .unspecified)
458
+ let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
459
+
460
+ let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
461
+ $0.responds(to: #selector(application(_:supportedInterfaceOrientationsFor:)))
462
+ }
463
+
464
+ // We want to create an intersection of all orientations set by subscribers.
465
+ let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
466
+ guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
467
+ return result
468
+ }
469
+ return requestedOrientation.intersection(result)
470
+ }
471
+ return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
472
+ }
473
+ #endif
474
+ }
475
+
476
+ #if os(iOS)
477
+ private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
478
+ // For now only iPad-specific orientations are supported
479
+ let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
480
+ var mask: UIInterfaceOrientationMask = []
481
+ guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
482
+ return mask
483
+ }
484
+
485
+ for orientation in orientations {
486
+ switch orientation {
487
+ case "UIInterfaceOrientationPortrait":
488
+ mask.insert(.portrait)
489
+ case "UIInterfaceOrientationLandscapeLeft":
490
+ mask.insert(.landscapeLeft)
491
+ case "UIInterfaceOrientationLandscapeRight":
492
+ mask.insert(.landscapeRight)
493
+ case "UIInterfaceOrientationPortraitUpsideDown":
494
+ mask.insert(.portraitUpsideDown)
495
+ default:
496
+ break
497
+ }
498
+ }
499
+ return mask
500
+ }
501
+ #endif // os(iOS)
@@ -372,4 +372,10 @@ static std::unordered_map<std::string, expo::ExpoViewComponentDescriptor::Flavor
372
372
  return NO;
373
373
  }
374
374
 
375
+ - (void)invalidate
376
+ {
377
+ // Default implementation does nothing.
378
+ [self prepareForRecycle];
379
+ }
380
+
375
381
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "3.0.20",
3
+ "version": "3.0.22",
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": "73d9bc0ee4f1e28cfda349dc36ee53d16fad0c5d"
68
+ "gitHead": "ea56136a4420322f46d00e4b1549595d8f85150e"
69
69
  }