expo 58.0.0-preview.0 → 58.0.0-preview.1

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.
@@ -10,7 +10,7 @@ buildscript {
10
10
  }
11
11
 
12
12
  group = 'host.exp.exponent'
13
- version = '58.0.0-preview.0'
13
+ version = '58.0.0-preview.1'
14
14
 
15
15
  expoModule {
16
16
  // We can't prebuild the module because it depends on the generated files.
@@ -21,7 +21,7 @@ android {
21
21
  namespace "expo.core"
22
22
  defaultConfig {
23
23
  versionCode 1
24
- versionName "58.0.0-preview.0"
24
+ versionName "58.0.0-preview.1"
25
25
  consumerProguardFiles("proguard-rules.pro")
26
26
  }
27
27
  testOptions {
@@ -0,0 +1,19 @@
1
+ package expo.modules
2
+
3
+ import android.app.Activity
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import com.facebook.react.common.build.ReactBuildConfig
7
+
8
+ // Android 17 blocks dev server traffic until the app holds the permission React Native's debug manifest declares.
9
+ internal object LocalNetworkPermission {
10
+ const val PERMISSION = "android.permission.ACCESS_LOCAL_NETWORK"
11
+ const val REQUEST_CODE = 0x4c4e
12
+
13
+ private const val FIRST_ENFORCED_SDK = 37
14
+
15
+ fun shouldRequest(activity: Activity): Boolean =
16
+ ReactBuildConfig.DEBUG &&
17
+ Build.VERSION.SDK_INT >= FIRST_ENFORCED_SDK &&
18
+ activity.checkSelfPermission(PERMISSION) != PackageManager.PERMISSION_GRANTED
19
+ }
@@ -30,13 +30,13 @@ import kotlinx.coroutines.CoroutineScope
30
30
  import kotlinx.coroutines.CoroutineStart
31
31
  import kotlinx.coroutines.Dispatchers
32
32
  import kotlinx.coroutines.launch
33
+ import kotlinx.coroutines.suspendCancellableCoroutine
33
34
  import kotlinx.coroutines.sync.Mutex
34
35
  import kotlinx.coroutines.sync.withLock
35
36
  import java.lang.reflect.Field
36
37
  import java.lang.reflect.Method
37
38
  import java.lang.reflect.Modifier
38
39
  import kotlin.coroutines.resume
39
- import kotlin.coroutines.suspendCoroutine
40
40
 
41
41
  class ReactActivityDelegateWrapper(
42
42
  private val activity: ReactActivity,
@@ -64,6 +64,7 @@ class ReactActivityDelegateWrapper(
64
64
  * A deferred that indicates when the app loading is ready
65
65
  */
66
66
  private val loadAppReady = CompletableDeferred<Unit>()
67
+ private var localNetworkPermissionResult: CompletableDeferred<Unit>? = null
67
68
 
68
69
  /**
69
70
  * A mutex to ensure all coroutines in a scope are running in atomic way.
@@ -161,6 +162,7 @@ class ReactActivityDelegateWrapper(
161
162
  mReactDelegate.isAccessible = true
162
163
  mReactDelegate.set(delegate, reactDelegate)
163
164
  if (mainComponentName != null) {
165
+ awaitLocalNetworkPermission()
164
166
  loadAppImpl(mainComponentName, supportsDelayLoad = false)
165
167
  }
166
168
  }
@@ -329,6 +331,10 @@ class ReactActivityDelegateWrapper(
329
331
  }
330
332
 
331
333
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
334
+ if (requestCode == LocalNetworkPermission.REQUEST_CODE) {
335
+ localNetworkPermissionResult?.complete(Unit)
336
+ return
337
+ }
332
338
  launchLifecycleScopeWithLock {
333
339
  loadAppReady.await()
334
340
  delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
@@ -431,11 +437,22 @@ class ReactActivityDelegateWrapper(
431
437
  }
432
438
  }
433
439
 
440
+ private suspend fun awaitLocalNetworkPermission() {
441
+ if (!LocalNetworkPermission.shouldRequest(activity)) {
442
+ return
443
+ }
444
+ val result = CompletableDeferred<Unit>()
445
+ localNetworkPermissionResult = result
446
+ activity.requestPermissions(arrayOf(LocalNetworkPermission.PERMISSION), LocalNetworkPermission.REQUEST_CODE)
447
+ result.await()
448
+ localNetworkPermissionResult = null
449
+ }
450
+
434
451
  private suspend fun awaitDelayLoadAppWhenReady(delayLoadAppHandler: DelayLoadAppHandler?) {
435
452
  if (delayLoadAppHandler == null) {
436
453
  return
437
454
  }
438
- suspendCoroutine { continuation ->
455
+ suspendCancellableCoroutine { continuation ->
439
456
  delayLoadAppHandler.whenReady {
440
457
  Utils.assertMainThread()
441
458
  continuation.resume(Unit)
@@ -0,0 +1,99 @@
1
+ package expo.modules
2
+
3
+ import android.content.pm.PackageManager
4
+ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
5
+ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
6
+ import com.facebook.soloader.SoLoader
7
+ import com.google.common.truth.Truth.assertThat
8
+ import io.mockk.every
9
+ import io.mockk.mockkObject
10
+ import io.mockk.mockkStatic
11
+ import io.mockk.unmockkAll
12
+ import io.mockk.verify
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
15
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
16
+ import kotlinx.coroutines.test.setMain
17
+ import org.junit.After
18
+ import org.junit.Before
19
+ import org.junit.Test
20
+ import org.junit.runner.RunWith
21
+ import org.robolectric.Robolectric
22
+ import org.robolectric.RobolectricTestRunner
23
+ import org.robolectric.Shadows.shadowOf
24
+ import org.robolectric.android.controller.ActivityController
25
+ import org.robolectric.annotation.Config
26
+
27
+ @RunWith(RobolectricTestRunner::class)
28
+ @Config(application = MockApplication::class)
29
+ internal class ReactActivityDelegateWrapperLocalNetworkPermissionTest {
30
+ private lateinit var activityController: ActivityController<MockActivity>
31
+ private val activity: MockActivity
32
+ get() = activityController.get()
33
+ private val wrapper: ReactActivityDelegateWrapper
34
+ get() = activity.reactActivityDelegate as ReactActivityDelegateWrapper
35
+
36
+ @OptIn(ExperimentalCoroutinesApi::class)
37
+ @Before
38
+ fun setUp() {
39
+ SoLoader.setInTestMode()
40
+ mockkObject(ExpoModulesPackage.Companion)
41
+ every { ExpoModulesPackage.Companion.packageList } returns listOf(MockPackageWithoutDelayHandler())
42
+ ReactNativeFeatureFlagsForTests.setUp()
43
+ mockkStatic(ReactNativeFeatureFlags::class)
44
+ every { ReactNativeFeatureFlags.enableBridgelessArchitecture() } returns true
45
+ Dispatchers.setMain(UnconfinedTestDispatcher())
46
+ mockkObject(LocalNetworkPermission)
47
+ }
48
+
49
+ @After
50
+ fun tearDown() {
51
+ unmockkAll()
52
+ }
53
+
54
+ private fun launchActivity() {
55
+ activityController = Robolectric.buildActivity(MockActivity::class.java)
56
+ .also { (it.get().application as MockApplication).bindCurrentActivity(it.get()) }
57
+ .setup()
58
+ }
59
+
60
+ private fun verifyLoadApp(times: Int) {
61
+ verify(exactly = times) { wrapper.invokeDelegateMethod("loadApp", arrayOf(String::class.java), arrayOf("main")) }
62
+ }
63
+
64
+ @Test
65
+ fun `requests the local network permission before loading the app`() {
66
+ every { LocalNetworkPermission.shouldRequest(any()) } returns true
67
+
68
+ launchActivity()
69
+
70
+ verifyLoadApp(0)
71
+ val request = shadowOf(activity).lastRequestedPermission
72
+ assertThat(request.requestedPermissions.toList()).containsExactly(LocalNetworkPermission.PERMISSION)
73
+
74
+ activity.onRequestPermissionsResult(request.requestCode, request.requestedPermissions, intArrayOf(PackageManager.PERMISSION_GRANTED))
75
+
76
+ verifyLoadApp(1)
77
+ }
78
+
79
+ @Test
80
+ fun `loads the app when the local network permission is denied`() {
81
+ every { LocalNetworkPermission.shouldRequest(any()) } returns true
82
+
83
+ launchActivity()
84
+ val request = shadowOf(activity).lastRequestedPermission
85
+ activity.onRequestPermissionsResult(request.requestCode, request.requestedPermissions, intArrayOf(PackageManager.PERMISSION_DENIED))
86
+
87
+ verifyLoadApp(1)
88
+ }
89
+
90
+ @Test
91
+ fun `loads the app immediately when the permission is not required`() {
92
+ every { LocalNetworkPermission.shouldRequest(any()) } returns false
93
+
94
+ launchActivity()
95
+
96
+ assertThat(shadowOf(activity).lastRequestedPermission).isNull()
97
+ verifyLoadApp(1)
98
+ }
99
+ }
package/build/Expo.d.ts CHANGED
@@ -3,7 +3,7 @@ export { disableErrorHandling } from './errors/ExpoErrorManager';
3
3
  export { default as registerRootComponent } from './launch/registerRootComponent';
4
4
  export { isRunningInExpoGo, getExpoGoProjectConfig } from './environment/ExpoGo';
5
5
  export { EventEmitter, SharedObject, SharedRef, NativeModule, UnavailabilityError, CodedError, requireNativeModule, requireOptionalNativeModule, requireNativeViewManager as requireNativeView, registerWebModule, reloadAppAsync, installOnUIRuntime, Platform, uuid, createSnapshotFriendlyRef, useReleasingSharedObject, useReleasingSharedObjectWithLifecycle, } from 'expo-modules-core';
6
- export type { EventSubscription, TypedArray, IntBasedTypedArray, UintBasedTypedArray, FloatBasedTypedArray, } from 'expo-modules-core';
6
+ export type { EventSubscription, TypedArray, IntBasedTypedArray, UintBasedTypedArray, FloatBasedTypedArray, ReleasingSharedObjectLifecycle, ReleasingSharedObjectLifecycleContext, } from 'expo-modules-core';
7
7
  export type {
8
8
  /** @deprecated Move to `SharedRef` with a type-only import instead */
9
9
  SharedRef as SharedRefType,
package/build/Expo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["Expo.ts"],"sourcesContent":[null],"names":["disableErrorHandling","default","registerRootComponent","isRunningInExpoGo","getExpoGoProjectConfig","EventEmitter","SharedObject","SharedRef","NativeModule","UnavailabilityError","CodedError","requireNativeModule","requireOptionalNativeModule","requireNativeViewManager","requireNativeView","registerWebModule","reloadAppAsync","installOnUIRuntime","Platform","uuid","createSnapshotFriendlyRef","useReleasingSharedObject","useReleasingSharedObjectWithLifecycle","PermissionStatus","createPermissionHook","useEvent","useEventListener"],"mappings":"AAAA,OAAO,YAAY;AAEnB,SAASA,oBAAoB,QAAQ,4BAA4B;AACjE,SAASC,WAAWC,qBAAqB,QAAQ,iCAAiC;AAElF,SAASC,iBAAiB,EAAEC,sBAAsB,QAAQ,uBAAuB;AAEjF,SACE,eAAe;AACfC,YAAY,EACZC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,mBAAmB,EACnBC,UAAU,EAEV,UAAU;AACVC,mBAAmB,EACnBC,2BAA2B,EAC3BC,4BAA4BC,iBAAiB,EAC7CC,iBAAiB,EACjBC,cAAc,EAEd,WAAW;AACXC,kBAAkB,EAElB,QAAQ;AACRC,QAAQ,EACRC,IAAI,EACJC,yBAAyB,EACzBC,wBAAwB,EACxBC,qCAAqC,QAChC,oBAAoB;AAqB3B,SACEC,gBAAgB,QAIX,oBAAoB;AAE3B,SAASC,oBAAoB,QAAQ,oBAAoB;AAEzD,SAASC,QAAQ,EAAEC,gBAAgB,QAAQ,mBAAmB"}
1
+ {"version":3,"sources":["Expo.ts"],"sourcesContent":[null],"names":["disableErrorHandling","default","registerRootComponent","isRunningInExpoGo","getExpoGoProjectConfig","EventEmitter","SharedObject","SharedRef","NativeModule","UnavailabilityError","CodedError","requireNativeModule","requireOptionalNativeModule","requireNativeViewManager","requireNativeView","registerWebModule","reloadAppAsync","installOnUIRuntime","Platform","uuid","createSnapshotFriendlyRef","useReleasingSharedObject","useReleasingSharedObjectWithLifecycle","PermissionStatus","createPermissionHook","useEvent","useEventListener"],"mappings":"AAAA,OAAO,YAAY;AAEnB,SAASA,oBAAoB,QAAQ,4BAA4B;AACjE,SAASC,WAAWC,qBAAqB,QAAQ,iCAAiC;AAElF,SAASC,iBAAiB,EAAEC,sBAAsB,QAAQ,uBAAuB;AAEjF,SACE,eAAe;AACfC,YAAY,EACZC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,mBAAmB,EACnBC,UAAU,EAEV,UAAU;AACVC,mBAAmB,EACnBC,2BAA2B,EAC3BC,4BAA4BC,iBAAiB,EAC7CC,iBAAiB,EACjBC,cAAc,EAEd,WAAW;AACXC,kBAAkB,EAElB,QAAQ;AACRC,QAAQ,EACRC,IAAI,EACJC,yBAAyB,EACzBC,wBAAwB,EACxBC,qCAAqC,QAChC,oBAAoB;AAuB3B,SACEC,gBAAgB,QAIX,oBAAoB;AAE3B,SAASC,oBAAoB,QAAQ,oBAAoB;AAEzD,SAASC,QAAQ,EAAEC,gBAAgB,QAAQ,mBAAmB"}
@@ -1,2 +1,2 @@
1
- import 'react-native/Libraries/Core/InitializeCore';
1
+ import 'react-native/setup-env';
2
2
  import '../../types';
@@ -2,7 +2,7 @@
2
2
  // https://wintercg.org/
3
3
  // WARN(@kitten): We must ensure that the core react-native globals are initialized before ours
4
4
  // Otherwise we're relying on `getModulesRunBeforeMainModule` which is unstable or can be missing
5
- import 'react-native/Libraries/Core/InitializeCore';
5
+ import 'react-native/setup-env';
6
6
  import '../../types';
7
7
  import { installAbortSignalPatch } from './AbortSignal';
8
8
  import { installFormDataPatch } from './FormData';
@@ -1 +1 @@
1
- {"version":3,"sources":["winter/runtime.native.ts"],"sourcesContent":[null],"names":["installAbortSignalPatch","installFormDataPatch","installGlobal","install","require","TextDecoder","TextDecoderStream","TextEncoderStream","URL","URLSearchParams","DOMException","ImportMetaRegistry","default","FormData","AbortSignal","Symbol","asyncIterator","for","useRnFetch","process","env","EXPO_PUBLIC_USE_RN_FETCH","globalThis","Headers","Error","fetch"],"mappings":"AAAA,wFAAwF;AACxF,wBAAwB;AAExB,+FAA+F;AAC/F,iGAAiG;AACjG,OAAO,6CAA6C;AACpD,OAAO,cAAc;AACrB,SAASA,uBAAuB,QAAQ,gBAAgB;AACxD,SAASC,oBAAoB,QAAQ,aAAa;AAClD,SAASC,iBAAiBC,OAAO,QAAQ,kBAAkB;AAE3D,gDAAgD;AAChDA,QAAQ,eAAe,IAAMC,QAAQ,iBAAiBC,WAAW;AACjE,gEAAgE;AAChEF,QAAQ,qBAAqB,IAAMC,QAAQ,uBAAuBE,iBAAiB;AACnF,gEAAgE;AAChEH,QAAQ,qBAAqB,IAAMC,QAAQ,uBAAuBG,iBAAiB;AACnF,mCAAmC;AACnCJ,QAAQ,OAAO,IAAMC,QAAQ,SAASI,GAAG;AACzC,+CAA+C;AAC/CL,QAAQ,mBAAmB,IAAMC,QAAQ,SAASK,eAAe;AACjE,mDAAmD;AACnDN,QAAQ,gBAAgB,IAAMC,QAAQ,kBAAkBM,YAAY;AACpE,sCAAsC;AACtC,kDAAkD;AAElDP,QAAQ,4BAA4B,IAAMC,QAAQ,wBAAwBO,kBAAkB;AAE5F,8EAA8E;AAC9ER,QAAQ,mBAAmB,IAAMC,QAAQ,2BAA2BQ,OAAO;AAE3EX,qBAAqBY;AACrBb,wBAAwBc;AAExB,6CAA6C;AAC7C,+EAA+E;AAC/EC,OAAOC,aAAa,KAAKD,OAAOE,GAAG,CAAC;AAEpC,MAAMC,aACJC,QAAQC,GAAG,CAACC,wBAAwB,KAAK,OAAOF,QAAQC,GAAG,CAACC,wBAAwB,KAAK;AAE3F,IAAI,CAACH,YAAY;IACf,sEAAsE;IACtE,sEAAsE;IACtE,IAAI,CAACI,WAAWC,OAAO,EAAE;QACvB,MAAM,IAAIC,MACR;IAEJ;IACArB,QAAQ,SAAS,IAAMC,QAAQ,WAAWqB,KAAK;AACjD"}
1
+ {"version":3,"sources":["winter/runtime.native.ts"],"sourcesContent":[null],"names":["installAbortSignalPatch","installFormDataPatch","installGlobal","install","require","TextDecoder","TextDecoderStream","TextEncoderStream","URL","URLSearchParams","DOMException","ImportMetaRegistry","default","FormData","AbortSignal","Symbol","asyncIterator","for","useRnFetch","process","env","EXPO_PUBLIC_USE_RN_FETCH","globalThis","Headers","Error","fetch"],"mappings":"AAAA,wFAAwF;AACxF,wBAAwB;AAExB,+FAA+F;AAC/F,iGAAiG;AACjG,OAAO,yBAAyB;AAChC,OAAO,cAAc;AACrB,SAASA,uBAAuB,QAAQ,gBAAgB;AACxD,SAASC,oBAAoB,QAAQ,aAAa;AAClD,SAASC,iBAAiBC,OAAO,QAAQ,kBAAkB;AAE3D,gDAAgD;AAChDA,QAAQ,eAAe,IAAMC,QAAQ,iBAAiBC,WAAW;AACjE,gEAAgE;AAChEF,QAAQ,qBAAqB,IAAMC,QAAQ,uBAAuBE,iBAAiB;AACnF,gEAAgE;AAChEH,QAAQ,qBAAqB,IAAMC,QAAQ,uBAAuBG,iBAAiB;AACnF,mCAAmC;AACnCJ,QAAQ,OAAO,IAAMC,QAAQ,SAASI,GAAG;AACzC,+CAA+C;AAC/CL,QAAQ,mBAAmB,IAAMC,QAAQ,SAASK,eAAe;AACjE,mDAAmD;AACnDN,QAAQ,gBAAgB,IAAMC,QAAQ,kBAAkBM,YAAY;AACpE,sCAAsC;AACtC,kDAAkD;AAElDP,QAAQ,4BAA4B,IAAMC,QAAQ,wBAAwBO,kBAAkB;AAE5F,8EAA8E;AAC9ER,QAAQ,mBAAmB,IAAMC,QAAQ,2BAA2BQ,OAAO;AAE3EX,qBAAqBY;AACrBb,wBAAwBc;AAExB,6CAA6C;AAC7C,+EAA+E;AAC/EC,OAAOC,aAAa,KAAKD,OAAOE,GAAG,CAAC;AAEpC,MAAMC,aACJC,QAAQC,GAAG,CAACC,wBAAwB,KAAK,OAAOF,QAAQC,GAAG,CAACC,wBAAwB,KAAK;AAE3F,IAAI,CAACH,YAAY;IACf,sEAAsE;IACtE,sEAAsE;IACtE,IAAI,CAACI,WAAWC,OAAO,EAAE;QACvB,MAAM,IAAIC,MACR;IAEJ;IACArB,QAAQ,SAAS,IAAMC,QAAQ,WAAWqB,KAAK;AACjD"}
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "@expo/dom-webview": "~58.0.0",
3
3
  "@expo/fingerprint": "~0.21.0",
4
- "@expo/metro-runtime": "~58.0.0",
4
+ "@expo/metro-runtime": "~58.0.1",
5
5
  "@expo/vector-icons": "^15.0.2",
6
- "@expo/ui": "~58.0.0",
6
+ "@expo/ui": "~58.0.1",
7
7
  "@react-native-async-storage/async-storage": "2.2.0",
8
8
  "@react-native-community/datetimepicker": "9.2.1",
9
9
  "@react-native-masked-view/masked-view": "0.3.2",
@@ -13,33 +13,33 @@
13
13
  "@react-native-picker/picker": "2.11.4",
14
14
  "@react-native-segmented-control/segmented-control": "2.5.7",
15
15
  "@stripe/stripe-react-native": "0.76.0",
16
- "eslint-config-expo": "~58.0.0",
16
+ "eslint-config-expo": "~58.0.1",
17
17
  "expo-analytics-amplitude": "~11.3.0",
18
18
  "expo-app-auth": "~11.1.0",
19
- "expo-app-intents": "~0.2.0",
19
+ "expo-app-intents": "~0.3.0",
20
20
  "expo-app-loader-provider": "~8.0.0",
21
- "expo-app-metrics": "~58.0.0",
21
+ "expo-app-metrics": "~58.0.1",
22
22
  "expo-apple-authentication": "~58.0.0",
23
- "expo-application": "~58.0.0",
24
- "expo-asset": "~58.0.0",
23
+ "expo-application": "~58.0.1",
24
+ "expo-asset": "~58.0.1",
25
25
  "expo-audio": "~58.0.0",
26
- "expo-auth-session": "~58.0.0",
27
- "expo-background-fetch": "~58.0.0",
28
- "expo-background-task": "~58.0.0",
26
+ "expo-auth-session": "~58.0.1",
27
+ "expo-background-fetch": "~58.0.1",
28
+ "expo-background-task": "~58.0.1",
29
29
  "expo-battery": "~58.0.0",
30
30
  "expo-blur": "~58.0.0",
31
31
  "expo-brightness": "~58.0.0",
32
- "expo-brownfield": "~58.0.0",
33
- "expo-build-properties": "~58.0.0",
32
+ "expo-brownfield": "~58.0.1",
33
+ "expo-build-properties": "~58.0.1",
34
34
  "expo-calendar": "~58.0.0",
35
- "expo-camera": "~58.0.0",
35
+ "expo-camera": "~58.0.1",
36
36
  "expo-cellular": "~58.0.0",
37
37
  "expo-checkbox": "~58.0.0",
38
38
  "expo-clipboard": "~58.0.0",
39
- "expo-constants": "~58.0.0",
40
- "expo-contacts": "~58.0.0",
41
- "expo-crypto": "~58.0.0",
42
- "expo-dev-client": "~58.0.0",
39
+ "expo-constants": "~58.0.1",
40
+ "expo-contacts": "~58.0.1",
41
+ "expo-crypto": "~58.0.1",
42
+ "expo-dev-client": "~58.0.1",
43
43
  "expo-device": "~58.0.0",
44
44
  "expo-document-picker": "~58.0.0",
45
45
  "expo-eas-client": "~58.0.0",
@@ -48,60 +48,60 @@
48
48
  "expo-gl": "~58.0.0",
49
49
  "expo-glass-effect": "~58.0.0",
50
50
  "expo-google-app-auth": "~8.3.0",
51
- "expo-haptics": "~58.0.0",
52
- "expo-image": "~58.0.0",
53
- "expo-image-loader": "~58.0.0",
54
- "expo-image-manipulator": "~58.0.0",
55
- "expo-image-picker": "~58.0.0",
51
+ "expo-haptics": "~58.0.1",
52
+ "expo-image": "~58.0.1",
53
+ "expo-image-loader": "~58.0.1",
54
+ "expo-image-manipulator": "~58.0.2",
55
+ "expo-image-picker": "~58.0.1",
56
56
  "expo-intent-launcher": "~58.0.0",
57
- "expo-insights": "~58.0.0",
57
+ "expo-insights": "~58.0.1",
58
58
  "expo-keep-awake": "~58.0.0",
59
59
  "expo-linear-gradient": "~58.0.0",
60
- "expo-linking": "~58.0.0",
60
+ "expo-linking": "~58.0.1",
61
61
  "expo-local-authentication": "~58.0.0",
62
62
  "expo-localization": "~58.0.0",
63
- "expo-location": "~58.0.0",
63
+ "expo-location": "~58.0.1",
64
64
  "expo-mail-composer": "~58.0.0",
65
65
  "expo-manifests": "~58.0.0",
66
- "expo-maps": "~58.0.0",
66
+ "expo-maps": "~58.0.1",
67
67
  "expo-mcp": "~0.2.1",
68
68
  "expo-media-library": "~58.0.0",
69
69
  "expo-mesh-gradient": "~58.0.0",
70
- "expo-module-template": "~58.0.0",
71
- "expo-modules-core": "~58.0.0",
70
+ "expo-module-template": "~58.0.1",
71
+ "expo-modules-core": "~58.0.1",
72
72
  "expo-navigation-bar": "~58.0.0",
73
73
  "expo-network": "~58.0.0",
74
- "expo-notifications": "~58.0.0",
75
- "expo-observe": "~58.0.0",
74
+ "expo-notifications": "~58.0.1",
75
+ "expo-observe": "~58.0.3",
76
76
  "expo-print": "~58.0.0",
77
77
  "expo-live-photo": "~58.0.0",
78
- "expo-router": "~58.0.0",
78
+ "expo-router": "~58.0.2",
79
79
  "expo-screen-capture": "~58.0.0",
80
80
  "expo-screen-orientation": "~58.0.0",
81
81
  "expo-secure-store": "~58.0.0",
82
82
  "expo-sensors": "~58.0.0",
83
83
  "expo-server": "~58.0.0",
84
- "expo-sharing": "~58.0.0",
84
+ "expo-sharing": "~58.0.3",
85
85
  "expo-sms": "~58.0.0",
86
86
  "expo-speech": "~58.0.0",
87
87
  "expo-splash-screen": "~58.0.0",
88
- "expo-sqlite": "~58.0.0",
88
+ "expo-sqlite": "~58.0.2",
89
89
  "expo-status-bar": "~58.0.0",
90
90
  "expo-store-review": "~58.0.0",
91
91
  "expo-symbols": "~58.0.0",
92
- "expo-system-ui": "~58.0.0",
93
- "expo-task-manager": "~58.0.0",
92
+ "expo-system-ui": "~58.0.1",
93
+ "expo-task-manager": "~58.0.2",
94
94
  "expo-tracking-transparency": "~58.0.0",
95
- "expo-updates": "~58.0.0",
95
+ "expo-updates": "~58.0.3",
96
96
  "expo-video-thumbnails": "~58.0.0",
97
- "expo-video": "~58.0.0",
97
+ "expo-video": "~58.0.1",
98
98
  "expo-web-browser": "~58.0.0",
99
- "expo-widgets": "~58.0.0",
100
- "jest-expo": "~58.0.0",
99
+ "expo-widgets": "~58.0.1",
100
+ "jest-expo": "~58.0.1",
101
101
  "lottie-react-native": "~7.5.0",
102
102
  "react": "19.2.3",
103
103
  "react-dom": "19.2.3",
104
- "react-native": "0.87.1",
104
+ "react-native": "0.88.0-rc.0",
105
105
  "react-native-web": "~0.21.0",
106
106
  "react-native-gesture-handler": "~3.2.1",
107
107
  "react-native-keyboard-controller": "1.22.4",
@@ -1,7 +1,6 @@
1
1
  // Copyright 2015-present 650 Industries. All rights reserved.
2
2
 
3
3
  import Foundation
4
- import ExpoModulesCore
5
4
  import React
6
5
  #if canImport(ExpoObjC)
7
6
  import ExpoObjC
@@ -15,14 +14,18 @@ import ExpoObjC
15
14
 
16
15
  Responsibilities:
17
16
  - Create the `UIWindow` from the connecting `UIWindowScene` and start React Native into it.
18
- - Re-feed scene life-cycle, URL, and user-activity events to the existing
19
- `ExpoAppDelegateSubscriberManager`, so app delegate subscribers keep working unchanged.
17
+ - Re-feed scene life-cycle, URL, user-activity, and quick-action events to the app delegate.
18
+
19
+ Requires the app delegate to be an `ExpoAppDelegate`; it forwards every event to the subscribers,
20
+ so one call reaches both subscribers and `AppDelegate` overrides.
20
21
  */
21
22
  @available(iOSApplicationExtension, unavailable)
22
23
  @objc(EXExpoAppSceneDelegate)
23
24
  open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
24
25
  open var window: UIWindow?
25
26
 
27
+ let forwarder = SceneEventForwarder()
28
+
26
29
  open func scene(
27
30
  _ scene: UIScene,
28
31
  willConnectTo session: UISceneSession,
@@ -31,12 +34,14 @@ open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
31
34
  guard let windowScene = scene as? UIWindowScene else {
32
35
  return
33
36
  }
34
- guard let provider = UIApplication.shared.delegate as? ExpoReactNativeFactoryProvider,
37
+ guard let appDelegate = UIApplication.shared.delegate as? ExpoAppDelegate,
38
+ let provider = appDelegate as? ExpoReactNativeFactoryProvider,
35
39
  let factory = provider.reactNativeFactory else {
36
40
  fatalError(
37
- "ExpoAppSceneDelegate couldn't start React Native because the app delegate doesn't provide a "
38
- + "React Native factory. Make sure your AppDelegate conforms to ExpoReactNativeFactoryProvider and "
39
- + "creates its RCTReactNativeFactory in application(_:didFinishLaunchingWithOptions:)."
41
+ "ExpoAppSceneDelegate couldn't start React Native because the app delegate isn't an "
42
+ + "ExpoAppDelegate that provides a React Native factory. Make sure your AppDelegate subclasses "
43
+ + "ExpoAppDelegate, conforms to ExpoReactNativeFactoryProvider and creates its "
44
+ + "RCTReactNativeFactory in application(_:didFinishLaunchingWithOptions:)."
40
45
  )
41
46
  }
42
47
 
@@ -64,43 +69,76 @@ open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
64
69
  )
65
70
 
66
71
  // Deep links / universal links.
67
- Self.route(urlContexts: connectionOptions.urlContexts)
68
- connectionOptions.userActivities.forEach { Self.route(userActivity: $0) }
72
+ connectionOptions.urlContexts.forEach {
73
+ forwarder.open(url: $0.url, options: Self.openURLOptions(from: $0.options))
74
+ }
75
+ connectionOptions.userActivities.forEach { forwarder.continue($0) }
76
+
77
+ #if os(iOS)
78
+ // A quick action that cold-starts the app arrives here instead of in
79
+ // `windowScene(_:performActionFor:completionHandler:)`, which UIKit only calls while running.
80
+ if let shortcutItem = connectionOptions.shortcutItem {
81
+ forwarder.perform(shortcutItem) { _ in }
82
+ }
83
+ #endif
69
84
  }
70
85
 
71
86
  open func sceneDidDisconnect(_ scene: UIScene) {
72
87
  window = nil
73
88
  }
74
89
 
75
- // In the scene lifecycle UIKit no longer calls the app delegate equivalents, so we forward
76
- // these to the subscriber manager to preserve existing subscriber behavior.
77
-
78
90
  open func sceneDidBecomeActive(_ scene: UIScene) {
79
- ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(UIApplication.shared)
91
+ forwarder.didBecomeActive()
80
92
  }
81
93
 
82
94
  open func sceneWillResignActive(_ scene: UIScene) {
83
- ExpoAppDelegateSubscriberManager.applicationWillResignActive(UIApplication.shared)
95
+ forwarder.willResignActive()
84
96
  }
85
97
 
86
98
  open func sceneWillEnterForeground(_ scene: UIScene) {
87
- ExpoAppDelegateSubscriberManager.applicationWillEnterForeground(UIApplication.shared)
99
+ forwarder.willEnterForeground()
88
100
  }
89
101
 
90
102
  open func sceneDidEnterBackground(_ scene: UIScene) {
91
- ExpoAppDelegateSubscriberManager.applicationDidEnterBackground(UIApplication.shared)
103
+ forwarder.didEnterBackground()
92
104
  }
93
105
 
94
106
  open func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
95
- Self.route(urlContexts: URLContexts)
107
+ URLContexts.forEach { forwarder.open(url: $0.url, options: Self.openURLOptions(from: $0.options)) }
108
+ }
109
+
110
+ open func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
111
+ forwarder.willContinueUserActivity(withType: userActivityType)
96
112
  }
97
113
 
98
114
  open func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
99
- Self.route(userActivity: userActivity)
115
+ forwarder.continue(userActivity)
116
+ }
117
+
118
+ open func scene(
119
+ _ scene: UIScene,
120
+ didFailToContinueUserActivityWithType userActivityType: String,
121
+ error: Error
122
+ ) {
123
+ forwarder.didFailToContinueUserActivity(withType: userActivityType, error: error)
100
124
  }
