expo 54.0.0-canary-20250630-547cd82 → 54.0.0-canary-20250709-136b77f

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.
@@ -32,7 +32,7 @@ buildscript {
32
32
  def reactNativeVersion = project.extensions.getByType(ExpoModuleExtension).reactNativeVersion
33
33
 
34
34
  group = 'host.exp.exponent'
35
- version = '54.0.0-canary-20250630-547cd82'
35
+ version = '54.0.0-canary-20250709-136b77f'
36
36
 
37
37
  expoModule {
38
38
  // We can't prebuild the module because it depends on the generated files.
@@ -43,7 +43,7 @@ android {
43
43
  namespace "expo.core"
44
44
  defaultConfig {
45
45
  versionCode 1
46
- versionName "54.0.0-canary-20250630-547cd82"
46
+ versionName "54.0.0-canary-20250709-136b77f"
47
47
  consumerProguardFiles("proguard-rules.pro")
48
48
  }
49
49
  testOptions {
@@ -77,6 +77,8 @@ dependencies { dependencyHandler ->
77
77
  testImplementation 'androidx.test:core:1.5.0'
78
78
  testImplementation "com.google.truth:truth:1.1.2"
79
79
  testImplementation 'io.mockk:mockk:1.13.5'
80
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
81
+ testImplementation 'org.robolectric:robolectric:4.15.1'
80
82
 
81
83
  if (useLegacyAutolinking) {
82
84
  // Link expo modules as dependencies of the adapter. It uses `api` configuration so they all will be visible for the app as well.
@@ -1,13 +1,20 @@
1
1
  package expo.modules
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.app.Activity
4
5
  import android.content.Context
5
6
  import android.content.Intent
7
+ import android.content.pm.ActivityInfo
6
8
  import android.content.res.Configuration
9
+ import android.os.Build
10
+ import android.os.Build.VERSION
7
11
  import android.os.Bundle
12
+ import android.util.Log
8
13
  import android.view.KeyEvent
9
14
  import android.view.ViewGroup
15
+ import androidx.annotation.VisibleForTesting
10
16
  import androidx.collection.ArrayMap
17
+ import androidx.lifecycle.lifecycleScope
11
18
  import com.facebook.react.ReactActivity
12
19
  import com.facebook.react.ReactActivityDelegate
13
20
  import com.facebook.react.ReactDelegate
@@ -18,17 +25,27 @@ import com.facebook.react.ReactNativeHost
18
25
  import com.facebook.react.ReactRootView
19
26
  import com.facebook.react.bridge.ReactContext
20
27
  import com.facebook.react.modules.core.PermissionListener
28
+ import expo.modules.core.interfaces.ReactActivityHandler.DelayLoadAppHandler
21
29
  import expo.modules.core.interfaces.ReactActivityLifecycleListener
22
30
  import expo.modules.kotlin.Utils
23
31
  import expo.modules.rncompatibility.ReactNativeFeatureFlags
32
+ import kotlinx.coroutines.CompletableDeferred
33
+ import kotlinx.coroutines.CoroutineScope
34
+ import kotlinx.coroutines.CoroutineStart
35
+ import kotlinx.coroutines.Dispatchers
36
+ import kotlinx.coroutines.launch
37
+ import kotlinx.coroutines.sync.Mutex
38
+ import kotlinx.coroutines.sync.withLock
24
39
  import java.lang.reflect.Field
25
40
  import java.lang.reflect.Method
26
41
  import java.lang.reflect.Modifier
42
+ import kotlin.coroutines.resume
43
+ import kotlin.coroutines.suspendCoroutine
27
44
 
28
45
  class ReactActivityDelegateWrapper(
29
46
  private val activity: ReactActivity,
30
47
  private val isNewArchitectureEnabled: Boolean,
31
- private var delegate: ReactActivityDelegate
48
+ @get:VisibleForTesting internal var delegate: ReactActivityDelegate
32
49
  ) : ReactActivityDelegate(activity, null) {
33
50
  constructor(activity: ReactActivity, delegate: ReactActivityDelegate) :
34
51
  this(activity, false, delegate)
@@ -44,12 +61,32 @@ class ReactActivityDelegateWrapper(
44
61
  private val _reactHost: ReactHost? by lazy {
45
62
  delegate.reactHost
46
63
  }
64
+ private val delayLoadAppHandler: DelayLoadAppHandler? by lazy {
65
+ reactActivityHandlers.asSequence()
66
+ .mapNotNull { it.getDelayLoadAppHandler(activity, reactNativeHost) }
67
+ .firstOrNull()
68
+ }
69
+
70
+ /**
71
+ * A deferred that indicates when the app loading is ready
72
+ */
73
+ private val loadAppReady = CompletableDeferred<Unit>()
47
74
 
48
75
  /**
49
- * When the app delay for `loadApp`, the ReactInstanceManager's lifecycle will be disrupted.
50
- * This flag indicates we should emit `onResume` after `loadApp`.
76
+ * A mutex to ensure all coroutines in a scope are running in atomic way.
77
+ *
78
+ * This is to act like [Activity] lifecycle,
79
+ * e.g. all work in [onResume] should be executed after all work in [onCreate] finished.
51
80
  */
52
- private var shouldEmitPendingResume = false
81
+ private val mutex = Mutex()
82
+
83
+ /**
84
+ * A [CoroutineScope] that binds its lifecycle as [ReactActivityDelegateWrapper].
85
+ * This is used for [onDestroy] because we need a longer lifecycle scope to call [onDestroy]
86
+ */
87
+ private val applicationCoroutineScope: CoroutineScope by lazy {
88
+ CoroutineScope(Dispatchers.Main)
89
+ }
53
90
 
54
91
  //region ReactActivityDelegate
55
92
 
@@ -82,53 +119,12 @@ class ReactActivityDelegateWrapper(
82
119
  }
83
120
 
84
121
  override fun loadApp(appKey: String?) {
85
- // Give modules a chance to wrap the ReactRootView in a container ViewGroup. If some module
86
- // wants to do this, we override the functionality of `loadApp` and call `setContentView` with
87
- // the new container view instead.
88
- val rootViewContainer = reactActivityHandlers.asSequence()
89
- .mapNotNull { it.createReactRootViewContainer(activity) }
90
- .firstOrNull()
91
- if (rootViewContainer != null) {
92
- val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
93
- mReactDelegate.isAccessible = true
94
- val reactDelegate = mReactDelegate[delegate] as ReactDelegate
95
-
96
- reactDelegate.loadApp(appKey)
97
- val reactRootView = reactDelegate.reactRootView
98
- (reactRootView?.parent as? ViewGroup)?.removeView(reactRootView)
99
- rootViewContainer.addView(reactRootView, ViewGroup.LayoutParams.MATCH_PARENT)
100
- activity.setContentView(rootViewContainer)
101
- reactActivityLifecycleListeners.forEach { listener ->
102
- listener.onContentChanged(activity)
103
- }
104
- return
105
- }
106
-
107
- val delayLoadAppHandler = reactActivityHandlers.asSequence()
108
- .mapNotNull { it.getDelayLoadAppHandler(activity, reactNativeHost) }
109
- .firstOrNull()
110
- if (delayLoadAppHandler != null) {
111
- shouldEmitPendingResume = true
112
- delayLoadAppHandler.whenReady {
113
- Utils.assertMainThread()
114
- invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
115
- reactActivityLifecycleListeners.forEach { listener ->
116
- listener.onContentChanged(activity)
117
- }
118
- if (shouldEmitPendingResume) {
119
- shouldEmitPendingResume = false
120
- onResume()
121
- }
122
- }
123
- return
124
- }
125
-
126
- invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
127
- reactActivityLifecycleListeners.forEach { listener ->
128
- listener.onContentChanged(activity)
122
+ launchLifecycleScopeWithLock(start = CoroutineStart.UNDISPATCHED) {
123
+ loadAppImpl(appKey, supportsDelayLoad = true)
129
124
  }
130
125
  }
131
126
 
127
+ @SuppressLint("DiscouragedPrivateApi")
132
128
  override fun onCreate(savedInstanceState: Bundle?) {
133
129
  // Give handlers a chance as early as possible to replace the wrapped delegate object.
134
130
  // If they do, we call the new wrapped delegate's `onCreate` instead of overriding it here.
@@ -144,38 +140,50 @@ class ReactActivityDelegateWrapper(
144
140
  mDelegateField.set(activity, newDelegate)
145
141
  delegate = newDelegate
146
142
 
147
- invokeDelegateMethod<Unit, Bundle?>("onCreate", arrayOf(Bundle::class.java), arrayOf(savedInstanceState))
143
+ delegate.onCreate(savedInstanceState)
148
144
  } else {
149
145
  // Since we just wrap `ReactActivityDelegate` but not inherit it, in its `onCreate`,
150
146
  // the calls to `createRootView()` or `getMainComponentName()` have no chances to be our wrapped methods.
151
147
  // Instead we intercept `ReactActivityDelegate.onCreate` and replace the `mReactDelegate` with our version.
152
148
  // That's not ideal but works.
153
- val launchOptions = composeLaunchOptions()
154
- val reactDelegate: ReactDelegate
155
- if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
156
- reactDelegate = ReactDelegate(
157
- plainActivity,
158
- reactHost,
159
- mainComponentName,
160
- launchOptions
161
- )
162
- } else {
163
- reactDelegate = object : ReactDelegate(
164
- plainActivity,
165
- reactNativeHost,
166
- mainComponentName,
167
- launchOptions
168
- ) {
169
- override fun createRootView(): ReactRootView {
170
- return this@ReactActivityDelegateWrapper.createRootView() ?: super.createRootView()
149
+
150
+ launchLifecycleScopeWithLock(start = CoroutineStart.UNDISPATCHED) {
151
+ awaitDelayLoadAppWhenReady(delayLoadAppHandler)
152
+ loadAppReady.complete(Unit)
153
+
154
+ if (VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled) {
155
+ activity.window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
156
+ }
157
+
158
+ val launchOptions = composeLaunchOptions()
159
+ val reactDelegate: ReactDelegate
160
+ if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
161
+ reactDelegate = ReactDelegate(
162
+ plainActivity,
163
+ reactHost,
164
+ mainComponentName,
165
+ launchOptions
166
+ )
167
+ } else {
168
+ reactDelegate = object : ReactDelegate(
169
+ plainActivity,
170
+ reactNativeHost,
171
+ mainComponentName,
172
+ launchOptions,
173
+ isFabricEnabled
174
+ ) {
175
+ override fun createRootView(): ReactRootView {
176
+ return this@ReactActivityDelegateWrapper.createRootView() ?: super.createRootView()
177
+ }
171
178
  }
172
179
  }
173
- }
174
- val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
175
- mReactDelegate.isAccessible = true
176
- mReactDelegate.set(delegate, reactDelegate)
177
- if (mainComponentName != null) {
178
- loadApp(mainComponentName)
180
+
181
+ val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
182
+ mReactDelegate.isAccessible = true
183
+ mReactDelegate.set(delegate, reactDelegate)
184
+ if (mainComponentName != null) {
185
+ loadAppImpl(mainComponentName, supportsDelayLoad = false)
186
+ }
179
187
  }
180
188
  }
181
189
 
@@ -185,46 +193,71 @@ class ReactActivityDelegateWrapper(
185
193
  }
186
194
 
187
195
  override fun onResume() {
188
- if (shouldEmitPendingResume) {
189
- return
190
- }
191
- invokeDelegateMethod<Unit>("onResume")
192
- reactActivityLifecycleListeners.forEach { listener ->
193
- listener.onResume(activity)
196
+ launchLifecycleScopeWithLock {
197
+ loadAppReady.await()
198
+ delegate.onResume()
199
+ reactActivityLifecycleListeners.forEach { listener ->
200
+ listener.onResume(activity)
201
+ }
194
202
  }
195
203
  }
196
204
 
197
205
  override fun onPause() {
198
- // If app is stopped before the delayed `loadApp`, we should cancel the pending resume
199
- // and avoid propagating the pause event because the state was never resumed.
200
- if (shouldEmitPendingResume) {
201
- shouldEmitPendingResume = false
202
- return
203
- }
204
- reactActivityLifecycleListeners.forEach { listener ->
205
- listener.onPause(activity)
206
+ launchLifecycleScopeWithLock {
207
+ loadAppReady.await()
208
+ reactActivityLifecycleListeners.forEach { listener ->
209
+ listener.onPause(activity)
210
+ }
211
+ if (delayLoadAppHandler != null) {
212
+ try {
213
+ // For the delay load case, we may enter a different call flow than react-native.
214
+ // For example, Activity stopped before delay load finished.
215
+ // We stop before the ReactActivityDelegate gets a chance to set up.
216
+ // In this case, we should catch the exceptions.
217
+ delegate.onPause()
218
+ } catch (e: Exception) {
219
+ Log.e(TAG, "Exception occurred during onPause with delayed app loading", e)
220
+ }
221
+ } else {
222
+ delegate.onPause()
223
+ }
206
224
  }
207
- return invokeDelegateMethod("onPause")
208
225
  }
209
226
 
210
227
  override fun onUserLeaveHint() {
211
- reactActivityLifecycleListeners.forEach { listener ->
212
- listener.onUserLeaveHint(activity)
228
+ launchLifecycleScopeWithLock {
229
+ loadAppReady.await()
230
+ reactActivityLifecycleListeners.forEach { listener ->
231
+ listener.onUserLeaveHint(activity)
232
+ }
233
+ delegate.onUserLeaveHint()
213
234
  }
214
- return invokeDelegateMethod("onUserLeaveHint")
215
235
  }
216
236
 
217
237
  override fun onDestroy() {
218
- // If app is stopped before the delayed `loadApp`, we should cancel the pending resume
219
- // and avoid propagating the destroy event because the state was never resumed.
220
- if (shouldEmitPendingResume) {
221
- shouldEmitPendingResume = false
222
- return
223
- }
224
- reactActivityLifecycleListeners.forEach { listener ->
225
- listener.onDestroy(activity)
238
+ // Note: use our `coroutineScope` for onDestroy here
239
+ // because the lifecycleScope destroyed before the executions.
240
+ applicationCoroutineScope.launch {
241
+ mutex.withLock {
242
+ loadAppReady.await()
243
+ reactActivityLifecycleListeners.forEach { listener ->
244
+ listener.onDestroy(activity)
245
+ }
246
+ if (delayLoadAppHandler != null) {
247
+ try {
248
+ // For the delay load case, we may enter a different call flow than react-native.
249
+ // For example, Activity stopped before delay load finished.
250
+ // We stop before the ReactActivityDelegate gets a chance to set up.
251
+ // In this case, we should catch the exceptions.
252
+ delegate.onDestroy()
253
+ } catch (e: Exception) {
254
+ Log.e(TAG, "Exception occurred during onDestroy with delayed app loading", e)
255
+ }
256
+ } else {
257
+ delegate.onDestroy()
258
+ }
259
+ }
226
260
  }
227
- return invokeDelegateMethod("onDestroy")
228
261
  }
229
262
 
230
263
  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -241,20 +274,28 @@ class ReactActivityDelegateWrapper(
241
274
  *
242
275
  * TODO (@bbarthec): fix it upstream?
243
276
  */
244
- if (!ReactNativeFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
245
- val reactContextListener = object : ReactInstanceEventListener {
246
- override fun onReactContextInitialized(context: ReactContext) {
247
- delegate.reactInstanceManager.removeReactInstanceEventListener(this)
248
- delegate.onActivityResult(requestCode, resultCode, data)
277
+ launchLifecycleScopeWithLock {
278
+ loadAppReady.await()
279
+ if (!ReactNativeFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
280
+ val reactContextListener = object : ReactInstanceEventListener {
281
+ override fun onReactContextInitialized(context: ReactContext) {
282
+ delegate.reactInstanceManager.removeReactInstanceEventListener(this)
283
+ delegate.onActivityResult(requestCode, resultCode, data)
284
+ }
249
285
  }
286
+ return@launchLifecycleScopeWithLock delegate.reactInstanceManager.addReactInstanceEventListener(
287
+ reactContextListener
288
+ )
250
289
  }
251
- return delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener)
252
- }
253
290
 
254
- delegate.onActivityResult(requestCode, resultCode, data)
291
+ delegate.onActivityResult(requestCode, resultCode, data)
292
+ }
255
293
  }
256
294
 
257
295
  override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
296
+ if (!loadAppReady.isCompleted) {
297
+ return false
298
+ }
258
299
  // if any of the handlers return true, intentionally consume the event instead of passing it
259
300
  // through to the delegate
260
301
  return reactActivityHandlers
@@ -263,6 +304,9 @@ class ReactActivityDelegateWrapper(
263
304
  }
264
305
 
265
306
  override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
307
+ if (!loadAppReady.isCompleted) {
308
+ return false
309
+ }
266
310
  // if any of the handlers return true, intentionally consume the event instead of passing it
267
311
  // through to the delegate
268
312
  return reactActivityHandlers
@@ -271,6 +315,9 @@ class ReactActivityDelegateWrapper(
271
315
  }
272
316
 
273
317
  override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
318
+ if (!loadAppReady.isCompleted) {
319
+ return false
320
+ }
274
321
  // if any of the handlers return true, intentionally consume the event instead of passing it
275
322
  // through to the delegate
276
323
  return reactActivityHandlers
@@ -279,6 +326,9 @@ class ReactActivityDelegateWrapper(
279
326
  }
280
327
 
281
328
  override fun onBackPressed(): Boolean {
329
+ if (!loadAppReady.isCompleted) {
330
+ return false
331
+ }
282
332
  val listenerResult = reactActivityLifecycleListeners
283
333
  .map(ReactActivityLifecycleListener::onBackPressed)
284
334
  .fold(false) { accu, current -> accu || current }
@@ -287,6 +337,9 @@ class ReactActivityDelegateWrapper(
287
337
  }
288
338
 
289
339
  override fun onNewIntent(intent: Intent?): Boolean {
340
+ if (!loadAppReady.isCompleted) {
341
+ return false
342
+ }
290
343
  val listenerResult = reactActivityLifecycleListeners
291
344
  .map { it.onNewIntent(intent) }
292
345
  .fold(false) { accu, current -> accu || current }
@@ -295,15 +348,24 @@ class ReactActivityDelegateWrapper(
295
348
  }
296
349
 
297
350
  override fun onWindowFocusChanged(hasFocus: Boolean) {
298
- delegate.onWindowFocusChanged(hasFocus)
351
+ launchLifecycleScopeWithLock {
352
+ loadAppReady.await()
353
+ delegate.onWindowFocusChanged(hasFocus)
354
+ }
299
355
  }
300
356
 
301
357
  override fun requestPermissions(permissions: Array<out String>?, requestCode: Int, listener: PermissionListener?) {
302
- delegate.requestPermissions(permissions, requestCode, listener)
358
+ launchLifecycleScopeWithLock {
359
+ loadAppReady.await()
360
+ delegate.requestPermissions(permissions, requestCode, listener)
361
+ }
303
362
  }
304
363
 
305
364
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, grantResults: IntArray?) {
306
- delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
365
+ launchLifecycleScopeWithLock {
366
+ loadAppReady.await()
367
+ delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
368
+ }
307
369
  }
308
370
 
309
371
  override fun getContext(): Context {
@@ -318,12 +380,19 @@ class ReactActivityDelegateWrapper(
318
380
  return invokeDelegateMethod("isFabricEnabled")
319
381
  }
320
382
 
383
+ override fun isWideColorGamutEnabled(): Boolean {
384
+ return invokeDelegateMethod("isWideColorGamutEnabled")
385
+ }
386
+
321
387
  override fun composeLaunchOptions(): Bundle? {
322
388
  return invokeDelegateMethod("composeLaunchOptions")
323
389
  }
324
390
 
325
391
  override fun onConfigurationChanged(newConfig: Configuration?) {
326
- delegate.onConfigurationChanged(newConfig)
392
+ launchLifecycleScopeWithLock {
393
+ loadAppReady.await()
394
+ delegate.onConfigurationChanged(newConfig)
395
+ }
327
396
  }
328
397
 
329
398
  //endregion
@@ -341,8 +410,9 @@ class ReactActivityDelegateWrapper(
341
410
  return method!!.invoke(delegate) as T
342
411
  }
343
412
 
413
+ @VisibleForTesting
344
414
  @Suppress("UNCHECKED_CAST")
345
- private fun <T, A> invokeDelegateMethod(
415
+ internal fun <T, A> invokeDelegateMethod(
346
416
  name: String,
347
417
  argTypes: Array<Class<*>>,
348
418
  args: Array<A>
@@ -356,5 +426,79 @@ class ReactActivityDelegateWrapper(
356
426
  return method!!.invoke(delegate, *args) as T
357
427
  }
358
428
 
429
+ private suspend fun loadAppImpl(appKey: String?, supportsDelayLoad: Boolean) {
430
+ // Give modules a chance to wrap the ReactRootView in a container ViewGroup. If some module
431
+ // wants to do this, we override the functionality of `loadApp` and call `setContentView` with
432
+ // the new container view instead.
433
+ val rootViewContainer = reactActivityHandlers.asSequence()
434
+ .mapNotNull { it.createReactRootViewContainer(activity) }
435
+ .firstOrNull()
436
+ if (rootViewContainer != null) {
437
+ val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
438
+ mReactDelegate.isAccessible = true
439
+ val reactDelegate = mReactDelegate[delegate] as ReactDelegate
440
+
441
+ reactDelegate.loadApp(appKey)
442
+ val reactRootView = reactDelegate.reactRootView
443
+ (reactRootView?.parent as? ViewGroup)?.removeView(reactRootView)
444
+ rootViewContainer.addView(reactRootView, ViewGroup.LayoutParams.MATCH_PARENT)
445
+ activity.setContentView(rootViewContainer)
446
+ reactActivityLifecycleListeners.forEach { listener ->
447
+ listener.onContentChanged(activity)
448
+ }
449
+ return
450
+ }
451
+
452
+ if (supportsDelayLoad) {
453
+ awaitDelayLoadAppWhenReady(delayLoadAppHandler)
454
+ invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
455
+ reactActivityLifecycleListeners.forEach { listener ->
456
+ listener.onContentChanged(activity)
457
+ }
458
+ return
459
+ }
460
+
461
+ invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
462
+ reactActivityLifecycleListeners.forEach { listener ->
463
+ listener.onContentChanged(activity)
464
+ }
465
+ }
466
+
467
+ private suspend fun awaitDelayLoadAppWhenReady(delayLoadAppHandler: DelayLoadAppHandler?) {
468
+ if (delayLoadAppHandler == null) {
469
+ return
470
+ }
471
+ suspendCoroutine { continuation ->
472
+ delayLoadAppHandler.whenReady {
473
+ Utils.assertMainThread()
474
+ continuation.resume(Unit)
475
+ }
476
+ }
477
+ }
478
+
479
+ private fun launchLifecycleScopeWithLock(
480
+ start: CoroutineStart = CoroutineStart.DEFAULT,
481
+ block: suspend CoroutineScope.() -> Unit
482
+ ) {
483
+ activity.lifecycleScope.launch(start = start) {
484
+ mutex.withLock {
485
+ block()
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Set the [loadAppReady] to completed state.
492
+ * This is only for unit tests when a test setups mocks and skips the [Activity] lifecycle.
493
+ */
494
+ @VisibleForTesting
495
+ internal fun setLoadAppReadyForTesting() {
496
+ loadAppReady.complete(Unit)
497
+ }
498
+
359
499
  //endregion
500
+
501
+ companion object {
502
+ private val TAG = ReactActivityDelegate::class.simpleName
503
+ }
360
504
  }
@@ -0,0 +1,282 @@
1
+ package expo.modules
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.content.Context
6
+ import com.facebook.react.ReactActivity
7
+ import com.facebook.react.ReactActivityDelegate
8
+ import com.facebook.react.ReactApplication
9
+ import com.facebook.react.ReactHost
10
+ import com.facebook.react.ReactNativeHost
11
+ import com.facebook.react.ReactRootView
12
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
13
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
14
+ import com.facebook.react.interfaces.fabric.ReactSurface
15
+ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
16
+ import com.facebook.soloader.SoLoader
17
+ import expo.modules.core.interfaces.Package
18
+ import expo.modules.core.interfaces.ReactActivityHandler
19
+ import expo.modules.core.interfaces.ReactActivityHandler.DelayLoadAppHandler
20
+ import expo.modules.core.interfaces.ReactActivityLifecycleListener
21
+ import io.mockk.MockKAnnotations
22
+ import io.mockk.every
23
+ import io.mockk.impl.annotations.RelaxedMockK
24
+ import io.mockk.mockk
25
+ import io.mockk.mockkObject
26
+ import io.mockk.mockkStatic
27
+ import io.mockk.slot
28
+ import io.mockk.spyk
29
+ import io.mockk.unmockkAll
30
+ import io.mockk.verify
31
+ import kotlinx.coroutines.Dispatchers
32
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
33
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
34
+ import kotlinx.coroutines.test.runTest
35
+ import kotlinx.coroutines.test.setMain
36
+ import org.junit.After
37
+ import org.junit.Before
38
+ import org.junit.Test
39
+ import org.junit.runner.RunWith
40
+ import org.robolectric.Robolectric
41
+ import org.robolectric.RobolectricTestRunner
42
+ import org.robolectric.android.controller.ActivityController
43
+ import org.robolectric.annotation.Config
44
+
45
+ @RunWith(RobolectricTestRunner::class)
46
+ @Config(application = MockApplication::class)
47
+ internal class ReactActivityDelegateWrapperDelayLoadTest {
48
+ @RelaxedMockK
49
+ private lateinit var delayLoadAppHandler: DelayLoadAppHandler
50
+
51
+ private lateinit var mockPackageWithDelay: MockPackageWithDelayHandler
52
+ private lateinit var mockPackageWithoutDelay: MockPackageWithoutDelayHandler
53
+ private lateinit var activityController: ActivityController<MockActivity>
54
+ private val activity: MockActivity
55
+ get() = activityController.get()
56
+
57
+ @OptIn(ExperimentalCoroutinesApi::class)
58
+ @Before
59
+ fun setUp() {
60
+ SoLoader.setInTestMode()
61
+ mockkObject(ExpoModulesPackage.Companion)
62
+ mockkStatic(ReactNativeFeatureFlags::class)
63
+ every { ReactNativeFeatureFlags.enableBridgelessArchitecture() } returns true
64
+ every { ReactNativeFeatureFlags.enableFabricRenderer() } returns true
65
+ Dispatchers.setMain(UnconfinedTestDispatcher())
66
+
67
+ MockKAnnotations.init(this)
68
+
69
+ mockPackageWithDelay = MockPackageWithDelayHandler(delayLoadAppHandler)
70
+ mockPackageWithoutDelay = MockPackageWithoutDelayHandler()
71
+ }
72
+
73
+ @After
74
+ fun tearDown() {
75
+ unmockkAll()
76
+ }
77
+
78
+ @Test
79
+ fun `should proceed loadApp immediately when no delayLoadAppHandler`() = runTest {
80
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithoutDelay)
81
+
82
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
83
+ .also {
84
+ val activity = it.get()
85
+ (activity.application as MockApplication).bindCurrentActivity(activity)
86
+ }
87
+ .setup()
88
+ val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
89
+ verify { spyDelegateWrapper.invokeDelegateMethod("loadApp", arrayOf(String::class.java), arrayOf("main")) }
90
+ }
91
+
92
+ @Test
93
+ fun `should block loadApp until delayLoadAppHandler finished`() = runTest {
94
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithDelay)
95
+
96
+ val callbackSlot = slot<Runnable>()
97
+ every { delayLoadAppHandler.whenReady(capture(callbackSlot)) } answers {
98
+ // Don't call the callback immediately to simulate delay
99
+ }
100
+
101
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
102
+ .also {
103
+ val activity = it.get()
104
+ (activity.application as MockApplication).bindCurrentActivity(activity)
105
+ }
106
+ .setup()
107
+
108
+ val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
109
+
110
+ verify(exactly = 0) { spyDelegateWrapper.invokeDelegateMethod("loadApp", arrayOf(String::class.java), arrayOf("main")) }
111
+ callbackSlot.captured.run()
112
+ verify(exactly = 1) { spyDelegateWrapper.invokeDelegateMethod("loadApp", arrayOf(String::class.java), arrayOf("main")) }
113
+ }
114
+
115
+ @Test
116
+ fun `should call lifecycle methods in correct order with delay load`() = runTest {
117
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithDelay)
118
+
119
+ val callbackSlot = slot<Runnable>()
120
+ every { delayLoadAppHandler.whenReady(capture(callbackSlot)) } answers {
121
+ // Don't call the callback immediately to simulate delay
122
+ }
123
+
124
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
125
+ .also {
126
+ val activity = it.get()
127
+ (activity.application as MockApplication).bindCurrentActivity(activity)
128
+ }
129
+ .setup()
130
+ val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
131
+ val spyDelegate = spyDelegateWrapper.delegate
132
+
133
+ verify(exactly = 1) { spyDelegateWrapper.onCreate(any()) }
134
+ verify(exactly = 1) { spyDelegateWrapper.onResume() }
135
+ verify(exactly = 0) { spyDelegate.onResume() }
136
+
137
+ callbackSlot.captured.run()
138
+ verify(exactly = 1) { spyDelegateWrapper.onResume() }
139
+ verify(exactly = 1) { spyDelegate.onResume() }
140
+ verify(exactly = 0) { spyDelegateWrapper.onPause() }
141
+ verify(exactly = 0) { spyDelegateWrapper.onDestroy() }
142
+ verify(exactly = 0) { spyDelegate.onPause() }
143
+ verify(exactly = 0) { spyDelegate.onDestroy() }
144
+
145
+ activityController.pause().stop().destroy()
146
+ verify(exactly = 1) { spyDelegateWrapper.onPause() }
147
+ verify(exactly = 1) { spyDelegateWrapper.onDestroy() }
148
+ verify(exactly = 1) { spyDelegate.onPause() }
149
+ verify(exactly = 1) { spyDelegate.onDestroy() }
150
+ }
151
+
152
+ @Test
153
+ fun `should have normal lifecycle when no delayLoadHandler`() = runTest {
154
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithoutDelay)
155
+
156
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
157
+ .also {
158
+ val activity = it.get()
159
+ (activity.application as MockApplication).bindCurrentActivity(activity)
160
+ }
161
+ .setup()
162
+ val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
163
+ val spyDelegate = spyDelegateWrapper.delegate
164
+
165
+ verify(exactly = 1) { spyDelegateWrapper.onCreate(any()) }
166
+ verify(exactly = 1) { spyDelegateWrapper.onResume() }
167
+ verify(exactly = 1) { spyDelegate.onResume() }
168
+
169
+ activityController.pause().stop().destroy()
170
+ verify(exactly = 1) { spyDelegateWrapper.onPause() }
171
+ verify(exactly = 1) { spyDelegateWrapper.onDestroy() }
172
+ verify(exactly = 1) { spyDelegate.onPause() }
173
+ verify(exactly = 1) { spyDelegate.onDestroy() }
174
+ }
175
+
176
+ @Test
177
+ fun `should cancel pending resume if activity destroy before delay load finished`() = runTest {
178
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithDelay)
179
+
180
+ val callbackSlot = slot<Runnable>()
181
+ every { delayLoadAppHandler.whenReady(capture(callbackSlot)) } answers {
182
+ // Don't call the callback immediately to simulate delay
183
+ }
184
+
185
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
186
+ .also {
187
+ val activity = it.get()
188
+ (activity.application as MockApplication).bindCurrentActivity(activity)
189
+ }
190
+ .setup()
191
+ val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
192
+ val spyDelegate = spyDelegateWrapper.delegate
193
+
194
+ verify(exactly = 1) { spyDelegateWrapper.onCreate(any()) }
195
+ verify(exactly = 1) { spyDelegateWrapper.onResume() }
196
+ verify(exactly = 0) { spyDelegate.onResume() }
197
+
198
+ activityController.pause().stop().destroy()
199
+ callbackSlot.captured.run()
200
+ verify(exactly = 0) { spyDelegate.onResume() }
201
+ }
202
+ }
203
+
204
+ internal class MockPackageWithDelayHandler(delayHandler: DelayLoadAppHandler) : Package {
205
+ private val reactActivityLifecycleListener = mockk<ReactActivityLifecycleListener>(relaxed = true)
206
+ private val reactActivityHandler = mockk<ReactActivityHandler>(relaxed = true)
207
+
208
+ init {
209
+ every { reactActivityHandler.createReactRootViewContainer(any()) } returns null
210
+ every { reactActivityHandler.onDidCreateReactActivityDelegate(any(), any()) } returns null
211
+ every { reactActivityHandler.getDelayLoadAppHandler(any(), any()) } returns delayHandler
212
+ }
213
+
214
+ override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener> {
215
+ return listOf(reactActivityLifecycleListener)
216
+ }
217
+
218
+ override fun createReactActivityHandlers(activityContext: Context?): List<ReactActivityHandler> {
219
+ return listOf(reactActivityHandler)
220
+ }
221
+ }
222
+
223
+ internal class MockPackageWithoutDelayHandler : Package {
224
+ private val reactActivityLifecycleListener = mockk<ReactActivityLifecycleListener>(relaxed = true)
225
+ private val reactActivityHandler = mockk<ReactActivityHandler>(relaxed = true)
226
+
227
+ init {
228
+ every { reactActivityHandler.createReactRootViewContainer(any()) } returns null
229
+ every { reactActivityHandler.onDidCreateReactActivityDelegate(any(), any()) } returns null
230
+ every { reactActivityHandler.getDelayLoadAppHandler(any(), any()) } returns null
231
+ }
232
+
233
+ override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener> {
234
+ return listOf(reactActivityLifecycleListener)
235
+ }
236
+
237
+ override fun createReactActivityHandlers(activityContext: Context?): List<ReactActivityHandler> {
238
+ return listOf(reactActivityHandler)
239
+ }
240
+ }
241
+
242
+ internal class MockApplication : Application(), ReactApplication {
243
+ private var currentActivity: Activity? = null
244
+
245
+ override fun onCreate() {
246
+ super.onCreate()
247
+ setTheme(androidx.appcompat.R.style.Theme_AppCompat)
248
+ }
249
+
250
+ internal fun bindCurrentActivity(activity: Activity?) {
251
+ currentActivity = activity
252
+ }
253
+
254
+ override val reactNativeHost: ReactNativeHost = mockk<ReactNativeHost>(relaxed = true)
255
+
256
+ override val reactHost: ReactHost by lazy {
257
+ mockk<ReactHost>(relaxed = true)
258
+ .also {
259
+ val mockReactSurface = mockk<ReactSurface>(relaxed = true)
260
+ every { mockReactSurface.view } returns ReactRootView(currentActivity)
261
+ every { it.createSurface(any(), any(), any()) } returns mockReactSurface
262
+ }
263
+ }
264
+ }
265
+
266
+ internal class MockActivity : ReactActivity() {
267
+ override fun getMainComponentName(): String = "main"
268
+
269
+ override fun createReactActivityDelegate(): ReactActivityDelegate = spyk(
270
+ ReactActivityDelegateWrapper(
271
+ this,
272
+ BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
273
+ spyk(
274
+ object : DefaultReactActivityDelegate(
275
+ this,
276
+ mainComponentName,
277
+ fabricEnabled
278
+ ) {}
279
+ )
280
+ )
281
+ )
282
+ }
@@ -20,9 +20,9 @@ import org.junit.Before
20
20
  import org.junit.Test
21
21
 
22
22
  internal class ReactActivityDelegateWrapperTest {
23
- lateinit var mockPackage0: MockPackage
23
+ private lateinit var mockPackage0: MockPackage
24
24
 
25
- lateinit var mockPackage1: MockPackage
25
+ private lateinit var mockPackage1: MockPackage
26
26
 
27
27
  @RelaxedMockK
28
28
  lateinit var activity: ReactActivity
@@ -47,6 +47,7 @@ internal class ReactActivityDelegateWrapperTest {
47
47
  @Test
48
48
  fun `onBackPressed should call each handler's callback just once`() {
49
49
  val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
50
+ delegateWrapper.setLoadAppReadyForTesting()
50
51
  every { mockPackage0.reactActivityLifecycleListener.onBackPressed() } returns true
51
52
 
52
53
  delegateWrapper.onBackPressed()
@@ -59,6 +60,7 @@ internal class ReactActivityDelegateWrapperTest {
59
60
  @Test
60
61
  fun `onBackPressed should return true if someone returns true`() {
61
62
  val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
63
+ delegateWrapper.setLoadAppReadyForTesting()
62
64
  every { mockPackage0.reactActivityLifecycleListener.onBackPressed() } returns false
63
65
  every { mockPackage1.reactActivityLifecycleListener.onBackPressed() } returns true
64
66
  every { delegate.onBackPressed() } returns false
@@ -71,6 +73,7 @@ internal class ReactActivityDelegateWrapperTest {
71
73
  fun `onNewIntent should call each handler's callback just once`() {
72
74
  val intent = mockk<Intent>()
73
75
  val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
76
+ delegateWrapper.setLoadAppReadyForTesting()
74
77
  every { mockPackage0.reactActivityLifecycleListener.onNewIntent(intent) } returns false
75
78
  every { mockPackage1.reactActivityLifecycleListener.onNewIntent(intent) } returns true
76
79
  every { delegate.onNewIntent(intent) } returns false
@@ -86,6 +89,7 @@ internal class ReactActivityDelegateWrapperTest {
86
89
  fun `onNewIntent should return true if someone returns true`() {
87
90
  val intent = mockk<Intent>()
88
91
  val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
92
+ delegateWrapper.setLoadAppReadyForTesting()
89
93
  every { mockPackage0.reactActivityLifecycleListener.onNewIntent(intent) } returns false
90
94
  every { mockPackage1.reactActivityLifecycleListener.onNewIntent(intent) } returns true
91
95
  every { delegate.onNewIntent(intent) } returns false
@@ -97,7 +101,7 @@ internal class ReactActivityDelegateWrapperTest {
97
101
 
98
102
  internal class MockPackage : Package {
99
103
  val reactActivityLifecycleListener = mockk<ReactActivityLifecycleListener>(relaxed = true)
100
- val reactActivityHandler = mockk<ReactActivityHandler>(relaxed = true)
104
+ private val reactActivityHandler = mockk<ReactActivityHandler>(relaxed = true)
101
105
 
102
106
  override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener> {
103
107
  return listOf(reactActivityLifecycleListener)
@@ -1,8 +1,8 @@
1
1
  {
2
- "@expo/fingerprint": "0.13.2-canary-20250630-547cd82",
3
- "@expo/metro-runtime": "6.0.0-canary-20250630-547cd82",
2
+ "@expo/fingerprint": "0.13.5-canary-20250709-136b77f",
3
+ "@expo/metro-runtime": "6.0.0-canary-20250709-136b77f",
4
4
  "@expo/vector-icons": "^14.1.0",
5
- "@expo/ui": "0.1.1-canary-20250630-547cd82",
5
+ "@expo/ui": "0.1.1-canary-20250709-136b77f",
6
6
  "@react-native-async-storage/async-storage": "2.2.0",
7
7
  "@react-native-community/datetimepicker": "8.4.1",
8
8
  "@react-native-masked-view/masked-view": "0.3.2",
@@ -12,86 +12,86 @@
12
12
  "@react-native-picker/picker": "2.11.1",
13
13
  "@react-native-segmented-control/segmented-control": "2.5.7",
14
14
  "@stripe/stripe-react-native": "0.45.0",
15
- "eslint-config-expo": "9.2.1-canary-20250630-547cd82",
15
+ "eslint-config-expo": "9.2.1-canary-20250709-136b77f",
16
16
  "expo-analytics-amplitude": "~11.3.0",
17
17
  "expo-app-auth": "~11.1.0",
18
18
  "expo-app-loader-provider": "~8.0.0",
19
- "expo-apple-authentication": "7.2.5-canary-20250630-547cd82",
20
- "expo-application": "6.1.5-canary-20250630-547cd82",
21
- "expo-asset": "11.2.0-canary-20250630-547cd82",
22
- "expo-audio": "0.4.8-canary-20250630-547cd82",
23
- "expo-auth-session": "6.1.6-canary-20250630-547cd82",
24
- "expo-av": "15.1.7-canary-20250630-547cd82",
25
- "expo-background-fetch": "13.1.6-canary-20250630-547cd82",
26
- "expo-background-task": "0.2.8-canary-20250630-547cd82",
27
- "expo-battery": "9.1.5-canary-20250630-547cd82",
28
- "expo-blur": "14.1.6-canary-20250630-547cd82",
29
- "expo-brightness": "13.1.5-canary-20250630-547cd82",
30
- "expo-build-properties": "0.14.7-canary-20250630-547cd82",
31
- "expo-calendar": "14.1.5-canary-20250630-547cd82",
32
- "expo-camera": "16.1.10-canary-20250630-547cd82",
33
- "expo-cellular": "7.1.5-canary-20250630-547cd82",
34
- "expo-checkbox": "4.1.5-canary-20250630-547cd82",
35
- "expo-clipboard": "7.1.5-canary-20250630-547cd82",
36
- "expo-constants": "17.1.7-canary-20250630-547cd82",
37
- "expo-contacts": "14.2.6-canary-20250630-547cd82",
38
- "expo-crypto": "14.1.6-canary-20250630-547cd82",
39
- "expo-dev-client": "5.1.9-canary-20250630-547cd82",
40
- "expo-device": "7.1.5-canary-20250630-547cd82",
41
- "expo-document-picker": "14.0.0-canary-20250630-547cd82",
42
- "expo-file-system": "18.2.0-canary-20250630-547cd82",
43
- "expo-font": "13.3.2-canary-20250630-547cd82",
44
- "expo-gl": "15.1.7-canary-20250630-547cd82",
19
+ "expo-apple-authentication": "7.2.5-canary-20250709-136b77f",
20
+ "expo-application": "6.1.6-canary-20250709-136b77f",
21
+ "expo-asset": "11.2.0-canary-20250709-136b77f",
22
+ "expo-audio": "0.4.9-canary-20250709-136b77f",
23
+ "expo-auth-session": "6.1.6-canary-20250709-136b77f",
24
+ "expo-av": "15.1.8-canary-20250709-136b77f",
25
+ "expo-background-fetch": "13.1.7-canary-20250709-136b77f",
26
+ "expo-background-task": "0.2.9-canary-20250709-136b77f",
27
+ "expo-battery": "9.1.5-canary-20250709-136b77f",
28
+ "expo-blur": "14.1.6-canary-20250709-136b77f",
29
+ "expo-brightness": "13.1.5-canary-20250709-136b77f",
30
+ "expo-build-properties": "0.15.0-canary-20250709-136b77f",
31
+ "expo-calendar": "14.1.5-canary-20250709-136b77f",
32
+ "expo-camera": "16.1.11-canary-20250709-136b77f",
33
+ "expo-cellular": "7.1.6-canary-20250709-136b77f",
34
+ "expo-checkbox": "4.1.5-canary-20250709-136b77f",
35
+ "expo-clipboard": "7.1.6-canary-20250709-136b77f",
36
+ "expo-constants": "17.1.8-canary-20250709-136b77f",
37
+ "expo-contacts": "14.2.6-canary-20250709-136b77f",
38
+ "expo-crypto": "14.1.6-canary-20250709-136b77f",
39
+ "expo-dev-client": "5.1.9-canary-20250709-136b77f",
40
+ "expo-device": "7.1.5-canary-20250709-136b77f",
41
+ "expo-document-picker": "14.0.0-canary-20250709-136b77f",
42
+ "expo-file-system": "18.2.0-canary-20250709-136b77f",
43
+ "expo-font": "13.4.0-canary-20250709-136b77f",
44
+ "expo-gl": "15.1.8-canary-20250709-136b77f",
45
45
  "expo-google-app-auth": "~8.3.0",
46
- "expo-haptics": "14.1.5-canary-20250630-547cd82",
47
- "expo-image": "2.3.1-canary-20250630-547cd82",
48
- "expo-image-loader": "5.1.1-canary-20250630-547cd82",
49
- "expo-image-manipulator": "13.1.8-canary-20250630-547cd82",
50
- "expo-image-picker": "17.0.0-canary-20250630-547cd82",
51
- "expo-intent-launcher": "12.1.6-canary-20250630-547cd82",
52
- "expo-insights": "0.9.4-canary-20250630-547cd82",
53
- "expo-keep-awake": "14.1.5-canary-20250630-547cd82",
54
- "expo-linear-gradient": "14.1.6-canary-20250630-547cd82",
55
- "expo-linking": "7.1.6-canary-20250630-547cd82",
56
- "expo-local-authentication": "16.1.0-canary-20250630-547cd82",
57
- "expo-localization": "16.1.6-canary-20250630-547cd82",
58
- "expo-location": "18.1.6-canary-20250630-547cd82",
59
- "expo-mail-composer": "14.1.6-canary-20250630-547cd82",
60
- "expo-manifests": "0.16.6-canary-20250630-547cd82",
61
- "expo-maps": "0.10.1-canary-20250630-547cd82",
62
- "expo-media-library": "17.1.8-canary-20250630-547cd82",
63
- "expo-mesh-gradient": "0.4.0-canary-20250630-547cd82",
64
- "expo-module-template": "10.16.7-canary-20250630-547cd82",
65
- "expo-modules-core": "2.5.0-canary-20250630-547cd82",
66
- "expo-navigation-bar": "4.2.7-canary-20250630-547cd82",
67
- "expo-network": "7.1.6-canary-20250630-547cd82",
68
- "expo-notifications": "0.31.4-canary-20250630-547cd82",
69
- "expo-print": "14.1.5-canary-20250630-547cd82",
70
- "expo-live-photo": "0.1.5-canary-20250630-547cd82",
71
- "expo-router": "5.2.0-canary-20250630-547cd82",
72
- "expo-screen-capture": "7.2.0-canary-20250630-547cd82",
73
- "expo-screen-orientation": "8.1.8-canary-20250630-547cd82",
74
- "expo-secure-store": "14.2.4-canary-20250630-547cd82",
75
- "expo-sensors": "14.1.5-canary-20250630-547cd82",
76
- "expo-sharing": "13.1.6-canary-20250630-547cd82",
77
- "expo-sms": "13.1.5-canary-20250630-547cd82",
78
- "expo-speech": "13.2.0-canary-20250630-547cd82",
79
- "expo-splash-screen": "0.30.10-canary-20250630-547cd82",
80
- "expo-sqlite": "15.2.13-canary-20250630-547cd82",
81
- "expo-status-bar": "2.2.4-canary-20250630-547cd82",
82
- "expo-store-review": "8.1.6-canary-20250630-547cd82",
83
- "expo-symbols": "0.4.6-canary-20250630-547cd82",
84
- "expo-system-ui": "5.0.10-canary-20250630-547cd82",
85
- "expo-task-manager": "13.1.6-canary-20250630-547cd82",
86
- "expo-tracking-transparency": "5.2.5-canary-20250630-547cd82",
87
- "expo-updates": "0.28.16-canary-20250630-547cd82",
88
- "expo-video-thumbnails": "9.1.4-canary-20250630-547cd82",
89
- "expo-video": "2.1.10-canary-20250630-547cd82",
90
- "expo-web-browser": "14.1.7-canary-20250630-547cd82",
91
- "jest-expo": "54.0.0-canary-20250630-547cd82",
46
+ "expo-haptics": "14.1.5-canary-20250709-136b77f",
47
+ "expo-image": "2.4.0-canary-20250709-136b77f",
48
+ "expo-image-loader": "5.1.1-canary-20250709-136b77f",
49
+ "expo-image-manipulator": "13.1.8-canary-20250709-136b77f",
50
+ "expo-image-picker": "17.0.0-canary-20250709-136b77f",
51
+ "expo-intent-launcher": "12.1.6-canary-20250709-136b77f",
52
+ "expo-insights": "0.9.4-canary-20250709-136b77f",
53
+ "expo-keep-awake": "14.1.5-canary-20250709-136b77f",
54
+ "expo-linear-gradient": "14.1.6-canary-20250709-136b77f",
55
+ "expo-linking": "7.1.8-canary-20250709-136b77f",
56
+ "expo-local-authentication": "16.1.0-canary-20250709-136b77f",
57
+ "expo-localization": "16.1.7-canary-20250709-136b77f",
58
+ "expo-location": "18.1.7-canary-20250709-136b77f",
59
+ "expo-mail-composer": "14.1.6-canary-20250709-136b77f",
60
+ "expo-manifests": "0.17.0-canary-20250709-136b77f",
61
+ "expo-maps": "0.10.1-canary-20250709-136b77f",
62
+ "expo-media-library": "17.1.8-canary-20250709-136b77f",
63
+ "expo-mesh-gradient": "0.4.0-canary-20250709-136b77f",
64
+ "expo-module-template": "10.16.8-canary-20250709-136b77f",
65
+ "expo-modules-core": "2.5.0-canary-20250709-136b77f",
66
+ "expo-navigation-bar": "4.2.8-canary-20250709-136b77f",
67
+ "expo-network": "7.1.6-canary-20250709-136b77f",
68
+ "expo-notifications": "0.31.5-canary-20250709-136b77f",
69
+ "expo-print": "14.1.5-canary-20250709-136b77f",
70
+ "expo-live-photo": "0.1.5-canary-20250709-136b77f",
71
+ "expo-router": "5.2.0-canary-20250709-136b77f",
72
+ "expo-screen-capture": "7.2.0-canary-20250709-136b77f",
73
+ "expo-screen-orientation": "8.1.8-canary-20250709-136b77f",
74
+ "expo-secure-store": "14.2.4-canary-20250709-136b77f",
75
+ "expo-sensors": "14.1.5-canary-20250709-136b77f",
76
+ "expo-sharing": "13.1.6-canary-20250709-136b77f",
77
+ "expo-sms": "13.1.5-canary-20250709-136b77f",
78
+ "expo-speech": "13.2.0-canary-20250709-136b77f",
79
+ "expo-splash-screen": "0.30.11-canary-20250709-136b77f",
80
+ "expo-sqlite": "15.2.15-canary-20250709-136b77f",
81
+ "expo-status-bar": "2.2.4-canary-20250709-136b77f",
82
+ "expo-store-review": "8.1.6-canary-20250709-136b77f",
83
+ "expo-symbols": "0.4.6-canary-20250709-136b77f",
84
+ "expo-system-ui": "5.0.11-canary-20250709-136b77f",
85
+ "expo-task-manager": "13.1.7-canary-20250709-136b77f",
86
+ "expo-tracking-transparency": "5.2.5-canary-20250709-136b77f",
87
+ "expo-updates": "0.29.0-canary-20250709-136b77f",
88
+ "expo-video-thumbnails": "9.1.4-canary-20250709-136b77f",
89
+ "expo-video": "2.1.10-canary-20250709-136b77f",
90
+ "expo-web-browser": "14.1.7-canary-20250709-136b77f",
91
+ "jest-expo": "54.0.0-canary-20250709-136b77f",
92
92
  "react": "19.1.0",
93
93
  "react-dom": "19.1.0",
94
- "react-native": "0.80.0",
94
+ "react-native": "0.80.1",
95
95
  "react-native-web": "~0.20.0",
96
96
  "react-native-edge-to-edge": "1.6.0",
97
97
  "react-native-gesture-handler": "~2.26.0",
@@ -105,7 +105,7 @@
105
105
  "react-native-view-shot": "4.0.3",
106
106
  "react-native-webview": "13.13.5",
107
107
  "sentry-expo": "~7.0.0",
108
- "unimodules-app-loader": "5.1.4-canary-20250630-547cd82",
108
+ "unimodules-app-loader": "5.1.4-canary-20250709-136b77f",
109
109
  "unimodules-image-loader-interface": "~6.1.0",
110
110
  "@shopify/react-native-skia": "v2.0.0-next.4",
111
111
  "@shopify/flash-list": "1.8.3",
@@ -0,0 +1 @@
1
+ export * from '@expo/config/paths';
@@ -0,0 +1 @@
1
+ module.exports = require('@expo/config/paths');
@@ -9,8 +9,6 @@ import ReactAppDependencyProvider
9
9
  by forwarding `UIApplicationDelegate` events to the subscribers.
10
10
 
11
11
  Keep functions and markers in sync with https://developer.apple.com/documentation/uikit/uiapplicationdelegate
12
-
13
- TODO vonovak check macOS support once RN macOS 78 goes out
14
12
  */
15
13
  @objc(EXExpoAppDelegate)
16
14
  open class ExpoAppDelegate: NSObject, ReactNativeFactoryProvider, UIApplicationDelegate {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "54.0.0-canary-20250630-547cd82",
3
+ "version": "54.0.0-canary-20250709-136b77f",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -30,8 +30,7 @@
30
30
  "metro-config.js",
31
31
  "metro-config.d.ts",
32
32
  "dom",
33
- "config.js",
34
- "config.d.ts",
33
+ "config",
35
34
  "config-plugins.js",
36
35
  "config-plugins.d.ts",
37
36
  "devtools.js",
@@ -74,20 +73,20 @@
74
73
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
75
74
  "dependencies": {
76
75
  "@babel/runtime": "^7.20.0",
77
- "@expo/cli": "1.0.0-canary-20250630-547cd82",
78
- "@expo/config": "11.0.11-canary-20250630-547cd82",
79
- "@expo/config-plugins": "10.1.0-canary-20250630-547cd82",
80
- "@expo/fingerprint": "0.13.2-canary-20250630-547cd82",
81
- "@expo/metro-config": "0.21.0-canary-20250630-547cd82",
76
+ "@expo/cli": "1.0.0-canary-20250709-136b77f",
77
+ "@expo/config": "11.0.14-canary-20250709-136b77f",
78
+ "@expo/config-plugins": "10.0.4-canary-20250709-136b77f",
79
+ "@expo/fingerprint": "0.13.5-canary-20250709-136b77f",
80
+ "@expo/metro-config": "0.21.0-canary-20250709-136b77f",
82
81
  "@expo/vector-icons": "^14.0.0",
83
- "babel-preset-expo": "13.3.0-canary-20250630-547cd82",
84
- "expo-asset": "11.2.0-canary-20250630-547cd82",
85
- "expo-constants": "17.1.7-canary-20250630-547cd82",
86
- "expo-file-system": "18.2.0-canary-20250630-547cd82",
87
- "expo-font": "13.3.2-canary-20250630-547cd82",
88
- "expo-keep-awake": "14.1.5-canary-20250630-547cd82",
89
- "expo-modules-autolinking": "2.1.13-canary-20250630-547cd82",
90
- "expo-modules-core": "2.5.0-canary-20250630-547cd82",
82
+ "babel-preset-expo": "13.3.0-canary-20250709-136b77f",
83
+ "expo-asset": "11.2.0-canary-20250709-136b77f",
84
+ "expo-constants": "17.1.8-canary-20250709-136b77f",
85
+ "expo-file-system": "18.2.0-canary-20250709-136b77f",
86
+ "expo-font": "13.4.0-canary-20250709-136b77f",
87
+ "expo-keep-awake": "14.1.5-canary-20250709-136b77f",
88
+ "expo-modules-autolinking": "2.1.15-canary-20250709-136b77f",
89
+ "expo-modules-core": "2.5.0-canary-20250709-136b77f",
91
90
  "pretty-format": "^29.7.0",
92
91
  "react-native-edge-to-edge": "1.6.0",
93
92
  "whatwg-url-without-unicode": "8.0.0-3"
@@ -96,16 +95,16 @@
96
95
  "@types/node": "^22.14.0",
97
96
  "@types/react": "~19.0.10",
98
97
  "@types/react-test-renderer": "^19.1.0",
99
- "expo-module-scripts": "4.1.9-canary-20250630-547cd82",
98
+ "expo-module-scripts": "4.1.10-canary-20250709-136b77f",
100
99
  "react": "19.1.0",
101
100
  "react-dom": "19.1.0",
102
- "react-native": "0.80.0",
101
+ "react-native": "0.80.1",
103
102
  "web-streams-polyfill": "^3.3.2",
104
103
  "ws": "^8.18.0"
105
104
  },
106
105
  "peerDependencies": {
107
- "@expo/dom-webview": "0.1.6-canary-20250630-547cd82",
108
- "@expo/metro-runtime": "6.0.0-canary-20250630-547cd82",
106
+ "@expo/dom-webview": "0.1.6-canary-20250709-136b77f",
107
+ "@expo/metro-runtime": "6.0.0-canary-20250709-136b77f",
109
108
  "metro-runtime": "*",
110
109
  "react": "*",
111
110
  "react-refresh": "*",
File without changes
File without changes