expo 53.0.17 → 53.0.18

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 = '53.0.17'
35
+ version = '53.0.18'
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 "53.0.17"
46
+ versionName "53.0.18"
47
47
  consumerProguardFiles("proguard-rules.pro")
48
48
  }
49
49
  testOptions {
@@ -29,7 +29,11 @@ import expo.modules.core.interfaces.ReactActivityHandler.DelayLoadAppHandler
29
29
  import expo.modules.core.interfaces.ReactActivityLifecycleListener
30
30
  import expo.modules.kotlin.Utils
31
31
  import expo.modules.rncompatibility.ReactNativeFeatureFlags
32
+ import kotlinx.coroutines.CancellationException
33
+ import kotlinx.coroutines.CompletableDeferred
34
+ import kotlinx.coroutines.CoroutineScope
32
35
  import kotlinx.coroutines.CoroutineStart
36
+ import kotlinx.coroutines.Dispatchers
33
37
  import kotlinx.coroutines.launch
34
38
  import java.lang.reflect.Field
35
39
  import java.lang.reflect.Method
@@ -63,10 +67,17 @@ class ReactActivityDelegateWrapper(
63
67
  }
64
68
 
65
69
  /**
66
- * When the app delay for `loadApp`, the React Native lifecycle will be disrupted.
67
- * This flag indicates we should emit `onResume` after `loadApp`.
70
+ * A deferred that indicates when the app loading is ready
68
71
  */
69
- private var shouldEmitPendingResume = false
72
+ private val loadAppReady = CompletableDeferred<Unit>()
73
+
74
+ /**
75
+ * A [CoroutineScope] that binds its lifecycle as [ReactActivityDelegateWrapper].
76
+ * This is used for [onDestroy] because we need a longer lifecycle scope to call [onDestroy]
77
+ */
78
+ private val applicationCoroutineScope: CoroutineScope by lazy {
79
+ CoroutineScope(Dispatchers.Main)
80
+ }
70
81
 
71
82
  //region ReactActivityDelegate
72
83
 
@@ -129,6 +140,7 @@ class ReactActivityDelegateWrapper(
129
140
 
130
141
  activity.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
131
142
  awaitDelayLoadAppWhenReady(delayLoadAppHandler)
143
+ loadAppReady.complete(Unit)
132
144
 
133
145
  if (VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled) {
134
146
  activity.window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
@@ -172,69 +184,75 @@ class ReactActivityDelegateWrapper(
172
184
  }
173
185
 
174
186
  override fun onResume() {
175
- if (shouldEmitPendingResume) {
176
- return
177
- }
178
- delegate.onResume()
179
- reactActivityLifecycleListeners.forEach { listener ->
180
- listener.onResume(activity)
187
+ activity.lifecycleScope.launch {
188
+ loadAppReady.await()
189
+ delegate.onResume()
190
+ reactActivityLifecycleListeners.forEach { listener ->
191
+ listener.onResume(activity)
192
+ }
181
193
  }
182
194
  }
183
195
 
184
196
  override fun onPause() {
185
- // If app is stopped before the delayed `loadApp`, we should cancel the pending resume
186
- // and avoid propagating the pause event because the state was never resumed.
187
- if (shouldEmitPendingResume) {
188
- shouldEmitPendingResume = false
189
- return
190
- }
191
- reactActivityLifecycleListeners.forEach { listener ->
192
- listener.onPause(activity)
193
- }
194
- if (delayLoadAppHandler != null) {
195
- try {
196
- // For the delay load case, we may enter a different call flow than react-native.
197
- // For example, Activity stopped before delay load finished.
198
- // We stop before the ReactActivityDelegate gets a chance to set up.
199
- // In this case, we should catch the exceptions.
197
+ activity.lifecycleScope.launch {
198
+ if (!loadAppReady.isCompleted) {
199
+ loadAppReady.completeExceptionally(CancellationException("Activity paused before app loaded"))
200
+ }
201
+ loadAppReady.await()
202
+ reactActivityLifecycleListeners.forEach { listener ->
203
+ listener.onPause(activity)
204
+ }
205
+ if (delayLoadAppHandler != null) {
206
+ try {
207
+ // For the delay load case, we may enter a different call flow than react-native.
208
+ // For example, Activity stopped before delay load finished.
209
+ // We stop before the ReactActivityDelegate gets a chance to set up.
210
+ // In this case, we should catch the exceptions.
211
+ delegate.onPause()
212
+ } catch (e: Exception) {
213
+ Log.e(TAG, "Exception occurred during onPause with delayed app loading", e)
214
+ }
215
+ } else {
200
216
  delegate.onPause()
201
- } catch (e: Exception) {
202
- Log.e(TAG, "Exception occurred during onPause with delayed app loading", e)
203
217
  }
204
- } else {
205
- delegate.onPause()
206
218
  }
207
219
  }
208
220
 
209
221
  override fun onUserLeaveHint() {
210
- reactActivityLifecycleListeners.forEach { listener ->
211
- listener.onUserLeaveHint(activity)
222
+ activity.lifecycleScope.launch {
223
+ loadAppReady.await()
224
+ reactActivityLifecycleListeners.forEach { listener ->
225
+ listener.onUserLeaveHint(activity)
226
+ }
227
+ delegate.onUserLeaveHint()
212
228
  }
213
- delegate.onUserLeaveHint()
214
229
  }
215
230
 
216
231
  override fun onDestroy() {
217
- // If app is stopped before the delayed `loadApp`, we should cancel the pending resume
218
- // and avoid propagating the destroy event because the state was never resumed.
219
- if (shouldEmitPendingResume) {
220
- shouldEmitPendingResume = false
221
- return
222
- }
223
- reactActivityLifecycleListeners.forEach { listener ->
224
- listener.onDestroy(activity)
225
- }
226
- if (delayLoadAppHandler != null) {
227
- try {
228
- // For the delay load case, we may enter a different call flow than react-native.
229
- // For example, Activity stopped before delay load finished.
230
- // We stop before the ReactActivityDelegate gets a chance to set up.
231
- // In this case, we should catch the exceptions.
232
+ // Note: use our `coroutineScope` for onDestroy here
233
+ // because the lifecycleScope destroyed before the executions.
234
+ applicationCoroutineScope.launch {
235
+ if (!loadAppReady.isCompleted) {
236
+ // Cancel loadAppReady so any waiting coroutines are notified
237
+ loadAppReady.completeExceptionally(CancellationException("Activity destroyed before app loaded"))
238
+ }
239
+ loadAppReady.await()
240
+ reactActivityLifecycleListeners.forEach { listener ->
241
+ listener.onDestroy(activity)
242
+ }
243
+ if (delayLoadAppHandler != null) {
244
+ try {
245
+ // For the delay load case, we may enter a different call flow than react-native.
246
+ // For example, Activity stopped before delay load finished.
247
+ // We stop before the ReactActivityDelegate gets a chance to set up.
248
+ // In this case, we should catch the exceptions.
249
+ delegate.onDestroy()
250
+ } catch (e: Exception) {
251
+ Log.e(TAG, "Exception occurred during onDestroy with delayed app loading", e)
252
+ }
253
+ } else {
232
254
  delegate.onDestroy()
233
- } catch (e: Exception) {
234
- Log.e(TAG, "Exception occurred during onDestroy with delayed app loading", e)
235
255
  }
236
- } else {
237
- delegate.onDestroy()
238
256
  }
239
257
  }
240
258
 
@@ -252,20 +270,26 @@ class ReactActivityDelegateWrapper(
252
270
  *
253
271
  * TODO (@bbarthec): fix it upstream?
254
272
  */
255
- if (!ReactNativeFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
256
- val reactContextListener = object : ReactInstanceEventListener {
257
- override fun onReactContextInitialized(context: ReactContext) {
258
- delegate.reactInstanceManager.removeReactInstanceEventListener(this)
259
- delegate.onActivityResult(requestCode, resultCode, data)
273
+ activity.lifecycleScope.launch {
274
+ loadAppReady.await()
275
+ if (!ReactNativeFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
276
+ val reactContextListener = object : ReactInstanceEventListener {
277
+ override fun onReactContextInitialized(context: ReactContext) {
278
+ delegate.reactInstanceManager.removeReactInstanceEventListener(this)
279
+ delegate.onActivityResult(requestCode, resultCode, data)
280
+ }
260
281
  }
282
+ return@launch delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener)
261
283
  }
262
- return delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener)
263
- }
264
284
 
265
- delegate.onActivityResult(requestCode, resultCode, data)
285
+ delegate.onActivityResult(requestCode, resultCode, data)
286
+ }
266
287
  }
267
288
 
268
289
  override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
290
+ if (!loadAppReady.isCompleted) {
291
+ return false
292
+ }
269
293
  // if any of the handlers return true, intentionally consume the event instead of passing it
270
294
  // through to the delegate
271
295
  return reactActivityHandlers
@@ -274,6 +298,9 @@ class ReactActivityDelegateWrapper(
274
298
  }
275
299
 
276
300
  override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
301
+ if (!loadAppReady.isCompleted) {
302
+ return false
303
+ }
277
304
  // if any of the handlers return true, intentionally consume the event instead of passing it
278
305
  // through to the delegate
279
306
  return reactActivityHandlers
@@ -282,6 +309,9 @@ class ReactActivityDelegateWrapper(
282
309
  }
283
310
 
284
311
  override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
312
+ if (!loadAppReady.isCompleted) {
313
+ return false
314
+ }
285
315
  // if any of the handlers return true, intentionally consume the event instead of passing it
286
316
  // through to the delegate
287
317
  return reactActivityHandlers
@@ -290,6 +320,9 @@ class ReactActivityDelegateWrapper(
290
320
  }
291
321
 
292
322
  override fun onBackPressed(): Boolean {
323
+ if (!loadAppReady.isCompleted) {
324
+ return false
325
+ }
293
326
  val listenerResult = reactActivityLifecycleListeners
294
327
  .map(ReactActivityLifecycleListener::onBackPressed)
295
328
  .fold(false) { accu, current -> accu || current }
@@ -298,6 +331,9 @@ class ReactActivityDelegateWrapper(
298
331
  }
299
332
 
300
333
  override fun onNewIntent(intent: Intent?): Boolean {
334
+ if (!loadAppReady.isCompleted) {
335
+ return false
336
+ }
301
337
  val listenerResult = reactActivityLifecycleListeners
302
338
  .map { it.onNewIntent(intent) }
303
339
  .fold(false) { accu, current -> accu || current }
@@ -306,15 +342,24 @@ class ReactActivityDelegateWrapper(
306
342
  }
307
343
 
308
344
  override fun onWindowFocusChanged(hasFocus: Boolean) {
309
- delegate.onWindowFocusChanged(hasFocus)
345
+ activity.lifecycleScope.launch {
346
+ loadAppReady.await()
347
+ delegate.onWindowFocusChanged(hasFocus)
348
+ }
310
349
  }
311
350
 
312
351
  override fun requestPermissions(permissions: Array<out String>?, requestCode: Int, listener: PermissionListener?) {
313
- delegate.requestPermissions(permissions, requestCode, listener)
352
+ activity.lifecycleScope.launch {
353
+ loadAppReady.await()
354
+ delegate.requestPermissions(permissions, requestCode, listener)
355
+ }
314
356
  }
315
357
 
316
358
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, grantResults: IntArray?) {
317
- delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
359
+ activity.lifecycleScope.launch {
360
+ loadAppReady.await()
361
+ delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
362
+ }
318
363
  }
319
364
 
320
365
  override fun getContext(): Context {
@@ -338,7 +383,10 @@ class ReactActivityDelegateWrapper(
338
383
  }
339
384
 
340
385
  override fun onConfigurationChanged(newConfig: Configuration?) {
341
- delegate.onConfigurationChanged(newConfig)
386
+ activity.lifecycleScope.launch {
387
+ loadAppReady.await()
388
+ delegate.onConfigurationChanged(newConfig)
389
+ }
342
390
  }
343
391
 
344
392
  //endregion
@@ -401,10 +449,6 @@ class ReactActivityDelegateWrapper(
401
449
  reactActivityLifecycleListeners.forEach { listener ->
402
450
  listener.onContentChanged(activity)
403
451
  }
404
- if (shouldEmitPendingResume) {
405
- shouldEmitPendingResume = false
406
- onResume()
407
- }
408
452
  return
409
453
  }
410
454
 
@@ -412,17 +456,12 @@ class ReactActivityDelegateWrapper(
412
456
  reactActivityLifecycleListeners.forEach { listener ->
413
457
  listener.onContentChanged(activity)
414
458
  }
415
- if (shouldEmitPendingResume) {
416
- shouldEmitPendingResume = false
417
- onResume()
418
- }
419
459
  }
420
460
 
421
461
  private suspend fun awaitDelayLoadAppWhenReady(delayLoadAppHandler: DelayLoadAppHandler?) {
422
462
  if (delayLoadAppHandler == null) {
423
463
  return
424
464
  }
425
- shouldEmitPendingResume = true
426
465
  suspendCoroutine { continuation ->
427
466
  delayLoadAppHandler.whenReady {
428
467
  Utils.assertMainThread()
@@ -431,6 +470,15 @@ class ReactActivityDelegateWrapper(
431
470
  }
432
471
  }
433
472
 
473
+ /**
474
+ * Set the [loadAppReady] to completed state.
475
+ * This is only for unit tests when a test setups mocks and skips the [Activity] lifecycle.
476
+ */
477
+ @VisibleForTesting
478
+ internal fun setLoadAppReadyForTesting() {
479
+ loadAppReady.complete(Unit)
480
+ }
481
+
434
482
  //endregion
435
483
 
436
484
  companion object {
@@ -135,7 +135,7 @@ internal class ReactActivityDelegateWrapperDelayLoadTest {
135
135
  verify(exactly = 0) { spyDelegate.onResume() }
136
136
 
137
137
  callbackSlot.captured.run()
138
- verify(exactly = 2) { spyDelegateWrapper.onResume() }
138
+ verify(exactly = 1) { spyDelegateWrapper.onResume() }
139
139
  verify(exactly = 1) { spyDelegate.onResume() }
140
140
  verify(exactly = 0) { spyDelegateWrapper.onPause() }
141
141
  verify(exactly = 0) { spyDelegateWrapper.onDestroy() }
@@ -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)
@@ -19,7 +19,7 @@
19
19
  "expo-apple-authentication": "~7.2.4",
20
20
  "expo-application": "~6.1.5",
21
21
  "expo-asset": "~11.1.7",
22
- "expo-audio": "~0.4.7",
22
+ "expo-audio": "~0.4.8",
23
23
  "expo-auth-session": "~6.2.1",
24
24
  "expo-av": "~15.1.7",
25
25
  "expo-background-fetch": "~13.1.6",
@@ -65,7 +65,7 @@
65
65
  "expo-modules-core": "~2.4.2",
66
66
  "expo-navigation-bar": "~4.2.7",
67
67
  "expo-network": "~7.1.5",
68
- "expo-notifications": "~0.31.3",
68
+ "expo-notifications": "~0.31.4",
69
69
  "expo-print": "~14.1.4",
70
70
  "expo-live-photo": "~0.1.4",
71
71
  "expo-router": "~5.1.3",
@@ -77,7 +77,7 @@
77
77
  "expo-sms": "~13.1.4",
78
78
  "expo-speech": "~13.1.7",
79
79
  "expo-splash-screen": "~0.30.10",
80
- "expo-sqlite": "~15.2.13",
80
+ "expo-sqlite": "~15.2.14",
81
81
  "expo-status-bar": "~2.2.3",
82
82
  "expo-store-review": "~8.1.5",
83
83
  "expo-symbols": "~0.4.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "53.0.17",
3
+ "version": "53.0.18",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -73,7 +73,7 @@
73
73
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
74
74
  "dependencies": {
75
75
  "@babel/runtime": "^7.20.0",
76
- "@expo/cli": "0.24.18",
76
+ "@expo/cli": "0.24.19",
77
77
  "@expo/config": "~11.0.12",
78
78
  "@expo/config-plugins": "~10.1.1",
79
79
  "@expo/fingerprint": "0.13.4",
@@ -85,7 +85,7 @@
85
85
  "expo-file-system": "~18.1.11",
86
86
  "expo-font": "~13.3.2",
87
87
  "expo-keep-awake": "~14.1.4",
88
- "expo-modules-autolinking": "2.1.13",
88
+ "expo-modules-autolinking": "2.1.14",
89
89
  "expo-modules-core": "2.4.2",
90
90
  "react-native-edge-to-edge": "1.6.0",
91
91
  "whatwg-url-without-unicode": "8.0.0-3"
@@ -119,5 +119,5 @@
119
119
  "optional": true
120
120
  }
121
121
  },
122
- "gitHead": "1c4a89b0c0adebb53ef84b4a6ac25864e4652917"
122
+ "gitHead": "03398e0a2fa4f1bceebbf3a73c7bfb956a3ed18b"
123
123
  }