expo-modules-core 57.0.18 → 57.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,23 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.20 — 2026-09-29
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed `Exception` reporting `undefined reason` to JavaScript instead of the description it was created with, which also affected every `promise.reject(code, description)` call. ([#50215](https://github.com/expo/expo/pull/50215) by [@Maher-Reven](https://github.com/Maher-Reven))
18
+ - [Android] Fixed async functions rejecting with `Cannot use shared object that was already released`, and `sharedObjectDidRelease` being called in the middle of a pending call, when the garbage collector took the JS object of a shared object receiver or argument before the call finished. ([#50513](https://github.com/expo/expo/pull/50513) by [@lukmccall](https://github.com/lukmccall))
19
+
20
+ ## 57.0.19 — 2026-09-24
21
+
22
+ ### 🐛 Bug fixes
23
+
24
+ - [iOS] Fixed SwiftUI view mounting blocking the main thread: `ViewProps` and `ShadowNodeProxy` now store their `objectWillChange` publisher instead of resolving it through Combine's global observable-object table on every access. ([#49528](https://github.com/expo/expo/pull/49528) by [@wielski](https://github.com/wielski))
25
+ - Fixed a crash while laying out an `RNHostView` with `matchContents` whose content is laid out more than once, for example a nested host or a Reanimated node. The hosted shadow node could be released before `replaceChild` finished reading it. ([#50456](https://github.com/expo/expo/pull/50456) by [@expo-bot](https://github.com/expo-bot))
26
+ - [Android] Fixed worklet callbacks crashing with `NoSuchFieldError` in minified release builds. ([#50502](https://github.com/expo/expo/issues/50502) by [@dhalenok](https://github.com/dhalenok), [#50510](https://github.com/expo/expo/pull/50510) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
27
+ - [Android] Replaced the hosting `ComposeView`'s view id with a tag for its `rememberSaveable` namespace, fixing a crash on `react-native-screens` tab switch and touches inside a `Host` reaching the wrong React Native view. ([#49964](https://github.com/expo/expo/issues/49964) by [@b2x-software](https://github.com/b2x-software), [#50299](https://github.com/expo/expo/pull/50299) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
28
+ - [iOS] Fixed the tap that closes a SwiftUI menu still reaching the React Native view underneath it in apps with no accessibility client attached. The menu gate no longer requires `accessibilityViewIsModal`, which UIKit only implements for the menu container while its accessibility bundle is loaded. ([#48765](https://github.com/expo/expo/issues/48765) by [@ayyoub-coder](https://github.com/ayyoub-coder), [#49916](https://github.com/expo/expo/pull/49916) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
29
+
13
30
  ## 57.0.18 — 2026-09-11
14
31
 
15
32
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.18'
30
+ version = '57.0.20'
31
31
 
32
32
  def isExpoModulesCoreTests = {
33
33
  Gradle gradle = getGradle()
@@ -94,7 +94,7 @@ android {
94
94
  defaultConfig {
95
95
  consumerProguardFiles 'proguard-rules.pro'
96
96
  versionCode 1
97
- versionName "57.0.18"
97
+ versionName "57.0.20"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -221,10 +221,10 @@ abstract class ExpoComposeView<T : ComposeProps>(
221
221
 
222
222
  private fun addComposeView() {
223
223
  val composeView = ComposeView(context).also {
224
- // Give each Host a unique id so its rememberSaveable state gets its own key.
225
- // All Hosts share the Activity's SavedStateRegistry (set below), so without an id
226
- // they'd collide on one key and only the first could save/restore state.
227
- it.id = generateViewId()
224
+ // Give each Host a unique rememberSaveable namespace. All Hosts share the Activity's
225
+ // SavedStateRegistry (set below), so without one they'd collide on a single key and only
226
+ // the first could save/restore state.
227
+ HostingViewSaveableState.assignNamespace(it)
228
228
  it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
229
229
  // Pin the composition to the Activity lifecycle so it survives
230
230
  // react-native-screens detaching inactive screens on every switch.
@@ -0,0 +1,23 @@
1
+ package expo.modules.kotlin.views
2
+
3
+ import android.view.View
4
+ import androidx.compose.ui.R as ComposeUiR
5
+ import java.util.concurrent.atomic.AtomicInteger
6
+
7
+ /**
8
+ * Gives each Compose hosting view a unique `rememberSaveable` namespace.
9
+ *
10
+ * Previous solutions tried to give view an id but it caused some other issues
11
+ * See https://github.com/expo/expo/issues/49964 and
12
+ * https://github.com/expo/expo/issues/49964#issuecomment-5714493768.
13
+ *
14
+ * So using a tag instead of an id is a better solution.
15
+ * Compose reads this tag before falling back to `View.id`, so we don't call `generateViewId()`.
16
+ */
17
+ internal object HostingViewSaveableState {
18
+ private val nextNamespace = AtomicInteger()
19
+
20
+ fun assignNamespace(view: View) {
21
+ view.setTag(ComposeUiR.id.compose_view_saveable_id_tag, "Host:${nextNamespace.getAndIncrement()}")
22
+ }
23
+ }
@@ -12,16 +12,19 @@ JavaCallback::CallbackContext::CallbackContext(
12
12
  jsi::Runtime &rt,
13
13
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
14
14
  std::optional<jsi::Function> resolveHolder,
15
- std::optional<jsi::Function> rejectHolder
15
+ std::optional<jsi::Function> rejectHolder,
16
+ std::vector<jsi::Value> retainedValues
16
17
  ) : react::LongLivedObject(rt),
17
18
  rt(rt),
18
19
  jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
19
20
  resolveHolder(std::move(resolveHolder)),
20
- rejectHolder(std::move(rejectHolder)) {}
21
+ rejectHolder(std::move(rejectHolder)),
22
+ retainedValues(std::move(retainedValues)) {}
21
23
 
22
24
  void JavaCallback::CallbackContext::invalidate() {
23
25
  resolveHolder.reset();
24
26
  rejectHolder.reset();
27
+ retainedValues.clear();
25
28
  allowRelease();
26
29
  }
27
30
 
@@ -32,13 +32,18 @@ public:
32
32
  jsi::Runtime &rt,
33
33
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
34
34
  std::optional<jsi::Function> resolveHolder,
35
- std::optional<jsi::Function> rejectHolder
35
+ std::optional<jsi::Function> rejectHolder,
36
+ std::vector<jsi::Value> retainedValues = {}
36
37
  );
37
38
 
38
39
  jsi::Runtime &rt;
39
40
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder;
40
41
  std::optional<jsi::Function> resolveHolder;
41
42
  std::optional<jsi::Function> rejectHolder;
43
+ /**
44
+ * JS values that have to stay alive until the promise is settled.
45
+ */
46
+ std::vector<jsi::Value> retainedValues;
42
47
 
43
48
  void invalidate();
44
49
  };
@@ -20,7 +20,8 @@ namespace expo {
20
20
  jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
21
21
  jsi::Function &&resolveFunction,
22
22
  jsi::Function &&rejectFunction,
23
- jsi::Runtime &rt
23
+ jsi::Runtime &rt,
24
+ std::vector<jsi::Value> &&retainedValues
24
25
  ) {
25
26
  JSIContext *jsiContext = getJSIContext(rt);
26
27
  std::shared_ptr<react::CallInvoker> jsInvoker = jsiContext->runtimeHolder->jsInvoker;
@@ -29,7 +30,8 @@ jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
29
30
  rt,
30
31
  std::move(jsInvoker),
31
32
  std::move(resolveFunction),
32
- std::move(rejectFunction)
33
+ std::move(rejectFunction),
34
+ std::move(retainedValues)
33
35
  );
34
36
 
35
37
  facebook::react::LongLivedObjectCollection::get(rt).add(callbackContext);
@@ -105,6 +107,11 @@ MethodMetadata::MethodMetadata(
105
107
  jni::global_ref<jobject> &&jBodyReference
106
108
  ) : info(std::move(info)),
107
109
  jBodyReference(std::move(jBodyReference)) {
110
+ for (size_t index = 0; index < this->info.argTypes.size(); index++) {
111
+ if (this->info.argTypes[index]->converter->retainsJSValue()) {
112
+ retainedArgIndices.push_back(index);
113
+ }
114
+ }
108
115
  }
109
116
 
110
117
  std::shared_ptr<jsi::Function> MethodMetadata::toJSFunction(
@@ -230,12 +237,18 @@ jsi::Function MethodMetadata::toAsyncFunction(
230
237
  auto globalConvertedArgs = (jobjectArray) env->NewGlobalRef(convertedArgs);
231
238
  env->DeleteLocalRef(convertedArgs);
232
239
 
240
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues = nullptr;
241
+ if (!thisPtr->retainedArgIndices.empty()) {
242
+ retainedValues = std::make_shared<std::vector<jsi::Value>>(
243
+ thisPtr->retainArguments(rt, thisValue, args, count)
244
+ );
245
+ }
246
+
233
247
  // Creates a JSI promise
234
- jsi::Value promise = Promise.callAsConstructor(
248
+ return Promise.callAsConstructor(
235
249
  rt,
236
- thisPtr->createPromiseBody(rt, globalConvertedArgs)
250
+ thisPtr->createPromiseBody(rt, globalConvertedArgs, std::move(retainedValues))
237
251
  );
238
- return promise;
239
252
  } catch (jni::JniException &jniException) {
240
253
  jni::local_ref<jni::JThrowable> unboxedThrowable = jniException.getThrowable();
241
254
  if (!unboxedThrowable->isInstanceOf(CodedException::javaClassLocal())) {
@@ -246,7 +259,7 @@ jsi::Function MethodMetadata::toAsyncFunction(
246
259
  auto code = codedException->getCode();
247
260
  auto message = codedException->getLocalizedMessage().value_or("");
248
261
 
249
- jsi::Value promise = Promise.callAsConstructor(
262
+ return Promise.callAsConstructor(
250
263
  rt,
251
264
  jsi::Function::createFromHostFunction(
252
265
  rt,
@@ -275,22 +288,49 @@ jsi::Function MethodMetadata::toAsyncFunction(
275
288
  }
276
289
  )
277
290
  );
278
-
279
- return promise;
280
291
  }
281
292
  }
282
293
  );
283
294
  }
284
295
 
296
+ std::vector<jsi::Value> MethodMetadata::retainArguments(
297
+ jsi::Runtime &rt,
298
+ const jsi::Value &thisValue,
299
+ const jsi::Value *args,
300
+ size_t count
301
+ ) const {
302
+ std::vector<jsi::Value> retained;
303
+ retained.reserve(retainedArgIndices.size());
304
+
305
+ // With an owner, `info.argTypes[0]` describes the receiver and the JS arguments start at index 1.
306
+ const size_t argsOffset = info.takesOwner ? 1 : 0;
307
+ for (size_t index : retainedArgIndices) {
308
+ if (info.takesOwner && index == 0) {
309
+ if (thisValue.isObject()) {
310
+ retained.emplace_back(rt, thisValue);
311
+ }
312
+ continue;
313
+ }
314
+
315
+ const size_t argIndex = index - argsOffset;
316
+ if (argIndex < count && args[argIndex].isObject()) {
317
+ retained.emplace_back(rt, args[argIndex]);
318
+ }
319
+ }
320
+
321
+ return retained;
322
+ }
323
+
285
324
  jsi::Function MethodMetadata::createPromiseBody(
286
325
  jsi::Runtime &runtime,
287
- jobjectArray globalArgs
326
+ jobjectArray globalArgs,
327
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues
288
328
  ) {
289
329
  return jsi::Function::createFromHostFunction(
290
330
  runtime,
291
331
  getJSIContext(runtime)->jsRegistry->getPropNameID(runtime, "promiseFn"),
292
332
  2,
293
- [this, globalArgs](
333
+ [this, globalArgs, retainedValues = std::move(retainedValues)](
294
334
  jsi::Runtime &rt,
295
335
  const jsi::Value &thisVal,
296
336
  const jsi::Value *promiseConstructorArgs,
@@ -303,10 +343,12 @@ jsi::Function MethodMetadata::createPromiseBody(
303
343
  jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
304
344
  jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
305
345
 
346
+ // The promise constructor calls this body synchronously, so the retained values are moved out once.
306
347
  jobject javaCallback = createJavaCallback(
307
348
  std::move(resolveJSIFn),
308
349
  std::move(rejectJSIFn),
309
- rt
350
+ rt,
351
+ retainedValues != nullptr ? std::move(*retainedValues) : std::vector<jsi::Value>()
310
352
  ).release();
311
353
 
312
354
  JNIEnv *env = jni::Environment::current();
@@ -107,9 +107,27 @@ private:
107
107
 
108
108
  jsi::Function createPromiseBody(
109
109
  jsi::Runtime &runtime,
110
- jobjectArray globalArgs
110
+ jobjectArray globalArgs,
111
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues
111
112
  );
112
113
 
114
+ /**
115
+ * Indexes into `info.argTypes` of the arguments whose JS values have to stay alive until an async call
116
+ * is settled, computed once from the converters. When the receiver is taken, index 0 is the receiver.
117
+ */
118
+ std::vector<size_t> retainedArgIndices;
119
+
120
+ /**
121
+ * Copies the JS values of the arguments listed in `retainedArgIndices`.
122
+ * They are kept alive until the returned promise is settled.
123
+ */
124
+ std::vector<jsi::Value> retainArguments(
125
+ jsi::Runtime &rt,
126
+ const jsi::Value &thisValue,
127
+ const jsi::Value *args,
128
+ size_t count
129
+ ) const;
130
+
113
131
  jobjectArray convertJSIArgsToJNI(
114
132
  JNIEnv *env,
115
133
  jsi::Runtime &rt,
@@ -9,6 +9,7 @@
9
9
  #if USE_HERMES
10
10
 
11
11
  #include <hermes/hermes.h>
12
+ #include <jsi/instrumentation.h>
12
13
 
13
14
  #include <utility>
14
15
 
@@ -69,6 +70,25 @@ jlong RuntimeHolder::createRuntime() {
69
70
  runtime = facebook::jsc::makeJSCRuntime();
70
71
  #endif
71
72
 
73
+ // Exposes a `gc()` function, so tests can force a full garbage collection.
74
+ auto gcPropName = jsi::PropNameID::forUtf8(*runtime, "gc");
75
+ runtime->global().setProperty(
76
+ *runtime,
77
+ gcPropName,
78
+ jsi::Function::createFromHostFunction(
79
+ *runtime,
80
+ gcPropName,
81
+ 0,
82
+ [](jsi::Runtime &rt,
83
+ const jsi::Value &thisVal,
84
+ const jsi::Value *args,
85
+ size_t count) {
86
+ rt.instrumentation().collectGarbage("test");
87
+ return jsi::Value::undefined();
88
+ }
89
+ )
90
+ );
91
+
72
92
  // By default "global" property isn't set.
73
93
  runtime->global().setProperty(
74
94
  *runtime,
@@ -2,6 +2,8 @@
2
2
 
3
3
  #pragma once
4
4
 
5
+ #include <algorithm>
6
+
5
7
  #include "../ExpoHeader.pch"
6
8
  #include "CppType.h"
7
9
 
@@ -41,6 +43,16 @@ public:
41
43
  JNIEnv *env,
42
44
  const jsi::Value &value
43
45
  ) const = 0;
46
+
47
+ /**
48
+ * Whether the JS value has to stay alive until an async call that received it is settled.
49
+ * It's true for converters that pass only a reference to Kotlin, like the shared object id.
50
+ * The native body works with the object behind that reference, so the JS value must not be
51
+ * garbage collected before the call ends. Composite converters ask their element converters.
52
+ */
53
+ virtual bool retainsJSValue() const {
54
+ return false;
55
+ }
44
56
  };
45
57
 
46
58
  /**
@@ -276,6 +288,10 @@ public:
276
288
  ) const override;
277
289
 
278
290
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
291
+
292
+ bool retainsJSValue() const override {
293
+ return true;
294
+ }
279
295
  };
280
296
 
281
297
  /**
@@ -312,6 +328,12 @@ public:
312
328
  const jsi::Value &value
313
329
  ) const override;
314
330
 
331
+ bool retainsJSValue() const override {
332
+ return std::any_of(converters.begin(), converters.end(), [](const auto &converter) {
333
+ return converter->retainsJSValue();
334
+ });
335
+ }
336
+
315
337
  private:
316
338
  std::vector<std::shared_ptr<FrontendConverter>> converters;
317
339
  };
@@ -365,6 +387,10 @@ public:
365
387
 
366
388
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
367
389
 
390
+ bool retainsJSValue() const override {
391
+ return parameterConverter->retainsJSValue();
392
+ }
393
+
368
394
  private:
369
395
  /**
370
396
  * A string representation of desired Java type.
@@ -396,6 +422,10 @@ public:
396
422
  ) const override;
397
423
 
398
424
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
425
+
426
+ bool retainsJSValue() const override {
427
+ return parameterConverter->retainsJSValue();
428
+ }
399
429
  private:
400
430
  /**
401
431
  * Converter used to convert array elements.
@@ -425,6 +455,10 @@ public:
425
455
  ) const override;
426
456
 
427
457
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
458
+
459
+ bool retainsJSValue() const override {
460
+ return valueConverter->retainsJSValue();
461
+ }
428
462
  private:
429
463
  /**
430
464
  * Converter used to convert values.
@@ -464,6 +498,10 @@ public:
464
498
  ) const override;
465
499
 
466
500
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
501
+
502
+ bool retainsJSValue() const override {
503
+ return parameterConverter->retainsJSValue();
504
+ }
467
505
  private:
468
506
  std::shared_ptr<FrontendConverter> parameterConverter;
469
507
  };
@@ -481,6 +519,10 @@ public:
481
519
  ) const override;
482
520
 
483
521
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
522
+
523
+ bool retainsJSValue() const override {
524
+ return parameterConverter->retainsJSValue();
525
+ }
484
526
  private:
485
527
  std::shared_ptr<FrontendConverter> parameterConverter;
486
528
  };
@@ -58,6 +58,20 @@ class JNIDeallocator(shouldCreateDestructorThread: Boolean = true) : AutoCloseab
58
58
  destructorThread?.interrupt()
59
59
  }
60
60
 
61
+ /**
62
+ * Deallocates the hybrid objects whose Java wrappers were already garbage collected,
63
+ * without waiting for the destructor thread. Returns how many objects were deallocated.
64
+ */
65
+ internal fun processPendingReferences(): Int = synchronized(this) {
66
+ var processed = 0
67
+ while (true) {
68
+ val current = referenceQueue.poll() ?: break
69
+ destructorMap.remove(current)?.resetNative()
70
+ processed++
71
+ }
72
+ processed
73
+ }
74
+
61
75
  /**
62
76
  * Returns references to all hybrid objects that contain references to the jsi value
63
77
  * and are present in the memory.
@@ -1,10 +1,12 @@
1
1
  package expo.modules.kotlin.jni.worklets
2
2
 
3
3
  import com.facebook.jni.HybridData
4
+ import expo.modules.core.interfaces.DoNotStrip
4
5
 
5
6
  class WorkletNativeRuntime(
6
7
  jsRuntimePointer: Long
7
8
  ) {
9
+ @DoNotStrip
8
10
  private val mHybridData = initHybrid(jsRuntimePointer)
9
11
 
10
12
  external fun initHybrid(jsRuntimePointer: Long): HybridData
@@ -10,6 +10,7 @@
10
10
  #include <react/renderer/core/LayoutableShadowNode.h>
11
11
 
12
12
  #include <algorithm>
13
+ #include <memory>
13
14
 
14
15
  #include "ContentOriginRegistry.h"
15
16
  #include "ExpoViewEventEmitter.h"
@@ -104,7 +105,7 @@ public:
104
105
  return ConcreteViewShadowNode::measureContent(layoutContext, layoutConstraints);
105
106
  }
106
107
 
107
- auto const *content = hostedContent();
108
+ auto const content = hostedContent();
108
109
 
109
110
  if (content == nullptr) {
110
111
  return {};
@@ -122,7 +123,7 @@ public:
122
123
  return;
123
124
  }
124
125
 
125
- auto const *content = hostedContent();
126
+ auto const content = hostedContent();
126
127
 
127
128
  if (content == nullptr) {
128
129
  return;
@@ -144,12 +145,15 @@ public:
144
145
  }
145
146
 
146
147
  private:
147
- const react::LayoutableShadowNode *hostedContent() const {
148
+ // Owning on purpose: `layout()` calls `replaceChild()`, which overwrites this node's only strong
149
+ // reference to the content — `RNHostView` is a leaf Yoga node, so Yoga keeps no second one, and
150
+ // `YogaLayoutableShadowNode::replaceChild()` reads `oldChild` right after.
151
+ std::shared_ptr<const react::LayoutableShadowNode> hostedContent() const {
148
152
  auto const &children = this->getChildren();
149
153
 
150
154
  return children.empty()
151
155
  ? nullptr
152
- : dynamic_cast<const react::LayoutableShadowNode *>(children.front().get());
156
+ : std::dynamic_pointer_cast<const react::LayoutableShadowNode>(children.front());
153
157
  }
154
158
 
155
159
  react::LayoutDirection resolvedLayoutDirection() const {
@@ -7,7 +7,7 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
7
7
  String describing the reason of the exception.
8
8
  */
9
9
  open var reason: String {
10
- "undefined reason"
10
+ customReason ?? "undefined reason"
11
11
  }
12
12
 
13
13
  /**
@@ -20,6 +20,12 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
20
20
  */
21
21
  let customCode: String?
22
22
 
23
+ /**
24
+ A custom reason of the exception, given as the `description` to the initializer that takes a name.
25
+ When unset, the `reason` is expected to come from an override in a subclass.
26
+ */
27
+ let customReason: String?
28
+
23
29
  /**
24
30
  The default initializer that captures the place in the code where the exception was created.
25
31
  - Warning: Call it only without arguments!
@@ -27,13 +33,14 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
27
33
  public init(file: String = #fileID, line: UInt = #line, function: String = #function) {
28
34
  self.origin = ExceptionOrigin(file: file, line: line, function: function)
29
35
  self.customCode = nil
36
+ self.customReason = nil
30
37
  }
31
38
 
32
39
  public init(name: String, description: String, code: String? = nil, file: String = #fileID, line: UInt = #line, function: String = #function) {
33
40
  self.origin = ExceptionOrigin(file: file, line: line, function: function)
34
41
  self.customCode = code
42
+ self.customReason = description
35
43
  self.name = name
36
- self.description = description
37
44
  }
38
45
 
39
46
  // MARK: - CodedError
@@ -1,7 +1,12 @@
1
1
  // Copyright 2025-present 650 Industries. All rights reserved.
2
2
 
3
+ import Combine
4
+
3
5
  extension ExpoSwiftUI {
4
6
  open class ShadowNodeProxy: ObservableObject, Record, @unchecked Sendable {
7
+ // Stored so `objectWillChange` is a plain property read instead of Combine's O(live objects) side-table lookup.
8
+ public let objectWillChange = ObservableObjectPublisher()
9
+
5
10
  public required init() {}
6
11
 
7
12
  // We use Double.nan to mark a dimension as unset. This value is passed to https://github.com/facebook/yoga/blob/49ee855f99fb67079c24d507a4ea1b6d80fa2ebf/yoga/style/StyleLength.h#L33
@@ -23,6 +23,9 @@ extension ExpoSwiftUI {
23
23
  It's a record that can be observed by SwiftUI to re-render on its changes.
24
24
  */
25
25
  open class ViewProps: ObservableObject, Record {
26
+ // Stored so `objectWillChange` is a plain property read instead of Combine's O(live objects) side-table lookup.
27
+ public let objectWillChange = ObservableObjectPublisher()
28
+
26
29
  public required init() {}
27
30
 
28
31
  public required init(rawProps: [String: Any], context: AppContext) throws {
@@ -31,24 +31,18 @@ internal final class SystemMenuTouchGate: UIGestureRecognizer, UIGestureRecogniz
31
31
  }
32
32
 
33
33
  /**
34
- Consider Menu to be open when the container is present, still accepts interaction, and is modal.
34
+ Consider Menu to be open while the container is present and still accepts interaction.
35
35
  UIKit turns `isUserInteractionEnabled` off the moment dismissal commits, so taps made during
36
- the dismiss animation pass through to the app again. `accessibilityViewIsModal` is how UIKit
37
- marks the container as blocking the content behind it — a semantic signal beside the class name.
36
+ the dismiss animation pass through to the app again.
38
37
  */
39
- static func isOpenContextMenuContainer(
40
- className: String,
41
- isUserInteractionEnabled: Bool,
42
- accessibilityViewIsModal: Bool
43
- ) -> Bool {
44
- return isUserInteractionEnabled && accessibilityViewIsModal && isContextMenuContainerClassName(className)
38
+ static func isOpenContextMenuContainer(className: String, isUserInteractionEnabled: Bool) -> Bool {
39
+ return isUserInteractionEnabled && isContextMenuContainerClassName(className)
45
40
  }
46
41
 
47
42
  static func isContextMenuContainer(_ view: UIView) -> Bool {
48
43
  return isOpenContextMenuContainer(
49
44
  className: NSStringFromClass(type(of: view)),
50
- isUserInteractionEnabled: view.isUserInteractionEnabled,
51
- accessibilityViewIsModal: view.accessibilityViewIsModal
45
+ isUserInteractionEnabled: view.isUserInteractionEnabled
52
46
  )
53
47
  }
54
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.18",
3
+ "version": "57.0.20",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@expo/expo-modules-macros-plugin": "0.6.1",
50
- "expo-modules-jsi": "~57.1.0",
50
+ "expo-modules-jsi": "~57.1.1",
51
51
  "invariant": "^2.2.4"
52
52
  },
53
53
  "peerDependencies": {
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "9e5319c0f821a27b7924841903abae50e2b41790",
69
+ "gitHead": "ad1efa3fa5802156660198ba5d787f91014d8df3",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",