125
+
126
+ open func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) {
127
+ forwarder.didUpdate(userActivity)
128
+ }
129
+
130
+ #if os(iOS)
131
+ open func windowScene(
132
+ _ windowScene: UIWindowScene,
133
+ performActionFor shortcutItem: UIApplicationShortcutItem,
134
+ completionHandler: @escaping (Bool) -> Void
135
+ ) {
136
+ forwarder.perform(shortcutItem, completionHandler: completionHandler)
137
+ }
138
+ #endif
101
139
  }
102
140
 
103
- // MARK: - Launch options & routing helpers
141
+ // MARK: - Launch options
104
142
 
105
143
  @available(iOSApplicationExtension, unavailable)
106
144
  extension ExpoAppSceneDelegate {
@@ -132,29 +170,6 @@ extension ExpoAppSceneDelegate {
132
170
  return launchOptions.isEmpty ? nil : launchOptions
133
171
  }
134
172
 
135
- /// Pass incoming URL contexts to both the subscriber manager and `RCTLinkingManager`.
136
- public static func route(urlContexts: Set<UIOpenURLContext>) {
137
- for context in urlContexts {
138
- let options = openURLOptions(from: context.options)
139
- _ = ExpoAppDelegateSubscriberManager.application(UIApplication.shared, open: context.url, options: options)
140
- RCTLinkingManager.application(UIApplication.shared, open: context.url, options: options)
141
- }
142
- }
143
-
144
- /// Passes an incoming `NSUserActivity` to both the subscriber manager and `RCTLinkingManager`.
145
- public static func route(userActivity: NSUserActivity) {
146
- _ = ExpoAppDelegateSubscriberManager.application(
147
- UIApplication.shared,
148
- continue: userActivity,
149
- restorationHandler: { _ in }
150
- )
151
- RCTLinkingManager.application(
152
- UIApplication.shared,
153
- continue: userActivity,
154
- restorationHandler: { _ in }
155
- )
156
- }
157
-
158
173
  private static func openURLOptions(
159
174
  from sceneOptions: UIScene.OpenURLOptions
160
175
  ) -> [UIApplication.OpenURLOptionsKey: Any] {
@@ -0,0 +1,154 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+ import React
5
+
6
+ #if os(iOS) || os(tvOS)
7
+
8
+ /**
9
+ Re-feeds scene events to the app delegate. Under the scene life cycle UIKit no longer calls the
10
+ app delegate's URL, user-activity, life-cycle and quick-action methods, so overrides in the app's
11
+ `AppDelegate: ExpoAppDelegate` would never run; `ExpoAppDelegate` forwards every event to the
12
+ subscribers, so one call reaches both.
13
+
14
+ Main-actor isolated: `UIWindowSceneDelegate` hands these events over on the main actor, and every
15
+ `ExpoAppDelegate` method they reach is main-actor isolated too.
16
+ */
17
+ @MainActor
18
+ @available(iOSApplicationExtension, unavailable)
19
+ struct SceneEventForwarder {
20
+ var appDelegate: () -> ExpoAppDelegate? = { UIApplication.shared.delegate as? ExpoAppDelegate }
21
+
22
+ func open(url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) {
23
+ let application = UIApplication.shared
24
+ let delegate = appDelegate()
25
+
26
+ notifyLinkingManagerUnlessAlreadyNotified(of: url) {
27
+ _ = delegate?.application(application, open: url, options: options)
28
+ } notify: {
29
+ RCTLinkingManager.application(application, open: url, options: options)
30
+ }
31
+ }
32
+
33
+ func `continue`(_ userActivity: NSUserActivity) {
34
+ let application = UIApplication.shared
35
+ let delegate = appDelegate()
36
+
37
+ // `RCTLinkingManager` only announces browsing-web activities, so there is nothing to dedupe
38
+ // against for the other activity types.
39
+ notifyLinkingManagerUnlessAlreadyNotified(of: userActivity.webpageURL) {
40
+ _ = delegate?.application(application, continue: userActivity, restorationHandler: { _ in })
41
+ } notify: {
42
+ RCTLinkingManager.application(application, continue: userActivity, restorationHandler: { _ in })
43
+ }
44
+ }
45
+
46
+ func willContinueUserActivity(withType userActivityType: String) {
47
+ // The scene callback has no return value, unlike its app-delegate counterpart. Calling the app
48
+ // delegate still lets every subscriber prepare; its aggregated result is intentionally ignored.
49
+ _ = appDelegate()?.application(
50
+ UIApplication.shared,
51
+ willContinueUserActivityWithType: userActivityType
52
+ )
53
+ }
54
+
55
+ func didFailToContinueUserActivity(withType userActivityType: String, error: Error) {
56
+ appDelegate()?.application(
57
+ UIApplication.shared,
58
+ didFailToContinueUserActivityWithType: userActivityType,
59
+ error: error
60
+ )
61
+ }
62
+
63
+ func didUpdate(_ userActivity: NSUserActivity) {
64
+ appDelegate()?.application(UIApplication.shared, didUpdate: userActivity)
65
+ }
66
+
67
+ func didBecomeActive() {
68
+ appDelegate()?.applicationDidBecomeActive(UIApplication.shared)
69
+ }
70
+
71
+ func willResignActive() {
72
+ appDelegate()?.applicationWillResignActive(UIApplication.shared)
73
+ }
74
+
75
+ func willEnterForeground() {
76
+ appDelegate()?.applicationWillEnterForeground(UIApplication.shared)
77
+ }
78
+
79
+ func didEnterBackground() {
80
+ appDelegate()?.applicationDidEnterBackground(UIApplication.shared)
81
+ }
82
+
83
+ #if os(iOS)
84
+ /// UIKit waits on `completionHandler`, so it has to run exactly once.
85
+ func perform(
86
+ _ shortcutItem: UIApplicationShortcutItem,
87
+ completionHandler: @escaping (Bool) -> Void
88
+ ) {
89
+ guard let delegate = appDelegate() else {
90
+ completionHandler(false)
91
+ return
92
+ }
93
+ delegate.application(
94
+ UIApplication.shared,
95
+ performActionFor: shortcutItem,
96
+ completionHandler: completionHandler
97
+ )
98
+ }
99
+ #endif
100
+
101
+ /// Runs `body`, then calls `notify` unless `RCTLinkingManager` was already notified about `url`
102
+ /// while `body` ran. Always calls `notify` when `url` is `nil`.
103
+ ///
104
+ /// App delegates generated by SDK 57 and older call `RCTLinkingManager` themselves, which would
105
+ /// deliver the JS `url` event twice now that their overrides run again, and no static check can
106
+ /// tell such an override apart — so listen for what `RCTLinkingManager` announces instead.
107
+ private func notifyLinkingManagerUnlessAlreadyNotified(
108
+ of url: URL?,
109
+ during body: () -> Void,
110
+ notify: () -> Void
111
+ ) {
112
+ guard let url else {
113
+ body()
114
+ notify()
115
+ return
116
+ }
117
+ let observer = LinkingManagerObserver(url: url)
118
+ body()
119
+ if !observer.wasNotified {
120
+ notify()
121
+ }
122
+ }
123
+ }
124
+
125
+ /// Observes the notification `RCTLinkingManager` posts for every link it handles. The name is
126
+ /// private to React Native's `RCTLinkingManager.mm` — expo-router spells it out the same way in
127
+ /// `ExpoHeadAppDelegateSubscriber.swift`. If React Native renames it, legacy overrides deliver a
128
+ /// duplicate `url` event again; a link is never lost.
129
+ private final class LinkingManagerObserver: NSObject {
130
+ private let expectedURL: String
131
+ private(set) var wasNotified = false
132
+
133
+ init(url: URL) {
134
+ expectedURL = url.absoluteString
135
+ super.init()
136
+ NotificationCenter.default.addObserver(
137
+ self,
138
+ selector: #selector(linkingManagerDidOpenURL(_:)),
139
+ name: Notification.Name("RCTOpenURLNotification"),
140
+ object: nil
141
+ )
142
+ }
143
+
144
+ deinit {
145
+ NotificationCenter.default.removeObserver(self)
146
+ }
147
+
148
+ @objc
149
+ private func linkingManagerDidOpenURL(_ notification: Notification) {
150
+ wasNotified = wasNotified || notification.userInfo?["url"] as? String == expectedURL
151
+ }
152
+ }
153
+
154
+ #endif
@@ -1,11 +1,13 @@
1
1
  // Copyright 2025-present 650 Industries. All rights reserved.
2
2
 
3
+ import ExpoModulesCore
4
+ import React
3
5
  import Testing
4
6
  @testable import Expo
5
7
 
6
8
  #if os(iOS) || os(tvOS)
7
9
 
8
- @Suite
10
+ @Suite(.serialized)
9
11
  struct ExpoAppSceneDelegateTests {
10
12
  @Test
11
13
  func `extends UIResponder`() {
@@ -22,6 +24,23 @@ struct ExpoAppSceneDelegateTests {
22
24
  #expect(ExpoAppSceneDelegate.self is UIWindowSceneDelegate.Type)
23
25
  }
24
26
 
27
+ @Test
28
+ @MainActor
29
+ func `exposes scene user activity callbacks to UIKit`() {
30
+ let sceneDelegate = ExpoAppSceneDelegate()
31
+ #expect(
32
+ sceneDelegate.responds(
33
+ to: #selector(UISceneDelegate.scene(_:willContinueUserActivityWithType:))
34
+ )
35
+ )
36
+ #expect(sceneDelegate.responds(to: #selector(UISceneDelegate.scene(_:didUpdate:))))
37
+ #expect(
38
+ sceneDelegate.responds(
39
+ to: #selector(UISceneDelegate.scene(_:didFailToContinueUserActivityWithType:error:))
40
+ )
41
+ )
42
+ }
43
+
25
44
  @Test
26
45
  @MainActor
27
46
  func `builds launch options carrying a cold-start URL`() {
@@ -54,6 +73,487 @@ struct ExpoAppSceneDelegateTests {
54
73
  func `returns nil launch options without a URL or user activity`() {
55
74
  #expect(ExpoAppSceneDelegate.launchOptions(url: nil, userActivity: nil) == nil)
56
75
  }
76
+
77
+ @Test
78
+ @MainActor
79
+ func `routes an opened URL through the app delegate`() {
80
+ let spy = SpyAppDelegate()
81
+ let url = URL(string: "bareexpo://scene-delegate/open-url")!
82
+ SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [.sourceApplication: "dev.expo.Payments"])
83
+ #expect(spy.openedURLs.count == 1)
84
+ #expect(spy.openedURLs.first?.url == url)
85
+ #expect(spy.openedURLs.first?.options[.sourceApplication] as? String == "dev.expo.Payments")
86
+ }
87
+
88
+ @Test
89
+ @MainActor
90
+ func `routes a continued user activity through the app delegate`() {
91
+ let spy = SpyAppDelegate()
92
+ let subscriber = UserActivityRecordingSubscriber()
93
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
94
+ let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
95
+ userActivity.webpageURL = URL(string: "https://expo.dev/scene-delegate")!
96
+ SceneEventForwarder(appDelegate: { spy }).continue(userActivity)
97
+ #expect(spy.continuedUserActivities.count == 1)
98
+ #expect(spy.continuedUserActivities.first === userActivity)
99
+ #expect(subscriber.continuations.filter { $0 === userActivity }.count == 1)
100
+ }
101
+
102
+ @Test
103
+ @MainActor
104
+ func `asks every subscriber to prepare for a scene user activity`() {
105
+ let activityType = "dev.expo.scene-delegate.prepare.\(UUID().uuidString)"
106
+ let firstSubscriber = UserActivityRecordingSubscriber(willContinueResult: false)
107
+ let handlingSubscriber = UserActivityRecordingSubscriber(willContinueResult: true)
108
+ let lastSubscriber = UserActivityRecordingSubscriber(willContinueResult: false)
109
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(firstSubscriber)
110
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(handlingSubscriber)
111
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(lastSubscriber)
112
+
113
+ let spy = SpyAppDelegate()
114
+ SceneEventForwarder(appDelegate: { spy }).willContinueUserActivity(withType: activityType)
115
+
116
+ #expect(spy.preparations.count == 1)
117
+ #expect(isSharedApplication(spy.preparations.first?.application))
118
+ #expect(spy.preparations.first?.activityType == activityType)
119
+ #expect(spy.continuedUserActivities.isEmpty)
120
+ for subscriber in [firstSubscriber, handlingSubscriber, lastSubscriber] {
121
+ let preparations = subscriber.preparations.filter { $0.activityType == activityType }
122
+ #expect(preparations.count == 1)
123
+ #expect(isSharedApplication(preparations.first?.application))
124
+ #expect(subscriber.continuations.isEmpty)
125
+ }
126
+ }
127
+
128
+ @Test
129
+ @MainActor
130
+ func `forwards scene user activity updates and failures without continuing`() {
131
+ let activityType = "dev.expo.scene-delegate.failure.\(UUID().uuidString)"
132
+ let userActivity = NSUserActivity(
133
+ activityType: "dev.expo.scene-delegate.update.\(UUID().uuidString)"
134
+ )
135
+ let error = NSError(
136
+ domain: "dev.expo.scene-delegate.handoff",
137
+ code: 17,
138
+ userInfo: [NSLocalizedDescriptionKey: "Handoff transfer failed"]
139
+ )
140
+ let subscriber = UserActivityRecordingSubscriber()
141
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
142
+
143
+ let spy = SpyAppDelegate()
144
+ let forwarder = SceneEventForwarder(appDelegate: { spy })
145
+ forwarder.didUpdate(userActivity)
146
+ forwarder.didFailToContinueUserActivity(withType: activityType, error: error)
147
+
148
+ #expect(spy.updatedUserActivities.count == 1)
149
+ #expect(isSharedApplication(spy.updatedUserActivities.first?.application))
150
+ #expect(spy.updatedUserActivities.first?.userActivity === userActivity)
151
+ #expect(spy.userActivityFailures.count == 1)
152
+ #expect(isSharedApplication(spy.userActivityFailures.first?.application))
153
+ #expect(spy.userActivityFailures.first?.activityType == activityType)
154
+ #expect((spy.userActivityFailures.first?.error as NSError?) === error)
155
+ #expect(spy.preparations.isEmpty)
156
+ #expect(spy.continuedUserActivities.isEmpty)
157
+ #expect(subscriber.updates.count == 1)
158
+ #expect(isSharedApplication(subscriber.updates.first?.application))
159
+ #expect(subscriber.updates.first?.userActivity === userActivity)
160
+ #expect(subscriber.failures.count == 1)
161
+ #expect(isSharedApplication(subscriber.failures.first?.application))
162
+ #expect(subscriber.failures.first?.activityType == activityType)
163
+ #expect((subscriber.failures.first?.error as NSError?) === error)
164
+ #expect(subscriber.preparations.isEmpty)
165
+ #expect(subscriber.continuations.isEmpty)
166
+ }
167
+
168
+ @Test
169
+ @MainActor
170
+ func `routes life cycle events through the app delegate`() {
171
+ let spy = SpyAppDelegate()
172
+ let forwarder = SceneEventForwarder(appDelegate: { spy })
173
+ forwarder.didBecomeActive()
174
+ forwarder.willResignActive()
175
+ forwarder.willEnterForeground()
176
+ forwarder.didEnterBackground()
177
+ #expect(spy.lifeCycleEvents == [
178
+ "applicationDidBecomeActive",
179
+ "applicationWillResignActive",
180
+ "applicationWillEnterForeground",
181
+ "applicationDidEnterBackground"
182
+ ])
183
+ }
184
+
185
+ #if os(iOS)
186
+ @Test
187
+ @MainActor
188
+ func `routes a quick action through the app delegate`() async {
189
+ let spy = SpyAppDelegate()
190
+ let shortcutItem = UIApplicationShortcutItem(
191
+ type: "dev.expo.bareexpo.run-tests",
192
+ localizedTitle: "Run tests"
193
+ )
194
+ var handled: [Bool] = []
195
+ // The subscriber manager completes on the next main-queue turn when a subscriber handles the
196
+ // action, so wait for the first reply instead of assuming a synchronous one.
197
+ await withCheckedContinuation { continuation in
198
+ var didResume = false
199
+ SceneEventForwarder(appDelegate: { spy }).perform(shortcutItem) { succeeded in
200
+ handled.append(succeeded)
201
+ if !didResume {
202
+ didResume = true
203
+ continuation.resume()
204
+ }
205
+ }
206
+ }
207
+ await drainMainQueue()
208
+ #expect(spy.shortcutItemTypes == [shortcutItem.type])
209
+ #expect(handled == [false])
210
+ }
211
+ #endif
212
+
213
+ @Test
214
+ @MainActor
215
+ func `notifies RCTLinkingManager once per routed URL`() {
216
+ let url = URL(string: "bareexpo://scene-delegate/linking")!
217
+ let recorder = OpenURLNotificationRecorder()
218
+ let spy = SpyAppDelegate()
219
+ SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [:])
220
+ #expect(recorder.count(of: url) == 1)
221
+ }
222
+
223
+ @Test
224
+ @MainActor
225
+ func `notifies RCTLinkingManager once when the delegate notifies it too`() {
226
+ let delegate = LegacyLinkingAppDelegate()
227
+ let url = URL(string: "bareexpo://scene-delegate/legacy-open-url")!
228
+ let recorder = OpenURLNotificationRecorder()
229
+ SceneEventForwarder(appDelegate: { delegate }).open(url: url, options: [:])
230
+ #expect(delegate.openedURLs == [url])
231
+ #expect(recorder.count(of: url) == 1)
232
+ }
233
+
234
+ @Test
235
+ @MainActor
236
+ func `notifies RCTLinkingManager once per user activity when the delegate notifies it too`() {
237
+ let delegate = LegacyLinkingAppDelegate()
238
+ let webpageURL = URL(string: "https://expo.dev/scene-delegate/legacy-activity")!
239
+ let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
240
+ userActivity.webpageURL = webpageURL
241
+ let recorder = OpenURLNotificationRecorder()
242
+ SceneEventForwarder(appDelegate: { delegate }).continue(userActivity)
243
+ #expect(delegate.continuedUserActivities.count == 1)
244
+ #expect(recorder.count(of: webpageURL) == 1)
245
+ }
246
+
247
+ @Test
248
+ @MainActor
249
+ func `does not notify subscribers without an ExpoAppDelegate`() {
250
+ let subscriber = URLRecordingSubscriber()
251
+ ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
252
+
253
+ let url = URL(string: "bareexpo://scene-delegate/no-app-delegate")!
254
+ let recorder = OpenURLNotificationRecorder()
255
+ SceneEventForwarder(appDelegate: { nil }).open(url: url, options: [:])
256
+ #expect(subscriber.count(of: url) == 0)
257
+ #expect(recorder.count(of: url) == 1)
258
+ }
259
+
260
+ #if os(iOS)
261
+ @Test
262
+ @MainActor
263
+ func `completes a quick action with false without an ExpoAppDelegate`() async {
264
+ let shortcutItem = UIApplicationShortcutItem(
265
+ type: "dev.expo.bareexpo.unhandled-action",
266
+ localizedTitle: "Unhandled action"
267
+ )
268
+ var handled: [Bool] = []
269
+ await withCheckedContinuation { continuation in
270
+ var didResume = false
271
+ SceneEventForwarder(appDelegate: { nil }).perform(shortcutItem) { succeeded in
272
+ handled.append(succeeded)
273
+ if !didResume {
274
+ didResume = true
275
+ continuation.resume()
276
+ }
277
+ }
278
+ }
279
+ await drainMainQueue()
280
+ #expect(handled == [false])
281
+ }
282
+ #endif
283
+
284
+ @Test
285
+ @MainActor
286
+ func `forwards to the app delegate of the running application`() {
287
+ let forwarder = ExpoAppSceneDelegate().forwarder
288
+ #expect(forwarder.appDelegate() === UIApplication.shared.delegate as? ExpoAppDelegate)
289
+ }
290
+ }
291
+
292
+ @MainActor
293
+ private func isSharedApplication(_ application: UIApplication?) -> Bool {
294
+ // A hostless XCTest runner can bridge `UIApplication.shared` as nil despite its nonoptional
295
+ // Swift declaration. Evaluate identity outside `#expect` so the macro does not capture it.
296
+ let sharedApplication: UIApplication? = UIApplication.shared
297
+ return application === sharedApplication
298
+ }
299
+
300
+ /// Records user-activity callbacks delivered through the app-delegate subscriber compatibility
301
+ /// layer. Subscribers cannot be unregistered, so tests use unique activity types and retain these
302
+ /// instances for the remainder of the test process.
303
+ private final class UserActivityRecordingSubscriber: NSObject, ExpoAppDelegateSubscriberProtocol {
304
+ struct Preparation {
305
+ let application: UIApplication?
306
+ let activityType: String
307
+ }
308
+
309
+ struct ActivityUpdate {
310
+ let application: UIApplication?
311
+ let userActivity: NSUserActivity
312
+ }
313
+
314
+ struct Failure {
315
+ let application: UIApplication?
316
+ let activityType: String
317
+ let error: Error
318
+ }
319
+
320
+ let willContinueResult: Bool
321
+ var preparations: [Preparation] = []
322
+ var continuations: [NSUserActivity] = []
323
+ var updates: [ActivityUpdate] = []
324
+ var failures: [Failure] = []
325
+
326
+ init(willContinueResult: Bool = false) {
327
+ self.willContinueResult = willContinueResult
328
+ }
329
+
330
+ func application(
331
+ _ application: UIApplication,
332
+ willContinueUserActivityWithType userActivityType: String
333
+ ) -> Bool {
334
+ preparations.append(Preparation(application: application, activityType: userActivityType))
335
+ return willContinueResult
336
+ }
337
+
338
+ func application(
339
+ _ application: UIApplication,
340
+ continue userActivity: NSUserActivity,
341
+ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
342
+ ) -> Bool {
343
+ continuations.append(userActivity)
344
+ restorationHandler(nil)
345
+ return false
346
+ }
347
+
348
+ func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
349
+ updates.append(ActivityUpdate(application: application, userActivity: userActivity))
350
+ }
351
+
352
+ func application(
353
+ _ application: UIApplication,
354
+ didFailToContinueUserActivityWithType userActivityType: String,
355
+ error: Error
356
+ ) {
357
+ failures.append(Failure(application: application, activityType: userActivityType, error: error))
358
+ }
359
+ }
360
+
361
+ /// App delegate shaped like the SDK ≤ 57 templates, which call `RCTLinkingManager` from their own
362
+ /// overrides. See `packages/expo-updates/e2e/fixtures/custom_init/AppDelegate.swift`.
363
+ private final class LegacyLinkingAppDelegate: ExpoAppDelegate {
364
+ var openedURLs: [URL] = []
365
+ var continuedUserActivities: [NSUserActivity] = []
366
+
367
+ override func application(
368
+ _ app: UIApplication,
369
+ open url: URL,
370
+ options: [UIApplication.OpenURLOptionsKey: Any]
371
+ ) -> Bool {
372
+ openedURLs.append(url)
373
+ let handled = super.application(app, open: url, options: options)
374
+ RCTLinkingManager.application(app, open: url, options: options)
375
+ return handled
376
+ }
377
+
378
+ override func application(
379
+ _ application: UIApplication,
380
+ continue userActivity: NSUserActivity,
381
+ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
382
+ ) -> Bool {
383
+ continuedUserActivities.append(userActivity)
384
+ let handled = super.application(application, continue: userActivity, restorationHandler: restorationHandler)
385
+ RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
386
+ return handled
387
+ }
388
+ }
389
+
390
+ /// Lets the main-queue blocks scheduled so far run, so a late duplicate reply is recorded before
391
+ /// the assertions.
392
+ @MainActor
393
+ private func drainMainQueue() async {
394
+ await withCheckedContinuation { continuation in
395
+ DispatchQueue.main.async { continuation.resume() }
396
+ }
397
+ }
398
+
399
+ /// Records the URLs a module subscriber receives. Registration is permanent for the process, so it
400
+ /// reports not having handled the URL to leave the other delegates' behavior untouched.
401
+ private final class URLRecordingSubscriber: NSObject, ExpoAppDelegateSubscriberProtocol {
402
+ private var urls: [URL] = []
403
+
404
+ func application(
405
+ _ app: UIApplication,
406
+ open url: URL,
407
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
408
+ ) -> Bool {
409
+ urls.append(url)
410
+ return false
411
+ }
412
+
413
+ func count(of url: URL) -> Int {
414
+ return urls.filter { $0 == url }.count
415
+ }
416
+ }
417
+
418
+ /// Records the events an `ExpoAppDelegate` subclass receives, then forwards them to `super` the way
419
+ /// an app's own `AppDelegate` override would.
420
+ private final class SpyAppDelegate: ExpoAppDelegate {
421
+ struct OpenedURL {
422
+ let url: URL
423
+ let options: [UIApplication.OpenURLOptionsKey: Any]
424
+ }
425
+
426
+ struct Preparation {
427
+ let application: UIApplication?
428
+ let activityType: String
429
+ }
430
+
431
+ struct ActivityUpdate {
432
+ let application: UIApplication?
433
+ let userActivity: NSUserActivity
434
+ }
435
+
436
+ struct UserActivityFailure {
437
+ let application: UIApplication?
438
+ let activityType: String
439
+ let error: Error
440
+ }
441
+
442
+ var openedURLs: [OpenedURL] = []
443
+ var continuedUserActivities: [NSUserActivity] = []
444
+ var preparations: [Preparation] = []
445
+ var updatedUserActivities: [ActivityUpdate] = []
446
+ var userActivityFailures: [UserActivityFailure] = []
447
+ var lifeCycleEvents: [String] = []
448
+ var shortcutItemTypes: [String] = []
449
+
450
+ override func application(
451
+ _ app: UIApplication,
452
+ open url: URL,
453
+ options: [UIApplication.OpenURLOptionsKey: Any]
454
+ ) -> Bool {
455
+ openedURLs.append(OpenedURL(url: url, options: options))
456
+ return super.application(app, open: url, options: options)
457
+ }
458
+
459
+ override func application(
460
+ _ application: UIApplication,
461
+ willContinueUserActivityWithType userActivityType: String
462
+ ) -> Bool {
463
+ preparations.append(Preparation(application: application, activityType: userActivityType))
464
+ return super.application(application, willContinueUserActivityWithType: userActivityType)
465
+ }
466
+
467
+ override func application(
468
+ _ application: UIApplication,
469
+ continue userActivity: NSUserActivity,
470
+ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
471
+ ) -> Bool {
472
+ continuedUserActivities.append(userActivity)
473
+ return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
474
+ }
475
+
476
+ override func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
477
+ updatedUserActivities.append(ActivityUpdate(application: application, userActivity: userActivity))
478
+ super.application(application, didUpdate: userActivity)
479
+ }
480
+
481
+ override func application(
482
+ _ application: UIApplication,
483
+ didFailToContinueUserActivityWithType userActivityType: String,
484
+ error: Error
485
+ ) {
486
+ userActivityFailures.append(
487
+ UserActivityFailure(application: application, activityType: userActivityType, error: error)
488
+ )
489
+ super.application(
490
+ application,
491
+ didFailToContinueUserActivityWithType: userActivityType,
492
+ error: error
493
+ )
494
+ }
495
+
496
+ override func applicationDidBecomeActive(_ application: UIApplication) {
497
+ lifeCycleEvents.append("applicationDidBecomeActive")
498
+ super.applicationDidBecomeActive(application)
499
+ }
500
+
501
+ override func applicationWillResignActive(_ application: UIApplication) {
502
+ lifeCycleEvents.append("applicationWillResignActive")
503
+ super.applicationWillResignActive(application)
504
+ }
505
+
506
+ override func applicationWillEnterForeground(_ application: UIApplication) {
507
+ lifeCycleEvents.append("applicationWillEnterForeground")
508
+ super.applicationWillEnterForeground(application)
509
+ }
510
+
511
+ override func applicationDidEnterBackground(_ application: UIApplication) {
512
+ lifeCycleEvents.append("applicationDidEnterBackground")
513
+ super.applicationDidEnterBackground(application)
514
+ }
515
+
516
+ #if os(iOS)
517
+ override func application(
518
+ _ application: UIApplication,
519
+ performActionFor shortcutItem: UIApplicationShortcutItem,
520
+ completionHandler: @escaping (Bool) -> Void
521
+ ) {
522
+ shortcutItemTypes.append(shortcutItem.type)
523
+ super.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
524
+ }
525
+ #endif
526
+ }
527
+
528
+ /// Counts the notifications `RCTLinkingManager` posts for handled URLs. React Native keeps the
529
+ /// notification name private to its implementation file, so it's spelled out here.
530
+ private final class OpenURLNotificationRecorder: NSObject {
531
+ private var urls: [String] = []
532
+
533
+ override init() {
534
+ super.init()
535
+ NotificationCenter.default.addObserver(
536
+ self,
537
+ selector: #selector(record(_:)),
538
+ name: Notification.Name("RCTOpenURLNotification"),
539
+ object: nil
540
+ )
541
+ }
542
+
543
+ deinit {
544
+ NotificationCenter.default.removeObserver(self)
545
+ }
546
+
547
+ func count(of url: URL) -> Int {
548
+ return urls.filter { $0 == url.absoluteString }.count
549
+ }
550
+
551
+ @objc
552
+ private func record(_ notification: Notification) {
553
+ if let url = notification.userInfo?["url"] as? String {
554
+ urls.append(url)
555
+ }
556
+ }
57
557
  }
