expo-widgets 55.0.4 → 55.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 CHANGED
@@ -10,6 +10,24 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.6 — 2026-03-18
14
+
15
+ ### 🎉 New features
16
+
17
+ - Pass environment to AppIntent ([#43925](https://github.com/expo/expo/pull/43925) by [@jakex7](https://github.com/jakex7))
18
+ - Automatically add `target` for `Button`. ([#43977](https://github.com/expo/expo/pull/43977) by [@jakex7](https://github.com/jakex7))
19
+ - Add support for `Link` view from `@expo/ui`. ([#43985](https://github.com/expo/expo/pull/43985) by [@jakex7](https://github.com/jakex7))
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - Add support for `React.Fragment`. ([#43833](https://github.com/expo/expo/pull/43833) by [@jakex7](https://github.com/jakex7))
24
+ - Add Button children support. ([#43832](https://github.com/expo/expo/pull/43832) by [@jakex7](https://github.com/jakex7))
25
+ - Remove unused `Compression` related code. ([#43981](https://github.com/expo/expo/pull/43981) by [@jakex7](https://github.com/jakex7))
26
+
27
+ ## 55.0.5 — 2026-03-17
28
+
29
+ _This version does not introduce any user-facing changes._
30
+
13
31
  ## 55.0.4 — 2026-03-11
14
32
 
15
33
  ### 🛠 Breaking changes
@@ -0,0 +1,61 @@
1
+ import { ReactElementNode } from './jsx-runtime-stub';
2
+
3
+ export function decorateInteractiveTargets(node: unknown) {
4
+ return decorateNode(node, {
5
+ nearestParentKey: null,
6
+ nextTargetIndex: {
7
+ current: 0,
8
+ },
9
+ typesToDecorate: ['Button'],
10
+ });
11
+ }
12
+
13
+ function decorateNode(
14
+ node: unknown,
15
+ context: {
16
+ nearestParentKey: string | null;
17
+ nextTargetIndex: {
18
+ current: number;
19
+ };
20
+ typesToDecorate: string[];
21
+ }
22
+ ): unknown {
23
+ if (Array.isArray(node)) {
24
+ return node.map((child) => decorateNode(child, context));
25
+ }
26
+
27
+ if (!isReactElementNode(node)) {
28
+ return node;
29
+ }
30
+
31
+ const props = node.props ?? {};
32
+
33
+ if (props.target == null && context.typesToDecorate.includes(node.type as string)) {
34
+ props.target = buildButtonTargetId(context.nextTargetIndex.current, context.nearestParentKey);
35
+ context.nextTargetIndex.current += 1;
36
+ }
37
+
38
+ if ('children' in props) {
39
+ props.children = decorateNode(props.children, {
40
+ nearestParentKey: node.key ?? context.nearestParentKey,
41
+ nextTargetIndex: context.nextTargetIndex,
42
+ typesToDecorate: context.typesToDecorate,
43
+ });
44
+ }
45
+
46
+ return node;
47
+ }
48
+
49
+ function isReactElementNode(node: unknown): node is ReactElementNode {
50
+ return Boolean(node) && typeof node === 'object' && 'type' in node && 'props' in node;
51
+ }
52
+
53
+ function buildButtonTargetId(index: number, parentKey: string | null) {
54
+ const baseTarget = `__expo_widgets_target_${index}`;
55
+
56
+ if (!parentKey) {
57
+ return baseTarget;
58
+ }
59
+
60
+ return `${baseTarget}_${parentKey}`;
61
+ }
package/bundle/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as swiftUI from '@expo/ui/swift-ui';
4
4
  import * as modifiers from '@expo/ui/swift-ui/modifiers';
5
5
 
6
+ import { decorateInteractiveTargets } from './decorator';
6
7
  import * as jsxRuntime from './jsx-runtime-stub';
7
8
  import * as React from './react-stub';
8
9
 
@@ -12,6 +13,7 @@ declare global {
12
13
  var __expoWidgetLayout: (props: Dictionary, environment: Dictionary) => Dictionary;
13
14
  var __expoWidgetRender: (props: Dictionary, environment: Dictionary) => Dictionary;
14
15
  var __expoWidgetHandlePress: (
16
+ props: Dictionary,
15
17
  environment: Dictionary & { target?: string }
16
18
  ) => Dictionary | undefined;
17
19
  }
@@ -23,7 +25,7 @@ const __expoWidgetRender = function (props: Dictionary, environment: Dictionary)
23
25
  decoratedEnvironment.date = new Date(timestamp as number);
24
26
  }
25
27
 
26
- return globalThis.__expoWidgetLayout(props, decoratedEnvironment as any);
28
+ return decorateInteractiveTargets(globalThis.__expoWidgetLayout(props, decoratedEnvironment));
27
29
  };
28
30
 
29
31
  const __expoWidgetHandlePress = function (
@@ -42,12 +44,10 @@ const __expoWidgetHandlePress = function (
42
44
  return props.onButtonPress();
43
45
  }
44
46
 
45
- if (props?.children && Array.isArray(props.children)) {
46
- for (const child of props.children) {
47
- const result = findAndCallOnPress(child as Dictionary);
48
- if (result) {
49
- return result;
50
- }
47
+ for (const child of React.Children.toArray(props?.children)) {
48
+ const result = findAndCallOnPress(child as Dictionary);
49
+ if (result) {
50
+ return result;
51
51
  }
52
52
  }
53
53
  }
@@ -61,6 +61,7 @@ Object.assign(globalThis, {
61
61
  ...modifiers,
62
62
  ...jsxRuntime,
63
63
  ...React,
64
+ React,
64
65
  __expoWidgetRender,
65
66
  __expoWidgetHandlePress,
66
67
  });
@@ -1,7 +1,16 @@
1
- export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.transitional.element');
2
- export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
1
+ import { Fragment } from './react-stub';
3
2
 
4
- function ReactElement(type: typeof ReactElement, key: string | null, props: Record<string, any>) {
3
+ export type ReactElementNode = {
4
+ type: unknown;
5
+ key: string | null;
6
+ props: Record<string, any>;
7
+ };
8
+
9
+ function ReactElement(
10
+ type: unknown,
11
+ key: string | null,
12
+ props: Record<string, any>
13
+ ): ReactElementNode {
5
14
  if (typeof type === 'function') {
6
15
  return (type as any)(props);
7
16
  }
@@ -15,7 +24,11 @@ function ReactElement(type: typeof ReactElement, key: string | null, props: Reco
15
24
  return element;
16
25
  }
17
26
 
18
- function jsxProd(type: typeof ReactElement, config: any, maybeKey?: string | number | bigint) {
27
+ function jsxProd(
28
+ type: unknown,
29
+ config: any,
30
+ maybeKey?: string | number | bigint
31
+ ): ReactElementNode {
19
32
  let key = null;
20
33
  if (maybeKey !== undefined) {
21
34
  key = '' + maybeKey;
@@ -45,7 +58,8 @@ function hasValidKey(config: any) {
45
58
 
46
59
  const jsxFileName = 'widget';
47
60
  export {
48
- REACT_FRAGMENT_TYPE as Fragment,
61
+ Fragment,
62
+ Fragment as _Fragment,
49
63
  jsxFileName as _jsxFileName,
50
64
  jsxProd,
51
65
  jsxProd as jsx,
@@ -1,3 +1,5 @@
1
+ export const Fragment = 'react.fragment';
2
+
1
3
  export const Children = {
2
4
  toArray(children: unknown) {
3
5
  if (children === undefined || children === null) {
@@ -15,11 +15,15 @@ struct WidgetUserInteraction: AppIntent {
15
15
  @Parameter(title: "entryIndex")
16
16
  var entryIndex: Int?
17
17
 
18
+ @Parameter(title: "environmentString")
19
+ var environmentString: String?
20
+
18
21
  init() {}
19
- init(source: String?, target: String?, entryIndex: Int?) {
22
+ init(source: String?, target: String?, entryIndex: Int?, environmentString: String?) {
20
23
  self.source = source
21
24
  self.target = target
22
25
  self.entryIndex = entryIndex
26
+ self.environmentString = environmentString
23
27
  }
24
28
 
25
29
  func perform() async throws -> some IntentResult {
@@ -34,15 +38,15 @@ struct WidgetUserInteraction: AppIntent {
34
38
  let entryIndex,
35
39
  let entry = timeline[entryIndex] as? [String: Any],
36
40
  let props = entry["props"] as? [String: Any],
37
- let context = createWidgetContext(layout: layout) else {
41
+ let context = createWidgetContext(layout: layout),
42
+ let environmentData = environmentString?.data(using: .utf8),
43
+ var environment = try? JSONSerialization.jsonObject(with: environmentData) as? [String: Any] else {
38
44
  return .result()
39
45
  }
40
- let pressEnvironment: [String: Any] = [
41
- "timestamp": Int(Date.now.timeIntervalSince1970 * 1000),
42
- "target": target as Any
43
- ]
46
+ environment["target"] = target
47
+
44
48
  let result = context.objectForKeyedSubscript("__expoWidgetHandlePress")?.call(
45
- withArguments: [props, pressEnvironment]
49
+ withArguments: [props, environment]
46
50
  )
47
51
  if let newProps = result?.toObject() as? [String: Any] {
48
52
  var newEntry = entry
@@ -6,6 +6,7 @@ final class ButtonProps: ExpoUI.ButtonProps {
6
6
  @Field var source: String?
7
7
  @Field var target: String?
8
8
  @Field var entryIndex: Int?
9
+ @Field var environmentString: String?
9
10
  }
10
11
 
11
12
  @available(iOS 17.0, *)
@@ -18,7 +19,8 @@ struct WidgetButtonView: ExpoSwiftUI.View {
18
19
  intent: WidgetUserInteraction(
19
20
  source: props.source,
20
21
  target: props.target,
21
- entryIndex: props.entryIndex
22
+ entryIndex: props.entryIndex,
23
+ environmentString: props.environmentString
22
24
  )
23
25
  ) {
24
26
  if let label = props.label {
@@ -18,27 +18,30 @@ extension ObjectIdentifier: @retroactive Encodable {
18
18
 
19
19
  public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild {
20
20
  let node: [String: Any]
21
- let source: String
21
+ let name: String
22
22
  let kind: WidgetsKind
23
23
  let entryIndex: Int?
24
+ let environmentString: String?
24
25
 
25
26
  let uuid = NodeIdentityWrapper(id: UUID())
26
27
  public var id: ObjectIdentifier {
27
28
  ObjectIdentifier(uuid)
28
29
  }
29
30
 
30
- public init(source: String, kind: WidgetsKind, node: [String: Any]) {
31
- self.source = source
31
+ public init(name: String, kind: WidgetsKind, node: [String: Any]) {
32
+ self.name = name
32
33
  self.kind = kind
33
34
  self.node = node
34
35
  self.entryIndex = nil
36
+ self.environmentString = nil
35
37
  }
36
38
 
37
- public init(source: String, kind: WidgetsKind, node: [String: Any], entryIndex: Int?) {
38
- self.source = source
39
+ public init(name: String, kind: WidgetsKind, node: [String: Any], entryIndex: Int?, environmentString: String?) {
40
+ self.name = name
39
41
  self.kind = kind
40
42
  self.node = node
41
43
  self.entryIndex = entryIndex
44
+ self.environmentString = environmentString
42
45
  }
43
46
 
44
47
  @ViewBuilder
@@ -81,18 +84,24 @@ public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild {
81
84
  switch kind {
82
85
  case .widget:
83
86
  render(WidgetButtonView.self, ButtonProps.self) { buttonProps in
84
- buttonProps.source = source
87
+ try updateChildren(buttonProps)
88
+ buttonProps.source = name
85
89
  buttonProps.entryIndex = entryIndex
90
+ buttonProps.environmentString = environmentString
86
91
  }
87
92
  case .liveActivity:
88
93
  render(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in
89
- buttonProps.source = source
94
+ try updateChildren(buttonProps)
95
+ buttonProps.source = name
90
96
  }
91
97
  }
92
98
  } else {
93
99
  render(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren)
94
100
  }
95
-
101
+ case "react.fragment":
102
+ render(FragmentView.self, FragmentProps.self, updateProps: updateChildren)
103
+ case "LinkView":
104
+ render(LinkView.self, LinkViewProps.self, updateProps: updateChildren)
96
105
  default:
97
106
  ZStack {
98
107
  Color.red.opacity(0.5)
@@ -129,9 +138,9 @@ public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild {
129
138
  if let props = node["props"] as? [String: Any] {
130
139
  if let children = props["children"] as? [Any] {
131
140
  let validChildren = children.compactMap { $0 as? [String: Any] }
132
- initialProps.children = validChildren.map { WidgetsDynamicView(source: source, kind: kind, node: $0, entryIndex: entryIndex) }
141
+ initialProps.children = validChildren.map { WidgetsDynamicView(name: name, kind: kind, node: $0, entryIndex: entryIndex, environmentString: environmentString) }
133
142
  } else if let child = props["children"] as? [String: Any] {
134
- initialProps.children = [WidgetsDynamicView(source: source, kind: kind, node: child, entryIndex: entryIndex)]
143
+ initialProps.children = [WidgetsDynamicView(name: name, kind: kind, node: child, entryIndex: entryIndex, environmentString: environmentString)]
135
144
  }
136
145
  }
137
146
  }
@@ -16,16 +16,24 @@ public struct WidgetsEntryView: View {
16
16
  return env
17
17
  }
18
18
 
19
+ private var widgetEnvironmentString: String? {
20
+ guard let data = try? JSONSerialization.data(withJSONObject: widgetEnvironment),
21
+ let jsonString = String(data: data, encoding: .utf8) else {
22
+ return nil
23
+ }
24
+ return jsonString
25
+ }
26
+
19
27
  public var body: some View {
20
- let layout = WidgetsStorage.getString(forKey: "__expo_widgets_\(entry.source)_layout") ?? ""
28
+ let layout = WidgetsStorage.getString(forKey: "__expo_widgets_\(entry.name)_layout") ?? ""
21
29
  let node = evaluateLayout(layout: layout, props: entry.props ?? [:], environment: widgetEnvironment)
22
30
 
23
31
  if let node {
24
32
  if #available(iOS 17.0, *) {
25
- WidgetsDynamicView(source: entry.source, kind: .widget, node: node, entryIndex: entry.entryIndex)
33
+ WidgetsDynamicView(name: entry.name, kind: .widget, node: node, entryIndex: entry.entryIndex, environmentString: widgetEnvironmentString)
26
34
  .containerBackground(.clear, for: .widget)
27
35
  } else {
28
- WidgetsDynamicView(source: entry.source, kind: .widget, node: node, entryIndex: entry.entryIndex)
36
+ WidgetsDynamicView(name: entry.name, kind: .widget, node: node, entryIndex: entry.entryIndex, environmentString: widgetEnvironmentString)
29
37
  }
30
38
  } else {
31
39
  EmptyView()
@@ -0,0 +1,13 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+ import ExpoUI
4
+
5
+ final class FragmentProps: ExpoUI.ButtonProps {}
6
+
7
+ struct FragmentView: ExpoSwiftUI.View {
8
+ @ObservedObject var props: FragmentProps
9
+
10
+ var body: some View {
11
+ Children()
12
+ }
13
+ }
@@ -9,9 +9,9 @@ struct LiveActivityBanner: View {
9
9
 
10
10
  var body: some View {
11
11
  if activityFamily == .small, let node = nodes?["bannerSmall"] as? [String: Any] {
12
- WidgetsDynamicView(source: context.activityID, kind: .liveActivity, node: node)
12
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
13
13
  } else if let node = nodes?["banner"] as? [String: Any] {
14
- WidgetsDynamicView(source: context.activityID, kind: .liveActivity, node: node)
14
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
15
15
  } else {
16
16
  EmptyView()
17
17
  }
@@ -2,7 +2,7 @@ import WidgetKit
2
2
 
3
3
  public struct WidgetsTimelineEntry: WidgetKit.TimelineEntry {
4
4
  public let date: Date
5
- public let source: String
5
+ public let name: String
6
6
  public let props: [String: Any]?
7
7
  public let entryIndex: Int?
8
8
  }
@@ -2,7 +2,7 @@ import WidgetKit
2
2
 
3
3
  public struct WidgetsTimelineProvider: TimelineProvider {
4
4
  public func placeholder(in context: Context) -> WidgetsTimelineEntry {
5
- WidgetsTimelineEntry(date: Date(), source: name, props: nil, entryIndex: nil)
5
+ WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil)
6
6
  }
7
7
 
8
8
  public func getSnapshot(
@@ -11,12 +11,12 @@ public struct WidgetsTimelineProvider: TimelineProvider {
11
11
  let groupIdentifier =
12
12
  Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String
13
13
  guard let groupIdentifier else {
14
- completion(WidgetsTimelineEntry(date: Date(), source: name, props: nil, entryIndex: nil))
14
+ completion(WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil))
15
15
  return
16
16
  }
17
17
 
18
18
  let entries = parseTimeline(identifier: groupIdentifier, name: name, family: context.family)
19
- completion(entries.first ?? WidgetsTimelineEntry(date: Date(), source: name, props: nil, entryIndex: nil))
19
+ completion(entries.first ?? WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil))
20
20
  }
21
21
 
22
22
  public func getTimeline(
@@ -9,7 +9,7 @@ func parseTimeline(identifier: String, name: String, family: WidgetFamily) -> [W
9
9
  if let entry = entry as? [String: Any], let timestamp = entry["timestamp"] as? Int, let props = entry["props"] as? [String: Any] {
10
10
  return WidgetsTimelineEntry(
11
11
  date: Date(timeIntervalSince1970: Double(timestamp) / 1000),
12
- source: name,
12
+ name: name,
13
13
  props: props,
14
14
  entryIndex: index
15
15
  )
@@ -65,7 +65,7 @@ private struct LiveActivitySectionView: View {
65
65
  environment: environment
66
66
  )
67
67
  if let node = nodes[sectionName] as? [String: Any] {
68
- WidgetsDynamicView(source: context.activityID, kind: .liveActivity, node: node)
68
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
69
69
  } else {
70
70
  EmptyView()
71
71
  }
@@ -86,7 +86,7 @@ private struct LiveActivityBannerView: View {
86
86
  if #available(iOS 18.0, *) {
87
87
  LiveActivityBanner(context: context, nodes: nodes)
88
88
  } else if let node = nodes["banner"] as? [String: Any] {
89
- WidgetsDynamicView(source: context.activityID, kind: .liveActivity, node: node)
89
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
90
90
  } else {
91
91
  EmptyView()
92
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-widgets",
3
- "version": "55.0.4",
3
+ "version": "55.0.6",
4
4
  "description": "Widgets.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "homepage": "https://docs.expo.dev/versions/latest/sdk/widgets/",
34
34
  "dependencies": {
35
35
  "@expo/plist": "^0.5.2",
36
- "@expo/ui": "~55.0.2"
36
+ "@expo/ui": "~55.0.4"
37
37
  },
38
38
  "devDependencies": {
39
39
  "expo-module-scripts": "^55.0.2"
@@ -42,5 +42,5 @@
42
42
  "expo": "*",
43
43
  "react": "*"
44
44
  },
45
- "gitHead": "bcdd2c239f8a92cdf5140e35cde768352630acd6"
45
+ "gitHead": "31afdbe5613d666148711cb3ec58c4b617a00c14"
46
46
  }
@@ -1,32 +0,0 @@
1
- import Foundation
2
- import Compression
3
-
4
- extension Data {
5
- func brotliCompressed() throws -> Data {
6
- var output = Data()
7
-
8
- let filter = try OutputFilter(.compress, using: .brotli, bufferCapacity: 65_536) { chunk in
9
- if let chunk = chunk {
10
- output.append(chunk)
11
- }
12
- }
13
-
14
- try filter.write(self)
15
- try filter.finalize()
16
- return output
17
- }
18
-
19
- func brotliDecompressed() throws -> Data {
20
- var output = Data()
21
-
22
- let filter = try OutputFilter(.decompress, using: .brotli, bufferCapacity: 65_536) { chunk in
23
- if let chunk = chunk {
24
- output.append(chunk)
25
- }
26
- }
27
-
28
- try filter.write(self)
29
- try filter.finalize()
30
- return output
31
- }
32
- }