expo-widgets 56.0.8 → 56.0.9

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,21 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.9 — 2026-05-15
14
+
15
+ ### 🎉 New features
16
+
17
+ - Use shared JS runtime. ([#45781](https://github.com/expo/expo/pull/45781) by [@jakex7](https://github.com/jakex7))
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - Fix Live Activity multiple evaluations. ([#45675](https://github.com/expo/expo/pull/45675) by [@nkopylov](https://github.com/nkopylov))
22
+ - Fix module precompile. ([#45715](https://github.com/expo/expo/pull/45715) by [@jakex7](https://github.com/jakex7))
23
+
24
+ ### 💡 Others
25
+
26
+ - Migrated to the single-payload `SharedObject.emit` API. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))
27
+
13
28
  ## 56.0.8 — 2026-05-13
14
29
 
15
30
  ### 🎉 New features
@@ -66,7 +66,7 @@ final class LiveActivity: SharedObject {
66
66
  pushTokenObserverTask = Task {
67
67
  for await data in activity.pushTokenUpdates {
68
68
  let token = data.reduce("") { $0 + String(format: "%02x", $1) }
69
- emit(event: onTokenReceived, arguments: [
69
+ emit(event: onTokenReceived, payload: [
70
70
  "activityId": activity.id,
71
71
  "pushToken": token
72
72
  ])
@@ -58,19 +58,25 @@ struct WidgetUserInteraction: AppIntent {
58
58
 
59
59
  guard let timeline,
60
60
  let entryIndex,
61
+ timeline.indices.contains(entryIndex),
61
62
  let entry = timeline[entryIndex] as? [String: Any],
62
63
  let props = entry["props"] as? [String: Any],
63
- let context = createWidgetContext(layout: layout),
64
64
  let environmentData = environmentString?.data(using: .utf8),
65
65
  var environment = try? JSONSerialization.jsonObject(with: environmentData) as? [String: Any] else {
66
66
  return .result()
67
67
  }
68
68
  environment["target"] = target
69
69
 
70
- let result = context.objectForKeyedSubscript("__expoWidgetHandlePress")?.call(
71
- withArguments: [props, environment]
72
- )
73
- if let newProps = result?.toObject() as? [String: Any] {
70
+ let newProps: [String: Any]?
71
+ switch evaluateWidgetButtonPress(layout: layout, props: props, environment: environment) {
72
+ case .success(let result):
73
+ newProps = result
74
+ case .failure(let error):
75
+ print("[ExpoWidgets] Button press evaluation failed: \(error.message)")
76
+ newProps = nil
77
+ }
78
+
79
+ if let newProps {
74
80
  var newEntry = entry
75
81
  if let originalProps = entry["props"] as? [String: Any] {
76
82
  newEntry["props"] = originalProps.merging(newProps) { _, new in new }
@@ -33,41 +33,27 @@ public func evaluateLayout(
33
33
  props: [String: Any],
34
34
  environment: [String: Any]
35
35
  ) -> [String: Any] {
36
- guard let context = createWidgetContext(layout: layout) else {
37
- return createRedBox(message: "Could not create context for layout evaluation.")
36
+ switch evaluateWidgetLayout(layout: layout, props: props, environment: environment) {
37
+ case .success(let result):
38
+ return result
39
+ case .failure(let error):
40
+ print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
41
+ return createRedBox(message: error.message)
38
42
  }
39
-
40
- let result = context.objectForKeyedSubscript("__expoWidgetRender")?.call(
41
- withArguments: [props, environment]
42
- )
43
- if let exception = context.exception {
44
- print("[ExpoWidgets] Layout evaluation failed: \(exception)")
45
- return createRedBox(message: exception.toString())
46
- }
47
- return result?.toObject() as? [String: Any] ?? createRedBox(message: "Expo widget render did not produce any results.")
48
43
  }
49
44
 
50
45
  func getLiveActivityNodes(forName name: String, props: String = "{}", environment: [String: Any]) -> [String: Any] {
51
46
  let layout = WidgetsStorage.getString(forKey: "__expo_widgets_live_activity_\(name)_layout") ?? ""
52
47
  let propsData = props.data(using: .utf8)
53
48
  let propsDict = propsData.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } ?? [:]
54
- guard let context = createWidgetContext(layout: layout) else {
55
- return ["banner": createRedBox(message: "Could not create context for layout evaluation.")]
56
- }
57
49
 
58
- var widgetEnvironment = environment
59
- widgetEnvironment["timestamp"] = Int(Date.now.timeIntervalSince1970 * 1000)
60
-
61
- let result = context.objectForKeyedSubscript("__expoWidgetRender")?.call(
62
- withArguments: [propsDict, environment]
63
- )
64
-
65
- if let exception = context.exception {
66
- print("[ExpoWidgets] Layout evaluation failed: \(exception)")
67
- return ["banner": createRedBox(message: exception.toString())]
50
+ switch evaluateWidgetLayout(layout: layout, props: propsDict, environment: environment) {
51
+ case .success(let result):
52
+ return result
53
+ case .failure(let error):
54
+ print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
55
+ return ["banner": createRedBox(message: error.message)]
68
56
  }
69
-
70
- return result?.toObject() as? [String: Any] ?? ["banner": createRedBox(message: "Expo widget render did not produce any results.")]
71
57
  }
72
58
 
73
59
  func getLiveActivityUrl(forName name: String) -> URL? {
@@ -24,27 +24,37 @@ public struct WidgetLiveActivity: Widget {
24
24
 
25
25
  public var body: some WidgetConfiguration {
26
26
  ActivityConfiguration(for: LiveActivityAttributes.self) { context in
27
- LiveActivityBannerView(context: context, environment: environment)
27
+ let nodes = getLiveActivityNodes(
28
+ forName: context.state.name,
29
+ props: context.state.props,
30
+ environment: environment
31
+ )
32
+ LiveActivityBannerView(context: context, nodes: nodes)
28
33
  } dynamicIsland: { context in
29
- DynamicIsland {
34
+ let nodes = getLiveActivityNodes(
35
+ forName: context.state.name,
36
+ props: context.state.props,
37
+ environment: environment
38
+ )
39
+ return DynamicIsland {
30
40
  DynamicIslandExpandedRegion(.center) {
31
- LiveActivitySectionView(context: context, environment: environment, sectionName: "expandedCenter")
41
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter")
32
42
  }
33
43
  DynamicIslandExpandedRegion(.leading) {
34
- LiveActivitySectionView(context: context, environment: environment, sectionName: "expandedLeading")
44
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading")
35
45
  }
36
46
  DynamicIslandExpandedRegion(.trailing) {
37
- LiveActivitySectionView(context: context, environment: environment, sectionName: "expandedTrailing")
47
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing")
38
48
  }
39
49
  DynamicIslandExpandedRegion(.bottom) {
40
- LiveActivitySectionView(context: context, environment: environment, sectionName: "expandedBottom")
50
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom")
41
51
  }
42
52
  } compactLeading: {
43
- LiveActivitySectionView(context: context, environment: environment, sectionName: "compactLeading")
53
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading")
44
54
  } compactTrailing: {
45
- LiveActivitySectionView(context: context, environment: environment, sectionName: "compactTrailing")
55
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing")
46
56
  } minimal: {
47
- LiveActivitySectionView(context: context, environment: environment, sectionName: "minimal")
57
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal")
48
58
  }
49
59
  .widgetURL(getLiveActivityUrl(forName: context.state.name))
50
60
  }
@@ -55,15 +65,10 @@ public struct WidgetLiveActivity: Widget {
55
65
  @available(iOS 16.1, *)
56
66
  private struct LiveActivitySectionView: View {
57
67
  let context: ActivityViewContext<LiveActivityAttributes>
58
- let environment: [String: Any]
68
+ let nodes: [String: Any]
59
69
  let sectionName: String
60
70
 
61
71
  var body: some View {
62
- let nodes = getLiveActivityNodes(
63
- forName: context.state.name,
64
- props: context.state.props,
65
- environment: environment
66
- )
67
72
  if let node = nodes[sectionName] as? [String: Any] {
68
73
  WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
69
74
  } else {
@@ -75,14 +80,9 @@ private struct LiveActivitySectionView: View {
75
80
  @available(iOS 16.1, *)
76
81
  private struct LiveActivityBannerView: View {
77
82
  var context: ActivityViewContext<LiveActivityAttributes>
78
- let environment: [String: Any]
83
+ let nodes: [String: Any]
79
84
 
80
85
  var body: some View {
81
- let nodes = getLiveActivityNodes(
82
- forName: context.state.name,
83
- props: context.state.props,
84
- environment: environment
85
- )
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] {
@@ -0,0 +1,142 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ struct WidgetJavaScriptError: Error {
5
+ let message: String
6
+ }
7
+
8
+ typealias WidgetJavaScriptResult<Value> = Result<Value, WidgetJavaScriptError>
9
+
10
+ private final class WidgetsJSRuntime {
11
+ static let shared = WidgetsJSRuntime()
12
+
13
+ private let lock = NSRecursiveLock()
14
+ private var context: JSContext?
15
+ private var bundleScript: String?
16
+ private var layoutCache: [String: JSValue] = [:]
17
+
18
+ private init() {}
19
+
20
+ func render(layout: String, props: [String: Any], environment: [String: Any]) -> WidgetJavaScriptResult<[String: Any]> {
21
+ call(layout: layout, functionName: "__expoWidgetRender", arguments: [props, environment]).flatMap { result in
22
+ guard let renderedNode = result?.toObject() as? [String: Any] else {
23
+ return .failure(WidgetJavaScriptError(message: "Expo widget render did not produce any results."))
24
+ }
25
+ return .success(renderedNode)
26
+ }
27
+ }
28
+
29
+ func handlePress(layout: String, props: [String: Any], environment: [String: Any]) -> WidgetJavaScriptResult<[String: Any]?> {
30
+ call(layout: layout, functionName: "__expoWidgetHandlePress", arguments: [props, environment])
31
+ .map { $0?.toObject() as? [String: Any] }
32
+ }
33
+
34
+ private func call(layout: String, functionName: String, arguments: [Any]) -> WidgetJavaScriptResult<JSValue?> {
35
+ lock.lock()
36
+ defer { lock.unlock() }
37
+
38
+ guard let context = getContext() else {
39
+ return .failure(WidgetJavaScriptError(message: "Could not create context for layout evaluation."))
40
+ }
41
+ guard let layoutValue = getLayoutValue(layout, in: context) else {
42
+ return .failure(WidgetJavaScriptError(message: contextExceptionMessage(context) ?? "Could not evaluate layout."))
43
+ }
44
+
45
+ context.exception = nil
46
+ context.setObject(layoutValue, forKeyedSubscript: "__expoWidgetLayout" as NSString)
47
+
48
+ let function = context.objectForKeyedSubscript(functionName)
49
+ guard let function, function.isObject else {
50
+ return .failure(WidgetJavaScriptError(message: "Expo widget runtime function \(functionName) is unavailable."))
51
+ }
52
+
53
+ let result = function.call(withArguments: arguments)
54
+ if let exceptionMessage = contextExceptionMessage(context) {
55
+ return .failure(WidgetJavaScriptError(message: exceptionMessage))
56
+ }
57
+ return .success(result)
58
+ }
59
+
60
+ private func getContext() -> JSContext? {
61
+ if let context {
62
+ return context
63
+ }
64
+
65
+ guard let context = JSContext() else {
66
+ return nil
67
+ }
68
+
69
+ guard let script = getBundleScript() else {
70
+ print("[ExpoWidgets] Missing ExpoWidgets.bundle")
71
+ return nil
72
+ }
73
+
74
+ context.evaluateScript(script)
75
+ if let exceptionMessage = contextExceptionMessage(context) {
76
+ print("[ExpoWidgets] Bundle evaluation failed: \(exceptionMessage)")
77
+ return nil
78
+ }
79
+
80
+ self.context = context
81
+ return context
82
+ }
83
+
84
+ private func getBundleScript() -> String? {
85
+ if let bundleScript {
86
+ return bundleScript
87
+ }
88
+
89
+ guard let bundleURL = Bundle.main.url(forResource: "ExpoWidgets", withExtension: "bundle"),
90
+ let bundle = Bundle(url: bundleURL),
91
+ let url = bundle.url(forResource: "ExpoWidgets", withExtension: "bundle"),
92
+ let script = try? String(contentsOf: url, encoding: .utf8) else {
93
+ return nil
94
+ }
95
+
96
+ bundleScript = script
97
+ return script
98
+ }
99
+
100
+ private func getLayoutValue(_ layout: String, in context: JSContext) -> JSValue? {
101
+ if let layoutValue = layoutCache[layout] {
102
+ return layoutValue
103
+ }
104
+
105
+ context.exception = nil
106
+ guard let layoutValue = context.evaluateScript("(\(layout))"),
107
+ !layoutValue.isUndefined else {
108
+ return nil
109
+ }
110
+ guard context.exception == nil else {
111
+ return nil
112
+ }
113
+
114
+ layoutCache[layout] = layoutValue
115
+ return layoutValue
116
+ }
117
+
118
+ private func contextExceptionMessage(_ context: JSContext) -> String? {
119
+ guard let exception = context.exception else {
120
+ return nil
121
+ }
122
+
123
+ context.exception = nil
124
+ return exception.toString() ?? "Unknown JavaScript exception."
125
+ }
126
+ }
127
+
128
+ func evaluateWidgetLayout(
129
+ layout: String,
130
+ props: [String: Any],
131
+ environment: [String: Any]
132
+ ) -> WidgetJavaScriptResult<[String: Any]> {
133
+ WidgetsJSRuntime.shared.render(layout: layout, props: props, environment: environment)
134
+ }
135
+
136
+ func evaluateWidgetButtonPress(
137
+ layout: String,
138
+ props: [String: Any],
139
+ environment: [String: Any]
140
+ ) -> WidgetJavaScriptResult<[String: Any]?> {
141
+ WidgetsJSRuntime.shared.handlePress(layout: layout, props: props, environment: environment)
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-widgets",
3
- "version": "56.0.8",
3
+ "version": "56.0.9",
4
4
  "description": "Widgets.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "homepage": "https://docs.expo.dev/versions/latest/sdk/widgets/",
23
23
  "dependencies": {
24
24
  "@expo/plist": "^0.6.0",
25
- "@expo/ui": "~56.0.7"
25
+ "@expo/ui": "~56.0.8"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@expo/spawn-async": "^1.7.2",
@@ -30,7 +30,7 @@
30
30
  "@types/react": "~19.2.0",
31
31
  "react-native": "0.85.3",
32
32
  "resolve-workspace-root": "^2.0.0",
33
- "expo": "56.0.0-preview.11",
33
+ "expo": "56.0.0-preview.12",
34
34
  "expo-module-scripts": "56.0.2"
35
35
  },
36
36
  "peerDependencies": {
@@ -44,7 +44,7 @@
44
44
  "./scripts/xcode-build-bundle.sh"
45
45
  ]
46
46
  },
47
- "gitHead": "51c27fce31a5b3a877a4b05d832dabf4a99db5e1",
47
+ "gitHead": "f26be3dd9396bf7c399a1d607865d0fabdbc0d64",
48
48
  "scripts": {
49
49
  "build": "expo-module build",
50
50
  "build:plugin": "expo-module build plugin",
@@ -0,0 +1 @@
1
+ {"root":["./src/index.ts","./src/withappgroupentitlements.ts","./src/withappinfoplist.ts","./src/witheasconfig.ts","./src/withioswarning.ts","./src/withpodslinking.ts","./src/withpushnotifications.ts","./src/withwidgetsourcefiles.ts","./src/types/widgetconfig.type.ts","./src/types/widgetfamily.type.ts","./src/xcode/addbuildphases.ts","./src/xcode/addpbxgroup.ts","./src/xcode/addproductfile.ts","./src/xcode/addtargetdependency.ts","./src/xcode/addtopbxnativetargetsection.ts","./src/xcode/addtopbxprojectsection.ts","./src/xcode/addxcconfigurationlist.ts","./src/xcode/withtargetxcodeproject.ts"],"version":"5.9.2"}
package/spm.config.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "React",
13
13
  "Hermes",
14
14
  "expo-modules-core/ExpoModulesCore",
15
+ "expo-modules-core/ExpoModulesWorklets",
15
16
  "@expo/ui/ExpoUI"
16
17
  ],
17
18
  "targets": [
@@ -25,6 +26,7 @@
25
26
  "React",
26
27
  "Hermes",
27
28
  "expo-modules-core/ExpoModulesCore",
29
+ "expo-modules-core/ExpoModulesWorklets",
28
30
  "@expo/ui/ExpoUI"
29
31
  ],
30
32
  "linkedFrameworks": [
@@ -1,23 +0,0 @@
1
- import Foundation
2
- import JavaScriptCore
3
-
4
- func createWidgetContext(layout: String) -> JSContext? {
5
- guard let context = JSContext() else {
6
- return nil
7
- }
8
-
9
- // Inject ExpoUI bundle
10
- guard let bundleURL = Bundle.main.url(forResource: "ExpoWidgets", withExtension: "bundle"),
11
- let bundle = Bundle(url: bundleURL),
12
- let url = bundle.url(forResource: "ExpoWidgets", withExtension: "bundle"),
13
- let bundleJS = try? String(contentsOf: url, encoding: .utf8) else {
14
- print("[ExpoWidgets] Missing ExpoWidgets.bundle")
15
- return nil
16
- }
17
- context.evaluateScript(bundleJS)
18
-
19
- // Inject layout
20
- let layoutValue = context.evaluateScript("(\(layout))")
21
- context.setObject(layoutValue, forKeyedSubscript: "__expoWidgetLayout" as NSString)
22
- return context
23
- }