58
558
 
59
559
  #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "58.0.0-preview.0",
3
+ "version": "58.0.0-preview.1",
4
4
  "description": "The Expo SDK",
5
5
  "keywords": [
6
6
  "expo"
@@ -198,40 +198,40 @@
198
198
  },
199
199
  "dependencies": {
200
200
  "@babel/runtime": "^7.20.0",
201
- "@expo/cli": "^58.0.0",
201
+ "@expo/cli": "^58.0.1",
202
202
  "@expo/config": "~58.0.0",
203
203
  "@expo/config-plugins": "~58.0.0",
204
204
  "@expo/devtools": "~58.0.0",
205
205
  "@expo/dom-webview": "~58.0.0",
206
206
  "@expo/fingerprint": "^0.21.0",
207
207
  "@expo/local-build-cache-provider": "^58.0.0",
208
- "@expo/log-box": "^58.0.0",
208
+ "@expo/log-box": "^58.0.1",
209
209
  "@expo/log-box-utils": "^58.0.0",
210
210
  "@expo/metro": "~56.1.0",
211
- "@expo/metro-config": "~58.0.0",
211
+ "@expo/metro-config": "~58.0.1",
212
212
  "@ungap/structured-clone": "^1.3.0",
213
- "babel-preset-expo": "~58.0.0",
214
- "expo-asset": "~58.0.0",
215
- "expo-constants": "~58.0.0",
213
+ "babel-preset-expo": "~58.0.1",
214
+ "expo-asset": "~58.0.1",
215
+ "expo-constants": "~58.0.1",
216
216
  "expo-file-system": "~58.0.0",
217
217
  "expo-font": "~58.0.0",
218
218
  "expo-keep-awake": "~58.0.0",
219
- "expo-modules-autolinking": "~58.0.0",
220
- "expo-modules-core": "~58.0.0",
219
+ "expo-modules-autolinking": "~58.0.1",
220
+ "expo-modules-core": "~58.0.1",
221
221
  "pretty-format": "^29.7.0",
222
222
  "react-refresh": "^0.14.2",
223
223
  "whatwg-url-minimum": "^0.2.0"
224
224
  },
