expo-invoke 2.6.15 → 2.8.0

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.
Files changed (152) hide show
  1. package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +35 -0
  2. package/android/src/main/java/expo/modules/invoke/InvokeSliceProvider.kt +9 -0
  3. package/build/plugin/android/withAndroidInvoke.d.ts +1 -0
  4. package/build/plugin/android/withAndroidInvoke.js +1 -0
  5. package/build/plugin/android/withAndroidInvoke.js.map +1 -1
  6. package/build/plugin/codegen/generateSwiftIntents.js +22 -0
  7. package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
  8. package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
  9. package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
  10. package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
  11. package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
  12. package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
  13. package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
  14. package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
  15. package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
  16. package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
  17. package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
  18. package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
  19. package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
  20. package/build/plugin/ios/widgets/withEasConfig.js +52 -0
  21. package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
  22. package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
  23. package/build/plugin/ios/widgets/withIosWarning.js +10 -0
  24. package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
  25. package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
  26. package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
  27. package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
  28. package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
  29. package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
  30. package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
  31. package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
  32. package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
  33. package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
  34. package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
  35. package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
  36. package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
  37. package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
  38. package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
  39. package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
  40. package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
  41. package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
  42. package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
  43. package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
  44. package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
  45. package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
  46. package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
  47. package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
  48. package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
  49. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
  50. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
  51. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
  52. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
  53. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
  54. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
  55. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
  56. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
  57. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
  58. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
  59. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
  60. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
  61. package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
  62. package/build/plugin/ios/withIOSInvoke.js +57 -26
  63. package/build/plugin/ios/withIOSInvoke.js.map +1 -1
  64. package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
  65. package/build/plugin/ios/withWidgetExtension.js +5 -0
  66. package/build/plugin/ios/withWidgetExtension.js.map +1 -1
  67. package/build/plugin/src/withInvoke.d.ts +34 -23
  68. package/build/plugin/src/withInvoke.js +16 -27
  69. package/build/plugin/src/withInvoke.js.map +1 -1
  70. package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
  71. package/build/plugin/types/WidgetConfig.type.js +3 -0
  72. package/build/plugin/types/WidgetConfig.type.js.map +1 -0
  73. package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
  74. package/build/plugin/types/WidgetFamily.type.js +13 -0
  75. package/build/plugin/types/WidgetFamily.type.js.map +1 -0
  76. package/build/src/hooks/useLiveActivity.d.ts +20 -10
  77. package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
  78. package/build/src/hooks/useLiveActivity.js +20 -10
  79. package/build/src/hooks/useLiveActivity.js.map +1 -1
  80. package/build/src/index.d.ts +2 -0
  81. package/build/src/index.d.ts.map +1 -1
  82. package/build/src/index.js +5 -0
  83. package/build/src/index.js.map +1 -1
  84. package/build/src/module.d.ts +4 -0
  85. package/build/src/module.d.ts.map +1 -1
  86. package/build/src/module.js +8 -0
  87. package/build/src/module.js.map +1 -1
  88. package/build/src/web/index.d.ts +10 -1
  89. package/build/src/web/index.d.ts.map +1 -1
  90. package/build/src/web/index.js +4 -0
  91. package/build/src/web/index.js.map +1 -1
  92. package/build/src/widgets/ExpoWidgets.d.ts +12 -0
  93. package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
  94. package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
  95. package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
  96. package/build/src/widgets/ExpoWidgets.ios.js +3 -0
  97. package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
  98. package/build/src/widgets/ExpoWidgets.js +42 -0
  99. package/build/src/widgets/ExpoWidgets.js.map +1 -0
  100. package/build/src/widgets/Widgets.d.ts +119 -0
  101. package/build/src/widgets/Widgets.d.ts.map +1 -0
  102. package/build/src/widgets/Widgets.js +158 -0
  103. package/build/src/widgets/Widgets.js.map +1 -0
  104. package/build/src/widgets/Widgets.types.d.ts +263 -0
  105. package/build/src/widgets/Widgets.types.d.ts.map +1 -0
  106. package/build/src/widgets/Widgets.types.js +2 -0
  107. package/build/src/widgets/Widgets.types.js.map +1 -0
  108. package/build/src/widgets/index.d.ts +3 -0
  109. package/build/src/widgets/index.d.ts.map +1 -0
  110. package/build/src/widgets/index.js +2 -0
  111. package/build/src/widgets/index.js.map +1 -0
  112. package/bundle/__tests__/decorator.test.ts +89 -0
  113. package/bundle/decorator.ts +61 -0
  114. package/bundle/expo-modules-core-stub.ts +1 -0
  115. package/bundle/expo-stub.ts +15 -0
  116. package/bundle/index.ts +69 -0
  117. package/bundle/jsx-runtime-stub.ts +71 -0
  118. package/bundle/react-native-stub.ts +3 -0
  119. package/bundle/react-stub.ts +13 -0
  120. package/expo-module.config.json +1 -1
  121. package/ios/LiveActivity/LiveActivityManager.swift +15 -14
  122. package/ios/Module/ExpoInvokeModule.swift +124 -0
  123. package/ios/Widget/WidgetManager.swift +25 -8
  124. package/ios/Widgets/LiveActivity.swift +81 -0
  125. package/ios/Widgets/LiveActivityFactory.swift +49 -0
  126. package/ios/Widgets/Widget/AppIntent.swift +130 -0
  127. package/ios/Widgets/Widget/Buttons.swift +62 -0
  128. package/ios/Widgets/Widget/DynamicView.swift +161 -0
  129. package/ios/Widgets/Widget/EntryView.swift +36 -0
  130. package/ios/Widgets/Widget/Fragment.swift +13 -0
  131. package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
  132. package/ios/Widgets/Widget/RedBoxView.swift +83 -0
  133. package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
  134. package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
  135. package/ios/Widgets/Widget/Type.swift +6 -0
  136. package/ios/Widgets/Widget/Utils.swift +116 -0
  137. package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
  138. package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
  139. package/ios/Widgets/WidgetObject.swift +31 -0
  140. package/ios/Widgets/WidgetsContext.swift +7 -0
  141. package/ios/Widgets/WidgetsEvents.swift +16 -0
  142. package/ios/Widgets/WidgetsExceptions.swift +25 -0
  143. package/ios/Widgets/WidgetsModule.swift +149 -0
  144. package/ios/Widgets/WidgetsRecords.swift +22 -0
  145. package/ios/Widgets/WidgetsStorage.swift +58 -0
  146. package/metro.widget.config.js +76 -0
  147. package/package.json +12 -2
  148. package/scripts/autolinking.rb +34 -0
  149. package/scripts/build-bundle.mjs +69 -0
  150. package/scripts/with-node.sh +44 -0
  151. package/scripts/xcode-build-bundle.sh +23 -0
  152. package/spm.config.json +40 -0
