expo-modules-core 57.0.4 → 57.0.6
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 +15 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/adapters/react/permissions/PermissionsService.kt +41 -4
- package/android/src/main/java/expo/modules/kotlin/views/ShadowNodeProxy.kt +31 -18
- package/ios/Core/Views/SwiftUI/SwiftUIHostingView.swift +19 -46
- package/ios/Core/Views/SwiftUI/SwiftUIViewProps.swift +1 -0
- package/package.json +3 -3
- package/prebuilds/output/debug/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/debug/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.6 — 2026-07-17
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] `ExpoSwiftUI.IgnoreSafeArea` that ignores the device and container safe area regions. ([#47619](https://github.com/expo/expo/pull/47619) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [Android] Fix `canAskAgain` returning `false` for re-requestable permissions in the "Ask every time" state. ([#46683](https://github.com/expo/expo/pull/46683) by [@alanjhughes](https://github.com/alanjhughes))
|
|
22
|
+
|
|
23
|
+
## 57.0.5 — 2026-07-15
|
|
24
|
+
|
|
25
|
+
_This version does not introduce any user-facing changes._
|
|
26
|
+
|
|
13
27
|
## 57.0.4 — 2026-07-15
|
|
14
28
|
|
|
15
29
|
### 🎉 New features
|
|
@@ -18,6 +32,7 @@
|
|
|
18
32
|
|
|
19
33
|
### 🐛 Bug fixes
|
|
20
34
|
|
|
35
|
+
- [Android] Fix Compose-hosted React Native content (e.g. the `@expo/ui` community `BottomSheet`) keeping a stale size after a rapid resize such as dismissing the software keyboard, leaving bottom-anchored content stranded mid-view. The final shadow node size update could be dropped when its one-shot pre-draw listener fired without a following draw pass; the pending update is now also posted to the view so the latest size always flushes. ([#47778](https://github.com/expo/expo/issues/47778) by [@zayyartun-cgm](https://github.com/zayyartun-cgm)) ([#47810](https://github.com/expo/expo/pull/47810) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
21
36
|
- [iOS] Fixed a crash when the runtime is reloaded while async function calls are still suspended in their native body. Concurrent functions now decode `this` and the arguments buffer synchronously inside the host call and schedule only converted native values, so no JSI-owned wrappers are destroyed off the JS thread after the runtime is torn down. ([#47716](https://github.com/expo/expo/issues/47716), [#47717](https://github.com/expo/expo/pull/47717) by [@AlirezaHadjar](https://github.com/AlirezaHadjar))
|
|
22
37
|
- [Android] Support `android.graphics.Color` arguments and props on Android below API 26 by adding a `ColorCompat` wrapper, since the class-based `Color` API (`Color.valueOf` and the float accessors) only exists on API 26 and above. ([#47546](https://github.com/expo/expo/issues/47546) by [@anasvemmully](https://github.com/anasvemmully)) ([#47575](https://github.com/expo/expo/pull/47575) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
23
38
|
- [iOS] Fix a `SIGABRT` crash when unmounting a SwiftUI view tree that still holds a focused field nested inside a container (e.g. an `@expo/ui` `TextField` inside an `HStack` or `LabeledContent` in a `Form`), such as dismissing a modal from a native header button. Resigning the first responder on unmount now recurses through the removed subtree instead of only handling a top-level focusable view. ([#47682](https://github.com/expo/expo/issues/47682) by [@andreavrr](https://github.com/andreavrr)) ([#47709](https://github.com/expo/expo/pull/47709) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
package/android/build.gradle
CHANGED
|
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
group = 'host.exp.exponent'
|
|
30
|
-
version = '57.0.
|
|
30
|
+
version = '57.0.6'
|
|
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.
|
|
97
|
+
versionName "57.0.6"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -27,6 +27,7 @@ import java.util.Queue
|
|
|
27
27
|
|
|
28
28
|
private const val PERMISSIONS_REQUEST: Int = 13
|
|
29
29
|
private const val PREFERENCE_FILENAME = "expo.modules.permissions.asked"
|
|
30
|
+
private const val BLOCKED_PERMISSION_KEY_PREFIX = "blocked:"
|
|
30
31
|
|
|
31
32
|
open class PermissionsService(val context: Context) : InternalModule, Permissions, LifecycleEventListener {
|
|
32
33
|
private var mActivityProvider: ActivityProvider? = null
|
|
@@ -49,6 +50,42 @@ open class PermissionsService(val context: Context) : InternalModule, Permission
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Android can't tell a permanently denied permission apart from a re-askable one ("Ask every time", or a revoked
|
|
55
|
+
* one-time grant) at query time, so we record it ourselves.
|
|
56
|
+
*/
|
|
57
|
+
private fun isBlocked(permission: String): Boolean =
|
|
58
|
+
mAskedPermissionsCache.getBoolean(blockedPermissionKey(permission), false)
|
|
59
|
+
|
|
60
|
+
private fun setBlocked(permission: String, blocked: Boolean) {
|
|
61
|
+
mAskedPermissionsCache.edit(commit = true) {
|
|
62
|
+
putBoolean(blockedPermissionKey(permission), blocked)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun blockedPermissionKey(permission: String): String = "$BLOCKED_PERMISSION_KEY_PREFIX$permission"
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Record whether each permission became permanently denied. A denied result with no rationale to show means the user blocked it.
|
|
70
|
+
* Anything else clears the flag. This is the only time the denied states are distinguishable,
|
|
71
|
+
* so we persist the result for later ``getPermissions` calls.
|
|
72
|
+
*/
|
|
73
|
+
private fun withBlockedTracking(listener: PermissionsResponseListener): PermissionsResponseListener =
|
|
74
|
+
PermissionsResponseListener { result ->
|
|
75
|
+
listener.onResult(
|
|
76
|
+
result.mapValues { (permission, response) ->
|
|
77
|
+
if (response.status == PermissionsStatus.DENIED) {
|
|
78
|
+
val blocked = !shouldShowRequestPermissionRationale(permission)
|
|
79
|
+
setBlocked(permission, blocked)
|
|
80
|
+
PermissionsResponse(response.status, !blocked)
|
|
81
|
+
} else {
|
|
82
|
+
setBlocked(permission, false)
|
|
83
|
+
response
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
override fun getExportedInterfaces(): List<Class<out Any>> = listOf(Permissions::class.java)
|
|
53
90
|
|
|
54
91
|
@Throws(IllegalStateException::class)
|
|
@@ -155,10 +192,10 @@ open class PermissionsService(val context: Context) : InternalModule, Permission
|
|
|
155
192
|
newListener.onResult(mutableMapOf())
|
|
156
193
|
return
|
|
157
194
|
}
|
|
158
|
-
askForManifestPermissions(permissionsToAsk, newListener)
|
|
195
|
+
askForManifestPermissions(permissionsToAsk, withBlockedTracking(newListener))
|
|
159
196
|
}
|
|
160
197
|
} else {
|
|
161
|
-
askForManifestPermissions(permissions, responseListener)
|
|
198
|
+
askForManifestPermissions(permissions, withBlockedTracking(responseListener))
|
|
162
199
|
}
|
|
163
200
|
}
|
|
164
201
|
|
|
@@ -213,7 +250,7 @@ open class PermissionsService(val context: Context) : InternalModule, Permission
|
|
|
213
250
|
return ContextCompat.checkSelfPermission(context, permission)
|
|
214
251
|
}
|
|
215
252
|
|
|
216
|
-
|
|
253
|
+
protected open fun shouldShowRequestPermissionRationale(permission: String): Boolean {
|
|
217
254
|
return mActivityProvider?.currentActivity?.let {
|
|
218
255
|
ActivityCompat.shouldShowRequestPermissionRationale(it, permission)
|
|
219
256
|
} == true
|
|
@@ -236,7 +273,7 @@ open class PermissionsService(val context: Context) : InternalModule, Permission
|
|
|
236
273
|
return PermissionsResponse(
|
|
237
274
|
status,
|
|
238
275
|
if (status == PermissionsStatus.DENIED) {
|
|
239
|
-
|
|
276
|
+
!isBlocked(permission)
|
|
240
277
|
} else {
|
|
241
278
|
true
|
|
242
279
|
}
|
|
@@ -10,6 +10,7 @@ class ShadowNodeProxy(expoView: ExpoView) {
|
|
|
10
10
|
|
|
11
11
|
private var pendingFlush: ((stateWrapper: Any) -> Unit)? = null
|
|
12
12
|
private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null
|
|
13
|
+
private val flushRunnable = Runnable { drainPendingFlush() }
|
|
13
14
|
|
|
14
15
|
// Schedule in predraw listener to avoid early return in re-entrancy
|
|
15
16
|
// We have a proper fix [here](https://github.com/facebook/react-native/pull/56311)
|
|
@@ -29,25 +30,37 @@ class ShadowNodeProxy(expoView: ExpoView) {
|
|
|
29
30
|
|
|
30
31
|
private fun scheduleFlush(flush: (stateWrapper: Any) -> Unit) {
|
|
31
32
|
pendingFlush = flush
|
|
32
|
-
val
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
val view = weakExpoView.get() ?: return
|
|
34
|
+
val observer = view.viewTreeObserver?.takeIf { it.isAlive }
|
|
35
|
+
|
|
36
|
+
if (observer != null) {
|
|
37
|
+
// Remove the previous attached listener
|
|
38
|
+
preDrawListener?.let(observer::removeOnPreDrawListener)
|
|
39
|
+
|
|
40
|
+
val listener = object : ViewTreeObserver.OnPreDrawListener {
|
|
41
|
+
override fun onPreDraw(): Boolean {
|
|
42
|
+
preDrawListener = null
|
|
43
|
+
// The view is attached while drawing, so this re-fetch returns the same
|
|
44
|
+
// observer that is dispatching us. removeOnPreDrawListener throws on a dead
|
|
45
|
+
// observer, hence the isAlive guard.
|
|
46
|
+
weakExpoView.get()?.viewTreeObserver?.takeIf { it.isAlive }?.removeOnPreDrawListener(this)
|
|
47
|
+
drainPendingFlush()
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
48
50
|
}
|
|
51
|
+
preDrawListener = listener
|
|
52
|
+
observer.addOnPreDrawListener(listener)
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
// Predraw listener do not get called for each keyboard transition event so we add a fallback flush to be called here
|
|
56
|
+
// https://github.com/expo/expo/issues/47778
|
|
57
|
+
view.removeCallbacks(flushRunnable)
|
|
58
|
+
view.post(flushRunnable)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun drainPendingFlush() {
|
|
62
|
+
val flushNow = pendingFlush ?: return
|
|
63
|
+
pendingFlush = null
|
|
64
|
+
weakExpoView.get()?.stateWrapper?.let { flushNow.invoke(it) }
|
|
52
65
|
}
|
|
53
66
|
}
|
|
@@ -60,11 +60,6 @@ extension ExpoSwiftUI {
|
|
|
60
60
|
*/
|
|
61
61
|
private let hostingController: UIHostingController<AnyView>
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
Tracks whether safe area has been configured (can only be set once on mount)
|
|
65
|
-
*/
|
|
66
|
-
private var hasSafeAreaBeenConfigured = false
|
|
67
|
-
|
|
68
63
|
/**
|
|
69
64
|
Initializes a SwiftUI hosting view with the given SwiftUI view type.
|
|
70
65
|
*/
|
|
@@ -121,11 +116,8 @@ extension ExpoSwiftUI {
|
|
|
121
116
|
log.error("Updating props for \(ContentView.self) has failed: \(error.localizedDescription)")
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
let ignoreSafeArea = safeAreaProps.ignoreSafeArea {
|
|
127
|
-
hostingController.disableSafeArea(ignoreSafeArea)
|
|
128
|
-
hasSafeAreaBeenConfigured = true
|
|
119
|
+
if let safeAreaProps = props as? SafeAreaControllable {
|
|
120
|
+
hostingController.setSafeAreaRegions(ignoring: safeAreaProps.ignoreSafeArea)
|
|
129
121
|
}
|
|
130
122
|
}
|
|
131
123
|
|
|
@@ -260,45 +252,26 @@ extension ExpoSwiftUI {
|
|
|
260
252
|
}
|
|
261
253
|
|
|
262
254
|
extension UIHostingController {
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
/// Applies the `ignoreSafeArea` mode reactively, restoring the default safe area when `nil` so
|
|
256
|
+
/// clearing the prop re-enables the safe area without an app reload.
|
|
257
|
+
func setSafeAreaRegions(ignoring mode: ExpoSwiftUI.IgnoreSafeArea?) {
|
|
258
|
+
// `safeAreaRegions` needs iOS 16.4+; the precompiled xcframework targets 16.0, so no-op below it.
|
|
259
|
+
guard #available(iOS 16.4, tvOS 16.4, macOS 13.3, *) else {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
var regions: SafeAreaRegions = .all
|
|
263
|
+
if let mode {
|
|
265
264
|
switch mode {
|
|
266
265
|
case .all:
|
|
267
|
-
|
|
266
|
+
regions = []
|
|
267
|
+
case .container:
|
|
268
|
+
regions.remove(.container)
|
|
268
269
|
case .keyboard:
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
// For older versions
|
|
273
|
-
// https://gist.github.com/steipete/da72299613dcc91e8d729e48b4bb582c
|
|
274
|
-
// https://developer.apple.com/forums/thread/658432
|
|
275
|
-
guard let viewClass = object_getClass(view) else { return }
|
|
276
|
-
|
|
277
|
-
let suffix = mode == .all ? "_IgnoresSafeArea" : "_IgnoresKeyboard"
|
|
278
|
-
let viewSubclassName = String(cString: class_getName(viewClass)).appending(suffix)
|
|
279
|
-
if let viewSubclass = NSClassFromString(viewSubclassName) {
|
|
280
|
-
object_setClass(view, viewSubclass)
|
|
281
|
-
} else {
|
|
282
|
-
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
|
|
283
|
-
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
|
|
284
|
-
|
|
285
|
-
if mode == .all,
|
|
286
|
-
let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
|
|
287
|
-
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
|
|
288
|
-
return .zero
|
|
289
|
-
}
|
|
290
|
-
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets),
|
|
291
|
-
imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if let method = class_getInstanceMethod(viewClass, NSSelectorFromString("keyboardWillShowWithNotification:")) {
|
|
295
|
-
let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in }
|
|
296
|
-
class_addMethod(viewSubclass, NSSelectorFromString("keyboardWillShowWithNotification:"),
|
|
297
|
-
imp_implementationWithBlock(keyboardWillShow), method_getTypeEncoding(method))
|
|
298
|
-
}
|
|
299
|
-
objc_registerClassPair(viewSubclass)
|
|
300
|
-
object_setClass(view, viewSubclass)
|
|
301
|
-
}
|
|
270
|
+
regions.remove(.keyboard)
|
|
302
271
|
}
|
|
303
272
|
}
|
|
273
|
+
if safeAreaRegions != regions {
|
|
274
|
+
safeAreaRegions = regions
|
|
275
|
+
}
|
|
276
|
+
}
|
|
304
277
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "57.0.
|
|
3
|
+
"version": "57.0.6",
|
|
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.0.
|
|
50
|
+
"expo-modules-jsi": "~57.0.3",
|
|
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": "
|
|
69
|
+
"gitHead": "0dac79c8469fa00b76fb8f402c47ed61f3d6317e",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|