@idealyst/live-activity 1.2.114
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/build.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityModule.kt +299 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityPackage.kt +16 -0
- package/android/src/main/java/io/idealyst/liveactivity/LiveUpdateNotification.kt +265 -0
- package/idealyst-live-activity.podspec +22 -0
- package/ios/IdealystLiveActivity-Bridging-Header.h +2 -0
- package/ios/IdealystLiveActivity.mm +55 -0
- package/ios/IdealystLiveActivity.swift +571 -0
- package/ios/Templates/ActivityAttributes.swift +66 -0
- package/ios/Templates/DeliveryActivityView.swift +143 -0
- package/ios/Templates/IdealystActivityBundle.swift +18 -0
- package/ios/Templates/MediaActivityView.swift +124 -0
- package/ios/Templates/ProgressActivityView.swift +164 -0
- package/ios/Templates/TimerActivityView.swift +110 -0
- package/package.json +80 -0
- package/src/NativeLiveActivitySpec.ts +49 -0
- package/src/activity/activity.native.ts +198 -0
- package/src/activity/activity.web.ts +78 -0
- package/src/activity/useLiveActivity.ts +267 -0
- package/src/constants.ts +39 -0
- package/src/errors.ts +57 -0
- package/src/index.native.ts +59 -0
- package/src/index.ts +61 -0
- package/src/index.web.ts +2 -0
- package/src/templates/presets.ts +91 -0
- package/src/templates/types.ts +14 -0
- package/src/types.ts +343 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(IdealystLiveActivity, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(isSupported)
|
|
7
|
+
|
|
8
|
+
RCT_EXTERN_METHOD(isEnabled:
|
|
9
|
+
(RCTPromiseResolveBlock)resolve
|
|
10
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(startActivity:
|
|
13
|
+
(NSString *)templateType
|
|
14
|
+
attributesJson:(NSString *)attributesJson
|
|
15
|
+
contentStateJson:(NSString *)contentStateJson
|
|
16
|
+
optionsJson:(NSString *)optionsJson
|
|
17
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
19
|
+
|
|
20
|
+
RCT_EXTERN_METHOD(updateActivity:
|
|
21
|
+
(NSString *)activityId
|
|
22
|
+
contentStateJson:(NSString *)contentStateJson
|
|
23
|
+
alertConfigJson:(NSString *)alertConfigJson
|
|
24
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
25
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
26
|
+
|
|
27
|
+
RCT_EXTERN_METHOD(endActivity:
|
|
28
|
+
(NSString *)activityId
|
|
29
|
+
finalContentStateJson:(NSString *)finalContentStateJson
|
|
30
|
+
dismissalPolicy:(NSString *)dismissalPolicy
|
|
31
|
+
dismissAfter:(double)dismissAfter
|
|
32
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
33
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
34
|
+
|
|
35
|
+
RCT_EXTERN_METHOD(endAllActivities:
|
|
36
|
+
(NSString *)dismissalPolicy
|
|
37
|
+
dismissAfter:(double)dismissAfter
|
|
38
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
39
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
40
|
+
|
|
41
|
+
RCT_EXTERN_METHOD(getActivity:
|
|
42
|
+
(NSString *)activityId
|
|
43
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
44
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
45
|
+
|
|
46
|
+
RCT_EXTERN_METHOD(listActivities:
|
|
47
|
+
(RCTPromiseResolveBlock)resolve
|
|
48
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
49
|
+
|
|
50
|
+
RCT_EXTERN_METHOD(getPushToken:
|
|
51
|
+
(NSString *)activityId
|
|
52
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
53
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
54
|
+
|
|
55
|
+
@end
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ActivityKit
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
@available(iOS 16.2, *)
|
|
6
|
+
@objc(IdealystLiveActivity)
|
|
7
|
+
class IdealystLiveActivity: RCTEventEmitter {
|
|
8
|
+
|
|
9
|
+
private var pushTokenTasks: [String: Task<Void, Never>] = [:]
|
|
10
|
+
private var activityStateTasks: [String: Task<Void, Never>] = [:]
|
|
11
|
+
|
|
12
|
+
// MARK: - RCTEventEmitter
|
|
13
|
+
|
|
14
|
+
override func supportedEvents() -> [String]! {
|
|
15
|
+
return ["IdealystLiveActivityEvent"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - Availability
|
|
23
|
+
|
|
24
|
+
@objc
|
|
25
|
+
func isSupported() -> Bool {
|
|
26
|
+
if #available(iOS 16.2, *) {
|
|
27
|
+
return ActivityAuthorizationInfo().areActivitiesEnabled
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc
|
|
33
|
+
func isEnabled(
|
|
34
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
35
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
36
|
+
) {
|
|
37
|
+
if #available(iOS 16.2, *) {
|
|
38
|
+
resolve(ActivityAuthorizationInfo().areActivitiesEnabled)
|
|
39
|
+
} else {
|
|
40
|
+
resolve(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Start Activity
|
|
45
|
+
|
|
46
|
+
@objc
|
|
47
|
+
func startActivity(
|
|
48
|
+
_ templateType: String,
|
|
49
|
+
attributesJson: String,
|
|
50
|
+
contentStateJson: String,
|
|
51
|
+
optionsJson: String,
|
|
52
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
53
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
54
|
+
) {
|
|
55
|
+
guard #available(iOS 16.2, *) else {
|
|
56
|
+
reject("not_supported", "Live Activities require iOS 16.2 or later", nil)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
do {
|
|
61
|
+
let optionsData = optionsJson.data(using: .utf8) ?? Data()
|
|
62
|
+
let options = try JSONSerialization.jsonObject(with: optionsData) as? [String: Any] ?? [:]
|
|
63
|
+
let enablePush = options["enablePushUpdates"] as? Bool ?? false
|
|
64
|
+
let iosOptions = options["ios"] as? [String: Any]
|
|
65
|
+
let relevanceScore = iosOptions?["relevanceScore"] as? Double
|
|
66
|
+
let staleDateMs = iosOptions?["staleDate"] as? Double
|
|
67
|
+
|
|
68
|
+
switch templateType {
|
|
69
|
+
case "delivery":
|
|
70
|
+
let result = try startTypedActivity(
|
|
71
|
+
DeliveryAttributes.self,
|
|
72
|
+
attributesJson: attributesJson,
|
|
73
|
+
contentStateJson: contentStateJson,
|
|
74
|
+
enablePush: enablePush,
|
|
75
|
+
relevanceScore: relevanceScore,
|
|
76
|
+
staleDateMs: staleDateMs
|
|
77
|
+
)
|
|
78
|
+
resolve(result)
|
|
79
|
+
|
|
80
|
+
case "timer":
|
|
81
|
+
let result = try startTypedActivity(
|
|
82
|
+
TimerAttributes.self,
|
|
83
|
+
attributesJson: attributesJson,
|
|
84
|
+
contentStateJson: contentStateJson,
|
|
85
|
+
enablePush: enablePush,
|
|
86
|
+
relevanceScore: relevanceScore,
|
|
87
|
+
staleDateMs: staleDateMs
|
|
88
|
+
)
|
|
89
|
+
resolve(result)
|
|
90
|
+
|
|
91
|
+
case "media":
|
|
92
|
+
let result = try startTypedActivity(
|
|
93
|
+
MediaAttributes.self,
|
|
94
|
+
attributesJson: attributesJson,
|
|
95
|
+
contentStateJson: contentStateJson,
|
|
96
|
+
enablePush: enablePush,
|
|
97
|
+
relevanceScore: relevanceScore,
|
|
98
|
+
staleDateMs: staleDateMs
|
|
99
|
+
)
|
|
100
|
+
resolve(result)
|
|
101
|
+
|
|
102
|
+
case "progress":
|
|
103
|
+
let result = try startTypedActivity(
|
|
104
|
+
ProgressAttributes.self,
|
|
105
|
+
attributesJson: attributesJson,
|
|
106
|
+
contentStateJson: contentStateJson,
|
|
107
|
+
enablePush: enablePush,
|
|
108
|
+
relevanceScore: relevanceScore,
|
|
109
|
+
staleDateMs: staleDateMs
|
|
110
|
+
)
|
|
111
|
+
resolve(result)
|
|
112
|
+
|
|
113
|
+
default:
|
|
114
|
+
reject("template_not_found", "Unknown template type: \(templateType)", nil)
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
reject("start_failed", error.localizedDescription, error)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@available(iOS 16.2, *)
|
|
122
|
+
private func startTypedActivity<T: IdealystActivityAttributes>(
|
|
123
|
+
_ type: T.Type,
|
|
124
|
+
attributesJson: String,
|
|
125
|
+
contentStateJson: String,
|
|
126
|
+
enablePush: Bool,
|
|
127
|
+
relevanceScore: Double?,
|
|
128
|
+
staleDateMs: Double?
|
|
129
|
+
) throws -> String {
|
|
130
|
+
let decoder = JSONDecoder()
|
|
131
|
+
let attributesData = attributesJson.data(using: .utf8)!
|
|
132
|
+
let contentData = contentStateJson.data(using: .utf8)!
|
|
133
|
+
|
|
134
|
+
let attributes = try decoder.decode(T.self, from: attributesData)
|
|
135
|
+
let contentState = try decoder.decode(T.ContentState.self, from: contentData)
|
|
136
|
+
|
|
137
|
+
let initialContent = ActivityContent(
|
|
138
|
+
state: contentState,
|
|
139
|
+
staleDate: staleDateMs.map { Date(timeIntervalSince1970: $0 / 1000) },
|
|
140
|
+
relevanceScore: relevanceScore ?? 0
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
let activity = try Activity.request(
|
|
144
|
+
attributes: attributes,
|
|
145
|
+
content: initialContent,
|
|
146
|
+
pushType: enablePush ? .token : nil
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Observe push token updates
|
|
150
|
+
if enablePush {
|
|
151
|
+
observePushTokenUpdates(for: activity)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Observe activity state changes
|
|
155
|
+
observeActivityState(for: activity)
|
|
156
|
+
|
|
157
|
+
return activityToJson(activity)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MARK: - Update Activity
|
|
161
|
+
|
|
162
|
+
@objc
|
|
163
|
+
func updateActivity(
|
|
164
|
+
_ activityId: String,
|
|
165
|
+
contentStateJson: String,
|
|
166
|
+
alertConfigJson: String?,
|
|
167
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
168
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
169
|
+
) {
|
|
170
|
+
guard #available(iOS 16.2, *) else {
|
|
171
|
+
reject("not_supported", "Live Activities require iOS 16.2 or later", nil)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Task {
|
|
176
|
+
do {
|
|
177
|
+
// Try each template type
|
|
178
|
+
if try await updateTypedActivity(DeliveryAttributes.self, activityId: activityId, contentStateJson: contentStateJson, alertConfigJson: alertConfigJson) {
|
|
179
|
+
resolve(nil)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if try await updateTypedActivity(TimerAttributes.self, activityId: activityId, contentStateJson: contentStateJson, alertConfigJson: alertConfigJson) {
|
|
183
|
+
resolve(nil)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
if try await updateTypedActivity(MediaAttributes.self, activityId: activityId, contentStateJson: contentStateJson, alertConfigJson: alertConfigJson) {
|
|
187
|
+
resolve(nil)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if try await updateTypedActivity(ProgressAttributes.self, activityId: activityId, contentStateJson: contentStateJson, alertConfigJson: alertConfigJson) {
|
|
191
|
+
resolve(nil)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
reject("activity_not_found", "No activity found with id: \(activityId)", nil)
|
|
196
|
+
} catch {
|
|
197
|
+
reject("update_failed", error.localizedDescription, error)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@available(iOS 16.2, *)
|
|
203
|
+
private func updateTypedActivity<T: IdealystActivityAttributes>(
|
|
204
|
+
_ type: T.Type,
|
|
205
|
+
activityId: String,
|
|
206
|
+
contentStateJson: String,
|
|
207
|
+
alertConfigJson: String?
|
|
208
|
+
) async throws -> Bool {
|
|
209
|
+
guard let activity = Activity<T>.activities.first(where: { $0.id == activityId }) else {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let decoder = JSONDecoder()
|
|
214
|
+
let contentData = contentStateJson.data(using: .utf8)!
|
|
215
|
+
let contentState = try decoder.decode(T.ContentState.self, from: contentData)
|
|
216
|
+
|
|
217
|
+
let updatedContent = ActivityContent(state: contentState, staleDate: nil)
|
|
218
|
+
|
|
219
|
+
if #available(iOS 16.2, *) {
|
|
220
|
+
if let alertJson = alertConfigJson,
|
|
221
|
+
let alertData = alertJson.data(using: .utf8),
|
|
222
|
+
let alertDict = try JSONSerialization.jsonObject(with: alertData) as? [String: Any] {
|
|
223
|
+
let alertConfig = AlertConfiguration(
|
|
224
|
+
title: LocalizedStringResource(stringLiteral: alertDict["title"] as? String ?? ""),
|
|
225
|
+
body: LocalizedStringResource(stringLiteral: alertDict["body"] as? String ?? ""),
|
|
226
|
+
sound: (alertDict["sound"] as? Bool ?? false) ? .default : nil
|
|
227
|
+
)
|
|
228
|
+
await activity.update(updatedContent, alertConfiguration: alertConfig)
|
|
229
|
+
} else {
|
|
230
|
+
await activity.update(updatedContent)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// MARK: - End Activity
|
|
238
|
+
|
|
239
|
+
@objc
|
|
240
|
+
func endActivity(
|
|
241
|
+
_ activityId: String,
|
|
242
|
+
finalContentStateJson: String?,
|
|
243
|
+
dismissalPolicy: String,
|
|
244
|
+
dismissAfter: Double,
|
|
245
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
246
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
247
|
+
) {
|
|
248
|
+
guard #available(iOS 16.2, *) else {
|
|
249
|
+
reject("not_supported", "Live Activities require iOS 16.2 or later", nil)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Task {
|
|
254
|
+
do {
|
|
255
|
+
if try await endTypedActivity(DeliveryAttributes.self, activityId: activityId, finalContentStateJson: finalContentStateJson, dismissalPolicy: dismissalPolicy, dismissAfter: dismissAfter) {
|
|
256
|
+
resolve(nil)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
if try await endTypedActivity(TimerAttributes.self, activityId: activityId, finalContentStateJson: finalContentStateJson, dismissalPolicy: dismissalPolicy, dismissAfter: dismissAfter) {
|
|
260
|
+
resolve(nil)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if try await endTypedActivity(MediaAttributes.self, activityId: activityId, finalContentStateJson: finalContentStateJson, dismissalPolicy: dismissalPolicy, dismissAfter: dismissAfter) {
|
|
264
|
+
resolve(nil)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if try await endTypedActivity(ProgressAttributes.self, activityId: activityId, finalContentStateJson: finalContentStateJson, dismissalPolicy: dismissalPolicy, dismissAfter: dismissAfter) {
|
|
268
|
+
resolve(nil)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
reject("activity_not_found", "No activity found with id: \(activityId)", nil)
|
|
273
|
+
} catch {
|
|
274
|
+
reject("end_failed", error.localizedDescription, error)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@available(iOS 16.2, *)
|
|
280
|
+
private func endTypedActivity<T: IdealystActivityAttributes>(
|
|
281
|
+
_ type: T.Type,
|
|
282
|
+
activityId: String,
|
|
283
|
+
finalContentStateJson: String?,
|
|
284
|
+
dismissalPolicy: String,
|
|
285
|
+
dismissAfter: Double
|
|
286
|
+
) async throws -> Bool {
|
|
287
|
+
guard let activity = Activity<T>.activities.first(where: { $0.id == activityId }) else {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Cancel observation tasks
|
|
292
|
+
pushTokenTasks[activityId]?.cancel()
|
|
293
|
+
pushTokenTasks.removeValue(forKey: activityId)
|
|
294
|
+
activityStateTasks[activityId]?.cancel()
|
|
295
|
+
activityStateTasks.removeValue(forKey: activityId)
|
|
296
|
+
|
|
297
|
+
let policy = parseDismissalPolicy(dismissalPolicy, dismissAfter: dismissAfter)
|
|
298
|
+
|
|
299
|
+
if let json = finalContentStateJson,
|
|
300
|
+
let data = json.data(using: .utf8) {
|
|
301
|
+
let decoder = JSONDecoder()
|
|
302
|
+
let finalState = try decoder.decode(T.ContentState.self, from: data)
|
|
303
|
+
let finalContent = ActivityContent(state: finalState, staleDate: nil)
|
|
304
|
+
await activity.end(finalContent, dismissalPolicy: policy)
|
|
305
|
+
} else {
|
|
306
|
+
await activity.end(nil, dismissalPolicy: policy)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@objc
|
|
313
|
+
func endAllActivities(
|
|
314
|
+
_ dismissalPolicy: String,
|
|
315
|
+
dismissAfter: Double,
|
|
316
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
317
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
318
|
+
) {
|
|
319
|
+
guard #available(iOS 16.2, *) else {
|
|
320
|
+
reject("not_supported", "Live Activities require iOS 16.2 or later", nil)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
Task {
|
|
325
|
+
let policy = parseDismissalPolicy(dismissalPolicy, dismissAfter: dismissAfter)
|
|
326
|
+
|
|
327
|
+
// Cancel all observation tasks
|
|
328
|
+
for task in pushTokenTasks.values { task.cancel() }
|
|
329
|
+
pushTokenTasks.removeAll()
|
|
330
|
+
for task in activityStateTasks.values { task.cancel() }
|
|
331
|
+
activityStateTasks.removeAll()
|
|
332
|
+
|
|
333
|
+
for activity in Activity<DeliveryAttributes>.activities {
|
|
334
|
+
await activity.end(nil, dismissalPolicy: policy)
|
|
335
|
+
}
|
|
336
|
+
for activity in Activity<TimerAttributes>.activities {
|
|
337
|
+
await activity.end(nil, dismissalPolicy: policy)
|
|
338
|
+
}
|
|
339
|
+
for activity in Activity<MediaAttributes>.activities {
|
|
340
|
+
await activity.end(nil, dismissalPolicy: policy)
|
|
341
|
+
}
|
|
342
|
+
for activity in Activity<ProgressAttributes>.activities {
|
|
343
|
+
await activity.end(nil, dismissalPolicy: policy)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
resolve(nil)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// MARK: - Queries
|
|
351
|
+
|
|
352
|
+
@objc
|
|
353
|
+
func getActivity(
|
|
354
|
+
_ activityId: String,
|
|
355
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
356
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
357
|
+
) {
|
|
358
|
+
guard #available(iOS 16.2, *) else {
|
|
359
|
+
resolve(nil)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if let json = findActivity(DeliveryAttributes.self, id: activityId) ??
|
|
364
|
+
findActivity(TimerAttributes.self, id: activityId) ??
|
|
365
|
+
findActivity(MediaAttributes.self, id: activityId) ??
|
|
366
|
+
findActivity(ProgressAttributes.self, id: activityId) {
|
|
367
|
+
resolve(json)
|
|
368
|
+
} else {
|
|
369
|
+
resolve(nil)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@objc
|
|
374
|
+
func listActivities(
|
|
375
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
376
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
377
|
+
) {
|
|
378
|
+
guard #available(iOS 16.2, *) else {
|
|
379
|
+
resolve("[]")
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var results: [String] = []
|
|
384
|
+
for activity in Activity<DeliveryAttributes>.activities {
|
|
385
|
+
results.append(activityToJson(activity))
|
|
386
|
+
}
|
|
387
|
+
for activity in Activity<TimerAttributes>.activities {
|
|
388
|
+
results.append(activityToJson(activity))
|
|
389
|
+
}
|
|
390
|
+
for activity in Activity<MediaAttributes>.activities {
|
|
391
|
+
results.append(activityToJson(activity))
|
|
392
|
+
}
|
|
393
|
+
for activity in Activity<ProgressAttributes>.activities {
|
|
394
|
+
results.append(activityToJson(activity))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
resolve("[\(results.joined(separator: ","))]")
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
@objc
|
|
401
|
+
func getPushToken(
|
|
402
|
+
_ activityId: String,
|
|
403
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
404
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
405
|
+
) {
|
|
406
|
+
guard #available(iOS 16.2, *) else {
|
|
407
|
+
resolve(nil)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if let token = findPushToken(DeliveryAttributes.self, id: activityId) ??
|
|
412
|
+
findPushToken(TimerAttributes.self, id: activityId) ??
|
|
413
|
+
findPushToken(MediaAttributes.self, id: activityId) ??
|
|
414
|
+
findPushToken(ProgressAttributes.self, id: activityId) {
|
|
415
|
+
resolve(token)
|
|
416
|
+
} else {
|
|
417
|
+
resolve(nil)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// MARK: - Private Helpers
|
|
422
|
+
|
|
423
|
+
@available(iOS 16.2, *)
|
|
424
|
+
private func observePushTokenUpdates<T: ActivityAttributes>(for activity: Activity<T>) {
|
|
425
|
+
let task = Task {
|
|
426
|
+
for await tokenData in activity.pushTokenUpdates {
|
|
427
|
+
let tokenString = tokenData.map { String(format: "%02x", $0) }.joined()
|
|
428
|
+
let event: [String: Any] = [
|
|
429
|
+
"type": "tokenUpdated",
|
|
430
|
+
"activityId": activity.id,
|
|
431
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
432
|
+
"payload": [
|
|
433
|
+
"token": [
|
|
434
|
+
"token": tokenString,
|
|
435
|
+
"platform": "ios",
|
|
436
|
+
"activityId": activity.id
|
|
437
|
+
]
|
|
438
|
+
]
|
|
439
|
+
]
|
|
440
|
+
self.sendEvent(withName: "IdealystLiveActivityEvent", body: event)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
pushTokenTasks[activity.id] = task
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
@available(iOS 16.2, *)
|
|
447
|
+
private func observeActivityState<T: ActivityAttributes>(for activity: Activity<T>) {
|
|
448
|
+
let task = Task {
|
|
449
|
+
for await state in activity.activityStateUpdates {
|
|
450
|
+
let stateString: String
|
|
451
|
+
switch state {
|
|
452
|
+
case .active: stateString = "active"
|
|
453
|
+
case .dismissed: stateString = "ended"
|
|
454
|
+
case .ended: stateString = "ended"
|
|
455
|
+
case .stale: stateString = "stale"
|
|
456
|
+
@unknown default: stateString = "unknown"
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let eventType = state == .stale ? "stale" : (state == .dismissed || state == .ended ? "ended" : "updated")
|
|
460
|
+
|
|
461
|
+
let event: [String: Any] = [
|
|
462
|
+
"type": eventType,
|
|
463
|
+
"activityId": activity.id,
|
|
464
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
465
|
+
"payload": [
|
|
466
|
+
"state": stateString
|
|
467
|
+
]
|
|
468
|
+
]
|
|
469
|
+
self.sendEvent(withName: "IdealystLiveActivityEvent", body: event)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
activityStateTasks[activity.id] = task
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@available(iOS 16.2, *)
|
|
476
|
+
private func activityToJson<T: ActivityAttributes>(_ activity: Activity<T>) -> String {
|
|
477
|
+
let encoder = JSONEncoder()
|
|
478
|
+
let stateString: String
|
|
479
|
+
switch activity.activityState {
|
|
480
|
+
case .active: stateString = "active"
|
|
481
|
+
case .dismissed: stateString = "ended"
|
|
482
|
+
case .ended: stateString = "ended"
|
|
483
|
+
case .stale: stateString = "stale"
|
|
484
|
+
@unknown default: stateString = "unknown"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
var dict: [String: Any] = [
|
|
488
|
+
"id": activity.id,
|
|
489
|
+
"state": stateString,
|
|
490
|
+
"startedAt": Date().timeIntervalSince1970 * 1000,
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
// Encode attributes and content state
|
|
494
|
+
if let attrData = try? encoder.encode(activity.attributes),
|
|
495
|
+
let attrDict = try? JSONSerialization.jsonObject(with: attrData) {
|
|
496
|
+
dict["attributes"] = attrDict
|
|
497
|
+
}
|
|
498
|
+
if let contentData = try? encoder.encode(activity.content.state),
|
|
499
|
+
let contentDict = try? JSONSerialization.jsonObject(with: contentData) {
|
|
500
|
+
dict["contentState"] = contentDict
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Determine template type from attributes type
|
|
504
|
+
dict["templateType"] = templateTypeFor(T.self)
|
|
505
|
+
|
|
506
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: dict),
|
|
507
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
508
|
+
return jsonString
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return "{}"
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@available(iOS 16.2, *)
|
|
515
|
+
private func findActivity<T: ActivityAttributes>(_ type: T.Type, id: String) -> String? {
|
|
516
|
+
guard let activity = Activity<T>.activities.first(where: { $0.id == id }) else {
|
|
517
|
+
return nil
|
|
518
|
+
}
|
|
519
|
+
return activityToJson(activity)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@available(iOS 16.2, *)
|
|
523
|
+
private func findPushToken<T: ActivityAttributes>(_ type: T.Type, id: String) -> String? {
|
|
524
|
+
guard let activity = Activity<T>.activities.first(where: { $0.id == id }) else {
|
|
525
|
+
return nil
|
|
526
|
+
}
|
|
527
|
+
guard let tokenData = activity.pushToken else {
|
|
528
|
+
return nil
|
|
529
|
+
}
|
|
530
|
+
let tokenString = tokenData.map { String(format: "%02x", $0) }.joined()
|
|
531
|
+
let dict: [String: Any] = [
|
|
532
|
+
"token": tokenString,
|
|
533
|
+
"platform": "ios",
|
|
534
|
+
"activityId": activity.id
|
|
535
|
+
]
|
|
536
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: dict),
|
|
537
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
538
|
+
return jsonString
|
|
539
|
+
}
|
|
540
|
+
return nil
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@available(iOS 16.2, *)
|
|
544
|
+
private func parseDismissalPolicy(_ policy: String, dismissAfter: Double) -> ActivityUIDismissalPolicy {
|
|
545
|
+
switch policy {
|
|
546
|
+
case "immediate":
|
|
547
|
+
return .immediate
|
|
548
|
+
case "afterDate":
|
|
549
|
+
if dismissAfter > 0 {
|
|
550
|
+
return .after(Date(timeIntervalSince1970: dismissAfter / 1000))
|
|
551
|
+
}
|
|
552
|
+
return .default
|
|
553
|
+
default:
|
|
554
|
+
return .default
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private func templateTypeFor<T: ActivityAttributes>(_ type: T.Type) -> String {
|
|
559
|
+
switch String(describing: type) {
|
|
560
|
+
case "DeliveryAttributes": return "delivery"
|
|
561
|
+
case "TimerAttributes": return "timer"
|
|
562
|
+
case "MediaAttributes": return "media"
|
|
563
|
+
case "ProgressAttributes": return "progress"
|
|
564
|
+
default: return "custom"
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// MARK: - Protocol for template attributes
|
|
570
|
+
|
|
571
|
+
protocol IdealystActivityAttributes: ActivityAttributes where ContentState: Codable {}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
// MARK: - Delivery Activity
|
|
5
|
+
|
|
6
|
+
struct DeliveryAttributes: IdealystActivityAttributes {
|
|
7
|
+
struct ContentState: Codable, Hashable {
|
|
8
|
+
var progress: Double
|
|
9
|
+
var status: String
|
|
10
|
+
var eta: Double?
|
|
11
|
+
var driverName: String?
|
|
12
|
+
var subtitle: String?
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var startLabel: String
|
|
16
|
+
var endLabel: String
|
|
17
|
+
var icon: String?
|
|
18
|
+
var accentColor: String?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// MARK: - Timer Activity
|
|
22
|
+
|
|
23
|
+
struct TimerAttributes: IdealystActivityAttributes {
|
|
24
|
+
struct ContentState: Codable, Hashable {
|
|
25
|
+
var endTime: Double
|
|
26
|
+
var isPaused: Bool?
|
|
27
|
+
var subtitle: String?
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var title: String
|
|
31
|
+
var icon: String?
|
|
32
|
+
var accentColor: String?
|
|
33
|
+
var showElapsed: Bool?
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Media Activity
|
|
37
|
+
|
|
38
|
+
struct MediaAttributes: IdealystActivityAttributes {
|
|
39
|
+
struct ContentState: Codable, Hashable {
|
|
40
|
+
var trackTitle: String
|
|
41
|
+
var artist: String?
|
|
42
|
+
var isPlaying: Bool
|
|
43
|
+
var progress: Double?
|
|
44
|
+
var duration: Double?
|
|
45
|
+
var position: Double?
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var title: String
|
|
49
|
+
var artworkUri: String?
|
|
50
|
+
var accentColor: String?
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// MARK: - Progress Activity
|
|
54
|
+
|
|
55
|
+
struct ProgressAttributes: IdealystActivityAttributes {
|
|
56
|
+
struct ContentState: Codable, Hashable {
|
|
57
|
+
var progress: Double
|
|
58
|
+
var status: String
|
|
59
|
+
var subtitle: String?
|
|
60
|
+
var indeterminate: Bool?
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var title: String
|
|
64
|
+
var icon: String?
|
|
65
|
+
var accentColor: String?
|
|
66
|
+
}
|