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
@@ -23,21 +23,18 @@ import Foundation
23
23
  throw LiveActivityError.invalidConfig
24
24
  }
25
25
 
26
- let activityType = config["activityType"] as? String ?? "UnknownActivity"
27
- let contentState = config["contentState"] as? [String: Any] ?? [:]
28
- let staleDate = config["staleDate"].flatMap { ($0 as? Double).map { Date(timeIntervalSince1970: $0 / 1000) } }
29
- let relevance = config["relevanceScore"] as? Double
30
-
31
26
  let activityId = UUID().uuidString
32
27
 
33
- // Store config for update/end actual ActivityKit usage requires generated ActivityAttributes
34
- // types, which the codegen produces. We store the info for the bridge layer to use.
35
- lock.lock()
36
- // Store a placeholder; concrete Activity objects are managed in generated code via
37
- // LiveActivityRegistry (generated by plugin/codegen/generateSwiftLiveActivities.ts)
38
- lock.unlock()
28
+ // When ActivityKit is available (iOS 16.1+) AND the plugin has run `npx expo prebuild`
29
+ // with `enableLiveActivities: true`, the generated InvokeLiveActivities.swift provides
30
+ // a real ActivityKit-backed extension on this class. That extension is called here.
31
+ // Without prebuild, activityId is returned but the activity is never registered.
32
+ #if canImport(ActivityKit)
33
+ if #available(iOS 16.1, *) {
34
+ try self.createActivity(activityId: activityId, config: config)
35
+ }
36
+ #endif
39
37
 
40
- _ = staleDate; _ = relevance; _ = contentState; _ = activityType
41
38
  return activityId
42
39
  }
43
40
 
@@ -72,8 +69,11 @@ import Foundation
72
69
  }
73
70
  }
74
71
 
75
- // LiveActivityRegistry is generated by generateSwiftLiveActivities.ts codegen.
76
- // Provide a stub so the module compiles without codegen having run yet.
72
+ // LiveActivityRegistry is generated by generateSwiftLiveActivities.ts codegen
73
+ // (runs during `npx expo prebuild` when enableLiveActivities: true).
74
+ // This stub compiles only when ActivityKit is unavailable so it doesn't conflict
75
+ // with the real implementation in InvokeLiveActivities.swift.
76
+ #if !canImport(ActivityKit)
77
77
  @objc public class LiveActivityRegistry: NSObject {
78
78
  @objc public static let shared = LiveActivityRegistry()
79
79
  private override init() {}
@@ -81,3 +81,4 @@ import Foundation
81
81
  @objc public func end(activityId: String, finalStateJson: String?) {}
82
82
  @objc public func listActivities() -> [[String: Any]] { return [] }
83
83
  }
84
+ #endif
@@ -123,6 +123,130 @@ public class ExpoInvokeModule: Module {
123
123
  )
124
124
  }
125
125
 
