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.
- package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +35 -0
- package/android/src/main/java/expo/modules/invoke/InvokeSliceProvider.kt +9 -0
- package/build/plugin/codegen/generateSwiftIntents.js +22 -0
- package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
- package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
- package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
- package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
- package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
- package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
- package/build/plugin/ios/widgets/withEasConfig.js +52 -0
- package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
- package/build/plugin/ios/widgets/withIosWarning.js +10 -0
- package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
- package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
- package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
- package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
- package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
- package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
- package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
- package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
- package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
- package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
- package/build/plugin/ios/withIOSInvoke.js +54 -25
- package/build/plugin/ios/withIOSInvoke.js.map +1 -1
- package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
- package/build/plugin/ios/withWidgetExtension.js +36 -6
- package/build/plugin/ios/withWidgetExtension.js.map +1 -1
- package/build/plugin/src/withInvoke.d.ts +34 -23
- package/build/plugin/src/withInvoke.js +16 -27
- package/build/plugin/src/withInvoke.js.map +1 -1
- package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
- package/build/plugin/types/WidgetConfig.type.js +3 -0
- package/build/plugin/types/WidgetConfig.type.js.map +1 -0
- package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
- package/build/plugin/types/WidgetFamily.type.js +13 -0
- package/build/plugin/types/WidgetFamily.type.js.map +1 -0
- package/build/src/hooks/useLiveActivity.d.ts +20 -10
- package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
- package/build/src/hooks/useLiveActivity.js +20 -10
- package/build/src/hooks/useLiveActivity.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +5 -0
- package/build/src/index.js.map +1 -1
- package/build/src/module.d.ts +4 -0
- package/build/src/module.d.ts.map +1 -1
- package/build/src/module.js +8 -0
- package/build/src/module.js.map +1 -1
- package/build/src/web/index.d.ts +10 -1
- package/build/src/web/index.d.ts.map +1 -1
- package/build/src/web/index.js +4 -0
- package/build/src/web/index.js.map +1 -1
- package/build/src/widgets/ExpoWidgets.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.js +3 -0
- package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
- package/build/src/widgets/ExpoWidgets.js +42 -0
- package/build/src/widgets/ExpoWidgets.js.map +1 -0
- package/build/src/widgets/Widgets.d.ts +119 -0
- package/build/src/widgets/Widgets.d.ts.map +1 -0
- package/build/src/widgets/Widgets.js +158 -0
- package/build/src/widgets/Widgets.js.map +1 -0
- package/build/src/widgets/Widgets.types.d.ts +263 -0
- package/build/src/widgets/Widgets.types.d.ts.map +1 -0
- package/build/src/widgets/Widgets.types.js +2 -0
- package/build/src/widgets/Widgets.types.js.map +1 -0
- package/build/src/widgets/index.d.ts +3 -0
- package/build/src/widgets/index.d.ts.map +1 -0
- package/build/src/widgets/index.js +2 -0
- package/build/src/widgets/index.js.map +1 -0
- package/bundle/__tests__/decorator.test.ts +89 -0
- package/bundle/decorator.ts +61 -0
- package/bundle/expo-modules-core-stub.ts +1 -0
- package/bundle/expo-stub.ts +15 -0
- package/bundle/index.ts +69 -0
- package/bundle/jsx-runtime-stub.ts +71 -0
- package/bundle/react-native-stub.ts +3 -0
- package/bundle/react-stub.ts +13 -0
- package/expo-module.config.json +1 -1
- package/ios/LiveActivity/LiveActivityManager.swift +15 -14
- package/ios/Module/ExpoInvokeModule.swift +124 -0
- package/ios/Widget/WidgetManager.swift +25 -8
- package/ios/Widgets/LiveActivity.swift +81 -0
- package/ios/Widgets/LiveActivityFactory.swift +49 -0
- package/ios/Widgets/Widget/AppIntent.swift +130 -0
- package/ios/Widgets/Widget/Buttons.swift +62 -0
- package/ios/Widgets/Widget/DynamicView.swift +161 -0
- package/ios/Widgets/Widget/EntryView.swift +36 -0
- package/ios/Widgets/Widget/Fragment.swift +13 -0
- package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
- package/ios/Widgets/Widget/RedBoxView.swift +83 -0
- package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
- package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
- package/ios/Widgets/Widget/Type.swift +6 -0
- package/ios/Widgets/Widget/Utils.swift +116 -0
- package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
- package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
- package/ios/Widgets/WidgetObject.swift +31 -0
- package/ios/Widgets/WidgetsContext.swift +7 -0
- package/ios/Widgets/WidgetsEvents.swift +16 -0
- package/ios/Widgets/WidgetsExceptions.swift +25 -0
- package/ios/Widgets/WidgetsModule.swift +149 -0
- package/ios/Widgets/WidgetsRecords.swift +22 -0
- package/ios/Widgets/WidgetsStorage.swift +58 -0
- package/metro.widget.config.js +76 -0
- package/package.json +11 -1
- package/scripts/autolinking.rb +34 -0
- package/scripts/build-bundle.mjs +69 -0
- package/scripts/with-node.sh +44 -0
- package/scripts/xcode-build-bundle.sh +23 -0
- 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
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
+
}
|