@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,39 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: 'com.android.library'
|
|
8
|
+
apply plugin: 'kotlin-android'
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "io.idealyst.liveactivity"
|
|
12
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 36)
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdkVersion safeExtGet('minSdkVersion', 24)
|
|
16
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 36)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
compileOptions {
|
|
20
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
21
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
kotlinOptions {
|
|
25
|
+
jvmTarget = '17'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sourceSets {
|
|
29
|
+
main {
|
|
30
|
+
java.srcDirs = ['src/main/java']
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dependencies {
|
|
36
|
+
implementation "com.facebook.react:react-android:+"
|
|
37
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.22')}"
|
|
38
|
+
implementation "androidx.core:core-ktx:1.13.1"
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
package="io.idealyst.liveactivity">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
5
|
+
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
|
|
6
|
+
|
|
7
|
+
</manifest>
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
package io.idealyst.liveactivity
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.*
|
|
4
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
8
|
+
|
|
9
|
+
class IdealystLiveActivityModule(private val reactContext: ReactApplicationContext) :
|
|
10
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
11
|
+
|
|
12
|
+
override fun getName(): String = "IdealystLiveActivity"
|
|
13
|
+
|
|
14
|
+
private val liveUpdateNotification by lazy { LiveUpdateNotification(reactContext) }
|
|
15
|
+
|
|
16
|
+
// Track active activities: activityId -> { notificationId, templateType, attributes, contentState, options, startedAt }
|
|
17
|
+
private val activeActivities = ConcurrentHashMap<String, ActivityRecord>()
|
|
18
|
+
|
|
19
|
+
private data class ActivityRecord(
|
|
20
|
+
val notificationId: Int,
|
|
21
|
+
val templateType: String,
|
|
22
|
+
val attributes: JSONObject,
|
|
23
|
+
val contentState: JSONObject,
|
|
24
|
+
val options: JSONObject,
|
|
25
|
+
val startedAt: Long
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// ---- Availability ----
|
|
29
|
+
|
|
30
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
31
|
+
fun isSupported(): Boolean {
|
|
32
|
+
return liveUpdateNotification.isProgressStyleSupported()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactMethod
|
|
36
|
+
fun isEnabled(promise: Promise) {
|
|
37
|
+
promise.resolve(liveUpdateNotification.canPostPromotedNotifications())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- Lifecycle ----
|
|
41
|
+
|
|
42
|
+
@ReactMethod
|
|
43
|
+
fun startActivity(
|
|
44
|
+
templateType: String,
|
|
45
|
+
attributesJson: String,
|
|
46
|
+
contentStateJson: String,
|
|
47
|
+
optionsJson: String,
|
|
48
|
+
promise: Promise
|
|
49
|
+
) {
|
|
50
|
+
try {
|
|
51
|
+
val attributes = JSONObject(attributesJson)
|
|
52
|
+
val contentState = JSONObject(contentStateJson)
|
|
53
|
+
val options = JSONObject(optionsJson)
|
|
54
|
+
val androidOptions = options.optJSONObject("android") ?: JSONObject()
|
|
55
|
+
|
|
56
|
+
val notificationId = androidOptions.optInt(
|
|
57
|
+
"notificationId",
|
|
58
|
+
LiveUpdateNotification.nextNotificationId()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
liveUpdateNotification.show(
|
|
62
|
+
notificationId, templateType, attributes, contentState, androidOptions
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
val activityId = "android_la_$notificationId"
|
|
66
|
+
val now = System.currentTimeMillis()
|
|
67
|
+
|
|
68
|
+
activeActivities[activityId] = ActivityRecord(
|
|
69
|
+
notificationId = notificationId,
|
|
70
|
+
templateType = templateType,
|
|
71
|
+
attributes = attributes,
|
|
72
|
+
contentState = contentState,
|
|
73
|
+
options = androidOptions,
|
|
74
|
+
startedAt = now
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Emit started event
|
|
78
|
+
emitEvent(JSONObject().apply {
|
|
79
|
+
put("type", "started")
|
|
80
|
+
put("activityId", activityId)
|
|
81
|
+
put("timestamp", now)
|
|
82
|
+
put("payload", JSONObject().put("state", "active"))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Return activity info
|
|
86
|
+
val info = JSONObject().apply {
|
|
87
|
+
put("id", activityId)
|
|
88
|
+
put("state", "active")
|
|
89
|
+
put("startedAt", now)
|
|
90
|
+
put("templateType", templateType)
|
|
91
|
+
put("attributes", attributes)
|
|
92
|
+
put("contentState", contentState)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
promise.resolve(info.toString())
|
|
96
|
+
} catch (e: Exception) {
|
|
97
|
+
promise.reject("start_failed", e.message, e)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@ReactMethod
|
|
102
|
+
fun updateActivity(
|
|
103
|
+
activityId: String,
|
|
104
|
+
contentStateJson: String,
|
|
105
|
+
alertConfigJson: String?,
|
|
106
|
+
promise: Promise
|
|
107
|
+
) {
|
|
108
|
+
try {
|
|
109
|
+
val record = activeActivities[activityId]
|
|
110
|
+
if (record == null) {
|
|
111
|
+
promise.reject("activity_not_found", "No activity found with id: $activityId")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
val newContentState = JSONObject(contentStateJson)
|
|
116
|
+
|
|
117
|
+
// Merge with existing content state
|
|
118
|
+
val mergedState = JSONObject(record.contentState.toString())
|
|
119
|
+
val keys = newContentState.keys()
|
|
120
|
+
while (keys.hasNext()) {
|
|
121
|
+
val key = keys.next()
|
|
122
|
+
mergedState.put(key, newContentState.get(key))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
liveUpdateNotification.update(
|
|
126
|
+
record.notificationId, record.templateType, mergedState, record.attributes, record.options
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// Update stored record
|
|
130
|
+
activeActivities[activityId] = record.copy(contentState = mergedState)
|
|
131
|
+
|
|
132
|
+
emitEvent(JSONObject().apply {
|
|
133
|
+
put("type", "updated")
|
|
134
|
+
put("activityId", activityId)
|
|
135
|
+
put("timestamp", System.currentTimeMillis())
|
|
136
|
+
put("payload", JSONObject().put("state", "active"))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
promise.resolve(null)
|
|
140
|
+
} catch (e: Exception) {
|
|
141
|
+
promise.reject("update_failed", e.message, e)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@ReactMethod
|
|
146
|
+
fun endActivity(
|
|
147
|
+
activityId: String,
|
|
148
|
+
finalContentStateJson: String?,
|
|
149
|
+
dismissalPolicy: String,
|
|
150
|
+
dismissAfter: Double,
|
|
151
|
+
promise: Promise
|
|
152
|
+
) {
|
|
153
|
+
try {
|
|
154
|
+
val record = activeActivities[activityId]
|
|
155
|
+
if (record == null) {
|
|
156
|
+
promise.reject("activity_not_found", "No activity found with id: $activityId")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If there's a final state, show it briefly before dismissing
|
|
161
|
+
if (finalContentStateJson != null) {
|
|
162
|
+
val finalState = JSONObject(finalContentStateJson)
|
|
163
|
+
val mergedState = JSONObject(record.contentState.toString())
|
|
164
|
+
val keys = finalState.keys()
|
|
165
|
+
while (keys.hasNext()) {
|
|
166
|
+
val key = keys.next()
|
|
167
|
+
mergedState.put(key, finalState.get(key))
|
|
168
|
+
}
|
|
169
|
+
liveUpdateNotification.update(
|
|
170
|
+
record.notificationId, record.templateType, mergedState, record.attributes, record.options
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Dismiss based on policy
|
|
175
|
+
when (dismissalPolicy) {
|
|
176
|
+
"immediate" -> {
|
|
177
|
+
liveUpdateNotification.dismiss(record.notificationId)
|
|
178
|
+
}
|
|
179
|
+
"afterDate" -> {
|
|
180
|
+
val delayMs = if (dismissAfter > 0) {
|
|
181
|
+
(dismissAfter - System.currentTimeMillis()).toLong().coerceAtLeast(0)
|
|
182
|
+
} else {
|
|
183
|
+
5000L // Default 5 second delay
|
|
184
|
+
}
|
|
185
|
+
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
186
|
+
liveUpdateNotification.dismiss(record.notificationId)
|
|
187
|
+
}, delayMs)
|
|
188
|
+
}
|
|
189
|
+
else -> {
|
|
190
|
+
// "default" — dismiss after a short delay (similar to iOS default)
|
|
191
|
+
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
192
|
+
liveUpdateNotification.dismiss(record.notificationId)
|
|
193
|
+
}, 5000L)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
activeActivities.remove(activityId)
|
|
198
|
+
|
|
199
|
+
emitEvent(JSONObject().apply {
|
|
200
|
+
put("type", "ended")
|
|
201
|
+
put("activityId", activityId)
|
|
202
|
+
put("timestamp", System.currentTimeMillis())
|
|
203
|
+
put("payload", JSONObject().put("state", "ended"))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
promise.resolve(null)
|
|
207
|
+
} catch (e: Exception) {
|
|
208
|
+
promise.reject("end_failed", e.message, e)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@ReactMethod
|
|
213
|
+
fun endAllActivities(
|
|
214
|
+
dismissalPolicy: String,
|
|
215
|
+
dismissAfter: Double,
|
|
216
|
+
promise: Promise
|
|
217
|
+
) {
|
|
218
|
+
try {
|
|
219
|
+
for ((activityId, record) in activeActivities) {
|
|
220
|
+
liveUpdateNotification.dismiss(record.notificationId)
|
|
221
|
+
|
|
222
|
+
emitEvent(JSONObject().apply {
|
|
223
|
+
put("type", "ended")
|
|
224
|
+
put("activityId", activityId)
|
|
225
|
+
put("timestamp", System.currentTimeMillis())
|
|
226
|
+
put("payload", JSONObject().put("state", "ended"))
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
activeActivities.clear()
|
|
230
|
+
promise.resolve(null)
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
promise.reject("end_failed", e.message, e)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- Queries ----
|
|
237
|
+
|
|
238
|
+
@ReactMethod
|
|
239
|
+
fun getActivity(activityId: String, promise: Promise) {
|
|
240
|
+
val record = activeActivities[activityId]
|
|
241
|
+
if (record == null) {
|
|
242
|
+
promise.resolve(null)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
promise.resolve(recordToJson(activityId, record).toString())
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@ReactMethod
|
|
250
|
+
fun listActivities(promise: Promise) {
|
|
251
|
+
val array = JSONArray()
|
|
252
|
+
for ((activityId, record) in activeActivities) {
|
|
253
|
+
array.put(recordToJson(activityId, record))
|
|
254
|
+
}
|
|
255
|
+
promise.resolve(array.toString())
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@ReactMethod
|
|
259
|
+
fun getPushToken(activityId: String, promise: Promise) {
|
|
260
|
+
// Android Live Updates don't have per-activity push tokens.
|
|
261
|
+
// FCM tokens are managed at the app level via @idealyst/notifications.
|
|
262
|
+
promise.resolve(null)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- Events ----
|
|
266
|
+
|
|
267
|
+
@ReactMethod
|
|
268
|
+
fun addListener(eventName: String) {
|
|
269
|
+
// Required for NativeEventEmitter
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@ReactMethod
|
|
273
|
+
fun removeListeners(count: Int) {
|
|
274
|
+
// Required for NativeEventEmitter
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun emitEvent(event: JSONObject) {
|
|
278
|
+
try {
|
|
279
|
+
reactContext
|
|
280
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
281
|
+
.emit("IdealystLiveActivityEvent", event.toString())
|
|
282
|
+
} catch (e: Exception) {
|
|
283
|
+
// JS not ready yet — ignore
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- Helpers ----
|
|
288
|
+
|
|
289
|
+
private fun recordToJson(activityId: String, record: ActivityRecord): JSONObject {
|
|
290
|
+
return JSONObject().apply {
|
|
291
|
+
put("id", activityId)
|
|
292
|
+
put("state", "active")
|
|
293
|
+
put("startedAt", record.startedAt)
|
|
294
|
+
put("templateType", record.templateType)
|
|
295
|
+
put("attributes", record.attributes)
|
|
296
|
+
put("contentState", record.contentState)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package io.idealyst.liveactivity
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class IdealystLiveActivityPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(IdealystLiveActivityModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
package io.idealyst.liveactivity
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps Android notification creation for Live Updates.
|
|
15
|
+
* Uses Notification.ProgressStyle on API 36+ (Android 16),
|
|
16
|
+
* falls back to standard progress notification on older versions.
|
|
17
|
+
*/
|
|
18
|
+
class LiveUpdateNotification(private val context: Context) {
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val DEFAULT_CHANNEL_ID = "idealyst_live_activity"
|
|
22
|
+
const val DEFAULT_CHANNEL_NAME = "Live Activities"
|
|
23
|
+
const val DEFAULT_CHANNEL_DESCRIPTION = "Real-time activity updates"
|
|
24
|
+
|
|
25
|
+
private var notificationIdCounter = 9000
|
|
26
|
+
fun nextNotificationId(): Int = notificationIdCounter++
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private val notificationManager =
|
|
30
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
31
|
+
|
|
32
|
+
init {
|
|
33
|
+
ensureChannel()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun ensureChannel() {
|
|
37
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
38
|
+
val existing = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID)
|
|
39
|
+
if (existing == null) {
|
|
40
|
+
val channel = NotificationChannel(
|
|
41
|
+
DEFAULT_CHANNEL_ID,
|
|
42
|
+
DEFAULT_CHANNEL_NAME,
|
|
43
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
44
|
+
).apply {
|
|
45
|
+
description = DEFAULT_CHANNEL_DESCRIPTION
|
|
46
|
+
}
|
|
47
|
+
notificationManager.createNotificationChannel(channel)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if the device supports Live Updates (ProgressStyle).
|
|
54
|
+
*/
|
|
55
|
+
fun isProgressStyleSupported(): Boolean {
|
|
56
|
+
return Build.VERSION.SDK_INT >= 36
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if the app can post promoted notifications (Live Updates).
|
|
61
|
+
*/
|
|
62
|
+
fun canPostPromotedNotifications(): Boolean {
|
|
63
|
+
if (Build.VERSION.SDK_INT < 36) return false
|
|
64
|
+
return try {
|
|
65
|
+
notificationManager.canPostPromotedNotifications()
|
|
66
|
+
} catch (e: Exception) {
|
|
67
|
+
false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create and show a Live Update notification.
|
|
73
|
+
* Returns the notification ID used.
|
|
74
|
+
*/
|
|
75
|
+
fun show(
|
|
76
|
+
notificationId: Int,
|
|
77
|
+
templateType: String,
|
|
78
|
+
attributes: JSONObject,
|
|
79
|
+
contentState: JSONObject,
|
|
80
|
+
options: JSONObject
|
|
81
|
+
): Int {
|
|
82
|
+
val channelId = options.optString("channelId", DEFAULT_CHANNEL_ID)
|
|
83
|
+
val smallIconName = options.optString("smallIcon", "ic_notification")
|
|
84
|
+
val deepLinkUrl = options.optString("deepLinkUrl", "")
|
|
85
|
+
|
|
86
|
+
val smallIconRes = getIconResource(smallIconName)
|
|
87
|
+
|
|
88
|
+
if (Build.VERSION.SDK_INT >= 36 && canPostPromotedNotifications()) {
|
|
89
|
+
showProgressStyleNotification(
|
|
90
|
+
notificationId, templateType, attributes, contentState,
|
|
91
|
+
channelId, smallIconRes, deepLinkUrl
|
|
92
|
+
)
|
|
93
|
+
} else {
|
|
94
|
+
showFallbackNotification(
|
|
95
|
+
notificationId, templateType, attributes, contentState,
|
|
96
|
+
channelId, smallIconRes, deepLinkUrl
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return notificationId
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update an existing Live Update notification.
|
|
105
|
+
*/
|
|
106
|
+
fun update(
|
|
107
|
+
notificationId: Int,
|
|
108
|
+
templateType: String,
|
|
109
|
+
contentState: JSONObject,
|
|
110
|
+
attributes: JSONObject,
|
|
111
|
+
options: JSONObject
|
|
112
|
+
) {
|
|
113
|
+
show(notificationId, templateType, attributes, contentState, options)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Dismiss a Live Update notification.
|
|
118
|
+
*/
|
|
119
|
+
fun dismiss(notificationId: Int) {
|
|
120
|
+
notificationManager.cancel(notificationId)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Dismiss all Live Update notifications.
|
|
125
|
+
*/
|
|
126
|
+
fun dismissAll() {
|
|
127
|
+
notificationManager.cancelAll()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- ProgressStyle (API 36+) ----
|
|
131
|
+
|
|
132
|
+
private fun showProgressStyleNotification(
|
|
133
|
+
notificationId: Int,
|
|
134
|
+
templateType: String,
|
|
135
|
+
attributes: JSONObject,
|
|
136
|
+
contentState: JSONObject,
|
|
137
|
+
channelId: String,
|
|
138
|
+
smallIconRes: Int,
|
|
139
|
+
deepLinkUrl: String
|
|
140
|
+
) {
|
|
141
|
+
if (Build.VERSION.SDK_INT < 36) return
|
|
142
|
+
|
|
143
|
+
val title = extractTitle(templateType, attributes, contentState)
|
|
144
|
+
val status = extractStatus(templateType, contentState)
|
|
145
|
+
val progress = contentState.optDouble("progress", -1.0)
|
|
146
|
+
|
|
147
|
+
val progressStyle = Notification.ProgressStyle()
|
|
148
|
+
|
|
149
|
+
if (progress in 0.0..1.0) {
|
|
150
|
+
progressStyle.setProgress((progress * 100).toInt())
|
|
151
|
+
} else {
|
|
152
|
+
progressStyle.setProgressIndeterminate(true)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val builder = Notification.Builder(context, channelId)
|
|
156
|
+
.setSmallIcon(smallIconRes)
|
|
157
|
+
.setContentTitle(title)
|
|
158
|
+
.setContentText(status)
|
|
159
|
+
.setStyle(progressStyle)
|
|
160
|
+
.setOngoing(true)
|
|
161
|
+
.setCategory(Notification.CATEGORY_PROGRESS)
|
|
162
|
+
|
|
163
|
+
if (deepLinkUrl.isNotEmpty()) {
|
|
164
|
+
val intent = createDeepLinkIntent(deepLinkUrl)
|
|
165
|
+
if (intent != null) {
|
|
166
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
167
|
+
context, notificationId, intent,
|
|
168
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
169
|
+
)
|
|
170
|
+
builder.setContentIntent(pendingIntent)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
notificationManager.notify(notificationId, builder.build())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- Fallback (pre-API 36) ----
|
|
178
|
+
|
|
179
|
+
private fun showFallbackNotification(
|
|
180
|
+
notificationId: Int,
|
|
181
|
+
templateType: String,
|
|
182
|
+
attributes: JSONObject,
|
|
183
|
+
contentState: JSONObject,
|
|
184
|
+
channelId: String,
|
|
185
|
+
smallIconRes: Int,
|
|
186
|
+
deepLinkUrl: String
|
|
187
|
+
) {
|
|
188
|
+
val title = extractTitle(templateType, attributes, contentState)
|
|
189
|
+
val status = extractStatus(templateType, contentState)
|
|
190
|
+
val progress = contentState.optDouble("progress", -1.0)
|
|
191
|
+
val indeterminate = contentState.optBoolean("indeterminate", false)
|
|
192
|
+
|
|
193
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
194
|
+
.setSmallIcon(smallIconRes)
|
|
195
|
+
.setContentTitle(title)
|
|
196
|
+
.setContentText(status)
|
|
197
|
+
.setOngoing(true)
|
|
198
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
199
|
+
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
|
200
|
+
|
|
201
|
+
if (indeterminate) {
|
|
202
|
+
builder.setProgress(0, 0, true)
|
|
203
|
+
} else if (progress in 0.0..1.0) {
|
|
204
|
+
builder.setProgress(100, (progress * 100).toInt(), false)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (deepLinkUrl.isNotEmpty()) {
|
|
208
|
+
val intent = createDeepLinkIntent(deepLinkUrl)
|
|
209
|
+
if (intent != null) {
|
|
210
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
211
|
+
context, notificationId, intent,
|
|
212
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
213
|
+
)
|
|
214
|
+
builder.setContentIntent(pendingIntent)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
notificationManager.notify(notificationId, builder.build())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Helpers ----
|
|
222
|
+
|
|
223
|
+
private fun extractTitle(
|
|
224
|
+
templateType: String,
|
|
225
|
+
attributes: JSONObject,
|
|
226
|
+
contentState: JSONObject
|
|
227
|
+
): String {
|
|
228
|
+
return when (templateType) {
|
|
229
|
+
"delivery" -> "${attributes.optString("startLabel", "")} → ${attributes.optString("endLabel", "")}"
|
|
230
|
+
"timer" -> attributes.optString("title", "Timer")
|
|
231
|
+
"media" -> contentState.optString("trackTitle", attributes.optString("title", "Now Playing"))
|
|
232
|
+
"progress" -> attributes.optString("title", "Progress")
|
|
233
|
+
else -> attributes.optString("title", "Live Activity")
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun extractStatus(templateType: String, contentState: JSONObject): String {
|
|
238
|
+
return when (templateType) {
|
|
239
|
+
"delivery" -> contentState.optString("status", "In progress")
|
|
240
|
+
"timer" -> contentState.optString("subtitle", "Running")
|
|
241
|
+
"media" -> {
|
|
242
|
+
val artist = contentState.optString("artist", "")
|
|
243
|
+
val playing = if (contentState.optBoolean("isPlaying", false)) "Playing" else "Paused"
|
|
244
|
+
if (artist.isNotEmpty()) "$artist • $playing" else playing
|
|
245
|
+
}
|
|
246
|
+
"progress" -> contentState.optString("status", "In progress")
|
|
247
|
+
else -> contentState.optString("status", "")
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private fun getIconResource(name: String): Int {
|
|
252
|
+
val res = context.resources.getIdentifier(name, "drawable", context.packageName)
|
|
253
|
+
return if (res != 0) res else android.R.drawable.ic_popup_sync
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private fun createDeepLinkIntent(url: String): Intent? {
|
|
257
|
+
return try {
|
|
258
|
+
val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url))
|
|
259
|
+
intent.setPackage(context.packageName)
|
|
260
|
+
intent
|
|
261
|
+
} catch (e: Exception) {
|
|
262
|
+
null
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "idealyst-live-activity"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["documentation"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = { "Idealyst" => "contact@idealyst.io" }
|
|
12
|
+
s.source = { :git => package["repository"]["url"], :tag => s.version }
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => "16.2" }
|
|
15
|
+
s.swift_version = "5.9"
|
|
16
|
+
|
|
17
|
+
s.source_files = "ios/**/*.{swift,h,m,mm}"
|
|
18
|
+
|
|
19
|
+
s.dependency "React-Core"
|
|
20
|
+
|
|
21
|
+
s.frameworks = "ActivityKit", "WidgetKit", "SwiftUI"
|
|
22
|
+
end
|