126
+ // ── Widget reload ─────────────────────────────────────────────────────────
127
+ Function("reloadWidget") { (widgetKind: String?) in
128
+ if #available(iOS 14.0, *) {
129
+ WidgetManager.reload(kind: widgetKind)
130
+ }
131
+ }
132
+
133
+ // ── Live Activity lifecycle (iOS 16.1+) ───────────────────────────────────
134
+ AsyncFunction("startLiveActivity") { (configJson: String) -> String in
135
+ if #available(iOS 16.1, *) {
136
+ return try await LiveActivityManager.shared.startActivity(configJson: configJson)
137
+ }
138
+ return ""
139
+ }
140
+
141
+ Function("updateLiveActivity") { (activityId: String, stateJson: String) in
142
+ if #available(iOS 16.1, *) {
143
+ LiveActivityManager.shared.updateActivity(activityId: activityId, stateJson: stateJson)
144
+ }
145
+ }
146
+
147
+ Function("endLiveActivity") { (activityId: String, finalStateJson: String?) in
148
+ if #available(iOS 16.1, *) {
149
+ LiveActivityManager.shared.endActivity(activityId: activityId, finalStateJson: finalStateJson)
150
+ }
151
+ }
152
+
153
+ AsyncFunction("getLiveActivities") { () -> String in
154
+ if #available(iOS 16.1, *) {
155
+ let activities = await LiveActivityManager.shared.listActivities()
156
+ if let data = try? JSONSerialization.data(withJSONObject: activities),
157
+ let json = String(data: data, encoding: .utf8) {
158
+ return json
159
+ }
160
+ }
161
+ return "[]"
162
+ }
163
+
164
+ // ── Focus filter result (iOS 16+) ─────────────────────────────────────────
165
+ Function("reportFocusFilterResult") { (filterId: String, success: Bool) in
166
+ if #available(iOS 16.0, *) {
167
+ FocusFilterRegistrar.shared.reportResult(filterId: filterId, success: success)
168
+ }
169
+ }
170
+
171
+ // ── App Entities (iOS 16.4+) ──────────────────────────────────────────────
172
+ Function("provideEntityResults") { (entityId: String, resultsJson: String) in
173
+ if #available(iOS 16.4, *) {
174
+ guard let data = resultsJson.data(using: .utf8),
175
+ let results = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
176
+ return
177
+ }
178
+ EntityQueryBridge.shared.receiveResults(entityId: entityId, results: results)
179
+ }
180
+ }
181
+
182
+ // ── Background intent (iOS 16+) ───────────────────────────────────────────
183
+ Function("registerBackgroundHandler") { (_: String) in
184
+ // No-op: the bridge routes events via writePendingIntent; JS registers via addIntentListener.
185
+ }
186
+
187
+ Function("reportBackgroundResult") { (intentId: String, resultJson: String) in
188
+ if #available(iOS 16.0, *) {
189
+ guard let data = resultJson.data(using: .utf8),
190
+ let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
191
+ return
192
+ }
193
+ let success = result["success"] as? Bool ?? false
194
+ let spokenResponse = result["spokenResponse"] as? String
195
+ let error = result["error"] as? String
196
+ ExpoInvokeBackgroundBridge.shared.reportResult(
197
+ intentId: intentId,
198
+ success: success,
199
+ spokenResponse: spokenResponse,
200
+ error: error
201
+ )
202
+ }
203
+ }
204
+
205
+ // ── Push-to-Talk (iOS 16+) ────────────────────────────────────────────────
206
+ AsyncFunction("startPTTSession") { (channelId: String, participantName: String?) in
207
+ if #available(iOS 16.0, *) {
208
+ try await PTTManager.shared.startSession(channelId: channelId, participantName: participantName)
209
+ }
210
+ }
211
+
212
+ AsyncFunction("endPTTSession") { (channelId: String) in
213
+ if #available(iOS 16.0, *) {
214
+ try await PTTManager.shared.endSession(channelId: channelId)
215
+ }
216
+ }
217
+
218
+ AsyncFunction("setTransmitting") { (channelId: String, isTransmitting: Bool) in
219
+ if #available(iOS 16.0, *) {
220
+ try await PTTManager.shared.setTransmitting(isTransmitting, channelId: channelId)
221
+ }
222
+ }
223
+
224
+ AsyncFunction("getPTTState") { (channelId: String) -> String in
225
+ let state: [String: Any]
226
+ if #available(iOS 16.0, *) {
227
+ state = PTTManager.shared.getState(channelId: channelId)
228
+ } else {
229
+ state = ["isActive": false, "isTransmitting": false]
230
+ }
231
+ guard let data = try? JSONSerialization.data(withJSONObject: state),
232
+ let json = String(data: data, encoding: .utf8) else {
233
+ return "{\"isActive\":false,\"isTransmitting\":false}"
234
+ }
235
+ return json
236
+ }
237
+
238
+ // ── Android Slices — no-op on iOS ─────────────────────────────────────────
239
+ Function("notifySliceChange") { (_: String) in /* iOS no-op */ }
240
+
241
+ // ── Persistent analytics — managed in JS, no native storage needed ────────
242
+ AsyncFunction("getAnalyticsPersisted") { () -> String in
243
+ return "{\"fired\":{},\"donated\":{},\"bySource\":{}}"
244
+ }
245
+
246
+ AsyncFunction("clearAnalyticsPersisted") {
247
+ // No-op: JS layer handles clearing via expo-file-system.
248
+ }
249
+
126
250
  // ── Focus mode ────────────────────────────────────────────────────────────
