expo 53.0.18 → 53.0.20

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.18'
35
+ version = '53.0.20'
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.18"
46
+ versionName "53.0.20"
47
47
  consumerProguardFiles("proguard-rules.pro")
48
48
  }
49
49
  testOptions {
@@ -29,12 +29,13 @@ 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
32
  import kotlinx.coroutines.CompletableDeferred
34
33
  import kotlinx.coroutines.CoroutineScope
35
34
  import kotlinx.coroutines.CoroutineStart
36
35
  import kotlinx.coroutines.Dispatchers
37
36
  import kotlinx.coroutines.launch
37
+ import kotlinx.coroutines.sync.Mutex
38
+ import kotlinx.coroutines.sync.withLock
38
39
  import java.lang.reflect.Field
39
40
  import java.lang.reflect.Method
40
41
  import java.lang.reflect.Modifier
@@ -71,6 +72,14 @@ class ReactActivityDelegateWrapper(
71
72
  */
72
73
  private val loadAppReady = CompletableDeferred<Unit>()
73
74
 
75
+ /**
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.
80
+ */
81
+ private val mutex = Mutex()
82
+
74
83
  /**
75
84
  * A [CoroutineScope] that binds its lifecycle as [ReactActivityDelegateWrapper].
76
85
  * This is used for [onDestroy] because we need a longer lifecycle scope to call [onDestroy]
@@ -110,7 +119,7 @@ class ReactActivityDelegateWrapper(
110
119
  }
111
120
 
112
121
  override fun loadApp(appKey: String?) {
113
- activity.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
122
+ launchLifecycleScopeWithLock(start = CoroutineStart.UNDISPATCHED) {
114
123
  loadAppImpl(appKey, supportsDelayLoad = true)
115
124
  }
116
125
  }
@@ -138,7 +147,7 @@ class ReactActivityDelegateWrapper(
138
147
  // Instead we intercept `ReactActivityDelegate.onCreate` and replace the `mReactDelegate` with our version.
139
148
  // That's not ideal but works.
140
149
 
141
- activity.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
150
+ launchLifecycleScopeWithLock(start = CoroutineStart.UNDISPATCHED) {
142
151
  awaitDelayLoadAppWhenReady(delayLoadAppHandler)
143
152
  loadAppReady.complete(Unit)
144
153
 
@@ -184,7 +193,7 @@ class ReactActivityDelegateWrapper(
184
193
  }
185
194
 
186
195
  override fun onResume() {
187
- activity.lifecycleScope.launch {
196
+ launchLifecycleScopeWithLock {
188
197
  loadAppReady.await()
189
198
  delegate.onResume()
190
199
  reactActivityLifecycleListeners.forEach { listener ->
@@ -194,10 +203,7 @@ class ReactActivityDelegateWrapper(
194
203
  }
195
204
 
196
205
  override fun onPause() {
197
- activity.lifecycleScope.launch {
198
- if (!loadAppReady.isCompleted) {
199
- loadAppReady.completeExceptionally(CancellationException("Activity paused before app loaded"))
200
- }
206
+ launchLifecycleScopeWithLock {
201
207
  loadAppReady.await()
202
208
  reactActivityLifecycleListeners.forEach { listener ->
203
209
  listener.onPause(activity)
@@ -219,7 +225,7 @@ class ReactActivityDelegateWrapper(
219
225
  }
220
226
 
221
227
  override fun onUserLeaveHint() {
222
- activity.lifecycleScope.launch {
228
+ launchLifecycleScopeWithLock {
223
229
  loadAppReady.await()
224
230
  reactActivityLifecycleListeners.forEach { listener ->
225
231
  listener.onUserLeaveHint(activity)
@@ -232,26 +238,24 @@ class ReactActivityDelegateWrapper(
232
238
  // Note: use our `coroutineScope` for onDestroy here
233
239
  // because the lifecycleScope destroyed before the executions.
234
240
  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.
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 {
249
257
  delegate.onDestroy()
250
- } catch (e: Exception) {
251
- Log.e(TAG, "Exception occurred during onDestroy with delayed app loading", e)
252
258
  }
253
- } else {
254
- delegate.onDestroy()
255
259
  }
256
260
  }
257
261
  }
@@ -270,7 +274,7 @@ class ReactActivityDelegateWrapper(
270
274
  *
271
275
  * TODO (@bbarthec): fix it upstream?
272
276
  */
273
- activity.lifecycleScope.launch {
277
+ launchLifecycleScopeWithLock {
274
278
  loadAppReady.await()
275
279
  if (!ReactNativeFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
276
280
  val reactContextListener = object : ReactInstanceEventListener {
@@ -279,7 +283,9 @@ class ReactActivityDelegateWrapper(
279
283
  delegate.onActivityResult(requestCode, resultCode, data)
280
284
  }
281
285
  }
282
- return@launch delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener)
286
+ return@launchLifecycleScopeWithLock delegate.reactInstanceManager.addReactInstanceEventListener(
287
+ reactContextListener
288
+ )
283
289
  }
284
290
 
285
291
  delegate.onActivityResult(requestCode, resultCode, data)
@@ -342,21 +348,21 @@ class ReactActivityDelegateWrapper(
342
348
  }
343
349
 
344
350
  override fun onWindowFocusChanged(hasFocus: Boolean) {
345
- activity.lifecycleScope.launch {
351
+ launchLifecycleScopeWithLock {
346
352
  loadAppReady.await()
347
353
  delegate.onWindowFocusChanged(hasFocus)
348
354
  }
349
355
  }
350
356
 
351
357
  override fun requestPermissions(permissions: Array<out String>?, requestCode: Int, listener: PermissionListener?) {
352
- activity.lifecycleScope.launch {
358
+ launchLifecycleScopeWithLock {
353
359
  loadAppReady.await()
354
360
  delegate.requestPermissions(permissions, requestCode, listener)
355
361
  }
356
362
  }
357
363
 
358
364
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, grantResults: IntArray?) {
359
- activity.lifecycleScope.launch {
365
+ launchLifecycleScopeWithLock {
360
366
  loadAppReady.await()
361
367
  delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
362
368
  }
@@ -383,7 +389,7 @@ class ReactActivityDelegateWrapper(
383
389
  }
384
390
 
385
391
  override fun onConfigurationChanged(newConfig: Configuration?) {
386
- activity.lifecycleScope.launch {
392
+ launchLifecycleScopeWithLock {
387
393
  loadAppReady.await()
388
394
  delegate.onConfigurationChanged(newConfig)
389
395
  }
@@ -470,6 +476,17 @@ class ReactActivityDelegateWrapper(
470
476
  }
471
477
  }
472
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
+
473
490
  /**
474
491
  * Set the [loadAppReady] to completed state.
475
492
  * This is only for unit tests when a test setups mocks and skips the [Activity] lifecycle.
@@ -173,60 +173,6 @@ internal class ReactActivityDelegateWrapperDelayLoadTest {
173
173
  verify(exactly = 1) { spyDelegate.onDestroy() }
174
174
  }
175
175
 
176
- @Test
177
- fun `should cancel pending resume if activity pause 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()
199
- callbackSlot.captured.run()
200
- verify(exactly = 0) { spyDelegate.onResume() }
201
- }
202
-
203
- @Test
204
- fun `should cancel pending resume if activity stop before delay load finished`() = runTest {
205
- every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithDelay)
206
-
207
- val callbackSlot = slot<Runnable>()
208
- every { delayLoadAppHandler.whenReady(capture(callbackSlot)) } answers {
209
- // Don't call the callback immediately to simulate delay
210
- }
211
-
212
- activityController = Robolectric.buildActivity(MockActivity::class.java)
213
- .also {
214
- val activity = it.get()
215
- (activity.application as MockApplication).bindCurrentActivity(activity)
216
- }
217
- .setup()
218
- val spyDelegateWrapper = activity.reactActivityDelegate as ReactActivityDelegateWrapper
219
- val spyDelegate = spyDelegateWrapper.delegate
220
-
221
- verify(exactly = 1) { spyDelegateWrapper.onCreate(any()) }
222
- verify(exactly = 1) { spyDelegateWrapper.onResume() }
223
- verify(exactly = 0) { spyDelegate.onResume() }
224
-
225
- activityController.pause().stop()
226
- callbackSlot.captured.run()
227
- verify(exactly = 0) { spyDelegate.onResume() }
228
- }
229
-
230
176
  @Test
231
177
  fun `should cancel pending resume if activity destroy before delay load finished`() = runTest {
232
178
  every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackageWithDelay)
@@ -1 +1 @@
1
- {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/winter/url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA+ClE,OAAO,QAAQ,4BAA4B,CAAC;IAE1C,KAAK,QAAQ,GAAG,IAAI,GAAG;QAAE,IAAI,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAErE,UAAU,cAAc;QACtB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;QACxC,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KAC/C;CACF;AAgDD,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/winter/url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA+ClE,OAAO,QAAQ,4BAA4B,CAAC;IAE1C,KAAK,QAAQ,GAAG,IAAI,GAAG;QAAE,IAAI,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAErE,UAAU,cAAc;QACtB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;QACxC,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KAC/C;CACF;AAiDD,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC"}
@@ -29,7 +29,7 @@
29
29
  "expo-brightness": "~13.1.4",
30
30
  "expo-build-properties": "~0.14.8",
31
31
  "expo-calendar": "~14.1.4",
32
- "expo-camera": "~16.1.10",
32
+ "expo-camera": "~16.1.11",
33
33
  "expo-cellular": "~7.1.5",
34
34
  "expo-checkbox": "~4.1.4",
35
35
  "expo-clipboard": "~7.1.5",
@@ -44,7 +44,7 @@
44
44
  "expo-gl": "~15.1.7",
45
45
  "expo-google-app-auth": "~8.3.0",
46
46
  "expo-haptics": "~14.1.4",
47
- "expo-image": "~2.3.2",
47
+ "expo-image": "~2.4.0",
48
48
  "expo-image-loader": "~5.1.0",
49
49
  "expo-image-manipulator": "~13.1.7",
50
50
  "expo-image-picker": "~16.1.4",
@@ -62,7 +62,7 @@
62
62
  "expo-media-library": "~17.1.7",
63
63
  "expo-mesh-gradient": "~0.3.4",
64
64
  "expo-module-template": "~10.16.7",
65
- "expo-modules-core": "~2.4.2",
65
+ "expo-modules-core": "~2.5.0",
66
66
  "expo-navigation-bar": "~4.2.7",
67
67
  "expo-network": "~7.1.5",
68
68
  "expo-notifications": "~0.31.4",
@@ -84,7 +84,7 @@
84
84
  "expo-system-ui": "~5.0.10",
85
85
  "expo-task-manager": "~13.1.6",
86
86
  "expo-tracking-transparency": "~5.2.4",
87
- "expo-updates": "~0.28.16",
87
+ "expo-updates": "~0.28.17",
88
88
  "expo-video-thumbnails": "~9.1.3",
89
89
  "expo-video": "~2.2.2",
90
90
  "expo-web-browser": "~14.2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "53.0.18",
3
+ "version": "53.0.20",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -73,9 +73,9 @@
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.19",
77
- "@expo/config": "~11.0.12",
78
- "@expo/config-plugins": "~10.1.1",
76
+ "@expo/cli": "0.24.20",
77
+ "@expo/config": "~11.0.13",
78
+ "@expo/config-plugins": "~10.1.2",
79
79
  "@expo/fingerprint": "0.13.4",
80
80
  "@expo/metro-config": "0.20.17",
81
81
  "@expo/vector-icons": "^14.0.0",
@@ -86,7 +86,7 @@
86
86
  "expo-font": "~13.3.2",
87
87
  "expo-keep-awake": "~14.1.4",
88
88
  "expo-modules-autolinking": "2.1.14",
89
- "expo-modules-core": "2.4.2",
89
+ "expo-modules-core": "2.5.0",
90
90
  "react-native-edge-to-edge": "1.6.0",
91
91
  "whatwg-url-without-unicode": "8.0.0-3"
92
92
  },
@@ -119,5 +119,5 @@
119
119
  "optional": true
120
120
  }
121
121
  },
122
- "gitHead": "03398e0a2fa4f1bceebbf3a73c7bfb956a3ed18b"
122
+ "gitHead": "b69736d615d303c2143e7682b37c93665e1ed3d9"
123
123
  }
@@ -12,6 +12,10 @@ describe(URL, () => {
12
12
  expect(new URL('http://acme.com').toString()).toBe('http://acme.com/');
13
13
  expect(new URL('/home', 'http://localhost:3000').toString()).toBe('http://localhost:3000/home');
14
14
  });
15
+ it(`supports canParse`, () => {
16
+ expect(URL.canParse('http://acme.com')).toBe(true);
17
+ expect(URL.canParse('invalid url')).toBe(false);
18
+ });
15
19
  });
16
20
 
17
21
  describe(URLSearchParams, () => {
package/src/winter/url.ts CHANGED
@@ -106,7 +106,8 @@ URL.revokeObjectURL = function revokeObjectURL(_url) {
106
106
 
107
107
  URL.canParse = function canParse(url: string, base?: string): boolean {
108
108
  try {
109
- URL(url, base);
109
+ // eslint-disable-next-line no-new
110
+ new URL(url, base);
110
111
  return true;
111
112
  } catch {
112
113
  return false;