@@ -0,0 +1,49 @@
1
+ import ExpoModulesCore
2
+ import ActivityKit
3
+
4
+ final class LiveActivityFactory: SharedObject {
5
+ let name: String
6
+
7
+ static var pushNotificationsEnabled: Bool {
8
+ Bundle.main.object(forInfoDictionaryKey: pushNotificationsEnabledKey) as? Bool ?? false
9
+ }
10
+
11
+ init(name: String, layout: String) {
12
+ self.name = name
13
+ WidgetsStorage.set(layout, forKey: "__expo_widgets_live_activity_\(name)_layout")
14
+ }
15
+
16
+ func start(props: String?, url: URL?) throws -> LiveActivity {
17
+ guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() }
18
+ guard ActivityAuthorizationInfo().areActivitiesEnabled else {
19
+ throw LiveActivitiesNotSupportedException()
20
+ }
21
+
22
+ if let url {
23
+ WidgetsStorage.set(url.absoluteString, forKey: "__expo_widgets_live_activity_\(name)_url")
24
+ }
25
+
26
+ do {
27
+ let initialState = LiveActivityAttributes.ContentState(name: name, props: props)
28
+ let activity = try Activity.request(
29
+ attributes: LiveActivityAttributes(),
30
+ content: .init(state: initialState, staleDate: nil),
31
+ pushType: LiveActivityFactory.pushNotificationsEnabled ? .token : nil
32
+ )
33
+
34
+ let instance = LiveActivity(id: activity.id, name: name)
35
+ instance.observePushTokenUpdates(for: activity, pushNotificationsEnabled: LiveActivityFactory.pushNotificationsEnabled)
36
+ return instance
37
+ } catch {
38
+ throw StartLiveActivityException(error.localizedDescription)
39
+ }
40
+ }
41
+
42
+ func getInstances() throws -> [LiveActivity] {
43
+ guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() }
44
+
45
+ return Activity<LiveActivityAttributes>.activities.map { activity in
46
+ LiveActivity(id: activity.id, name: name)
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,130 @@
1
+ import AppIntents
2
+ import WidgetKit
3
+
4
+ struct WidgetReload: AppIntent {
5
+ // title is not used for non-discoverable intents, but it is required
6
+ static var title: LocalizedStringResource = "Reload widget"
7
+ static var isDiscoverable: Bool = false
8
+ @Parameter(title: "source")
9
+ var source: String?
10
+
11
+ init() {}
12
+ init(source: String?) {
13
+ self.source = source
14
+ }
15
+
16
+ func perform() async throws -> some IntentResult {
17
+ guard let source else {
18
+ return .result()
19
+ }
20
+
21
+ WidgetCenter.shared.reloadTimelines(ofKind: source)
22
+ return .result()
23
+ }
24
+ }
25
+
26
+ @available(iOS 16.0, *)
27
+ struct WidgetUserInteraction: AppIntent {
28
+ // title is not used for non-discoverable intents, but it is required
29
+ static var title: LocalizedStringResource = "User Interaction"
30
+ static var isDiscoverable: Bool = false
31
+ @Parameter(title: "source")
32
+ var source: String?
33
+
34
+ @Parameter(title: "target")
35
+ var target: String?
36
+
37
+ @Parameter(title: "entryIndex")
38
+ var entryIndex: Int?
39
+
40
+ @Parameter(title: "environmentString")
41
+ var environmentString: String?
42
+
43
+ init() {}
44
+ init(source: String?, target: String?, entryIndex: Int?, environmentString: String?) {
45
+ self.source = source
46
+ self.target = target
47
+ self.entryIndex = entryIndex
48
+ self.environmentString = environmentString
49
+ }
50
+
51
+ func perform() async throws -> some IntentResult {
52
+ guard let source else {
53
+ return .result()
54
+ }
55
+
56
+ let layout = WidgetsStorage.getString(forKey: "__expo_widgets_\(source)_layout") ?? ""
57
+ let timeline = WidgetsStorage.getArray(forKey: "__expo_widgets_\(source)_timeline")
58
+
59
+ guard let timeline,
60
+ let entryIndex,
61
+ timeline.indices.contains(entryIndex),
62
+ let entry = timeline[entryIndex] as? [String: Any],
63
+ let props = entry["props"] as? [String: Any],
64
+ let environmentData = environmentString?.data(using: .utf8),
65
+ var environment = try? JSONSerialization.jsonObject(with: environmentData) as? [String: Any] else {
66
+ return .result()
67
+ }
68
+ environment["target"] = target
69
+
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 {
80
+ var newEntry = entry
81
+ if let originalProps = entry["props"] as? [String: Any] {
82
+ newEntry["props"] = originalProps.merging(newProps) { _, new in new }
83
+ }
84
+ guard var newTimeline = timeline as? [[String: Any]] else {
85
+ return .result()
86
+ }
87
+ newTimeline[entryIndex] = newEntry
88
+ WidgetsStorage.set(newTimeline, forKey: "__expo_widgets_\(source)_timeline")
89
+ }
90
+
91
+ WidgetsEvents.shared.sendNotification(type: .userEvent, data: [
92
+ "source": source as Any,
93
+ "target": target as Any,
94
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000)
95
+ ])
96
+
97
+ WidgetCenter.shared.reloadTimelines(ofKind: source)
98
+
99
+ return .result()
100
+ }
101
+ }
102
+
103
+ @available(iOS 16.0, *)
104
+ struct LiveActivityUserInteraction: LiveActivityIntent {
105
+ // title is not used for non-discoverable intents, but it is required
106
+ static var title: LocalizedStringResource = "User Interaction"
107
+ static var isDiscoverable: Bool = false
108
+
109
+ @Parameter(title: "source")
110
+ var source: String?
111
+
112
+ @Parameter(title: "target")
113
+ var target: String?
114
+
115
+ init() {}
116
+ init(source: String?, target: String?) {
117
+ self.source = source
118
+ self.target = target
119
+ }
120
+
121
+ func perform() async throws -> some IntentResult {
122
+ WidgetsEvents.shared.sendNotification(type: .userEvent, data: [
123
+ "source": source as Any,
124
+ "target": target as Any,
125
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000)
126
+ ])
127
+
128
+ return .result()
129
+ }
130
+ }
@@ -0,0 +1,62 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+ import ExpoUI
4
+
5
+ final class ButtonProps: ExpoUI.ButtonProps {
6
+ @Field var source: String?
7
+ @Field var target: String?
8
+ @Field var entryIndex: Int?
9
+ @Field var environmentString: String?
10
+ }
11
+
12
+ @available(iOS 17.0, *)
13
+ struct WidgetButtonView: ExpoSwiftUI.View {
14
+ @ObservedObject var props: ButtonProps
15
+
16
+ var body: some View {
17
+ SwiftUI.Button(
18
+ role: props.role?.toNativeRole(),
19
+ intent: WidgetUserInteraction(
20
+ source: props.source,
21
+ target: props.target,
22
+ entryIndex: props.entryIndex,
23
+ environmentString: props.environmentString
24
+ )
25
+ ) {
26
+ if let label = props.label {
27
+ if let systemImage = props.systemImage {
28
+ Label(title: { Text(label) }, icon: { Image(systemName: systemImage) })
29
+ } else {
30
+ Text(label)
31
+ }
32
+ } else {
33
+ Children()
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ @available(iOS 17.0, *)
40
+ struct LiveActivityButtonView: ExpoSwiftUI.View {
41
+ @ObservedObject var props: ButtonProps
42
+
43
+ var body: some View {
44
+ SwiftUI.Button(
45
+ role: props.role?.toNativeRole(),
46
+ intent: LiveActivityUserInteraction(
47
+ source: props.source,
48
+ target: props.target
49
+ )
50
+ ) {
51
+ if let label = props.label {
52
+ if let systemImage = props.systemImage {
53
+ Label(title: { Text(label) }, icon: { Image(systemName: systemImage) })
54
+ } else {
55
+ Text(label)
56
+ }
57
+ } else {
58
+ Children()
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,161 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+ import ExpoUI
4
+
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
+ }
30
+
31
+ 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
37
+ }
38
+
39
+ 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
45
+ }
46
+
47
+ @ViewBuilder
48
+ public var body: some View {
49
+ switch node["type"] as? String {
50
+ case "TextView":
51
+ render(TextView.self, TextViewProps.self, updateProps: updateChildren)
52
+ case "HStackView":
53
+ render(HStackView.self, HStackViewProps.self, updateProps: updateChildren)
54
+ case "VStackView":
55
+ render(VStackView.self, VStackViewProps.self, updateProps: updateChildren)
56
+ case "ZStackView":
57
+ render(ZStackView.self, ZStackViewProps.self, updateProps: updateChildren)
58
+ case "RectangleView":
59
+ render(RectangleView.self, RectangleViewProps.self)
60
+ case "RoundedRectangleView":
61
+ render(RoundedRectangleView.self, RoundedRectangleViewProps.self)
62
+ case "CapsuleView":
63
+ render(CapsuleView.self, CapsuleViewProps.self)
64
+ case "CircleView":
65
+ render(CircleView.self, CircleViewProps.self)
66
+ case "ImageView":
67
+ render(ImageView.self, ImageViewProps.self)
68
+ case "AccessoryWidgetBackgroundView":
69
+ render(AccessoryWidgetBackgroundView.self, AccessoryWidgetBackgroundProps.self)
70
+ case "DividerView":
71
+ render(DividerView.self, DividerProps.self)
72
+ case "EllipseView":
73
+ render(EllipseView.self, EllipseViewProps.self)
74
+ case "LabelView":
75
+ render(LabelView.self, LabelViewProps.self)
76
+ case "ProgressView":
77
+ render(ProgressView.self, ProgressViewProps.self)
78
+ case "SpacerView":
79
+ render(SpacerView.self, SpacerViewProps.self)
80
+ case "UnevenRoundedRectangleView":
81
+ render(UnevenRoundedRectangleView.self, UnevenRoundedRectangleViewProps.self)
82
+ case "GaugeView":
83
+ render(GaugeView.self, GaugeProps.self)
84
+ case "ChartView":
85
+ render(ChartView.self, ChartProps.self)
86
+ case "Button":
87
+ if #available(iOS 17.0, *) {
88
+ switch kind {
89
+ case .widget:
90
+ render(WidgetButtonView.self, ButtonProps.self) { buttonProps in
91
+ try updateChildren(buttonProps)
92
+ buttonProps.source = name
93
+ buttonProps.entryIndex = entryIndex
94
+ buttonProps.environmentString = environmentString
95
+ }
96
+ case .liveActivity:
97
+ render(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in
98
+ try updateChildren(buttonProps)
99
+ buttonProps.source = name
100
+ }
101
+ }
102
+ } else {
103
+ render(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren)
104
+ }
105
+ case "react.fragment":
106
+ render(FragmentView.self, FragmentProps.self, updateProps: updateChildren)
107
+ case "LinkView":
108
+ render(LinkView.self, LinkViewProps.self, updateProps: updateChildren)
109
+ #if DEBUG
110
+ case "RedBoxView":
111
+ render(RedBoxView.self, RedBoxViewProps.self) { redBoxProps in
112
+ redBoxProps.source = name
113
+ redBoxProps.kind = kind
114
+ }
115
+ default:
116
+ ZStack {
117
+ Color.red.opacity(0.5)
118
+ Text("Unable to get the view for: \(node["type"] as? String ?? "undefined")")
119
+ }
120
+ #else
121
+ default:
122
+ EmptyView()
123
+ #endif
124
+ }
125
+ }
126
+
127
+ // MARK: - Render Method
128
+
129
+ @ViewBuilder
130
+ private func render<P, V>(_ viewType: V.Type, _ propsType: P.Type, updateProps: ((_ initialProps: P) throws -> Void)? = nil) -> some View
131
+ where P: UIBaseViewProps, V: ExpoSwiftUI.View, V.Props == P {
132
+ // immediately invoked closure {}() here because we can't use 'do-catch' inside @ViewBuilder
133
+ {
134
+ do {
135
+ if let rawProps = node["props"] as? [String: Any] {
136
+ let props = try propsType.init(rawProps: rawProps, context: WidgetsContext.shared.context)
137
+ try updateProps?(props)
138
+ // 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
139
+ return AnyView(UIBaseView<P, V>(props: props).transition(.identity))
140
+ }
141
+ return AnyView(EmptyView())
142
+ } catch {
143
+ return AnyView(EmptyView())
144
+ }
145
+ }()
146
+ }
147
+
148
+ // MARK: - Function that sets children as DynamicView
149
+
150
+ private func updateChildren<P>(_ initialProps: P) throws
151
+ where P: UIBaseViewProps {
152
+ if let props = node["props"] as? [String: Any] {
153
+ if let children = props["children"] as? [Any] {
154
+ let validChildren = children.compactMap { $0 as? [String: Any] }
155
+ initialProps.children = validChildren.map { WidgetsDynamicView(name: name, kind: kind, node: $0, entryIndex: entryIndex, environmentString: environmentString) }
156
+ } else if let child = props["children"] as? [String: Any] {
157
+ initialProps.children = [WidgetsDynamicView(name: name, kind: kind, node: child, entryIndex: entryIndex, environmentString: environmentString)]
158
+ }
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,36 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+ import WidgetKit
4
+
5
+ public struct WidgetsEntryView: View {
6
+ @Environment(\.self) var environment
7
+ var entry: WidgetsTimelineProvider.Entry
8
+
9
+ public init(entry: WidgetsTimelineProvider.Entry) {
10
+ self.entry = entry
11
+ }
12
+
13
+ private var widgetEnvironment: [String: Any] {
14
+ var env: [String: Any] = getWidgetEnvironment(environment: environment)
15
+ env["timestamp"] = Int(entry.date.timeIntervalSince1970 * 1000)
16
+ return env
17
+ }
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
+
27
+ public var body: some View {
28
+ if let layout = WidgetsStorage.getString(forKey: "__expo_widgets_\(entry.name)_layout"),
29
+ !layout.isEmpty {
30
+ let node = evaluateLayout(layout: layout, props: entry.props, environment: widgetEnvironment)
31
+ WidgetsDynamicView(name: entry.name, kind: .widget, node: node, entryIndex: entry.entryIndex, environmentString: widgetEnvironmentString)
32
+ } else {
33
+ WidgetsDynamicView(name: entry.name, kind: .widget, node: createRedBox(message: "No layout found for \(WidgetsStorage.appGroupIdentifier ?? "")::\(entry.name)"), entryIndex: entry.entryIndex, environmentString: widgetEnvironmentString)
34
+ }
35
+ }
36
+ }
@@ -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
+ }
@@ -0,0 +1,19 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ @available(iOS 18.0, *)
5
+ struct LiveActivityBanner: View {
6
+ @Environment(\.activityFamily) var activityFamily
7
+ var context: ActivityViewContext<LiveActivityAttributes>
8
+ var nodes: [String: Any]?
9
+
10
+ var body: some View {
11
+ if activityFamily == .small, let node = nodes?["bannerSmall"] as? [String: Any] {
12
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
13
+ } else if let node = nodes?["banner"] as? [String: Any] {
14
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
15
+ } else {
16
+ EmptyView()
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,83 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+ import ExpoUI
4
+
5
+ public final class RedBoxViewProps: UIBaseViewProps {
6
+ @Field var message: String
7
+ @Field var source: String?
8
+ @Field var stack: String?
9
+ var kind: WidgetsKind = .widget
10
+ }
11
+
12
+ public struct RedBoxView: ExpoSwiftUI.View {
13
+ @ObservedObject public var props: RedBoxViewProps
14
+
15
+ public init(props: RedBoxViewProps) {
16
+ self.props = props
17
+ }
18
+
19
+ public var body: some View {
20
+ FullSizeZStack(kind: props.kind) {
21
+ Color.red
22
+ VStack(alignment: .leading, spacing: 2) {
23
+ HStack(spacing: 6) {
24
+ Image(systemName: "exclamationmark.triangle.fill")
25
+ Text("Error").font(.headline)
26
+ Spacer()
27
+ if #available(iOS 17.0, *) {
28
+ Button(intent: WidgetReload(source: props.source)) {
29
+ Image(systemName: "arrow.clockwise")
30
+ }
31
+ .buttonStyle(.plain)
32
+ .padding(4)
33
+ }
34
+ }
35
+ .foregroundStyle(.white)
36
+
37
+ Text(props.message)
38
+ .font(.system(size: 14).bold().monospaced())
39
+ .foregroundStyle(.white.opacity(0.9))
40
+ .modifier(RedBoxBody())
41
+
42
+ if let stack = props.stack, !stack.isEmpty {
43
+ Text(stack)
44
+ .font(.system(size: 11).monospaced())
45
+ .foregroundStyle(.white.opacity(0.75))
46
+ .modifier(RedBoxBody())
47
+ }
48
+ }
49
+ .padding(12)
50
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
51
+ }
52
+ }
53
+ }
54
+
55
+ private struct RedBoxBody: ViewModifier {
56
+ @ViewBuilder
57
+ func body(content: Content) -> some View {
58
+ let base = content.lineLimit(nil).fixedSize(horizontal: false, vertical: true)
59
+ if #available(iOS 26.0, *) {
60
+ base.lineHeight(.tight)
61
+ } else {
62
+ base
63
+ }
64
+ }
65
+ }
66
+
67
+ public struct FullSizeZStack<Content: View>: View {
68
+ let kind: WidgetsKind
69
+ let content: Content
70
+
71
+ init(kind: WidgetsKind = .widget, @ViewBuilder _ content: () -> Content) {
72
+ self.kind = kind
73
+ self.content = content()
74
+ }
75
+
76
+ public var body: some View {
77
+ if #available(iOS 17.0, *), kind == .widget {
78
+ ZStack { content }.containerRelativeFrame([.horizontal, .vertical])
79
+ } else {
80
+ ZStack { content }.frame(maxWidth: .infinity)
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,8 @@
1
+ import WidgetKit
2
+
3
+ public struct WidgetsTimelineEntry: WidgetKit.TimelineEntry {
4
+ public let date: Date
5
+ public let name: String
6
+ public let props: [String: Any]?
7
+ public let entryIndex: Int?
8
+ }
@@ -0,0 +1,45 @@
1
+ import WidgetKit
2
+
3
+ public struct WidgetsTimelineProvider: TimelineProvider {
4
+ public func placeholder(in context: Context) -> WidgetsTimelineEntry {
5
+ WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil)
6
+ }
7
+
8
+ public func getSnapshot(
9
+ in context: Context, completion: @escaping @Sendable (WidgetsTimelineEntry) -> Void
10
+ ) {
11
+ let groupIdentifier =
12
+ Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String
13
+ guard let groupIdentifier else {
14
+ completion(WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil))
15
+ return
16
+ }
17
+
18
+ let entries = parseTimeline(identifier: groupIdentifier, name: name, family: context.family)
19
+ completion(entries.first ?? WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil))
20
+ }
21
+
22
+ public func getTimeline(
23
+ in context: Context,
24
+ completion: @escaping @Sendable (Timeline<WidgetsTimelineEntry>) -> Void
25
+ ) {
26
+ let groupIdentifier =
27
+ Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String
28
+ guard let groupIdentifier else {
29
+ fatalError("Could not get the app group identifier from Info.plist")
30
+ }
31
+
32
+ let entries = parseTimeline(identifier: groupIdentifier, name: name, family: context.family)
33
+
34
+ let timeline = Timeline<WidgetsTimelineEntry>(entries: entries, policy: .atEnd)
35
+ completion(timeline)
36
+ }
37
+
38
+ public typealias Entry = WidgetsTimelineEntry
39
+
40
+ let name: String
41
+
42
+ public init(name: String) {
43
+ self.name = name
44
+ }
45
+ }
@@ -0,0 +1,6 @@
1
+ import WidgetKit
2
+
3
+ public enum WidgetsKind {
4
+ case widget
5
+ case liveActivity
6
+ }