127
251
  Function("getFocusMode") { () -> String? in
128
252
  return FocusFilterManager.currentFocusMode()
@@ -1,19 +1,36 @@
1
1
  import Foundation
2
2
 
3
- /// Triggers a reload of all WidgetKit widget timelines so they display fresh data
4
- /// after setWidgetData() is called.
3
+ /// Triggers WidgetKit timeline reloads via reflection to avoid hard-linking WidgetKit.
5
4
  public struct WidgetManager {
5
+
6
6
  @available(iOS 14.0, *)
7
7
  public static func reloadAll() {
8
- // WidgetCenter is in WidgetKit use performSelector to avoid hard linking
9
- // the framework in packages that don't need it.
10
- if let widgetCenter = NSClassFromString("WidgetKit.WidgetCenter") as? NSObject.Type,
11
- let center = widgetCenter.value(forKey: "shared") as? NSObject {
12
- center.performSelector(inBackground: #selector(NSObject.reloadAllTimelines), with: nil)
8
+ guard let center = widgetCenter() else { return }
9
+ center.performSelector(inBackground: #selector(NSObject._invokeReloadAllTimelines), with: nil)
10
+ }
11
+
12
+ /// Reload a specific widget kind. Falls back to reloading all timelines if kind is nil.
13
+ @available(iOS 14.0, *)
14
+ public static func reload(kind: String?) {
15
+ guard let kind = kind, !kind.isEmpty else {
16
+ reloadAll()
17
+ return
13
18
  }
19
+ guard let center = widgetCenter() else { return }
20
+ let sel = NSSelectorFromString("reloadTimelinesOfKind:")
21
+ if center.responds(to: sel) {
22
+ center.perform(sel, with: kind)
23
+ } else {
24
+ center.performSelector(inBackground: #selector(NSObject._invokeReloadAllTimelines), with: nil)
25
+ }
26
+ }
27
+
28
+ private static func widgetCenter() -> NSObject? {
29
+ guard let cls = NSClassFromString("WidgetKit.WidgetCenter") as? NSObject.Type else { return nil }
30
+ return cls.value(forKey: "shared") as? NSObject
14
31
  }
15
32
  }
16
33
 
17
34
  extension NSObject {
18
- @objc func reloadAllTimelines() {}
35
+ @objc func _invokeReloadAllTimelines() {}
19
36
  }
@@ -0,0 +1,81 @@
1
+ import ExpoModulesCore
2
+ import ActivityKit
3
+
4
+ final class LiveActivity: SharedObject {
5
+ let id: String
6
+ let name: String
7
+ private var pushTokenObserverTask: Task<Void, Never>?
8
+
9
+ init(id: String, name: String) {
10
+ self.id = id
11
+ self.name = name
12
+ super.init()
13
+ }
14
+
15
+ func update(props: String?) async throws {
16
+ guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() }
17
+
18
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
19
+ throw LiveActivityNotFoundException(id)
20
+ }
21
+
22
+ let newState = LiveActivityAttributes.ContentState(name: name, props: props)
23
+ await activity.update(ActivityContent(state: newState, staleDate: nil))
24
+ }
25
+
26
+ func end(dismissalPolicy: LiveActivityDismissalPolicy?, afterDate: Date?, props: String?, contentDate: Date?) async throws {
27
+ guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() }
28
+
29
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
30
+ throw LiveActivityNotFoundException(id)
31
+ }
32
+
33
+ let content: ActivityContent<LiveActivityAttributes.ContentState>?
34
+ if let props {
35
+ content = ActivityContent(state: LiveActivityAttributes.ContentState(name: name, props: props), staleDate: nil)
36
+ } else {
37
+ content = nil
38
+ }
39
+
40
+ guard let contentDate, #available(iOS 17.2, *) else {
41
+ return await activity.end(content, dismissalPolicy: dismissalPolicy?.toDismissalPolicy(date: afterDate) ?? .default)
42
+ }
43
+
44
+ await activity.end(content, dismissalPolicy: dismissalPolicy?.toDismissalPolicy(date: afterDate) ?? .default, timestamp: contentDate)
45
+ }
46
+
47
+ func getPushToken() throws -> String? {
48
+ guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() }
49
+
50
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
51
+ throw LiveActivityNotFoundException(id)
52
+ }
53
+
54
+ guard let tokenData = activity.pushToken else { return nil }
55
+
56
+ return tokenData.reduce("") { $0 + String(format: "%02x", $1) }
57
+ }
58
+
59
+ @available(iOS 16.1, *)
60
+ func observePushTokenUpdates(for activity: Activity<LiveActivityAttributes>, pushNotificationsEnabled: Bool) {
61
+ guard pushNotificationsEnabled else {
62
+ return
63
+ }
64
+
65
+ pushTokenObserverTask?.cancel()
66
+ pushTokenObserverTask = Task {
67
+ for await data in activity.pushTokenUpdates {
68
+ let token = data.reduce("") { $0 + String(format: "%02x", $1) }
69
+ emit(event: onTokenReceived, payload: [
70
+ "activityId": activity.id,
71
+ "pushToken": token
72
+ ])
73
+ }
74
+ }
75
+ }
76
+
77
+ override func sharedObjectWillRelease() {
78
+ pushTokenObserverTask?.cancel()
79
+ pushTokenObserverTask = nil
80
+ }
81
+ }
@@ -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
+ }