expo-invoke 2.6.15 → 2.7.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 (149) 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/codegen/generateSwiftIntents.js +22 -0
  4. package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
  5. package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
  6. package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
  7. package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
  8. package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
  9. package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
  10. package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
  11. package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
  12. package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
  13. package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
  14. package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
  15. package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
  16. package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
  17. package/build/plugin/ios/widgets/withEasConfig.js +52 -0
  18. package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
  19. package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
  20. package/build/plugin/ios/widgets/withIosWarning.js +10 -0
  21. package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
  22. package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
  23. package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
  24. package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
  25. package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
  26. package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
  27. package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
  28. package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
  29. package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
  30. package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
  31. package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
  32. package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
  33. package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
  34. package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
  35. package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
  36. package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
  37. package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
  38. package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
  39. package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
  40. package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
  41. package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
  42. package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
  43. package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
  44. package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
  45. package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
  46. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
  47. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
  48. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
  49. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
  50. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
  51. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
  52. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
  53. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
  54. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
  55. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
  56. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
  57. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
  58. package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
  59. package/build/plugin/ios/withIOSInvoke.js +54 -25
  60. package/build/plugin/ios/withIOSInvoke.js.map +1 -1
  61. package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
  62. package/build/plugin/ios/withWidgetExtension.js +5 -0
  63. package/build/plugin/ios/withWidgetExtension.js.map +1 -1
  64. package/build/plugin/src/withInvoke.d.ts +34 -23
  65. package/build/plugin/src/withInvoke.js +16 -27
  66. package/build/plugin/src/withInvoke.js.map +1 -1
  67. package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
  68. package/build/plugin/types/WidgetConfig.type.js +3 -0
  69. package/build/plugin/types/WidgetConfig.type.js.map +1 -0
  70. package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
  71. package/build/plugin/types/WidgetFamily.type.js +13 -0
  72. package/build/plugin/types/WidgetFamily.type.js.map +1 -0
  73. package/build/src/hooks/useLiveActivity.d.ts +20 -10
  74. package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
  75. package/build/src/hooks/useLiveActivity.js +20 -10
  76. package/build/src/hooks/useLiveActivity.js.map +1 -1
  77. package/build/src/index.d.ts +2 -0
  78. package/build/src/index.d.ts.map +1 -1
  79. package/build/src/index.js +5 -0
  80. package/build/src/index.js.map +1 -1
  81. package/build/src/module.d.ts +4 -0
  82. package/build/src/module.d.ts.map +1 -1
  83. package/build/src/module.js +8 -0
  84. package/build/src/module.js.map +1 -1
  85. package/build/src/web/index.d.ts +10 -1
  86. package/build/src/web/index.d.ts.map +1 -1
  87. package/build/src/web/index.js +4 -0
  88. package/build/src/web/index.js.map +1 -1
  89. package/build/src/widgets/ExpoWidgets.d.ts +12 -0
  90. package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
  91. package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
  92. package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
  93. package/build/src/widgets/ExpoWidgets.ios.js +3 -0
  94. package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
  95. package/build/src/widgets/ExpoWidgets.js +42 -0
  96. package/build/src/widgets/ExpoWidgets.js.map +1 -0
  97. package/build/src/widgets/Widgets.d.ts +119 -0
  98. package/build/src/widgets/Widgets.d.ts.map +1 -0
  99. package/build/src/widgets/Widgets.js +158 -0
  100. package/build/src/widgets/Widgets.js.map +1 -0
  101. package/build/src/widgets/Widgets.types.d.ts +263 -0
  102. package/build/src/widgets/Widgets.types.d.ts.map +1 -0
  103. package/build/src/widgets/Widgets.types.js +2 -0
  104. package/build/src/widgets/Widgets.types.js.map +1 -0
  105. package/build/src/widgets/index.d.ts +3 -0
  106. package/build/src/widgets/index.d.ts.map +1 -0
  107. package/build/src/widgets/index.js +2 -0
  108. package/build/src/widgets/index.js.map +1 -0
  109. package/bundle/__tests__/decorator.test.ts +89 -0
  110. package/bundle/decorator.ts +61 -0
  111. package/bundle/expo-modules-core-stub.ts +1 -0
  112. package/bundle/expo-stub.ts +15 -0
  113. package/bundle/index.ts +69 -0
  114. package/bundle/jsx-runtime-stub.ts +71 -0
  115. package/bundle/react-native-stub.ts +3 -0
  116. package/bundle/react-stub.ts +13 -0
  117. package/expo-module.config.json +1 -1
  118. package/ios/LiveActivity/LiveActivityManager.swift +15 -14
  119. package/ios/Module/ExpoInvokeModule.swift +124 -0
  120. package/ios/Widget/WidgetManager.swift +25 -8
  121. package/ios/Widgets/LiveActivity.swift +81 -0
  122. package/ios/Widgets/LiveActivityFactory.swift +49 -0
  123. package/ios/Widgets/Widget/AppIntent.swift +130 -0
  124. package/ios/Widgets/Widget/Buttons.swift +62 -0
  125. package/ios/Widgets/Widget/DynamicView.swift +161 -0
  126. package/ios/Widgets/Widget/EntryView.swift +36 -0
  127. package/ios/Widgets/Widget/Fragment.swift +13 -0
  128. package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
  129. package/ios/Widgets/Widget/RedBoxView.swift +83 -0
  130. package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
  131. package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
  132. package/ios/Widgets/Widget/Type.swift +6 -0
  133. package/ios/Widgets/Widget/Utils.swift +116 -0
  134. package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
  135. package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
  136. package/ios/Widgets/WidgetObject.swift +31 -0
  137. package/ios/Widgets/WidgetsContext.swift +7 -0
  138. package/ios/Widgets/WidgetsEvents.swift +16 -0
  139. package/ios/Widgets/WidgetsExceptions.swift +25 -0
  140. package/ios/Widgets/WidgetsModule.swift +149 -0
  141. package/ios/Widgets/WidgetsRecords.swift +22 -0
  142. package/ios/Widgets/WidgetsStorage.swift +58 -0
  143. package/metro.widget.config.js +76 -0
  144. package/package.json +11 -1
  145. package/scripts/autolinking.rb +34 -0
  146. package/scripts/build-bundle.mjs +69 -0
  147. package/scripts/with-node.sh +44 -0
  148. package/scripts/xcode-build-bundle.sh +23 -0
  149. package/spm.config.json +40 -0
