expo-invoke 2.6.14 → 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 +36 -6
  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,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
+ }
@@ -0,0 +1,76 @@
1
+ const { getDefaultConfig } = require('expo/metro-config');
2
+ const path = require('path');
3
+ const { resolveWorkspaceRoot } = require('resolve-workspace-root');
4
+
5
+ const projectRoot = process.cwd();
6
+ const config = getDefaultConfig(projectRoot);
7
+
8
+ const expoStubPath = path.resolve(__dirname, './bundle/expo-stub.ts');
9
+ const expoModulesCoreStubPath = path.resolve(__dirname, './bundle/expo-modules-core-stub.ts');
10
+ const reactStubPath = path.resolve(__dirname, './bundle/react-stub.ts');
11
+ const reactNativeStubPath = path.resolve(__dirname, './bundle/react-native-stub.ts');
12
+ const jsxRuntimeStubPath = path.resolve(__dirname, './bundle/jsx-runtime-stub.ts');
13
+
14
+ // The `projectRoot` won't be included by default, since we alter it to be `__dirname`
15
+ // to bundle from `expo-widgets` as the main module
16
+ // NOTE: We check the `watchFolders` to start with `projectRoot`, since `expo/metro-config`
17
+ // might add folders if we're in a monorepo
18
+ const watchFolders = config.watchFolders;
19
+ if (!watchFolders.some((entry) => !entry.startsWith(projectRoot))) {
20
+ watchFolders.push(projectRoot);
21
+ }
22
+ // If expo-widgets is outside the user's project (e.g., symlinked, in a different monorepo,
23
+ // or as an installed dependency), add its workspace root so Metro can resolve its dependencies.
24
+ const rel = path.relative(projectRoot, __dirname);
25
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
26
+ const widgetWorkspaceRoot = resolveWorkspaceRoot(__dirname);
27
+ if (widgetWorkspaceRoot && !watchFolders.includes(widgetWorkspaceRoot)) {
28
+ watchFolders.push(widgetWorkspaceRoot);
29
+ }
30
+ }
31
+
32
+ const buildConfig = {
33
+ ...config,
34
+ projectRoot: __dirname, // Override root to be expo-widgets
35
+ watchFolders,
36
+ resolver: {
37
+ ...config.resolver,
38
+ resolveRequest(context, moduleName, platform) {
39
+ const fileSpecifierRe = /^[\\/]|^\.\.?(?:$|[\\/])/i;
40
+ if (fileSpecifierRe.test(moduleName)) {
41
+ return context.resolveRequest(context, moduleName, platform);
42
+ }
43
+ switch (moduleName) {
44
+ case 'expo':
45
+ return { type: 'sourceFile', filePath: expoStubPath };
46
+ case 'expo-modules-core':
47
+ return { type: 'sourceFile', filePath: expoModulesCoreStubPath };
48
+ case 'react':
49
+ return { type: 'sourceFile', filePath: reactStubPath };
50
+ case 'react/jsx-runtime':
51
+ case 'react/jsx-dev-runtime':
52
+ return { type: 'sourceFile', filePath: jsxRuntimeStubPath };
53
+ case 'react-native':
54
+ return { type: 'sourceFile', filePath: reactNativeStubPath };
55
+ case 'react-native-worklets':
56
+ case 'react-native-reanimated':
57
+ return { type: 'empty' };
58
+ default:
59
+ return context.resolveRequest(context, moduleName, platform);
60
+ }
61
+ },
62
+ },
63
+ transformer: {
64
+ ...config.transformer,
65
+ enableBabelRCLookup: false,
66
+ getTransformOptions: async () => ({
67
+ transform: { experimentalImportSupport: false, inlineRequires: false },
68
+ }),
69
+ },
70
+ serializer: {
71
+ ...config.serializer,
72
+ getPolyfills: () => [],
73
+ },
74
+ };
75
+
76
+ module.exports = buildConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-invoke",
3
- "version": "2.6.14",
3
+ "version": "2.7.0",
4
4
  "description": "The complete native-surface-to-JS bridge for Expo. One intent config → Siri, Google Assistant, home screen widgets, Dynamic Island, app icon menus, notification actions, NFC, QR, deep links and more — all through a single useInvoke() hook.",
5
5
  "main": "build/src/index.js",
6
6
  "module": "build/src/index.js",
@@ -24,6 +24,10 @@
24
24
  "ios",
25
25
  "android/src",
26
26
  "android/build.gradle",
27
+ "bundle",
28
+ "scripts",
29
+ "metro.widget.config.js",
30
+ "spm.config.json",
27
31
  "expo-module.config.json",
28
32
  "README.md",
29
33
  "MIGRATION.md",
@@ -77,6 +81,7 @@
77
81
  },
