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