@@ -0,0 +1,116 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+ import Foundation
4
+
5
+ func parseTimeline(identifier: String, name: String, family: WidgetFamily) -> [WidgetsTimelineEntry] {
6
+ guard let timeline = WidgetsStorage.getArray(forKey: "__expo_widgets_\(name)_timeline") else {
7
+ return [WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil)]
8
+ }
9
+
10
+ let entries: [WidgetsTimelineEntry?] = timeline.enumerated().map { index, entry in
11
+ if let entry = entry as? [String: Any], let timestamp = entry["timestamp"] as? Int, let props = entry["props"] as? [String: Any] {
12
+ return WidgetsTimelineEntry(
13
+ date: Date(timeIntervalSince1970: Double(timestamp) / 1000),
14
+ name: name,
15
+ props: props,
16
+ entryIndex: index
17
+ )
18
+ }
19
+ return nil
20
+ }
21
+
22
+ return entries.compactMap(\.self)
23
+ }
24
+
25
+ public func createRedBox(message: String, stack: String? = nil) -> [String: Any] {
26
+ var props: [String: Any] = ["message": message]
27
+ if let stack {
28
+ props["stack"] = stack
29
+ }
30
+ return ["type": "RedBoxView", "props": props]
31
+ }
32
+
33
+ public func evaluateLayout(
34
+ layout: String,
35
+ props: [String: Any]?,
36
+ environment: [String: Any]
37
+ ) -> [String: Any] {
38
+ switch evaluateWidgetLayout(layout: layout, props: props, environment: environment) {
39
+ case .success(let result):
40
+ return result
41
+ case .failure(let error):
42
+ print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
43
+ return createRedBox(message: error.message)
44
+ }
45
+ }
46
+
47
+ func getLiveActivityNodes(forName name: String, props: String? = nil, environment: [String: Any]) -> [String: Any] {
48
+ let layout = WidgetsStorage.getString(forKey: "__expo_widgets_live_activity_\(name)_layout") ?? ""
49
+ let propsDict = props.flatMap { props in
50
+ props.data(using: .utf8).flatMap {
51
+ try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any]
52
+ }
53
+ }
54
+
55
+ switch evaluateWidgetLayout(layout: layout, props: propsDict, environment: environment) {
56
+ case .success(let result):
57
+ return result
58
+ case .failure(let error):
59
+ print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
60
+ return ["banner": createRedBox(message: error.message)]
61
+ }
62
+ }
63
+
64
+ func getLiveActivityUrl(forName name: String) -> URL? {
65
+ guard let urlString = WidgetsStorage.getString(forKey: "__expo_widgets_live_activity_\(name)_url") else {
66
+ return nil
67
+ }
68
+ return URL(string: urlString)
69
+ }
70
+
71
+ public func getWidgetEnvironment(environment: EnvironmentValues) -> [String: Any] {
72
+ var env: [String: Any] = [
73
+ "showsContainerBackground": environment.showsWidgetContainerBackground,
74
+ "widgetFamily": environment.widgetFamily.description,
75
+ "colorScheme": "\(environment.colorScheme)"
76
+ ]
77
+
78
+ if #available(iOS 16.0, *) {
79
+ env["isLuminanceReduced"] = environment.isLuminanceReduced
80
+ env["widgetRenderingMode"] = environment.widgetRenderingMode.description
81
+ env["showsWidgetLabel"] = environment.showsWidgetLabel
82
+ }
83
+ if #available(iOS 17.0, *) {
84
+ env["widgetContentMargins"] = [
85
+ "top": environment.widgetContentMargins.top,
86
+ "bottom": environment.widgetContentMargins.bottom,
87
+ "leading": environment.widgetContentMargins.leading,
88
+ "trailing": environment.widgetContentMargins.trailing,
89
+ ]
90
+ }
91
+ if #available(iOS 26.0, *) {
92
+ env["levelOfDetail"] = environment.levelOfDetail == .simplified ? "simplified" : environment.levelOfDetail == .default ? "default" : nil
93
+ }
94
+ return env
95
+ }
96
+
97
+ func getLiveActivityEnvironment(environment: EnvironmentValues) -> [String: Any] {
98
+ var env: [String: Any] = [
99
+ "colorScheme": "\(environment.colorScheme)"
100
+ ]
101
+
102
+ if #available(iOS 16.0, *) {
103
+ env["isLuminanceReduced"] = environment.isLuminanceReduced
104
+ }
105
+ if #available(iOS 16.1, *) {
106
+ env["isActivityFullscreen"] = environment.isActivityFullscreen
107
+ }
108
+ if #available(iOS 18.0, *) {
109
+ env["isActivityUpdateReduced"] = environment.isActivityUpdateReduced
110
+ env["activityFamily"] = "\(environment.activityFamily)"
111
+ }
112
+ if #available(iOS 26.0, *) {
113
+ env["levelOfDetail"] = environment.levelOfDetail == .simplified ? "simplified" : environment.levelOfDetail == .default ? "default" : nil
114
+ }
115
+ return env
116
+ }
@@ -0,0 +1,104 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+ import ExpoModulesCore
4
+ import ActivityKit
5
+
6
+ struct LiveActivityAttributes: ActivityAttributes {
7
+ public struct ContentState: Codable, Hashable {
8
+ var name: String
9
+ var props: String?
10
+ }
11
+ }
12
+
13
+ @available(iOS 16.1, *)
14
+ public struct WidgetLiveActivity: Widget {
15
+ @Environment(\.self) var env
16
+
17
+ let widgetContext: AppContext = AppContext()
18
+
19
+ var environment: [String: Any] {
20
+ return getLiveActivityEnvironment(environment: env)
21
+ }
22
+
23
+ public init() {}
24
+
25
+ public var body: some WidgetConfiguration {
26
+ ActivityConfiguration(for: LiveActivityAttributes.self) { context in
27
+ let nodes = getLiveActivityNodes(
28
+ forName: context.state.name,
29
+ props: context.state.props,
30
+ environment: environment
31
+ )
32
+ LiveActivityBannerView(context: context, nodes: nodes)
33
+ } dynamicIsland: { context in
34
+ let nodes = getLiveActivityNodes(
35
+ forName: context.state.name,
36
+ props: context.state.props,
37
+ environment: environment
38
+ )
39
+ return DynamicIsland {
40
+ DynamicIslandExpandedRegion(.center) {
41
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter")
42
+ }
43
+ DynamicIslandExpandedRegion(.leading) {
44
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading")
45
+ }
46
+ DynamicIslandExpandedRegion(.trailing) {
47
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing")
48
+ }
49
+ DynamicIslandExpandedRegion(.bottom) {
50
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom")
51
+ }
52
+ } compactLeading: {
53
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading")
54
+ } compactTrailing: {
55
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing")
56
+ } minimal: {
57
+ LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal")
58
+ }
59
+ .widgetURL(getLiveActivityUrl(forName: context.state.name))
60
+ }
61
+ .supplementalActivityFamiliesIfAvailable()
62
+ }
63
+ }
64
+
65
+ @available(iOS 16.1, *)
66
+ private struct LiveActivitySectionView: View {
67
+ let context: ActivityViewContext<LiveActivityAttributes>
68
+ let nodes: [String: Any]
69
+ let sectionName: String
70
+
71
+ var body: some View {
72
+ if let node = nodes[sectionName] as? [String: Any] {
73
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
74
+ } else {
75
+ EmptyView()
76
+ }
77
+ }
78
+ }
79
+
80
+ @available(iOS 16.1, *)
81
+ private struct LiveActivityBannerView: View {
82
+ var context: ActivityViewContext<LiveActivityAttributes>
83
+ let nodes: [String: Any]
84
+
85
+ var body: some View {
86
+ if #available(iOS 18.0, *) {
87
+ LiveActivityBanner(context: context, nodes: nodes)
88
+ } else if let node = nodes["banner"] as? [String: Any] {
89
+ WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
90
+ } else {
91
+ EmptyView()
92
+ }
93
+ }
94
+ }
95
+
96
+ extension WidgetConfiguration {
97
+ func supplementalActivityFamiliesIfAvailable() -> some WidgetConfiguration {
98
+ if #available(iOS 18.0, iOSApplicationExtension 18.0, *) {
99
+ return self.supplementalActivityFamilies([.small, .medium])
100
+ } else {
101
+ return self
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,148 @@
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") { context in
22
+ [props.map { $0 } ?? JSValue(undefinedIn: context) as Any, environment]
23
+ }.flatMap { result in
24
+ guard let renderedNode = result?.toObject() as? [String: Any] else {
25
+ return .failure(WidgetJavaScriptError(message: "Expo widget render did not produce any results."))
26
+ }
27
+ return .success(renderedNode)
28
+ }
29
+ }
30
+
31
+ func handlePress(layout: String, props: [String: Any], environment: [String: Any]) -> WidgetJavaScriptResult<[String: Any]?> {
32
+ call(layout: layout, functionName: "__expoWidgetHandlePress") { _ in [props, environment] }
33
+ .map { $0?.toObject() as? [String: Any] }
34
+ }
35
+
36
+ private func call(
37
+ layout: String,
38
+ functionName: String,
39
+ arguments: (JSContext) -> [Any]
40
+ ) -> WidgetJavaScriptResult<JSValue?> {
41
+ lock.lock()
42
+ defer { lock.unlock() }
43
+
44
+ guard let context = getContext() else {
45
+ return .failure(WidgetJavaScriptError(message: "Could not create context for layout evaluation."))
46
+ }
47
+ guard let layoutValue = getLayoutValue(layout, in: context) else {
48
+ return .failure(WidgetJavaScriptError(message: contextExceptionMessage(context) ?? "Could not evaluate layout."))
49
+ }
50
+
51
+ context.exception = nil
52
+ context.setObject(layoutValue, forKeyedSubscript: "__expoWidgetLayout" as NSString)
53
+
54
+ let function = context.objectForKeyedSubscript(functionName)
55
+ guard let function, function.isObject else {
56
+ return .failure(WidgetJavaScriptError(message: "Expo widget runtime function \(functionName) is unavailable."))
57
+ }
58
+
59
+ let result = function.call(withArguments: arguments(context))
60
+ if let exceptionMessage = contextExceptionMessage(context) {
61
+ return .failure(WidgetJavaScriptError(message: exceptionMessage))
62
+ }
63
+ return .success(result)
64
+ }
65
+
66
+ private func getContext() -> JSContext? {
67
+ if let context {
68
+ return context
69
+ }
70
+
71
+ guard let context = JSContext() else {
72
+ return nil
73
+ }
74
+
75
+ guard let script = getBundleScript() else {
76
+ print("[ExpoWidgets] Missing ExpoWidgets.bundle")
77
+ return nil
78
+ }
79
+
80
+ context.evaluateScript(script)
81
+ if let exceptionMessage = contextExceptionMessage(context) {
82
+ print("[ExpoWidgets] Bundle evaluation failed: \(exceptionMessage)")
83
+ return nil
84
+ }
85
+
86
+ self.context = context
87
+ return context
88
+ }
89
+
90
+ private func getBundleScript() -> String? {
91
+ if let bundleScript {
92
+ return bundleScript
93
+ }
94
+
95
+ guard let bundleURL = Bundle.main.url(forResource: "ExpoWidgets", withExtension: "bundle"),
96
+ let bundle = Bundle(url: bundleURL),
97
+ let url = bundle.url(forResource: "ExpoWidgets", withExtension: "bundle"),
98
+ let script = try? String(contentsOf: url, encoding: .utf8) else {
99
+ return nil
100
+ }
101
+
102
+ bundleScript = script
103
+ return script
104
+ }
105
+
106
+ private func getLayoutValue(_ layout: String, in context: JSContext) -> JSValue? {
107
+ if let layoutValue = layoutCache[layout] {
108
+ return layoutValue
109
+ }
110
+
111
+ context.exception = nil
112
+ guard let layoutValue = context.evaluateScript("(\(layout))"),
113
+ !layoutValue.isUndefined else {
114
+ return nil
115
+ }
116
+ guard context.exception == nil else {
117
+ return nil
118
+ }
119
+
120
+ layoutCache[layout] = layoutValue
121
+ return layoutValue
122
+ }
123
+
124
+ private func contextExceptionMessage(_ context: JSContext) -> String? {
125
+ guard let exception = context.exception else {
126
+ return nil
127
+ }
128
+
129
+ context.exception = nil
130
+ return exception.toString() ?? "Unknown JavaScript exception."
131
+ }
132
+ }
133
+
134
+ func evaluateWidgetLayout(
135
+ layout: String,
136
+ props: [String: Any]?,
137
+ environment: [String: Any]
138
+ ) -> WidgetJavaScriptResult<[String: Any]> {
139
+ WidgetsJSRuntime.shared.render(layout: layout, props: props, environment: environment)
140
+ }
141
+
142
+ func evaluateWidgetButtonPress(
143
+ layout: String,
144
+ props: [String: Any],
145
+ environment: [String: Any]
146
+ ) -> WidgetJavaScriptResult<[String: Any]?> {
147
+ WidgetsJSRuntime.shared.handlePress(layout: layout, props: props, environment: environment)
148
+ }
@@ -0,0 +1,31 @@
1
+ import ExpoModulesCore
2
+ import WidgetKit
3
+
4
+ final class WidgetObject: SharedObject {
5
+ let name: String
6
+ init(name: String, layout: String) {
7
+ self.name = name
8
+ WidgetsStorage.set(layout, forKey: "__expo_widgets_\(name)_layout")
9
+ }
10
+
11
+ func reload() {
12
+ WidgetCenter.shared.reloadTimelines(ofKind: name)
13
+ }
14
+
15
+ func updateTimeline(entries: [WidgetsJSTimelineEntry]) throws {
16
+ if WidgetsStorage.getString(forKey: "__expo_widgets_\(name)_layout") == nil {
17
+ throw UpdatedTimelineWithoutLayout(name)
18
+ }
19
+ WidgetsStorage.set(entries.map { $0.toDictionary() }, forKey: "__expo_widgets_\(name)_timeline")
20
+
21
+ self.reload()
22
+ }
23
+
24
+ func getTimeline() throws -> [WidgetsJSTimelineEntry] {
25
+ guard let entries = WidgetsStorage.getArray(forKey: "__expo_widgets_\(name)_timeline") as? [[String: Any]],
26
+ let appContext else {
27
+ return []
28
+ }
29
+ return try entries.map { try WidgetsJSTimelineEntry(from: $0, appContext: appContext) }
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import ExpoModulesCore
2
+
3
+ // Shared AppContext for widgets
4
+ struct WidgetsContext {
5
+ static let shared = WidgetsContext()
6
+ let context: AppContext = AppContext()
7
+ }
@@ -0,0 +1,16 @@
1
+ public enum WidgetsEventType: String {
2
+ case userEvent = "ExpoWidgetsUserInteraction"
3
+ }
4
+
5
+ public class WidgetsEvents {
6
+ public static let shared = WidgetsEvents()
7
+
8
+ private init() {}
9
+
10
+ public func sendNotification(type: WidgetsEventType, data: [String: Any]) {
11
+ var eventData = data
12
+ eventData["type"] = type.rawValue
13
+ let userInfo = ["eventData": eventData] as [String: Any]
14
+ NotificationCenter.default.post(name: onUserInteractionNotification, object: nil, userInfo: userInfo)
15
+ }
16
+ }
@@ -0,0 +1,25 @@
1
+ import ExpoModulesCore
2
+
3
+ internal final class LiveActivitiesNotSupportedException: Exception, @unchecked Sendable {
4
+ override var reason: String {
5
+ "Live Activities are not supported on this device"
6
+ }
7
+ }
8
+
9
+ internal final class StartLiveActivityException: GenericException<String>, @unchecked Sendable {
10
+ override var reason: String {
11
+ "Failed to start live activity: \(param)"
12
+ }
13
+ }
14
+
15
+ internal final class LiveActivityNotFoundException: GenericException<String>, @unchecked Sendable {
16
+ override var reason: String {
17
+ "Can't find live activity with id: \(param)"
18
+ }
19
+ }
20
+
21
+ internal final class UpdatedTimelineWithoutLayout: GenericException<String>, @unchecked Sendable {
22
+ override var reason: String {
23
+ "Cannot update widget timeline without a layout, first register a layout for: \(param)"
24
+ }
25
+ }
@@ -0,0 +1,149 @@
1
+ import ExpoModulesCore
2
+ import ActivityKit
3
+ import WidgetKit
4
+
5
+ let pushNotificationsEnabledKey: String = "ExpoWidgets_EnablePushNotifications"
6
+
7
+ let onUserInteraction = "onExpoWidgetsUserInteraction"
8
+ let onPushToStartTokenReceived = "onExpoWidgetsPushToStartTokenReceived"
9
+ let onTokenReceived = "onExpoWidgetsTokenReceived"
10
+ let onUserInteractionNotification = Notification.Name(onUserInteraction)
11
+
12
+ public final class WidgetsModule: Module {
13
+ var pushToStartTokenObserverTask: Task<Void, Never>?
14
+
15
+ public func definition() -> ModuleDefinition {
16
+ Name("ExpoWidgets")
17
+
18
+ Events(onPushToStartTokenReceived, onTokenReceived, onUserInteraction)
19
+
20
+ OnStartObserving(onUserInteraction) {
21
+ NotificationCenter.default.addObserver(
22
+ self,
23
+ selector: #selector(handleUserInteractionNotification),
24
+ name: onUserInteractionNotification,
25
+ object: nil
26
+ )
27
+ }
28
+
29
+ OnStopObserving(onUserInteraction) {
30
+ NotificationCenter.default.removeObserver(
31
+ self,
32
+ name: onUserInteractionNotification,
33
+ object: nil
34
+ )
35
+ }
36
+
37
+ OnStartObserving(onPushToStartTokenReceived) {
38
+ if pushNotificationsEnabled {
39
+ observePushToStartToken()
40
+ }
41
+ }
42
+
43
+ OnStopObserving(onPushToStartTokenReceived) {
44
+ pushToStartTokenObserverTask?.cancel()
45
+ pushToStartTokenObserverTask = nil
46
+ }
47
+
48
+ Constant("widgetsDirectory") { () -> String? in
49
+ guard let appGroupIdentifier = WidgetsStorage.appGroupIdentifier,
50
+ let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
51
+ return nil
52
+ }
53
+ let directoryUrl = containerUrl.appendingPathComponent("ExpoWidgets", isDirectory: true)
54
+ do {
55
+ try FileManager.default.createDirectory(at: directoryUrl, withIntermediateDirectories: true)
56
+ return directoryUrl.absoluteString
57
+ } catch {
58
+ return nil
59
+ }
60
+ }
61
+
62
+ Function("reloadAllWidgets") {
63
+ WidgetCenter.shared.reloadAllTimelines()
64
+ }
65
+
66
+ Class("Widget", WidgetObject.self) {
67
+ Constructor { (name: String, layout: String) in
68
+ WidgetObject(name: name, layout: layout)
69
+ }
70
+
71
+ Function("reload") { (widget: WidgetObject) in
72
+ widget.reload()
73
+ }
74
+
75
+ Function("updateTimeline") { (widget: WidgetObject, entries: [WidgetsJSTimelineEntry]) in
76
+ try widget.updateTimeline(entries: entries)
77
+ }
78
+
79
+ Function("getTimeline") { (widget: WidgetObject) in
80
+ try widget.getTimeline()
81
+ }
82
+ }
83
+
84
+ Class("LiveActivityFactory", LiveActivityFactory.self) {
85
+ Constructor { (name: String, layout: String) in
86
+ LiveActivityFactory(name: name, layout: layout)
87
+ }
88
+
89
+ Function("start") { (liveActivity: LiveActivityFactory, props: String?, url: URL?) in
90
+ try liveActivity.start(props: props, url: url)
91
+ }
92
+
93
+ Function("getInstances") { (liveActivity: LiveActivityFactory) in
94
+ try liveActivity.getInstances()
95
+ }
96
+ }
97
+
98
+ Class("LiveActivity", LiveActivity.self) {
99
+ AsyncFunction("update") { (instance: LiveActivity, props: String?) in
100
+ try await instance.update(props: props)
101
+ }
102
+
103
+ AsyncFunction("end") { (instance: LiveActivity, dismissalPolicy: LiveActivityDismissalPolicy?, afterDate: Date?, props: String?, contentDate: Date?) in
104
+ try await instance.end(dismissalPolicy: dismissalPolicy, afterDate: afterDate, props: props, contentDate: contentDate)
105
+ }
106
+
107
+ AsyncFunction("getPushToken") { (instance: LiveActivity) in
108
+ try instance.getPushToken()
109
+ }
110
+ }
111
+ }
112
+
113
+ @objc func handleUserInteractionNotification(_ notification: Notification) {
114
+ guard let userInfo = notification.userInfo as? [String: Any],
115
+ let eventData = userInfo["eventData"] as? [String: Any]
116
+ else { return }
117
+ self.sendEvent(onUserInteraction, eventData)
118
+ }
119
+
120
+ private func sendPushToStartToken(activityPushToStartToken: String) {
121
+ sendEvent(
122
+ onPushToStartTokenReceived,
123
+ [
124
+ "activityPushToStartToken": activityPushToStartToken
125
+ ]
126
+ )
127
+ }
128
+
129
+ private func observePushToStartToken() {
130
+ guard #available(iOS 17.2, *), ActivityAuthorizationInfo().areActivitiesEnabled else { return }
131
+ pushToStartTokenObserverTask = Task {
132
+ let initialToken = (Activity<LiveActivityAttributes>.pushToStartToken?.reduce("") { $0 + String(format: "%02x", $1) })
133
+ if let initialToken {
134
+ sendPushToStartToken(activityPushToStartToken: initialToken)
135
+ }
136
+
137
+ for await data in Activity<LiveActivityAttributes>.pushToStartTokenUpdates {
138
+ let token = data.reduce("") { $0 + String(format: "%02x", $1) }
139
+ if token != initialToken {
140
+ sendPushToStartToken(activityPushToStartToken: token)
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ private var pushNotificationsEnabled: Bool {
147
+ Bundle.main.object(forInfoDictionaryKey: pushNotificationsEnabledKey) as? Bool ?? false
148
+ }
149
+ }
@@ -0,0 +1,22 @@
1
+ import ExpoModulesCore
2
+ import ActivityKit
3
+
4
+ struct WidgetsJSTimelineEntry: Record {
5
+ @Field var timestamp: Int
6
+ @Field var props: [String: Any] = [:]
7
+ }
8
+
9
+ internal enum LiveActivityDismissalPolicy: String, Enumerable {
10
+ case `default`
11
+ case immediate
12
+ case after
13
+
14
+ @available(iOS 16.1, *)
15
+ internal func toDismissalPolicy(date: Date?) -> ActivityUIDismissalPolicy {
16
+ return switch self {
17
+ case .default: .default
18
+ case .immediate: .immediate
19
+ case .after: .after(date ?? Date.now)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,58 @@
1
+ public enum WidgetsStorage {
2
+ public static var appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String
3
+ static let defaults = UserDefaults(suiteName: appGroupIdentifier)
4
+
5
+ static func set(_ value: [String: Any], forKey key: String) {
6
+ guard let defaults else { return }
7
+
8
+ defaults.set(value, forKey: key)
9
+ }
10
+
11
+ static func set(_ value: [[String: Any]], forKey key: String) {
12
+ guard let defaults else { return }
13
+
14
+ defaults.set(value, forKey: key)
15
+ }
16
+
17
+ static func set(_ value: String, forKey key: String) {
18
+ guard let defaults else { return }
19
+
20
+ defaults.set(value, forKey: key)
21
+ }
22
+
23
+ static func set(_ value: Data, forKey key: String) {
24
+ guard let defaults else { return }
25
+
26
+ defaults.set(value, forKey: key)
27
+ }
28
+
29
+ public static func getDictionary(forKey key: String) -> [String: Any]? {
30
+ guard let defaults else { return nil }
31
+
32
+ return defaults.dictionary(forKey: key)
33
+ }
34
+
35
+ public static func getArray(forKey key: String) -> [Any]? {
36
+ guard let defaults else { return nil }
37
+
38
+ return defaults.array(forKey: key)
39
+ }
40
+
41
+ public static func getData(forKey key: String) -> Data? {
42
+ guard let defaults else { return nil }
43
+
44
+ return defaults.data(forKey: key)
45
+ }
46
+
47
+ public static func getString(forKey key: String) -> String? {
48
+ guard let defaults else { return nil }
49
+
50
+ return defaults.string(forKey: key)
51
+ }
52
+
53
+ static func removeObject(forKey key: String) {
54
+ guard let defaults else { return }
55
+
56
+ defaults.removeObject(forKey: key)
57
+ }
58
+ }