225
225
  "devDependencies": {
226
226
  "@expo/dom-webview": "58.0.0",
227
- "@expo/metro-runtime": "58.0.0",
227
+ "@expo/metro-runtime": "58.0.1",
228
228
  "@types/node": "^22.14.0",
229
229
  "@types/react": "~19.2.0",
230
- "expo-updates": "58.0.0",
230
+ "expo-updates": "58.0.3",
231
231
  "npm-run-all2": "^8.0.4",
232
232
  "react": "19.2.3",
233
233
  "react-dom": "19.2.3",
234
- "react-native": "0.87.1",
234
+ "react-native": "0.88.0-rc.0",
235
235
  "rimraf": "^6.1.2",
236
236
  "web-streams-polyfill": "^3.3.2"
237
237
  },
@@ -267,7 +267,7 @@
267
267
  "engines": {
268
268
  "node": "^22.13.0 || ^24.3.0 || ^26.0.0 || >=27.0.0"
269
269
  },
270
- "gitHead": "c543a7181fc32e14f42d89550087b80ac5c490f2",
270
+ "gitHead": "40afe3afa42f1dcca3dcba7ffbb3641859bdf2f0",
271
271
  "scripts": {
272
272
  "build": "../expo-module-scripts/bin/expo-build src",
273
273
  "clean": "rimraf build",
package/src/Expo.ts CHANGED
@@ -38,6 +38,8 @@ export type {
38
38
  IntBasedTypedArray,
39
39
  UintBasedTypedArray,
40
40
  FloatBasedTypedArray,
41
+ ReleasingSharedObjectLifecycle,
42
+ ReleasingSharedObjectLifecycleContext,
41
43
  } from 'expo-modules-core';
42
44
 
43
45
  export type {
@@ -3,7 +3,7 @@
3
3
 
4
4
  // WARN(@kitten): We must ensure that the core react-native globals are initialized before ours
5
5
  // Otherwise we're relying on `getModulesRunBeforeMainModule` which is unstable or can be missing
6
- import 'react-native/Libraries/Core/InitializeCore';
6
+ import 'react-native/setup-env';
7
7
  import '../../types';
8
8
  import { installAbortSignalPatch } from './AbortSignal';
9
9
  import { installFormDataPatch } from './FormData';
package/template.tgz CHANGED
Binary file