expo-modules-core 57.0.9 → 57.0.11

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,19 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.11 — 2026-08-14
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS] Added `SceneGeometry.foregroundScene()` for resolving the foregrounded scene without a view. ([#48318](https://github.com/expo/expo/pull/48318) by [@alanjhughes](https://github.com/alanjhughes))
18
+ - [iOS] Added `SceneGeometry` for reading bounds, safe area, display scale and interface orientation from the scene a view belongs to. ([#48168](https://github.com/expo/expo/pull/48168) by [@alanjhughes](https://github.com/alanjhughes))
19
+
20
+ ## 57.0.10 — 2026-08-06
21
+
22
+ ### 💡 Others
23
+
24
+ - [Android] Ignore already-settled promises. ([#46770](https://github.com/expo/expo/pull/46770) by [@jakex7](https://github.com/jakex7))
25
+
13
26
  ## 57.0.9 — 2026-08-04
14
27
 
15
28
  ### 🎉 New features
@@ -19,6 +32,7 @@
19
32
 
20
33
  ### 🐛 Bug fixes
21
34
 
35
+ - [iOS] Fixed the tap that closes a SwiftUI menu also pressing the React Native view underneath it. ([#48419](https://github.com/expo/expo/issues/48419) by [@bohdanstefaniuk](https://github.com/bohdanstefaniuk)) ([#48463](https://github.com/expo/expo/pull/48463) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
22
36
  - [Android] Fixed hosted Compose views missing layout after reattachment or in-place configuration changes. ([#48370](https://github.com/expo/expo/issues/48370) by [@lujjjh](https://github.com/lujjjh))
23
37
  - [iOS] Fixed infinite recursion and `EXC_BAD_ACCESS` when encoding native `Date` values to JavaScript. ([#48239](https://github.com/expo/expo/issues/48239), [#48240](https://github.com/expo/expo/pull/48240) by [@samuelcorsan](https://github.com/samuelcorsan))
24
38
  - [iOS] Fixed a stack overflow crash when converting to JavaScript a `Convertible` value that keeps the default `convertResult` implementation, such as `CMTime`. The value now converts to `undefined` and logs a warning. ([#48266](https://github.com/expo/expo/pull/48266) by [@tsapeta](https://github.com/tsapeta))
@@ -88,6 +102,7 @@ _This version does not introduce any user-facing changes._
88
102
  - [iOS] `@ExpoModule` now synthesizes the module's JavaScript name from the class name or its `@ExpoModule("CustomName")` argument, so a `Name(…)` definition entry is no longer required. ([#46938](https://github.com/expo/expo/pull/46938) by [@tsapeta](https://github.com/tsapeta))
89
103
  - [iOS] Added `SharedObject.native(from:)` that recovers the native shared object paired with a JavaScript object, with a generic overload that returns the concrete subclass directly. ([#47054](https://github.com/expo/expo/pull/47054) by [@tsapeta](https://github.com/tsapeta))
90
104
  - [iOS] Added `Module.emit` that sends an event directly to the module's own JavaScript object, mirroring `SharedObject.emit`. Both now share an `EventEmitter` protocol. ([#46555](https://github.com/expo/expo/pull/46555) by [@tsapeta](https://github.com/tsapeta))
105
+ - Add `useReleasingSharedObjectWithLifecycle` hook. ([#46494](https://github.com/expo/expo/pull/46494) by [@behenate](https://github.com/behenate))
91
106
 
92
107
  ### 🐛 Bug fixes
93
108
 
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.9'
30
+ version = '57.0.11'
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.9"
97
+ versionName "57.0.11"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -1,11 +1,8 @@
1
1
  package expo.modules.kotlin.jni
2
2
 
3
- import expo.modules.BuildConfig
4
3
  import expo.modules.core.interfaces.DoNotStrip
5
4
  import expo.modules.kotlin.AppContext
6
5
  import expo.modules.kotlin.Promise
7
- import expo.modules.kotlin.exception.PromiseAlreadySettledException
8
- import expo.modules.kotlin.logger
9
6
  import java.lang.ref.WeakReference
10
7
 
11
8
  @DoNotStrip
@@ -61,17 +58,7 @@ class PromiseImpl @DoNotStrip internal constructor(
61
58
 
62
59
  private inline fun checkIfWasSettled(body: () -> Unit) {
63
60
  if (wasSettled) {
64
- val exception = PromiseAlreadySettledException(fullFunctionName ?: "unknown")
65
- val jsLogger = appContextHolder?.get()?.jsLogger
66
- // We want to report that a promise was settled twice in the development build.
67
- // However, in production, the app should crash.
68
- if (BuildConfig.DEBUG && jsLogger != null) {
69
- jsLogger.error("Trying to resolve promise that was already settled", exception)
70
- logger.error("Trying to resolve promise that was already settled", exception)
71
- return
72
- }
73
-
74
- throw exception
61
+ return
75
62
  }
76
63
 
77
64
  body()
@@ -1 +1 @@
1
- {"version":3,"file":"useReleasingSharedObject.d.ts","sourceRoot":"","sources":["../../src/hooks/useReleasingSharedObject.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,SAAS,YAAY,EACzE,OAAO,EAAE,MAAM,aAAa,EAC5B,YAAY,EAAE,cAAc,GAC3B,aAAa,CAoDf"}
1
+ {"version":3,"file":"useReleasingSharedObject.d.ts","sourceRoot":"","sources":["../../src/hooks/useReleasingSharedObject.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,SAAS,YAAY,EACzE,OAAO,EAAE,MAAM,aAAa,EAC5B,YAAY,EAAE,cAAc,GAC3B,aAAa,CAOf"}
@@ -0,0 +1,49 @@
1
+ import type { DependencyList } from 'react';
2
+ import type { SharedObject } from '../ts-declarations/SharedObject';
3
+ export type ReleasingSharedObjectLifecycleContext = {
4
+ /**
5
+ * The dependency values from the last committed object lifecycle decision.
6
+ */
7
+ previousDependencies: DependencyList;
8
+ /**
9
+ * The dependency values from the current render.
10
+ */
11
+ dependencies: DependencyList;
12
+ };
13
+ export type ReleasingSharedObjectLifecycle<TSharedObject extends SharedObject> = {
14
+ /**
15
+ * Creates the shared object when the hook initializes or when `shouldRecreate` returns `true`.
16
+ */
17
+ factory: () => TSharedObject;
18
+ /**
19
+ * Called during render when dependencies change to decide whether to replace the object.
20
+ * Return `false` to keep the current object and handle the dependency change with the `update` function.
21
+ * When omitted or `true`, dependency changes recreate the object, matching `useReleasingSharedObject`.
22
+ *
23
+ * Must be a pure function with no side effects — it is called during the render phase and
24
+ * React may invoke it more than once with the same inputs.
25
+ */
26
+ shouldRecreate?: (object: TSharedObject, context: ReleasingSharedObjectLifecycleContext) => boolean;
27
+ /**
28
+ * Called after commit when dependencies changed and `shouldRecreate` returned `false`.
29
+ * Has no effect unless `shouldRecreate` is provided and returns `false` for the changed
30
+ * dependencies.
31
+ *
32
+ * If the returned `Promise` rejects, the error is logged with `console.error`. Handle errors
33
+ * inside `update` if specific error handling is needed.
34
+ *
35
+ * If a subsequent dependency change or unmount requires the object to be released while an
36
+ * async update is still in-flight, the release is deferred until the update settles.
37
+ */
38
+ update?: (object: TSharedObject, context: ReleasingSharedObjectLifecycleContext) => void | Promise<void>;
39
+ /**
40
+ * Releases an object after it has been replaced or when the component unmounts.
41
+ * When omitted, the object's `release` method is called.
42
+ */
43
+ release?: (object: TSharedObject) => void;
44
+ };
45
+ /**
46
+ * Returns a shared object, delegating dependency changes to lifecycle callbacks.
47
+ */
48
+ export declare function useReleasingSharedObjectWithLifecycle<TSharedObject extends SharedObject>(lifecycle: ReleasingSharedObjectLifecycle<TSharedObject>, dependencies: DependencyList): TSharedObject;
49
+ //# sourceMappingURL=useReleasingSharedObjectWithLifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useReleasingSharedObjectWithLifecycle.d.ts","sourceRoot":"","sources":["../../src/hooks/useReleasingSharedObjectWithLifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,MAAM,MAAM,qCAAqC,GAAG;IAClD;;OAEG;IACH,oBAAoB,EAAE,cAAc,CAAC;IAErC;;OAEG;IACH,YAAY,EAAE,cAAc,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,8BAA8B,CAAC,aAAa,SAAS,YAAY,IAAI;IAC/E;;OAEG;IACH,OAAO,EAAE,MAAM,aAAa,CAAC;IAE7B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,qCAAqC,KAC3C,OAAO,CAAC;IAEb;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,CACP,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,qCAAqC,KAC3C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;CAC3C,CAAC;AAcF;;GAEG;AACH,wBAAgB,qCAAqC,CAAC,aAAa,SAAS,YAAY,EACtF,SAAS,EAAE,8BAA8B,CAAC,aAAa,CAAC,EACxD,YAAY,EAAE,cAAc,GAC3B,aAAa,CAqGf"}
package/build/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from './PermissionsInterface';
17
17
  export * from './PermissionsHook';
18
18
  export * from './Refs';
19
19
  export * from './hooks/useReleasingSharedObject';
20
+ export * from './hooks/useReleasingSharedObjectWithLifecycle';
20
21
  export * from './reload';
21
22
  export { CodedError } from './errors/CodedError';
22
23
  export { UnavailabilityError } from './errors/UnavailabilityError';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,0BAA0B,CAAC;AAClC,OAAO,0BAA0B,CAAC;AAClC,OAAO,YAAY,CAAC;AAEpB,mBAAmB,0BAA0B,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAElC,cAAc,QAAQ,CAAC;AAEvB,cAAc,kCAAkC,CAAC;AACjD,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,0BAA0B,CAAC;AAClC,OAAO,0BAA0B,CAAC;AAClC,OAAO,YAAY,CAAC;AAEpB,mBAAmB,0BAA0B,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAElC,cAAc,QAAQ,CAAC;AAEvB,cAAc,kCAAkC,CAAC;AACjD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -214,6 +214,14 @@ extension ExpoSwiftUI {
214
214
  public override func didMoveToWindow() {
215
215
  super.didMoveToWindow()
216
216
 
217
+ #if os(iOS)
218
+ if let window {
219
+ // SwiftUI content can open a menu, and UIKit passes the tap that closes it through to
220
+ // React Native underneath. The gate stops that tap from reaching the view below.
221
+ SystemMenuTouchGate.install(in: window)
222
+ }
223
+ #endif
224
+
217
225
  if window != nil, let parentController = reactViewController() {
218
226
  #if !os(macOS)
219
227
  if parentController as? UINavigationController == nil && parentController as? UITabBarController == nil {
@@ -0,0 +1,127 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ #if os(iOS)
4
+ // Also brings in the subclass header, required to set `state` from the touch callbacks
5
+ // and to call `ignore(_:for:)` on React Native's touch handler.
6
+ import UIKit.UIGestureRecognizerSubclass
7
+
8
+ /**
9
+ We create a custom gesture recognizer to stop React Root listening to touches when user taps background while Menu is opened.
10
+ Menu when opened attaches a container view `_UIContextMenuContainerView` to the window.
11
+ On tapping it, React Root's `RCTSurfaceTouchHandler` also listens to the touch and causes Pressables to fire onPress.
12
+ */
13
+ internal final class SystemMenuTouchGate: UIGestureRecognizer {
14
+ init() {
15
+ super.init(target: nil, action: nil)
16
+ // Both default to true. Never hold a touch back, and never let React Native's handler see a
17
+ // reason to cancel: it calls `_cancelTouches` for any outside recognizer that cancels in view.
18
+ delaysTouchesEnded = false
19
+ cancelsTouchesInView = false
20
+ }
21
+
22
+ // MARK: - Detecting an open menu
23
+
24
+ /**
25
+ The one view UIKit adds to the window for a presented context menu.
26
+ */
27
+ static func isContextMenuContainerClassName(_ className: String) -> Bool {
28
+ return className == "_UI".appending("ContextMenuContainerView")
29
+ }
30
+
31
+ /**
32
+ Consider Menu to be open when the container is present, still accepts interaction, and is modal.
33
+ UIKit turns `isUserInteractionEnabled` off the moment dismissal commits, so taps made during
34
+ the dismiss animation pass through to the app again. `accessibilityViewIsModal` is how UIKit
35
+ marks the container as blocking the content behind it — a semantic signal beside the class name.
36
+ */
37
+ static func isOpenContextMenuContainer(
38
+ className: String,
39
+ isUserInteractionEnabled: Bool,
40
+ accessibilityViewIsModal: Bool
41
+ ) -> Bool {
42
+ return isUserInteractionEnabled && accessibilityViewIsModal && isContextMenuContainerClassName(className)
43
+ }
44
+
45
+ static func isContextMenuContainer(_ view: UIView) -> Bool {
46
+ return isOpenContextMenuContainer(
47
+ className: NSStringFromClass(type(of: view)),
48
+ isUserInteractionEnabled: view.isUserInteractionEnabled,
49
+ accessibilityViewIsModal: view.accessibilityViewIsModal
50
+ )
51
+ }
52
+
53
+ /**
54
+ UIKit adds the container as a direct subview of the window, above the app's content, so only the
55
+ window's own subviews are worth checking. Anything deeper belongs to the app itself.
56
+ */
57
+ static func isShowingContextMenu(
58
+ in window: UIWindow,
59
+ isContainer: @MainActor (UIView) -> Bool = SystemMenuTouchGate.isContextMenuContainer
60
+ ) -> Bool {
61
+ for subview in window.subviews where isContainer(subview) {
62
+ return true
63
+ }
64
+ return false
65
+ }
66
+
67
+ // MARK: - Cancelling React Native's touches
68
+
69
+ /**
70
+ Matched by name so that this file needs no React Native headers.
71
+ */
72
+ static func isSurfaceTouchHandlerClassName(_ className: String) -> Bool {
73
+ return className == "RCTSurfaceTouchHandler"
74
+ }
75
+
76
+ static func isSurfaceTouchHandler(_ recognizer: UIGestureRecognizer) -> Bool {
77
+ return isSurfaceTouchHandlerClassName(NSStringFromClass(type(of: recognizer)))
78
+ }
79
+
80
+ /**
81
+ React Native's touch handlers along the touched view's superview chain.
82
+ */
83
+ static func surfaceTouchHandlers(
84
+ above view: UIView?,
85
+ isSurfaceTouchHandler: @MainActor (UIGestureRecognizer) -> Bool = SystemMenuTouchGate.isSurfaceTouchHandler
86
+ ) -> [UIGestureRecognizer] {
87
+ var handlers: [UIGestureRecognizer] = []
88
+ var current = view
89
+ while let view = current {
90
+ for recognizer in view.gestureRecognizers ?? [] where isSurfaceTouchHandler(recognizer) {
91
+ handlers.append(recognizer)
92
+ }
93
+ current = view.superview
94
+ }
95
+ return handlers
96
+ }
97
+
98
+ // MARK: - Installing
99
+
100
+ /**
101
+ Adds one gate to the window. The window owns it for as long as it lives, and the gate stays
102
+ inert until a menu is actually open, so there is nothing to tear down per host.
103
+ */
104
+ static func install(in window: UIWindow) {
105
+ let isInstalled = window.gestureRecognizers?.contains { $0 is SystemMenuTouchGate } ?? false
106
+ guard !isInstalled else {
107
+ return
108
+ }
109
+ window.addGestureRecognizer(SystemMenuTouchGate())
110
+ }
111
+
112
+ // MARK: - UIGestureRecognizer
113
+
114
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
115
+ super.touchesBegan(touches, with: event)
116
+
117
+ if let window = view as? UIWindow, Self.isShowingContextMenu(in: window) {
118
+ for touch in touches {
119
+ for handler in Self.surfaceTouchHandlers(above: touch.view) {
120
+ handler.ignore(touch, for: event)
121
+ }
122
+ }
123
+ }
124
+ state = .failed
125
+ }
126
+ }
127
+ #endif
@@ -0,0 +1,85 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ #if os(iOS) || os(tvOS)
4
+
5
+ import UIKit
6
+
7
+ /**
8
+ Geometry lookups that start from the view asking the question, because iOS 27 makes iPhone apps
9
+ freely resizable. Neither `UIScreen.main` nor the first key window in any connected scene describes
10
+ the space a given view has, since either can belong to a differently sized scene.
11
+ */
12
+ public enum SceneGeometry {
13
+ public static func windowScene(for view: UIView? = nil) -> UIWindowScene? {
14
+ if let scene = view?.window?.windowScene {
15
+ return scene
16
+ }
17
+ return foregroundScene() ?? windowScenes().first
18
+ }
19
+
20
+ /// Deliberately strict, for callers deciding whether to attach a window to a scene at all.
21
+ public static func foregroundActiveScene() -> UIWindowScene? {
22
+ return windowScenes().first { $0.activationState == .foregroundActive }
23
+ }
24
+
25
+ /// Nil when no scene is on screen, so callers don't present UI into a background scene.
26
+ public static func foregroundScene() -> UIWindowScene? {
27
+ let scenes = windowScenes()
28
+ return scenes.first { $0.activationState == .foregroundActive }
29
+ ?? scenes.first { $0.activationState == .foregroundInactive }
30
+ }
31
+
32
+ private static func windowScenes() -> [UIWindowScene] {
33
+ return UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
34
+ }
35
+
36
+ public static func keyWindow(for view: UIView? = nil) -> UIWindow? {
37
+ guard let scene = windowScene(for: view) else {
38
+ return nil
39
+ }
40
+ return scene.windows.first { $0.isKeyWindow } ?? scene.windows.first
41
+ }
42
+
43
+ public static func bounds(for view: UIView? = nil) -> CGRect {
44
+ return (view?.window ?? keyWindow(for: view))?.bounds ?? .zero
45
+ }
46
+
47
+ public static func safeAreaSize(for view: UIView? = nil) -> CGSize {
48
+ guard let window = view?.window ?? keyWindow(for: view) else {
49
+ return .zero
50
+ }
51
+ let safeArea = window.safeAreaLayoutGuide.layoutFrame.size
52
+ if safeArea.width > 0 && safeArea.height > 0 {
53
+ return safeArea
54
+ }
55
+ return window.bounds.size
56
+ }
57
+
58
+ /// Never cache the result. Scale can differ per scene.
59
+ public static func displayScale(for view: UIView? = nil) -> CGFloat {
60
+ return resolveDisplayScale(candidates: [
61
+ view?.traitCollection.displayScale,
62
+ windowScene(for: view)?.traitCollection.displayScale,
63
+ UITraitCollection.current.displayScale
64
+ ])
65
+ }
66
+
67
+ /// Returns the first positive candidate. `UITraitCollection` reports an unspecified scale as 0.
68
+ internal static func resolveDisplayScale(candidates: [CGFloat?]) -> CGFloat {
69
+ for case let candidate? in candidates where candidate > 0 {
70
+ return candidate
71
+ }
72
+ return 1
73
+ }
74
+ }
75
+
76
+ #if os(iOS)
77
+ public extension SceneGeometry {
78
+ /// Reads `effectiveGeometry` because `UIWindowScene.interfaceOrientation` is deprecated as of iOS 26.
79
+ static func interfaceOrientation(for view: UIView? = nil) -> UIInterfaceOrientation {
80
+ return windowScene(for: view)?.effectiveGeometry.interfaceOrientation ?? .unknown
81
+ }
82
+ }
83
+ #endif
84
+
85
+ #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.9",
3
+ "version": "57.0.11",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "fb5049481ebd5cadd499c4d1f1d5e47f9ac678be",
69
+ "gitHead": "125b8d4472432db3dc4ae047730432d7bd5bd30c",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import type { DependencyList } from 'react';
4
- import { useEffect, useMemo, useRef } from 'react';
5
4
 
5
+ import { useReleasingSharedObjectWithLifecycle } from './useReleasingSharedObjectWithLifecycle';
6
6
  import type { SharedObject } from '../ts-declarations/SharedObject';
7
7
 
8
8
  /**
@@ -12,55 +12,10 @@ export function useReleasingSharedObject<TSharedObject extends SharedObject>(
12
12
  factory: () => TSharedObject,
13
13
  dependencies: DependencyList
14
14
  ): TSharedObject {
15
- const objectRef = useRef<TSharedObject | null>(null);
16
- const objectRefToRelease = useRef<TSharedObject | null>(null);
17
- const isFastRefresh = useRef(false);
18
- const previousDependencies = useRef<DependencyList>(dependencies);
19
-
20
- if (objectRef.current == null) {
21
- objectRef.current = factory();
22
- }
23
-
24
- const object = useMemo(() => {
25
- let newObject = objectRef.current;
26
- const dependenciesAreEqual =
27
- previousDependencies.current?.length === dependencies.length &&
28
- dependencies.every((value, index) => value === previousDependencies.current[index]);
29
-
30
- // If the dependencies have changed, schedule the previous object for release and create a new one,
31
- // otherwise this has been called because of an unrelated fast refresh, and we don't want to release the object.
32
- if (!newObject || !dependenciesAreEqual) {
33
- objectRefToRelease.current = objectRef.current;
34
- newObject = factory();
35
- objectRef.current = newObject;
36
- previousDependencies.current = dependencies;
37
- }
38
- return newObject;
39
- }, dependencies);
40
-
41
- useEffect(() => {
42
- // When the object changes, release the previous one - it is important to do this in a useEffect, so that we don't release
43
- // the object during render.
44
- if (objectRefToRelease.current) {
45
- objectRefToRelease.current.release();
46
- objectRefToRelease.current = null;
47
- }
48
- }, [object]);
49
-
50
- useMemo(() => {
51
- isFastRefresh.current = true;
52
- }, []);
53
-
54
- useEffect(() => {
55
- isFastRefresh.current = false;
56
-
57
- return () => {
58
- // This will be called on every fast refresh and on unmount, but we only want to release the object on unmount.
59
- if (!isFastRefresh.current && objectRef.current) {
60
- objectRef.current.release();
61
- }
62
- };
63
- }, []);
64
-
65
- return object;
15
+ return useReleasingSharedObjectWithLifecycle(
16
+ {
17
+ factory,
18
+ },
19
+ dependencies
20
+ );
66
21
  }
@@ -0,0 +1,181 @@
1
+ 'use client';
2
+
3
+ import type { DependencyList } from 'react';
4
+ import { useEffect, useMemo, useRef } from 'react';
5
+
6
+ import type { SharedObject } from '../ts-declarations/SharedObject';
7
+
8
+ export type ReleasingSharedObjectLifecycleContext = {
9
+ /**
10
+ * The dependency values from the last committed object lifecycle decision.
11
+ */
12
+ previousDependencies: DependencyList;
13
+
14
+ /**
15
+ * The dependency values from the current render.
16
+ */
17
+ dependencies: DependencyList;
18
+ };
19
+
20
+ export type ReleasingSharedObjectLifecycle<TSharedObject extends SharedObject> = {
21
+ /**
22
+ * Creates the shared object when the hook initializes or when `shouldRecreate` returns `true`.
23
+ */
24
+ factory: () => TSharedObject;
25
+
26
+ /**
27
+ * Called during render when dependencies change to decide whether to replace the object.
28
+ * Return `false` to keep the current object and handle the dependency change with the `update` function.
29
+ * When omitted or `true`, dependency changes recreate the object, matching `useReleasingSharedObject`.
30
+ *
31
+ * Must be a pure function with no side effects — it is called during the render phase and
32
+ * React may invoke it more than once with the same inputs.
33
+ */
34
+ shouldRecreate?: (
35
+ object: TSharedObject,
36
+ context: ReleasingSharedObjectLifecycleContext
37
+ ) => boolean;
38
+
39
+ /**
40
+ * Called after commit when dependencies changed and `shouldRecreate` returned `false`.
41
+ * Has no effect unless `shouldRecreate` is provided and returns `false` for the changed
42
+ * dependencies.
43
+ *
44
+ * If the returned `Promise` rejects, the error is logged with `console.error`. Handle errors
45
+ * inside `update` if specific error handling is needed.
46
+ *
47
+ * If a subsequent dependency change or unmount requires the object to be released while an
48
+ * async update is still in-flight, the release is deferred until the update settles.
49
+ */
50
+ update?: (
51
+ object: TSharedObject,
52
+ context: ReleasingSharedObjectLifecycleContext
53
+ ) => void | Promise<void>;
54
+
55
+ /**
56
+ * Releases an object after it has been replaced or when the component unmounts.
57
+ * When omitted, the object's `release` method is called.
58
+ */
59
+ release?: (object: TSharedObject) => void;
60
+ };
61
+
62
+ type PendingUpdate<TSharedObject extends SharedObject> = {
63
+ object: TSharedObject;
64
+ context: ReleasingSharedObjectLifecycleContext;
65
+ };
66
+
67
+ function dependenciesAreEqual(previousDependencies: DependencyList, dependencies: DependencyList) {
68
+ return (
69
+ previousDependencies.length === dependencies.length &&
70
+ dependencies.every((value, index) => value === previousDependencies[index])
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Returns a shared object, delegating dependency changes to lifecycle callbacks.
76
+ */
77
+ export function useReleasingSharedObjectWithLifecycle<TSharedObject extends SharedObject>(
78
+ lifecycle: ReleasingSharedObjectLifecycle<TSharedObject>,
79
+ dependencies: DependencyList
80
+ ): TSharedObject {
81
+ const objectRef = useRef<TSharedObject | null>(null);
82
+ const objectRefToRelease = useRef<TSharedObject | null>(null);
83
+ const pendingUpdateRef = useRef<PendingUpdate<TSharedObject> | null>(null);
84
+ const pendingUpdatePromiseRef = useRef<Promise<void> | null>(null);
85
+ const isFastRefresh = useRef(false);
86
+ const previousDependencies = useRef<DependencyList>(dependencies);
87
+ const lifecycleRef = useRef(lifecycle);
88
+
89
+ // Keep lifecycle callbacks fresh without making effects depend on the lifecycle object identity.
90
+ lifecycleRef.current = lifecycle;
91
+
92
+ if (objectRef.current == null) {
93
+ objectRef.current = lifecycleRef.current.factory();
94
+ }
95
+
96
+ const object = useMemo(() => {
97
+ let newObject = objectRef.current;
98
+ const context = {
99
+ previousDependencies: previousDependencies.current,
100
+ dependencies,
101
+ };
102
+
103
+ // If the dependencies have changed, let the caller decide whether the object should be
104
+ // replaced or updated in place. Otherwise this has been called because of an unrelated
105
+ // fast refresh, and we don't want to release the object.
106
+ if (!newObject || !dependenciesAreEqual(previousDependencies.current, dependencies)) {
107
+ if (!newObject || (lifecycleRef.current.shouldRecreate?.(newObject, context) ?? true)) {
108
+ objectRefToRelease.current = objectRef.current;
109
+ newObject = lifecycleRef.current.factory();
110
+ objectRef.current = newObject;
111
+ } else if (lifecycleRef.current.update) {
112
+ pendingUpdateRef.current = {
113
+ object: newObject,
114
+ context,
115
+ };
116
+ }
117
+ previousDependencies.current = dependencies;
118
+ }
119
+ return newObject;
120
+ }, dependencies);
121
+
122
+ function releaseObject(obj: TSharedObject) {
123
+ (lifecycleRef.current.release ?? ((o: TSharedObject) => o.release()))(obj);
124
+ }
125
+
126
+ useEffect(() => {
127
+ // When the object changes, release the previous one - it is important to do this in a useEffect, so that we don't release
128
+ // the object during render. If an async update is still in-flight, defer the release until it settles.
129
+ if (objectRefToRelease.current) {
130
+ const toRelease = objectRefToRelease.current;
131
+ objectRefToRelease.current = null;
132
+ const doRelease = () => releaseObject(toRelease);
133
+ if (pendingUpdatePromiseRef.current) {
134
+ pendingUpdatePromiseRef.current.then(doRelease, doRelease);
135
+ } else {
136
+ doRelease();
137
+ }
138
+ }
139
+
140
+ if (pendingUpdateRef.current) {
141
+ const pendingUpdate = pendingUpdateRef.current;
142
+ pendingUpdateRef.current = null;
143
+ const result = lifecycleRef.current.update?.(pendingUpdate.object, pendingUpdate.context);
144
+ if (result instanceof Promise) {
145
+ pendingUpdatePromiseRef.current = result;
146
+ result.then(
147
+ () => {
148
+ pendingUpdatePromiseRef.current = null;
149
+ },
150
+ (error) => {
151
+ pendingUpdatePromiseRef.current = null;
152
+ console.error(error);
153
+ }
154
+ );
155
+ }
156
+ }
157
+ }, dependencies);
158
+
159
+ useMemo(() => {
160
+ isFastRefresh.current = true;
161
+ }, []);
162
+
163
+ useEffect(() => {
164
+ isFastRefresh.current = false;
165
+
166
+ return () => {
167
+ // This will be called on every fast refresh and on unmount, but we only want to release the object on unmount.
168
+ if (!isFastRefresh.current && objectRef.current) {
169
+ const obj = objectRef.current;
170
+ const doRelease = () => releaseObject(obj);
171
+ if (pendingUpdatePromiseRef.current) {
172
+ pendingUpdatePromiseRef.current.then(doRelease, doRelease);
173
+ } else {
174
+ doRelease();
175
+ }
176
+ }
177
+ };
178
+ }, []);
179
+
180
+ return object;
181
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from './PermissionsHook';
25
25
  export * from './Refs';
26
26
 
27
27
  export * from './hooks/useReleasingSharedObject';
28
+ export * from './hooks/useReleasingSharedObjectWithLifecycle';
28
29
  export * from './reload';
29
30
 
30
31
  // Errors