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.
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/LocalNetworkPermission.kt +19 -0
- package/android/src/main/java/expo/modules/ReactActivityDelegateWrapper.kt +19 -2
- package/android/src/test/java/expo/modules/ReactActivityDelegateWrapperLocalNetworkPermissionTest.kt +99 -0
- package/build/Expo.d.ts +1 -1
- package/build/Expo.js.map +1 -1
- package/build/winter/runtime.native.d.ts +1 -1
- package/build/winter/runtime.native.js +1 -1
- package/build/winter/runtime.native.js.map +1 -1
- package/bundledNativeModules.json +40 -40
- package/ios/Expo/ExpoAppSceneDelegate.swift +57 -42
- package/ios/Expo/SceneEventForwarder.swift +154 -0
- package/ios/Tests/ExpoAppSceneDelegateTests.swift +501 -1
- package/package.json +13 -13
- package/src/Expo.ts +2 -0
- package/src/winter/runtime.native.ts +1 -1
- package/template.tgz +0 -0
package/android/build.gradle
CHANGED
|
@@ -10,7 +10,7 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
group = 'host.exp.exponent'
|
|
13
|
-
version = '58.0.0-preview.
|
|
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.
|
|
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
|
-
|
|
455
|
+
suspendCancellableCoroutine { continuation ->
|
|
439
456
|
delayLoadAppHandler.whenReady {
|
|
440
457
|
Utils.assertMainThread()
|
|
441
458
|
continuation.resume(Unit)
|
package/android/src/test/java/expo/modules/ReactActivityDelegateWrapperLocalNetworkPermissionTest.kt
ADDED
|
@@ -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;
|
|
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/
|
|
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/
|
|
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,
|
|
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.
|
|
4
|
+
"@expo/metro-runtime": "~58.0.1",
|
|
5
5
|
"@expo/vector-icons": "^15.0.2",
|
|
6
|
-
"@expo/ui": "~58.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.
|
|
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.
|
|
19
|
+
"expo-app-intents": "~0.3.0",
|
|
20
20
|
"expo-app-loader-provider": "~8.0.0",
|
|
21
|
-
"expo-app-metrics": "~58.0.
|
|
21
|
+
"expo-app-metrics": "~58.0.1",
|
|
22
22
|
"expo-apple-authentication": "~58.0.0",
|
|
23
|
-
"expo-application": "~58.0.
|
|
24
|
-
"expo-asset": "~58.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.
|
|
27
|
-
"expo-background-fetch": "~58.0.
|
|
28
|
-
"expo-background-task": "~58.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.
|
|
33
|
-
"expo-build-properties": "~58.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.
|
|
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.
|
|
40
|
-
"expo-contacts": "~58.0.
|
|
41
|
-
"expo-crypto": "~58.0.
|
|
42
|
-
"expo-dev-client": "~58.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.
|
|
52
|
-
"expo-image": "~58.0.
|
|
53
|
-
"expo-image-loader": "~58.0.
|
|
54
|
-
"expo-image-manipulator": "~58.0.
|
|
55
|
-
"expo-image-picker": "~58.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
71
|
-
"expo-modules-core": "~58.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.
|
|
75
|
-
"expo-observe": "~58.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.
|
|
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.
|
|
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.
|
|
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.
|
|
93
|
-
"expo-task-manager": "~58.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.
|
|
95
|
+
"expo-updates": "~58.0.3",
|
|
96
96
|
"expo-video-thumbnails": "~58.0.0",
|
|
97
|
-
"expo-video": "~58.0.
|
|
97
|
+
"expo-video": "~58.0.1",
|
|
98
98
|
"expo-web-browser": "~58.0.0",
|
|
99
|
-
"expo-widgets": "~58.0.
|
|
100
|
-
"jest-expo": "~58.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.
|
|
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,
|
|
19
|
-
|
|
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
|
|
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
|
|
38
|
-
+ "React Native factory. Make sure your AppDelegate
|
|
39
|
-
+ "creates its
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
91
|
+
forwarder.didBecomeActive()
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
open func sceneWillResignActive(_ scene: UIScene) {
|
|
83
|
-
|
|
95
|
+
forwarder.willResignActive()
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
open func sceneWillEnterForeground(_ scene: UIScene) {
|
|
87
|
-
|
|
99
|
+
forwarder.willEnterForeground()
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
open func sceneDidEnterBackground(_ scene: UIScene) {
|
|
91
|
-
|
|
103
|
+
forwarder.didEnterBackground()
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
open func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
95
|
-
Self.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
211
|
+
"@expo/metro-config": "~58.0.1",
|
|
212
212
|
"@ungap/structured-clone": "^1.3.0",
|
|
213
|
-
"babel-preset-expo": "~58.0.
|
|
214
|
-
"expo-asset": "~58.0.
|
|
215
|
-
"expo-constants": "~58.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.
|
|
220
|
-
"expo-modules-core": "~58.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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
@@ -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/
|
|
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
|