expo-modules-core 2.0.0-preview.1 → 2.0.0-preview.3

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/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 2.0.0-preview.3 — 2024-10-24
14
+
15
+ _This version does not introduce any user-facing changes._
16
+
17
+ ## 2.0.0-preview.2 — 2024-10-24
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - [iOS] Fix optionals conversion. ([#32239](https://github.com/expo/expo/pull/32239) by [@aleqsio](https://github.com/aleqsio))
22
+ - Fixed retain cycle for `ExpoRequestCdpInterceptor`. ([#32289](https://github.com/expo/expo/pull/32289) by [@kudo](https://github.com/kudo))
23
+
24
+ ### 💡 Others
25
+
26
+ - [android] Add enum event support to OnStartObserving and OnStopObserving. ([#32251](https://github.com/expo/expo/pull/32251), [#32287](https://github.com/expo/expo/pull/32287) by [@wschurman](https://github.com/wschurman))
27
+
13
28
  ## 2.0.0-preview.1 — 2024-10-22
14
29
 
15
30
  ### 🐛 Bug fixes
@@ -56,6 +71,7 @@
56
71
 
57
72
  ### 🐛 Bug fixes
58
73
 
74
+ - [Android] Fixed `RNHeadlessAppLoader` class for New Architecture support. ([#32146](https://github.com/expo/expo/pull/32146) by [@chrfalch](https://github.com/chrfalch))
59
75
  - [iOS] Fix using enums as optional arguments. ([#32147](https://github.com/expo/expo/pull/32147) by [@aleqsio](https://github.com/aleqsio))
60
76
  - [Android] Change JS return type for kotlin `null` to be `null` instead of `undefined`. ([#31301](https://github.com/expo/expo/pull/31301) by [@aleqsio](https://github.com/aleqsio))
61
77
  - [iOS] Swift `Enumerable`s did not correctly convert to JS values. ([#30191](https://github.com/expo/expo/pull/30191) by [@vonovak](https://github.com/vonovak))
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3
3
  apply plugin: 'com.android.library'
4
4
 
5
5
  group = 'host.exp.exponent'
6
- version = '2.0.0-preview.1'
6
+ version = '2.0.0-preview.3'
7
7
 
8
8
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
9
9
  apply from: expoModulesCorePlugin
@@ -67,7 +67,7 @@ android {
67
67
  defaultConfig {
68
68
  consumerProguardFiles 'proguard-rules.pro'
69
69
  versionCode 1
70
- versionName "2.0.0-preview.1"
70
+ versionName "2.0.0-preview.3"
71
71
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
72
72
 
73
73
  testInstrumentationRunner "expo.modules.TestRunner"
@@ -1,16 +1,18 @@
1
1
  package expo.modules.adapters.react.apploader
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
4
5
  import com.facebook.react.ReactApplication
5
6
  import com.facebook.react.ReactInstanceEventListener
6
7
  import com.facebook.react.ReactInstanceManager
7
8
  import com.facebook.react.bridge.ReactContext
8
9
  import com.facebook.react.common.LifecycleState
10
+ import expo.modules.BuildConfig
9
11
  import expo.modules.apploader.HeadlessAppLoader
10
12
  import expo.modules.core.interfaces.Consumer
11
13
  import expo.modules.core.interfaces.DoNotStrip
12
14
 
13
- private val appRecords: MutableMap<String, ReactInstanceManager> = mutableMapOf()
15
+ private val appRecords: MutableMap<String, ReactContext> = mutableMapOf()
14
16
 
15
17
  class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context) : HeadlessAppLoader {
16
18
 
@@ -22,16 +24,35 @@ class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context)
22
24
  }
23
25
 
24
26
  if (context.applicationContext is ReactApplication) {
25
- val reactInstanceManager = (context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager
26
27
  if (!appRecords.containsKey(params.appScopeKey)) {
27
- reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceEventListener {
28
- override fun onReactContextInitialized(context: ReactContext) {
29
- HeadlessAppLoaderNotifier.notifyAppLoaded(params.appScopeKey)
30
- callback?.apply(true)
31
- }
32
- })
33
- appRecords[params.appScopeKey] = reactInstanceManager
34
- if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
28
+ // In old arch reactHost will be null
29
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
30
+ // New architecture
31
+ val reactHost = (context as ReactApplication).reactHost ?: throw IllegalStateException("Your application does not have a valid reactHost")
32
+ reactHost.addReactInstanceEventListener(
33
+ object : ReactInstanceEventListener {
34
+ override fun onReactContextInitialized(context: ReactContext) {
35
+ reactHost.removeReactInstanceEventListener(this)
36
+ HeadlessAppLoaderNotifier.notifyAppLoaded(params.appScopeKey)
37
+ appRecords[params.appScopeKey] = context
38
+ callback?.apply(true)
39
+ }
40
+ }
41
+ )
42
+ reactHost.start()
43
+ } else {
44
+ // Old architecture
45
+ val reactInstanceManager = (context as ReactApplication).reactNativeHost.reactInstanceManager
46
+ reactInstanceManager.addReactInstanceEventListener(
47
+ object : ReactInstanceEventListener {
48
+ override fun onReactContextInitialized(context: ReactContext) {
49
+ HeadlessAppLoaderNotifier.notifyAppLoaded(params.appScopeKey)
50
+ reactInstanceManager.removeReactInstanceEventListener(this)
51
+ appRecords[params.appScopeKey] = context
52
+ callback?.apply(true)
53
+ }
54
+ }
55
+ )
35
56
  reactInstanceManager.createReactContextInBackground()
36
57
  }
37
58
  } else {
@@ -42,18 +63,34 @@ class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context)
42
63
  }
43
64
  }
44
65
 
66
+ @SuppressLint("VisibleForTests")
45
67
  override fun invalidateApp(appScopeKey: String?): Boolean {
46
68
  return if (appRecords.containsKey(appScopeKey) && appRecords[appScopeKey] != null) {
47
- val appRecord: ReactInstanceManager = appRecords[appScopeKey]!!
48
- android.os.Handler(context.mainLooper).post {
49
- // Only destroy the `ReactInstanceManager` if it does not bind with an Activity.
50
- // And The Activity would take over the ownership of `ReactInstanceManager`.
51
- // This case happens when a user clicks a background task triggered notification immediately.
52
- if (appRecord.lifecycleState == LifecycleState.BEFORE_CREATE) {
53
- appRecord.destroy()
69
+ val reactContext = appRecords[appScopeKey] ?: return false
70
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
71
+ // New architecture
72
+ val reactHost = (reactContext.baseContext as ReactApplication).reactHost ?: throw IllegalStateException("Your application does not have a valid reactHost")
73
+ android.os.Handler(reactContext.mainLooper).post {
74
+ reactHost.destroy("Closing headless task app", null)
75
+ HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
76
+ appRecords.remove(appScopeKey)
77
+ }
78
+ } else {
79
+ // Old architecture
80
+ val reactNativeHost = (reactContext as ReactApplication).reactNativeHost
81
+ if (reactNativeHost.hasInstance()) {
82
+ val reactInstanceManager: ReactInstanceManager = reactNativeHost.reactInstanceManager
83
+ android.os.Handler(reactContext.mainLooper).post {
84
+ // Only destroy the `ReactInstanceManager` if it does not bind with an Activity.
85
+ // And The Activity would take over the ownership of `ReactInstanceManager`.
86
+ // This case happens when a user clicks a background task triggered notification immediately.
87
+ if (reactInstanceManager.lifecycleState == LifecycleState.BEFORE_CREATE) {
88
+ reactInstanceManager.destroy()
89
+ }
90
+ HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
91
+ appRecords.remove(appScopeKey)
92
+ }
54
93
  }
55
- HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
56
- appRecords.remove(appScopeKey)
57
94
  }
58
95
  true
59
96
  } else {
@@ -61,8 +98,18 @@ class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context)
61
98
  }
62
99
  }
63
100
 
64
- override fun isRunning(appScopeKey: String?): Boolean =
65
- appRecords.contains(appScopeKey) && appRecords[appScopeKey]!!.hasStartedCreatingInitialContext()
101
+ override fun isRunning(appScopeKey: String?): Boolean {
102
+ val reactContext = appRecords[appScopeKey] ?: return false
103
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
104
+ // New architecture - We can return true since the fact that we have a reactContext
105
+ // means that we've already called start on the reactHost
106
+ return true
107
+ } else {
108
+ // Old architecture
109
+ val reactNativeHost = (reactContext.baseContext as ReactApplication).reactNativeHost
110
+ return reactNativeHost.reactInstanceManager.hasStartedCreatingInitialContext()
111
+ }
112
+ }
66
113
 
67
114
  //endregion HeadlessAppLoader
68
115
  }
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
14
14
  import okhttp3.Request
15
15
  import okhttp3.Response
16
16
  import okhttp3.ResponseBody
17
+ import java.lang.ref.WeakReference
17
18
  import java.math.BigDecimal
18
19
  import java.math.RoundingMode
19
20
 
@@ -22,18 +23,18 @@ import java.math.RoundingMode
22
23
  * dispatch CDP (Chrome DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/) events.
23
24
  */
24
25
  object ExpoRequestCdpInterceptor : ExpoNetworkInspectOkHttpInterceptorsDelegate {
25
- private var delegate: Delegate? = null
26
+ private var delegate: WeakReference<Delegate?> = WeakReference(null)
26
27
  internal var coroutineScope = CoroutineScope(Dispatchers.Default)
27
28
 
28
29
  fun setDelegate(delegate: Delegate?) {
29
30
  coroutineScope.launch {
30
- this@ExpoRequestCdpInterceptor.delegate = delegate
31
+ this@ExpoRequestCdpInterceptor.delegate = WeakReference(delegate)
31
32
  }
32
33
  }
33
34
 
34
35
  private fun dispatchEvent(event: Event) {
35
36
  coroutineScope.launch {
36
- this@ExpoRequestCdpInterceptor.delegate?.dispatch(event.toJson())
37
+ this@ExpoRequestCdpInterceptor.delegate.get()?.dispatch(event.toJson())
37
38
  }
38
39
  }
39
40
 
@@ -7,9 +7,6 @@ import expo.modules.kotlin.providers.AppContextProvider
7
7
  import expo.modules.kotlin.tracing.trace
8
8
  import expo.modules.kotlin.types.Enumerable
9
9
  import kotlinx.coroutines.CoroutineScope
10
- import kotlin.reflect.KProperty1
11
- import kotlin.reflect.full.declaredMemberProperties
12
- import kotlin.reflect.full.primaryConstructor
13
10
 
14
11
  abstract class Module : AppContextProvider {
15
12
 
@@ -54,25 +51,6 @@ abstract class Module : AppContextProvider {
54
51
  }
55
52
 
56
53
  abstract fun definition(): ModuleDefinitionData
57
-
58
- private fun <T> convertEnumToString(enumValue: T): String where T : Enumerable, T : Enum<T> {
59
- val enumClass = enumValue::class
60
- val primaryConstructor = enumClass.primaryConstructor
61
- if (primaryConstructor?.parameters?.size == 1) {
62
- val parameterName = primaryConstructor.parameters.first().name
63
- val parameterProperty = enumClass
64
- .declaredMemberProperties
65
- .find { it.name == parameterName }
66
-
67
- requireNotNull(parameterProperty) { "Cannot find a property for $parameterName parameter" }
68
- require(parameterProperty.returnType.classifier == String::class) { "The enum parameter has to be a string." }
69
-
70
- @Suppress("UNCHECKED_CAST")
71
- return (parameterProperty as KProperty1<T, String>).get(enumValue)
72
- }
73
-
74
- return enumValue.name
75
- }
76
54
  }
77
55
 
78
56
  @Suppress("FunctionName")
@@ -0,0 +1,25 @@
1
+ package expo.modules.kotlin.modules
2
+
3
+ import expo.modules.kotlin.types.Enumerable
4
+ import kotlin.reflect.KProperty1
5
+ import kotlin.reflect.full.declaredMemberProperties
6
+ import kotlin.reflect.full.primaryConstructor
7
+
8
+ internal fun <T> convertEnumToString(enumValue: T): String where T : Enumerable, T : Enum<T> {
9
+ val enumClass = enumValue::class
10
+ val primaryConstructor = enumClass.primaryConstructor
11
+ if (primaryConstructor?.parameters?.size == 1) {
12
+ val parameterName = primaryConstructor.parameters.first().name
13
+ val parameterProperty = enumClass
14
+ .declaredMemberProperties
15
+ .find { it.name == parameterName }
16
+
17
+ requireNotNull(parameterProperty) { "Cannot find a property for $parameterName parameter" }
18
+ require(parameterProperty.returnType.classifier == String::class) { "The enum parameter has to be a string." }
19
+
20
+ @Suppress("UNCHECKED_CAST")
21
+ return (parameterProperty as KProperty1<T, String>).get(enumValue)
22
+ }
23
+
24
+ return enumValue.name
25
+ }
@@ -18,6 +18,7 @@ import expo.modules.kotlin.jni.JavaScriptModuleObject
18
18
  import expo.modules.kotlin.jni.decorators.JSDecoratorsBridgingObject
19
19
  import expo.modules.kotlin.modules.Module
20
20
  import expo.modules.kotlin.modules.ModuleDefinitionBuilder
21
+ import expo.modules.kotlin.modules.convertEnumToString
21
22
  import expo.modules.kotlin.types.Enumerable
22
23
  import expo.modules.kotlin.types.enforceType
23
24
  import expo.modules.kotlin.types.toArgsArray
@@ -473,6 +474,13 @@ open class ObjectDefinitionBuilder {
473
474
  }
474
475
  }
475
476
 
477
+ /**
478
+ * Creates module's lifecycle listener that is called right after the first event listener is added for given event.
479
+ */
480
+ fun <T> OnStartObserving(enum: T, body: () -> Unit) where T : Enumerable, T : Enum<T> {
481
+ OnStartObserving(convertEnumToString(enum), body)
482
+ }
483
+
476
484
  /**
477
485
  * Creates module's lifecycle listener that is called right after the first event listener is added.
478
486
  */
@@ -499,6 +507,13 @@ open class ObjectDefinitionBuilder {
499
507
  }
500
508
  }
501
509
 
510
+ /**
511
+ * Creates module's lifecycle listener that is called right after all event listeners are removed for given event.
512
+ */
513
+ fun <T> OnStopObserving(enum: T, body: () -> Unit) where T : Enumerable, T : Enum<T> {
514
+ OnStopObserving(convertEnumToString(enum), body)
515
+ }
516
+
502
517
  /**
503
518
  * Creates module's lifecycle listener that is called right after all event listeners are removed.
504
519
  */
@@ -25,7 +25,7 @@ internal struct DynamicOptionalType: AnyDynamicType {
25
25
  if jsValue.isUndefined() || jsValue.isNull() {
26
26
  return Optional<Any>.none as Any
27
27
  }
28
- return try appContext.converter.toNative(jsValue, wrappedType)
28
+ return try wrappedType.cast(jsValue: jsValue, appContext: appContext)
29
29
  }
30
30
 
31
31
  func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
@@ -10,7 +10,7 @@ public protocol AnySharedRef {
10
10
  to native instances among different independent libraries.
11
11
  */
12
12
  open class SharedRef<RefType>: SharedObject, AnySharedRef {
13
- public let ref: RefType
13
+ public var ref: RefType
14
14
 
15
15
  open var nativeRefType: String {
16
16
  "unknown"
@@ -8,7 +8,7 @@ import Foundation
8
8
  */
9
9
  @objc(EXRequestCdpInterceptor)
10
10
  public final class ExpoRequestCdpInterceptor: NSObject, ExpoRequestInterceptorProtocolDelegate {
11
- private var delegate: ExpoRequestCdpInterceptorDelegate?
11
+ private weak var delegate: ExpoRequestCdpInterceptorDelegate?
12
12
  public var dispatchQueue = DispatchQueue(label: "expo.requestCdpInterceptor")
13
13
 
14
14
  override private init() {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "2.0.0-preview.1",
3
+ "version": "2.0.0-preview.3",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -44,5 +44,5 @@
44
44
  "@testing-library/react-native": "^12.5.2",
45
45
  "expo-module-scripts": "^4.0.0"
46
46
  },
47
- "gitHead": "f7adac0a6b82ab484a8254a68c3808ba6f2afde5"
47
+ "gitHead": "2e4f18d41da033c5ced0a4a045d91cf5250016b7"
48
48
  }