expo-widgets 58.0.0 → 58.0.2

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/.npmignore CHANGED
@@ -17,3 +17,8 @@ __rsc_tests__
17
17
  /android/src/androidTest/
18
18
  /android/src/test/
19
19
  /ios/Tests
20
+
21
+ # Exclude native build output (pnpm pack ignores nested .gitignore files)
22
+ .cxx/
23
+ .gradle/
24
+ /android/build/
package/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.2 — 2026-09-15
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android] Add support for the `Image` component. ([#50116](https://github.com/expo/expo/pull/50116) by [@jakex7](https://github.com/jakex7))
18
+
19
+ ## 58.0.1 — 2026-09-14
20
+
21
+ ### 🎉 New features
22
+
23
+ - [iOS] Add support for `Overlay`, `Background`, and `Mask` in widgets and Live Activities. ([#49810](https://github.com/expo/expo/pull/49810) by [@jakex7](https://github.com/jakex7))
24
+
25
+ ### 🐛 Bug fixes
26
+
27
+ - [iOS] Preserve SwiftUI view identity across widget and Live Activity updates so system update animations can run. ([#49810](https://github.com/expo/expo/pull/49810) by [@jakex7](https://github.com/jakex7))
28
+ - [Android] Fix Gradle build failure when no Android widget is configured. ([#50038](https://github.com/expo/expo/pull/50038) by [@keith-kurak](https://github.com/keith-kurak))
29
+
30
+ ### 💡 Others
31
+
32
+ - Upgrade React Native to 0.88.0-rc.0 ([#49910](https://github.com/expo/expo/pull/49910) by [@gabrieldonadel](https://github.com/gabrieldonadel))
33
+
13
34
  ## 58.0.0 — 2026-09-10
14
35
 
15
36
  ### 🛠 Breaking changes
@@ -6,7 +6,7 @@ plugins {
6
6
  }
7
7
 
8
8
  group = 'expo.modules.widgets'
9
- version = '58.0.0'
9
+ version = '58.0.2'
10
10
 
11
11
  def expoWidgetsPackageDirFile = project.projectDir.parentFile
12
12
  def expoWidgetsPackageDir = expoWidgetsPackageDirFile.absolutePath
@@ -39,7 +39,7 @@ android {
39
39
  namespace "expo.modules.widgets"
40
40
  defaultConfig {
41
41
  versionCode 1
42
- versionName '58.0.0'
42
+ versionName '58.0.2'
43
43
 
44
44
  externalNativeBuild {
45
45
  cmake {
@@ -108,7 +108,7 @@ def generateExpoWidgetsLayoutRegistry = tasks.register("generateExpoWidgetsLayou
108
108
  inputs.file file("$expoWidgetsPackageDir/metro.config.js")
109
109
  inputs.file file("$expoWidgetsPackageDir/scripts/build-layout-registry.mjs")
110
110
  inputs.file file("$expoWidgetsPackageDir/bundle/layout-registry-entry.js")
111
- inputs.file(expoWidgetsLayoutRegistryConfigFile).optional()
111
+ inputs.files(expoWidgetsLayoutRegistryConfigFile)
112
112
  outputs.file(expoWidgetsLayoutRegistryOutput)
113
113
 
114
114
  def command = [
@@ -143,6 +143,6 @@ dependencies {
143
143
  implementation 'com.facebook.react:react-android'
144
144
  implementation 'com.facebook.react:hermes-android'
145
145
  implementation expoModule.getExpoDependency("expo-ui")
146
- api 'io.github.jakex7.peek:peek-emittables:0.2.0'
147
- implementation "androidx.glance:glance:1.2.0-rc01"
146
+ api 'io.github.jakex7.peek:peek-emittables:0.3.0'
147
+ implementation "androidx.glance:glance:1.2.0"
148
148
  }
@@ -1,7 +1,11 @@
1
+ @file:SuppressLint("RestrictedApi")
2
+
1
3
  package expo.modules.widgets
2
4
 
5
+ import android.annotation.SuppressLint
3
6
  import android.content.Context
4
7
  import android.graphics.Color as AndroidColor
8
+ import androidx.compose.ui.unit.DpSize
5
9
  import androidx.compose.ui.unit.TextUnit
6
10
  import androidx.compose.ui.unit.dp
7
11
  import androidx.compose.ui.unit.sp
@@ -22,6 +26,8 @@ import expo.modules.ui.FillMaxHeightParams
22
26
  import expo.modules.ui.FillMaxSizeParams
23
27
  import expo.modules.ui.FillMaxWidthParams
24
28
  import expo.modules.ui.HeightParams
29
+ import expo.modules.ui.graphics.ImageSource
30
+ import expo.modules.ui.image.ImageContentScale
25
31
  import expo.modules.ui.LayoutProps
26
32
  import expo.modules.ui.LinearProgressIndicatorProps
27
33
  import expo.modules.ui.LoadingIndicatorProps
@@ -54,6 +60,9 @@ import expo.modules.ui.convertibles.VerticalArrangementDefault
54
60
  import androidx.compose.ui.graphics.Color
55
61
  import androidx.glance.Emittable
56
62
  import androidx.glance.EmittableButton
63
+ import androidx.glance.EmittableImage
64
+ import androidx.glance.ImageProvider
65
+ import androidx.glance.TintColorFilterParams
57
66
  import androidx.glance.GlanceModifier
58
67
  import androidx.glance.action.clickable
59
68
  import androidx.glance.appwidget.CheckboxDefaults
@@ -67,6 +76,7 @@ import androidx.glance.appwidget.RadioButtonDefaults
67
76
  import androidx.glance.appwidget.SwitchDefaults
68
77
  import androidx.glance.background
69
78
  import androidx.glance.layout.Alignment
79
+ import androidx.glance.layout.ContentScale
70
80
  import androidx.glance.layout.EmittableBox
71
81
  import androidx.glance.layout.EmittableColumn
72
82
  import androidx.glance.layout.EmittableRow
@@ -80,6 +90,8 @@ import androidx.glance.layout.size
80
90
  import androidx.glance.layout.width
81
91
  import androidx.glance.layout.wrapContentHeight
82
92
  import androidx.glance.layout.wrapContentWidth
93
+ import androidx.glance.semantics.contentDescription
94
+ import androidx.glance.semantics.semantics
83
95
  import androidx.glance.text.EmittableText
84
96
  import androidx.glance.text.FontStyle
85
97
  import androidx.glance.text.FontWeight
@@ -92,6 +104,7 @@ import androidx.glance.appwidget.lazy.EmittableLazyColumn
92
104
  import androidx.glance.appwidget.lazy.EmittableLazyListItem
93
105
  import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
94
106
  import io.github.jakex7.peek.glance.determinateCircularProgressIndicatorEmittable
107
+ import kotlin.math.roundToInt
95
108
 
96
109
  private val DefaultCheckedColor = ColorProvider(Color(0xff6750a4))
97
110
  private val DefaultUncheckedColor = ColorProvider(Color(0xff79747e))
@@ -99,13 +112,14 @@ private val DefaultCheckedTrackColor = ColorProvider(Color(0xffe8def8))
99
112
  private val DefaultUncheckedTrackColor = ColorProvider(Color(0xffe7e0ec))
100
113
 
101
114
  private class WidgetConverterContext(
102
- override val applicationContext: Context
115
+ override val applicationContext: Context,
116
+ val widgetSize: DpSize
103
117
  ) : ConverterContext {
104
118
  override val runtime: Runtime? = null
105
119
  }
106
120
 
107
- internal fun ReadableMap.toPeekRoot(context: Context, source: String): Emittable {
108
- val converterContext = WidgetConverterContext(context)
121
+ internal fun ReadableMap.toPeekRoot(context: Context, source: String, size: DpSize): Emittable {
122
+ val converterContext = WidgetConverterContext(context, size)
109
123
  return EmittableColumn().also { root ->
110
124
  root.children += toPeekNodes(converterContext, source)
111
125
  }
@@ -120,12 +134,13 @@ private fun createErrorText(message: String): EmittableText {
120
134
  }
121
135
  }
122
136
 
123
- private fun ReadableMap.toPeekNodes(converterContext: ConverterContext, source: String): List<Emittable> {
137
+ private fun ReadableMap.toPeekNodes(converterContext: WidgetConverterContext, source: String): List<Emittable> {
124
138
  return when (typeName()) {
125
139
  "BoxView" -> listOf(toPeekBox(converterContext, source))
126
140
  "CheckboxView" -> listOf(toPeekCheckBox(converterContext))
127
141
  "CircularProgressIndicatorView" -> listOf(toPeekCircularProgress(converterContext))
128
142
  "ColumnView" -> listOf(toPeekColumn(converterContext, source))
143
+ "ImageView" -> listOf(toPeekImage(converterContext))
129
144
  "LazyColumnView" -> listOf(toPeekLazyColumn(converterContext, source))
130
145
  "LinearProgressIndicatorView" -> listOf(toPeekLinearProgress(converterContext))
131
146
  "LoadingIndicatorView" -> listOf(toPeekLoadingIndicator(converterContext))
@@ -143,7 +158,7 @@ private fun ReadableMap.toPeekNodes(converterContext: ConverterContext, source:
143
158
  }
144
159
  }
145
160
 
146
- private fun ReadableMap.toPeekBox(converterContext: ConverterContext, source: String): EmittableBox {
161
+ private fun ReadableMap.toPeekBox(converterContext: WidgetConverterContext, source: String): EmittableBox {
147
162
  val props = props<LayoutProps>(converterContext)
148
163
  return EmittableBox().also {
149
164
  it.modifier = props.modifiers.toPeekModifier(converterContext)
@@ -152,7 +167,7 @@ private fun ReadableMap.toPeekBox(converterContext: ConverterContext, source: St
152
167
  }
153
168
  }
154
169
 
155
- private fun ReadableMap.toPeekRow(converterContext: ConverterContext, source: String): EmittableRow {
170
+ private fun ReadableMap.toPeekRow(converterContext: WidgetConverterContext, source: String): EmittableRow {
156
171
  val props = props<LayoutProps>(converterContext)
157
172
  return EmittableRow().also {
158
173
  it.modifier = props.modifiers.toPeekModifier(converterContext)
@@ -162,7 +177,7 @@ private fun ReadableMap.toPeekRow(converterContext: ConverterContext, source: St
162
177
  }
163
178
  }
164
179
 
165
- private fun ReadableMap.toPeekColumn(converterContext: ConverterContext, source: String): EmittableColumn {
180
+ private fun ReadableMap.toPeekColumn(converterContext: WidgetConverterContext, source: String): EmittableColumn {
166
181
  val props = props<LayoutProps>(converterContext)
167
182
  return EmittableColumn().also {
168
183
  it.modifier = props.modifiers.toPeekModifier(converterContext)
@@ -172,7 +187,7 @@ private fun ReadableMap.toPeekColumn(converterContext: ConverterContext, source:
172
187
  }
173
188
  }
174
189
 
175
- private fun ReadableMap.toPeekLazyColumn(converterContext: ConverterContext, source: String): EmittableLazyColumn {
190
+ private fun ReadableMap.toPeekLazyColumn(converterContext: WidgetConverterContext, source: String): EmittableLazyColumn {
176
191
  val props = props<LayoutProps>(converterContext)
177
192
  val alignment = props.toPeekHorizontalAlignment()
178
193
  return EmittableLazyColumn().also { lazyColumn ->
@@ -214,7 +229,68 @@ private fun ReadableMap.toPeekText(converterContext: ConverterContext): Emittabl
214
229
  }
215
230
  }
216
231
 
217
- private fun ReadableMap.toPeekButton(converterContext: ConverterContext, source: String): Emittable {
232
+ private fun ReadableMap.toPeekImage(converterContext: WidgetConverterContext): Emittable {
233
+ val props = props<WidgetImageProps>(converterContext)
234
+ val source = props.source
235
+ ?: return createErrorText("Image has no source. Pass source={{ uri }} with a drawable resource name or a file URI.")
236
+ val provider = runCatching {
237
+ loadWidgetImage(converterContext.applicationContext, source, props.imageTarget(converterContext))
238
+ }.getOrElse { error ->
239
+ return createErrorText(error.message ?: "Image '${source.uri}' could not be loaded.")
240
+ }
241
+
242
+ return EmittableImage().also {
243
+ it.provider = provider
244
+ it.modifier = props.imageModifier(converterContext)
245
+ it.contentScale = props.contentScale.toGlanceContentScale()
246
+ it.colorFilterParams = props.tint.toGlanceColorProvider()?.let(::TintColorFilterParams)
247
+ it.alpha = props.alpha.coerceIn(0f, 1f)
248
+ }
249
+ }
250
+
251
+ private fun WidgetImageProps.imageModifier(converterContext: ConverterContext): GlanceModifier {
252
+ val modifier = modifiers.toPeekModifier(converterContext)
253
+ val description = contentDescription ?: return modifier
254
+ return modifier.semantics { contentDescription = description }
255
+ }
256
+
257
+ private fun WidgetImageProps.imageTarget(converterContext: WidgetConverterContext): WidgetImageTarget {
258
+ val metrics = converterContext.applicationContext.resources.displayMetrics
259
+ var widthDp = converterContext.widgetSize.width.value.takeIf { it.isFinite() && it > 0f }
260
+ ?: (metrics.widthPixels / metrics.density)
261
+ var heightDp = converterContext.widgetSize.height.value.takeIf { it.isFinite() && it > 0f }
262
+ ?: (metrics.heightPixels / metrics.density)
263
+
264
+ for (modifier in modifiers) {
265
+ when (modifier["\$type"]?.asString()) {
266
+ "size" -> modifier.asRecord<SizeParams>(converterContext)?.let {
267
+ widthDp = it.width.toFloat()
268
+ heightDp = it.height.toFloat()
269
+ }
270
+
271
+ "width" -> modifier.asRecord<WidthParams>(converterContext)?.let { widthDp = it.width.toFloat() }
272
+ "height" -> modifier.asRecord<HeightParams>(converterContext)?.let { heightDp = it.height.toFloat() }
273
+ }
274
+ }
275
+
276
+ return WidgetImageTarget(
277
+ widthPx = (widthDp * metrics.density).roundToInt().coerceAtLeast(1),
278
+ heightPx = (heightDp * metrics.density).roundToInt().coerceAtLeast(1)
279
+ )
280
+ }
281
+
282
+ private fun ImageContentScale.toGlanceContentScale(): ContentScale {
283
+ return when (this) {
284
+ ImageContentScale.FIT -> ContentScale.Fit
285
+ ImageContentScale.CROP -> ContentScale.Crop
286
+ ImageContentScale.FILL_BOUNDS -> ContentScale.FillBounds
287
+ // RemoteViews only offers fit, crop and stretch, so the remaining scales map to the nearest one.
288
+ ImageContentScale.INSIDE, ImageContentScale.NONE -> ContentScale.Fit
289
+ ImageContentScale.FILL_WIDTH, ImageContentScale.FILL_HEIGHT -> ContentScale.Crop
290
+ }
291
+ }
292
+
293
+ private fun ReadableMap.toPeekButton(converterContext: WidgetConverterContext, source: String): Emittable {
218
294
  val props = props<WidgetButtonProps>(converterContext)
219
295
  val children = children()
220
296
  val action =
@@ -603,3 +679,13 @@ internal data class WidgetButtonProps(
603
679
  val modifiers: ModifierList = emptyList(),
604
680
  val target: String? = null
605
681
  ) : ComposeProps
682
+
683
+ @OptimizedComposeProps
684
+ internal data class WidgetImageProps(
685
+ val source: ImageSource? = null,
686
+ val contentScale: ImageContentScale = ImageContentScale.FIT,
687
+ val contentDescription: String? = null,
688
+ val tint: AndroidColor? = null,
689
+ val alpha: Float = 1f,
690
+ val modifiers: ModifierList = emptyList()
691
+ ) : ComposeProps
@@ -1,5 +1,7 @@
1
+ @file:SuppressLint("RestrictedApi")
1
2
  package expo.modules.widgets
2
3
 
4
+ import android.annotation.SuppressLint
3
5
  import android.content.Context
4
6
  import android.os.Bundle
5
7
  import androidx.compose.ui.unit.DpSize
@@ -26,6 +28,6 @@ internal class ExpoWidgetsPeekWidget(
26
28
  val environment = getWidgetEnvironment(appContext)
27
29
  val node = evaluateLayout(appContext, layout, props, environment)
28
30
 
29
- return node.toPeekRoot(appContext, widgetName)
31
+ return node.toPeekRoot(appContext, widgetName, size)
30
32
  }
31
33
  }
@@ -0,0 +1,159 @@
1
+ @file:SuppressLint("RestrictedApi")
2
+
3
+ package expo.modules.widgets
4
+
5
+ import android.annotation.SuppressLint
6
+ import android.content.Context
7
+ import android.graphics.Bitmap
8
+ import android.graphics.BitmapFactory
9
+ import android.graphics.ImageDecoder
10
+ import android.graphics.Matrix
11
+ import android.media.ExifInterface
12
+ import android.net.Uri
13
+ import android.os.Build
14
+ import androidx.glance.ImageProvider
15
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
16
+ import expo.modules.ui.graphics.ImageSource
17
+ import java.io.FileNotFoundException
18
+ import java.io.InputStream
19
+ import kotlin.math.max
20
+
21
+ internal data class WidgetImageTarget(val widthPx: Int, val heightPx: Int)
22
+
23
+ // RemoteViews rejects a widget update whose bitmaps exceed its memory budget.
24
+ private const val MAX_WIDGET_IMAGE_PIXELS = 1_048_576L
25
+ private const val MAX_WIDGET_IMAGE_DIMENSION_PX = 2048
26
+
27
+ internal fun loadWidgetImage(context: Context, source: ImageSource, target: WidgetImageTarget): ImageProvider {
28
+ val uri = Uri.parse(source.uri)
29
+ return when (uri.scheme) {
30
+ null -> ImageProvider(resolveDrawableId(context, source.uri))
31
+ "file", "content", "android.resource" -> ImageProvider(decodeBitmap(context, uri, target))
32
+ "http", "https" -> throw IllegalArgumentException(
33
+ "Remote images are not supported in widgets ('${source.uri}'). Download the image to widgetsDirectory from the app and pass its file URI."
34
+ )
35
+
36
+ else -> throw IllegalArgumentException(
37
+ "Image URI scheme '${uri.scheme}' is not supported in widgets ('${source.uri}'). Pass a drawable resource name or a file URI."
38
+ )
39
+ }
40
+ }
41
+
42
+ private fun resolveDrawableId(context: Context, name: String): Int {
43
+ val resourceId = ResourceDrawableIdHelper.getResourceDrawableId(context, name)
44
+ require(resourceId > 0) {
45
+ "Image resource '$name' was not found in the app's drawables. Add it to the app's res/drawable directory or pass a file URI."
46
+ }
47
+ return resourceId
48
+ }
49
+
50
+ private fun decodeBitmap(context: Context, uri: Uri, target: WidgetImageTarget): Bitmap {
51
+ return runCatching {
52
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
53
+ ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) { decoder, info, _ ->
54
+ decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
55
+ decoder.setTargetSampleSize(calculateInSampleSize(info.size.width, info.size.height, target))
56
+ }
57
+ } else {
58
+ decodeWithBitmapFactory(context, uri, target)
59
+ }
60
+ }.getOrElse { error ->
61
+ throw IllegalArgumentException(
62
+ if (error is FileNotFoundException) {
63
+ "Image '$uri' does not exist. Copy the image to widgetsDirectory before updating the widget."
64
+ } else {
65
+ "Image '$uri' could not be decoded. Widgets support bitmap formats such as PNG, JPEG and WebP."
66
+ },
67
+ error
68
+ )
69
+ }
70
+ }
71
+
72
+ private fun decodeWithBitmapFactory(context: Context, uri: Uri, target: WidgetImageTarget): Bitmap {
73
+ val open: () -> InputStream = {
74
+ context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException(uri.toString())
75
+ }
76
+ val orientation = open().use { input ->
77
+ runCatching {
78
+ ExifInterface(input).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
79
+ }.getOrDefault(ExifInterface.ORIENTATION_NORMAL)
80
+ }
81
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
82
+ open().use { input -> BitmapFactory.decodeStream(input, null, bounds) }
83
+ require(bounds.outWidth > 0 && bounds.outHeight > 0) { "Image '$uri' has no decodable bounds." }
84
+
85
+ val swapsDimensions = orientation.swapsDimensions()
86
+ val options = BitmapFactory.Options().apply {
87
+ inSampleSize = calculateInSampleSize(
88
+ sourceWidth = if (swapsDimensions) {
89
+ bounds.outHeight
90
+ } else {
91
+ bounds.outWidth
92
+ },
93
+ sourceHeight = if (swapsDimensions) {
94
+ bounds.outWidth
95
+ } else {
96
+ bounds.outHeight
97
+ },
98
+ target = target
99
+ )
100
+ }
101
+ val bitmap = requireNotNull(open().use { input -> BitmapFactory.decodeStream(input, null, options) }) {
102
+ "Image '$uri' could not be decoded."
103
+ }
104
+ return bitmap.applyExifOrientation(orientation)
105
+ }
106
+
107
+ private fun calculateInSampleSize(sourceWidth: Int, sourceHeight: Int, target: WidgetImageTarget): Int {
108
+ val targetWidth = target.widthPx.coerceAtLeast(1)
109
+ val targetHeight = target.heightPx.coerceAtLeast(1)
110
+ var sampleSize = 1
111
+ while (sourceWidth / (sampleSize * 2) >= targetWidth && sourceHeight / (sampleSize * 2) >= targetHeight) {
112
+ sampleSize *= 2
113
+ }
114
+ while (
115
+ (sourceWidth.toLong() / sampleSize) * (sourceHeight.toLong() / sampleSize) > MAX_WIDGET_IMAGE_PIXELS ||
116
+ max(sourceWidth, sourceHeight) / sampleSize > MAX_WIDGET_IMAGE_DIMENSION_PX
117
+ ) {
118
+ sampleSize *= 2
119
+ }
120
+ return sampleSize
121
+ }
122
+
123
+ private fun Bitmap.applyExifOrientation(orientation: Int): Bitmap {
124
+ val matrix = Matrix()
125
+ when (orientation) {
126
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
127
+ ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
128
+ ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
129
+ matrix.setRotate(180f)
130
+ matrix.postScale(-1f, 1f)
131
+ }
132
+
133
+ ExifInterface.ORIENTATION_TRANSPOSE -> {
134
+ matrix.setRotate(90f)
135
+ matrix.postScale(-1f, 1f)
136
+ }
137
+
138
+ ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
139
+ ExifInterface.ORIENTATION_TRANSVERSE -> {
140
+ matrix.setRotate(-90f)
141
+ matrix.postScale(-1f, 1f)
142
+ }
143
+
144
+ ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
145
+ else -> return this
146
+ }
147
+ return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true).also { oriented ->
148
+ if (oriented !== this) {
149
+ recycle()
150
+ }
151
+ }
152
+ }
153
+
154
+ private fun Int.swapsDimensions(): Boolean {
155
+ return this == ExifInterface.ORIENTATION_TRANSPOSE ||
156
+ this == ExifInterface.ORIENTATION_ROTATE_90 ||
157
+ this == ExifInterface.ORIENTATION_TRANSVERSE ||
158
+ this == ExifInterface.ORIENTATION_ROTATE_270
159
+ }
package/bundle/index.ts CHANGED
@@ -18,6 +18,44 @@ declare global {
18
18
  var __expoWidgetEnvironment: Dictionary | undefined;
19
19
  }
20
20
 
21
+ /**
22
+ * Resolve components at the render boundary so parents can inspect their JSX children.
23
+ * Identity is the native type plus the outermost explicit key, or flattened sibling index.
24
+ * Keys must be unique among flattened siblings; dynamic lists should provide explicit keys.
25
+ */
26
+ function normalizeWidgetTree(node: unknown, position: string | number = 0): unknown {
27
+ let key: string | null = null;
28
+ while (React.isValidElement(node) && typeof node.type === 'function') {
29
+ key ??= node.key;
30
+ const rendered = node.type(node.props);
31
+ node = Array.isArray(rendered)
32
+ ? jsxRuntime.jsx(React.Fragment, { children: rendered })
33
+ : rendered;
34
+ }
35
+
36
+ if (Array.isArray(node)) {
37
+ return node.flat(Infinity).map((child, index) => normalizeWidgetTree(child, index));
38
+ }
39
+ if (React.isValidElement(node)) {
40
+ const props = { ...node.props };
41
+ if ('children' in props) {
42
+ props.children = normalizeWidgetTree(props.children);
43
+ }
44
+ return {
45
+ ...node,
46
+ props,
47
+ __expoWidgetIdentity: JSON.stringify([node.type, key ?? node.key ?? position]),
48
+ };
49
+ }
50
+ if (node && typeof node === 'object') {
51
+ // Live Activity layouts return a map of section names to roots.
52
+ return Object.fromEntries(
53
+ Object.entries(node).map(([section, child]) => [section, normalizeWidgetTree(child, section)])
54
+ );
55
+ }
56
+ return node;
57
+ }
58
+
21
59
  const __expoWidgetRender = function (props: Dictionary, environment: Dictionary) {
22
60
  // `materialColors` backs the native expo-ui module stub and stays out of the layout environment.
23
61
  const { timestamp, materialColors, ...rest } = environment;
@@ -27,7 +65,9 @@ const __expoWidgetRender = function (props: Dictionary, environment: Dictionary)
27
65
  }
28
66
  globalThis.__expoWidgetEnvironment = { ...decoratedEnvironment, materialColors };
29
67
 
30
- return decorateInteractiveTargets(globalThis.__expoWidgetLayout(props, decoratedEnvironment));
68
+ return decorateInteractiveTargets(
69
+ normalizeWidgetTree(globalThis.__expoWidgetLayout(props, decoratedEnvironment))
70
+ );
31
71
  };
32
72
 
33
73
  const __expoWidgetHandlePress = function (
@@ -62,10 +102,10 @@ const __expoWidgetHandlePress = function (
62
102
  };
63
103
 
64
104
  Object.assign(globalThis, {
65
- ...uiGlobals,
66
- ...jsxRuntime,
67
- ...React,
68
105
  ...ReactNative,
106
+ ...React,
107
+ ...jsxRuntime,
108
+ ...uiGlobals,
69
109
  React,
70
110
  __expoWidgetRender,
71
111
  __expoWidgetHandlePress,
@@ -4,63 +4,16 @@ export type ReactElementNode = {
4
4
  type: unknown;
5
5
  key: string | null;
6
6
  props: Record<string, any>;
7
+ __expoWidgetIdentity?: string;
7
8
  };
8
9
 
9
- function ReactElement(
10
- type: unknown,
11
- key: string | null,
12
- props: Record<string, any>
13
- ): ReactElementNode {
14
- if (typeof type === 'function') {
15
- return (type as any)(props);
16
- }
17
-
18
- const element = {
19
- type,
20
- key,
21
- props,
22
- };
23
-
24
- return element;
25
- }
26
-
27
10
  function jsxProd(
28
11
  type: unknown,
29
12
  config: any,
30
13
  maybeKey?: string | number | bigint
31
14
  ): ReactElementNode {
32
- let key = null;
33
- if (maybeKey !== undefined) {
34
- key = '' + maybeKey;
35
- }
36
- if (hasValidKey(config)) {
37
- key = '' + config.key;
38
- }
39
-
40
- let props: Record<string, any>;
41
- if (!('key' in config)) {
42
- props = config;
43
- } else {
44
- props = {};
45
- for (const propName in config) {
46
- if (propName !== 'key') {
47
- props[propName] = config[propName];
48
- }
49
- }
50
- }
51
-
52
- // React flattens nested children arrays (e.g. a `.map()` result mixed with sibling
53
- // elements) during reconciliation. There is no reconciler here - the element tree is
54
- // serialized for the native widget parsers, which expect a flat children list.
55
- if (Array.isArray(props.children)) {
56
- props.children = props.children.flat(Infinity);
57
- }
58
-
59
- return ReactElement(type, key, props);
60
- }
61
-
62
- function hasValidKey(config: any) {
63
- return config.key !== undefined;
15
+ const { key = maybeKey, ...props } = config;
16
+ return { type, key: key === undefined ? null : String(key), props };
64
17
  }
65
18
 
66
19
  const jsxFileName = 'widget';
@@ -2,10 +2,30 @@ export const PlatformColor = (...names: string[]) => {
2
2
  return { semantic: names };
3
3
  };
4
4
 
5
- export const Image = {
6
- resolveAssetSource: {},
5
+ type ResolvedImageSource = {
6
+ uri: string;
7
+ width?: number;
8
+ height?: number;
9
+ scale?: number;
7
10
  };
8
11
 
12
+ function resolveAssetSource(source: unknown): ResolvedImageSource | null {
13
+ if (source == null) {
14
+ return null;
15
+ }
16
+ if (typeof source === 'object' && typeof (source as ResolvedImageSource).uri === 'string') {
17
+ return source as ResolvedImageSource;
18
+ }
19
+ if (typeof source === 'number') {
20
+ throw new Error(
21
+ 'Bundled require() image sources are not supported inside widgets. Copy the image to widgetsDirectory and pass source={{ uri }} instead.'
22
+ );
23
+ }
24
+ throw new Error('Widget Image source must be an object containing a uri string.');
25
+ }
26
+
27
+ export const Image = { resolveAssetSource };
28
+
9
29
  export const useColorScheme = (): 'light' | 'dark' | null => {
10
30
  return (globalThis.__expoWidgetEnvironment?.colorScheme as 'light' | 'dark' | undefined) ?? null;
11
31
  };
@@ -1,15 +1,17 @@
1
+ import type { ReactElementNode } from './jsx-runtime-stub';
2
+
1
3
  export const Fragment = 'react.fragment';
2
4
 
3
5
  export const Children = {
4
6
  toArray(children: unknown) {
5
- if (children === undefined || children === null) {
6
- return [];
7
- }
8
- return Array.isArray(children) ? children : [children];
7
+ return [children]
8
+ .flat(Infinity)
9
+ .filter((child) => child !== undefined && child !== null && typeof child !== 'boolean');
9
10
  },
10
11
  };
11
- export const isValidElement = (value: unknown) => {
12
- return Boolean(value) && typeof value === 'object' && 'type' in (value as object);
12
+
13
+ export const isValidElement = (value: unknown): value is ReactElementNode => {
14
+ return value !== null && typeof value === 'object' && 'type' in value && 'props' in value;
13
15
  };
14
16
 
15
17
  // TODO(@jakex7): Make this a proper React reconciler in the future. For now, we just need a stub to allow the widget bundle to compile with @expo/ui/jetpack-compose.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "id": "io.github.jakex7.peek.glance-fork",
11
11
  "group": "io.github.jakex7.peek",
12
- "version": "0.2.0"
12
+ "version": "0.3.0"
13
13
  }
14
14
  ]
15
15
  }
@@ -0,0 +1,31 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+
4
+ private final class FallbackIdentity {}
5
+
6
+ struct WidgetsChildView<Content: View>: ExpoSwiftUI.StringIdentityChild {
7
+ let childView: Content
8
+ let stringIdentity: String?
9
+ private let fallbackIdentity: FallbackIdentity?
10
+
11
+ init(childView: Content, stringIdentity: String?) {
12
+ self.childView = childView
13
+ self.stringIdentity = stringIdentity
14
+ fallbackIdentity = stringIdentity == nil ? FallbackIdentity() : nil
15
+ }
16
+
17
+ var id: ObjectIdentifier {
18
+ guard let fallbackIdentity else {
19
+ preconditionFailure("Keyed widget children must use childIdentity")
20
+ }
21
+ return ObjectIdentifier(fallbackIdentity)
22
+ }
23
+
24
+ var uiView: UIView? {
25
+ nil
26
+ }
27
+
28
+ var body: some View {
29
+ childView
30
+ }
31
+ }
@@ -1,183 +1,26 @@
1
1
  import SwiftUI
2
2
  import ExpoModulesCore
3
- import ExpoUI
4
3
 
5
- // TODO(@jakex7): Hack to satisfy ExpoSwiftUI.AnyChild with random UUID value
6
- class NodeIdentityWrapper {
7
- let id: UUID
8
- init(id: UUID) {
9
- self.id = id
10
- }
11
- }
12
- extension ObjectIdentifier: @retroactive Encodable {
13
- public func encode(to encoder: Encoder) throws {
14
- var container = encoder.singleValueContainer()
15
- try container.encode(String(describing: self))
16
- }
17
- }
18
-
19
- public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild {
20
- let node: [String: Any]
21
- let name: String
22
- let kind: WidgetsKind
23
- let entryIndex: Int?
24
- let environmentString: String?
25
-
26
- let uuid = NodeIdentityWrapper(id: UUID())
27
- public var id: ObjectIdentifier {
28
- ObjectIdentifier(uuid)
29
- }
4
+ public struct WidgetsDynamicView: View {
5
+ private let child: any ExpoSwiftUI.AnyChild
30
6
 
31
7
  public init(name: String, kind: WidgetsKind, node: [String: Any]) {
32
- self.name = name
33
- self.kind = kind
34
- self.node = node
35
- self.entryIndex = nil
36
- self.environmentString = nil
8
+ self.init(name: name, kind: kind, node: node, entryIndex: nil, environmentString: nil)
37
9
  }
38
10
 
39
11
  public init(name: String, kind: WidgetsKind, node: [String: Any], entryIndex: Int?, environmentString: String?) {
40
- self.name = name
41
- self.kind = kind
42
- self.node = node
43
- self.entryIndex = entryIndex
44
- self.environmentString = environmentString
12
+ child = WidgetsViewRenderer(
13
+ name: name,
14
+ kind: kind,
15
+ node: node,
16
+ entryIndex: entryIndex,
17
+ environmentString: environmentString
18
+ ).render()
45
19
  }
46
20
 
47
- @ViewBuilder
48
21
  public var body: some View {
49
- switch node["type"] as? String {
50
- case "TextView":
51
- // TextView applies common modifiers internally so concatenated text keeps
52
- // its SwiftUI.Text representation. Avoid applying those modifiers again.
53
- render(TextView.self, TextViewProps.self, updateProps: updateChildren, wrapInUIBaseView: false)
54
- case "HStackView":
55
- render(HStackView.self, HStackViewProps.self, updateProps: updateChildren)
56
- case "VStackView":
57
- render(VStackView.self, VStackViewProps.self, updateProps: updateChildren)
58
- case "ZStackView":
59
- render(ZStackView.self, ZStackViewProps.self, updateProps: updateChildren)
60
- case "RectangleView":
61
- render(RectangleView.self, RectangleViewProps.self)
62
- case "RoundedRectangleView":
63
- render(RoundedRectangleView.self, RoundedRectangleViewProps.self)
64
- case "CapsuleView":
65
- render(CapsuleView.self, CapsuleViewProps.self)
66
- case "CircleView":
67
- render(CircleView.self, CircleViewProps.self)
68
- case "ImageView":
69
- render(ImageView.self, ImageViewProps.self)
70
- case "AccessoryWidgetBackgroundView":
71
- render(AccessoryWidgetBackgroundView.self, AccessoryWidgetBackgroundProps.self)
72
- case "DividerView":
73
- render(DividerView.self, DividerProps.self)
74
- case "EllipseView":
75
- render(EllipseView.self, EllipseViewProps.self)
76
- case "LabelView":
77
- render(LabelView.self, LabelViewProps.self)
78
- case "ProgressView":
79
- render(ProgressView.self, ProgressViewProps.self)
80
- case "SpacerView":
81
- render(SpacerView.self, SpacerViewProps.self)
82
- case "UnevenRoundedRectangleView":
83
- render(UnevenRoundedRectangleView.self, UnevenRoundedRectangleViewProps.self)
84
- case "GaugeView":
85
- render(GaugeView.self, GaugeProps.self)
86
- case "ChartView":
87
- render(ChartView.self, ChartProps.self)
88
- case "Button":
89
- if #available(iOS 17.0, *) {
90
- switch kind {
91
- case .widget:
92
- render(WidgetButtonView.self, ButtonProps.self) { buttonProps in
93
- try updateChildren(buttonProps)
94
- buttonProps.source = name
95
- buttonProps.entryIndex = entryIndex
96
- buttonProps.environmentString = environmentString
97
- }
98
- case .liveActivity:
99
- render(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in
100
- try updateChildren(buttonProps)
101
- buttonProps.source = name
102
- }
103
- }
104
- } else {
105
- render(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren)
106
- }
107
- case "react.fragment":
108
- render(FragmentView.self, FragmentProps.self, updateProps: updateChildren)
109
- case "LinkView":
110
- render(LinkView.self, LinkViewProps.self, updateProps: updateChildren)
111
- #if DEBUG
112
- case "RedBoxView":
113
- render(RedBoxView.self, RedBoxViewProps.self) { redBoxProps in
114
- redBoxProps.source = name
115
- redBoxProps.kind = kind
116
- }
117
- default:
118
- ZStack {
119
- Color.red.opacity(0.5)
120
- Text("Unable to get the view for: \(node["type"] as? String ?? "undefined")")
121
- }
122
- #else
123
- default:
124
- EmptyView()
125
- #endif
126
- }
127
- }
128
-
129
- // MARK: - Render Method
130
-
131
- @ViewBuilder
132
- private func render<P, V>(
133
- _ viewType: V.Type,
134
- _ propsType: P.Type,
135
- updateProps: ((_ initialProps: P) throws -> Void)? = nil,
136
- wrapInUIBaseView: Bool = true
137
- ) -> some View
138
- where P: UIBaseViewProps, V: ExpoSwiftUI.View, V.Props == P {
139
- // immediately invoked closure {}() here because we can't use 'do-catch' inside @ViewBuilder
140
- {
141
- do {
142
- if let rawProps = node["props"] as? [String: Any] {
143
- let props = try propsType.init(rawProps: rawProps, context: WidgetsContext.shared.context)
144
- try updateProps?(props)
145
- // TODO(@jakex7): Prevent unwanted transition when view is updated with new props - we want to have the same view instance recreated with new props instead of creating a new view instance and transitioning to it
146
- if wrapInUIBaseView {
147
- return AnyView(UIBaseView<P, V>(props: props).transition(.identity))
148
- }
149
- return AnyView(V(props: props).transition(.identity))
150
- }
151
- return AnyView(EmptyView())
152
- } catch {
153
- return AnyView(EmptyView())
154
- }
155
- }()
156
- }
157
-
158
- // MARK: - Function that sets children as DynamicView
159
-
160
- private func updateChildren<P>(_ initialProps: P) throws
161
- where P: UIBaseViewProps {
162
- if let props = node["props"] as? [String: Any] {
163
- if let children = props["children"] as? [Any] {
164
- let validChildren = flattenChildNodes(children)
165
- initialProps.children = validChildren.map { WidgetsDynamicView(name: name, kind: kind, node: $0, entryIndex: entryIndex, environmentString: environmentString) }
166
- } else if let child = props["children"] as? [String: Any] {
167
- initialProps.children = [WidgetsDynamicView(name: name, kind: kind, node: child, entryIndex: entryIndex, environmentString: environmentString)]
168
- }
169
- }
170
- }
171
-
172
- private func flattenChildNodes(_ children: [Any]) -> [[String: Any]] {
173
- return children.flatMap { child -> [[String: Any]] in
174
- if let node = child as? [String: Any] {
175
- return [node]
176
- }
177
- if let nested = child as? [Any] {
178
- return flattenChildNodes(nested)
179
- }
180
- return []
181
- }
22
+ // Roots are not inside Children()'s ForEach, so consume their identity here as well.
23
+ let view: any View = child.childView
24
+ AnyView(view).id(child.childIdentity)
182
25
  }
183
26
  }
@@ -0,0 +1,167 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+ import ExpoUI
4
+
5
+ @MainActor
6
+ struct WidgetsViewRenderer {
7
+ let name: String
8
+ let kind: WidgetsKind
9
+ let node: [String: Any]
10
+ let entryIndex: Int?
11
+ let environmentString: String?
12
+
13
+ func render() -> any ExpoSwiftUI.AnyChild {
14
+ switch node["type"] as? String {
15
+ case "TextView":
16
+ // TextView applies common modifiers internally so concatenated text keeps
17
+ // its SwiftUI.Text representation. Avoid applying those modifiers again.
18
+ return render(TextView.self, TextViewProps.self, updateProps: updateChildren)
19
+ case "SlotView":
20
+ return render(SlotView.self, SlotViewProps.self, updateProps: updateChildren)
21
+ case "HStackView":
22
+ return renderWithModifiers(HStackView.self, HStackViewProps.self, updateProps: updateChildren)
23
+ case "VStackView":
24
+ return renderWithModifiers(VStackView.self, VStackViewProps.self, updateProps: updateChildren)
25
+ case "ZStackView":
26
+ return renderWithModifiers(ZStackView.self, ZStackViewProps.self, updateProps: updateChildren)
27
+ case "OverlayView":
28
+ return renderWithModifiers(OverlayView.self, OverlayViewProps.self, updateProps: updateChildren)
29
+ case "BackgroundView":
30
+ return renderWithModifiers(BackgroundView.self, BackgroundViewProps.self, updateProps: updateChildren)
31
+ case "MaskView":
32
+ return renderWithModifiers(MaskView.self, MaskViewProps.self, updateProps: updateChildren)
33
+ case "RectangleView":
34
+ return renderWithModifiers(RectangleView.self, RectangleViewProps.self)
35
+ case "RoundedRectangleView":
36
+ return renderWithModifiers(RoundedRectangleView.self, RoundedRectangleViewProps.self)
37
+ case "CapsuleView":
38
+ return renderWithModifiers(CapsuleView.self, CapsuleViewProps.self)
39
+ case "CircleView":
40
+ return renderWithModifiers(CircleView.self, CircleViewProps.self)
41
+ case "ImageView":
42
+ return renderWithModifiers(ImageView.self, ImageViewProps.self)
43
+ case "AccessoryWidgetBackgroundView":
44
+ return renderWithModifiers(AccessoryWidgetBackgroundView.self, AccessoryWidgetBackgroundProps.self)
45
+ case "DividerView":
46
+ return renderWithModifiers(DividerView.self, DividerProps.self)
47
+ case "EllipseView":
48
+ return renderWithModifiers(EllipseView.self, EllipseViewProps.self)
49
+ case "LabelView":
50
+ return renderWithModifiers(LabelView.self, LabelViewProps.self)
51
+ case "ProgressView":
52
+ return renderWithModifiers(ProgressView.self, ProgressViewProps.self)
53
+ case "SpacerView":
54
+ return renderWithModifiers(SpacerView.self, SpacerViewProps.self)
55
+ case "UnevenRoundedRectangleView":
56
+ return renderWithModifiers(UnevenRoundedRectangleView.self, UnevenRoundedRectangleViewProps.self)
57
+ case "GaugeView":
58
+ return renderWithModifiers(GaugeView.self, GaugeProps.self)
59
+ case "ChartView":
60
+ return renderWithModifiers(ChartView.self, ChartProps.self)
61
+ case "Button":
62
+ if #available(iOS 17.0, *) {
63
+ switch kind {
64
+ case .widget:
65
+ return renderWithModifiers(WidgetButtonView.self, ButtonProps.self) { buttonProps in
66
+ try updateChildren(buttonProps)
67
+ buttonProps.source = name
68
+ buttonProps.entryIndex = entryIndex
69
+ buttonProps.environmentString = environmentString
70
+ }
71
+ case .liveActivity:
72
+ return renderWithModifiers(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in
73
+ try updateChildren(buttonProps)
74
+ buttonProps.source = name
75
+ }
76
+ }
77
+ } else {
78
+ return renderWithModifiers(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren)
79
+ }
80
+ case "react.fragment":
81
+ return renderWithModifiers(FragmentView.self, FragmentProps.self, updateProps: updateChildren)
82
+ case "LinkView":
83
+ return renderWithModifiers(LinkView.self, LinkViewProps.self, updateProps: updateChildren)
84
+ #if DEBUG
85
+ case "RedBoxView":
86
+ return renderWithModifiers(RedBoxView.self, RedBoxViewProps.self) { redBoxProps in
87
+ redBoxProps.source = name
88
+ redBoxProps.kind = kind
89
+ }
90
+ default:
91
+ return makeChild(
92
+ ZStack {
93
+ Color.red.opacity(0.5)
94
+ Text("Unable to get the view for: \(node["type"] as? String ?? "undefined")")
95
+ }
96
+ )
97
+ #else
98
+ default:
99
+ return makeChild(EmptyView())
100
+ #endif
101
+ }
102
+ }
103
+
104
+ // MARK: - Render Method
105
+
106
+ private func renderWithModifiers<P, V>(
107
+ _ viewType: V.Type,
108
+ _ propsType: P.Type,
109
+ updateProps: ((_ initialProps: P) throws -> Void)? = nil
110
+ ) -> any ExpoSwiftUI.AnyChild
111
+ where P: UIBaseViewProps, V: ExpoSwiftUI.View, V.Props == P {
112
+ render(UIBaseView<P, V>.self, propsType, updateProps: updateProps)
113
+ }
114
+
115
+ private func render<P, V>(
116
+ _ viewType: V.Type,
117
+ _ propsType: P.Type,
118
+ updateProps: ((_ initialProps: P) throws -> Void)? = nil
119
+ ) -> any ExpoSwiftUI.AnyChild
120
+ where P: ExpoSwiftUI.ViewProps, V: ExpoSwiftUI.View, V.Props == P {
121
+ do {
122
+ guard let rawProps = node["props"] as? [String: Any] else {
123
+ return makeChild(EmptyView())
124
+ }
125
+ let props = try propsType.init(rawProps: rawProps, context: WidgetsContext.shared.context)
126
+ try updateProps?(props)
127
+ return makeChild(V(props: props))
128
+ } catch {
129
+ return makeChild(EmptyView())
130
+ }
131
+ }
132
+
133
+ // MARK: - Child Views
134
+
135
+ private func updateChildren<P>(_ initialProps: P) throws
136
+ where P: ExpoSwiftUI.ViewProps {
137
+ if let props = node["props"] as? [String: Any] {
138
+ if let children = props["children"] as? [Any] {
139
+ let validChildren = flattenChildNodes(children)
140
+ initialProps.children = validChildren.map {
141
+ WidgetsViewRenderer(name: name, kind: kind, node: $0, entryIndex: entryIndex, environmentString: environmentString).render()
142
+ }
143
+ } else if let child = props["children"] as? [String: Any] {
144
+ initialProps.children = [
145
+ WidgetsViewRenderer(name: name, kind: kind, node: child, entryIndex: entryIndex, environmentString: environmentString).render()
146
+ ]
147
+ }
148
+ }
149
+ }
150
+
151
+ private func makeChild<Content: View>(_ view: Content) -> any ExpoSwiftUI.AnyChild {
152
+ // Preserve the native view type for ExpoUI's slot and text inspection.
153
+ WidgetsChildView(childView: view, stringIdentity: node["__expoWidgetIdentity"] as? String)
154
+ }
155
+
156
+ private func flattenChildNodes(_ children: [Any]) -> [[String: Any]] {
157
+ return children.flatMap { child -> [[String: Any]] in
158
+ if let node = child as? [String: Any] {
159
+ return [node]
160
+ }
161
+ if let nested = child as? [Any] {
162
+ return flattenChildNodes(nested)
163
+ }
164
+ return []
165
+ }
166
+ }
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-widgets",
3
- "version": "58.0.0",
3
+ "version": "58.0.2",
4
4
  "description": "Widgets.",
5
5
  "keywords": [
6
6
  "expo",
@@ -28,16 +28,16 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@expo/plist": "^0.9.0",
31
- "@expo/ui": "~58.0.0"
31
+ "@expo/ui": "~58.0.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@expo/spawn-async": "^1.8.0",
35
35
  "@types/jest": "^29.2.1",
36
36
  "@types/node": "^22.14.0",
37
37
  "@types/react": "~19.2.0",
38
- "expo": "58.0.0-preview.0",
38
+ "expo": "58.0.0-preview.2",
39
39
  "expo-module-scripts": "56.0.3",
40
- "react-native": "0.87.1",
40
+ "react-native": "0.88.0-rc.0",
41
41
  "resolve-workspace-root": "^2.0.0"
42
42
  },
43
43
  "peerDependencies": {
@@ -45,7 +45,7 @@
45
45
  "react": "*",
46
46
  "react-native": "*"
47
47
  },
48
- "gitHead": "c543a7181fc32e14f42d89550087b80ac5c490f2",
48
+ "gitHead": "313e67f5c9dfb16f9deca74a271cd1d4f33a6f82",
49
49
  "scripts": {
50
50
  "build": "pnpm run build:lib && pnpm run build:plugin",
51
51
  "build:lib": "expo-build src",