78
82
  "peerDependencies": {
79
83
  "@expo/config-plugins": ">=7.0.0",
84
+ "@expo/ui": ">=56.0.0",
80
85
  "expo": ">=50.0.0",
81
86
  "expo-modules-core": ">=1.12.0",
82
87
  "react": ">=18.0.0",
@@ -86,6 +91,9 @@
86
91
  "@expo/config-plugins": {
87
92
  "optional": false
88
93
  },
94
+ "@expo/ui": {
95
+ "optional": true
96
+ },
89
97
  "expo-modules-core": {
90
98
  "optional": true
91
99
  },
@@ -94,6 +102,8 @@
94
102
  }
95
103
  },
96
104
  "devDependencies": {
105
+ "@expo/plist": "^0.7.0",
106
+ "@expo/spawn-async": "^1.7.2",
97
107
  "@types/jest": "^29.5.14",
98
108
  "@types/node": "^20.19.41",
99
109
  "@types/react": "^19.2.16",
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+ require 'pathname'
3
+ require 'colored2' # dependency of CocoaPods
4
+
5
+ require File.join(File.dirname(`node --print "require.resolve('expo-modules-autolinking/package.json', { paths: ['#{__dir__}'] })"`), "scripts/ios/autolinking_manager")
6
+ require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
7
+
8
+ def use_expo_modules_widgets!(options = {})
9
+ output = Expo::AutolinkingManager.new(self, @current_target_definition, options).resolve
10
+
11
+ all_packages = output["modules"].map { |mod| mod["packageName"] }
12
+ used_packages = ["expo", "expo-invoke", "@expo/ui"]
13
+ packages_to_exclude = all_packages - used_packages
14
+
15
+ options[:exclude] = packages_to_exclude
16
+ use_expo_modules!(options)
17
+ end
18
+
19
+ def use_expo_native_module!(config_command = $default_command)
20
+ config = list_native_modules!(config_command)
21
+ config[:ios_packages].select! { |package| package[:name] == "expo" }
22
+
23
+ return link_native_modules!(config)
24
+ end
25
+
26
+ def expo_widgets_post_install(installer)
27
+ installer.pods_project.targets.each do |target|
28
+ if target.name == 'ExpoModulesCore' || target.name == 'ExpoUI' || target.name.start_with?('React-')
29
+ target.build_configurations.each do |config|
30
+ config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No'
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Build the ExpoWidgets.bundle.
4
+ // Use `yarn build:bundle` to run this script.
5
+
6
+ import spawn from '@expo/spawn-async';
7
+ import fs from 'node:fs';
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ import { argv } from 'node:process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ // NOTE: Modified from expo-constants/expo-updates entrypoint. We cd into the right folder anyway
14
+ const [, , possibleProjectRoot = process.cwd(), platform = 'ios', bundleOutput] = argv;
15
+
16
+ // TODO: Verify we can remove projectRoot validation, now that we no longer
17
+ // support React Native <= 62
18
+ let projectRoot;
19
+ if (fs.existsSync(path.join(possibleProjectRoot, 'package.json'))) {
20
+ projectRoot = possibleProjectRoot;
21
+ } else if (fs.existsSync(path.join(possibleProjectRoot, '..', 'package.json'))) {
22
+ projectRoot = path.resolve(possibleProjectRoot, '..');
23
+ } else {
24
+ throw new Error(
25
+ `Unable to locate project (no package.json found) at path: ${possibleProjectRoot}`
26
+ );
27
+ }
28
+
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = path.dirname(__filename);
31
+ const require = createRequire(__dirname);
32
+
33
+ // NODE_BINARY is set for Xcode builds via the `with-node.sh` script.
34
+ const nodePath = process.env.NODE_BINARY || 'node';
35
+ const outputDir = path.join(__dirname, '../bundle/build');
36
+ const defaultBundleOutput = path.join(outputDir, 'ExpoWidgets.bundle');
37
+ const appBundlePath = path.resolve(projectRoot, bundleOutput ?? defaultBundleOutput);
38
+
39
+ await fs.promises.rm(appBundlePath, { recursive: true, force: true });
40
+ await fs.promises.mkdir(path.dirname(appBundlePath), { recursive: true });
41
+
42
+ const result = await spawn(
43
+ nodePath,
44
+ [
45
+ require.resolve('expo/bin/cli'),
46
+ 'export:embed',
47
+ '--platform',
48
+ platform,
49
+ '--bundle-output',
50
+ appBundlePath,
51
+ '--entry-file',
52
+ path.join(__dirname, '../bundle/index.ts'),
53
+ '--dev',
54
+ 'false',
55
+ '--skip-server',
56
+ ],
57
+ {
58
+ stdio: 'inherit',
59
+ cwd: projectRoot,
60
+ env: {
61
+ ...process.env,
62
+ EXPO_OVERRIDE_METRO_CONFIG: path.join(__dirname, '../metro.widget.config.js'),
63
+ },
64
+ }
65
+ );
66
+
67
+ if (result.error) {
68
+ process.exit(1);
69
+ }