rn-live-activities
v0.1.0
Published
Cross-platform Live Activities for React Native: iOS ActivityKit and Android 16 Live Updates, driven by local calls or push events
Maintainers
Readme
rn-live-activities
Live Activities for React Native on both platforms with one API:
- iOS 16.2+: ActivityKit Live Activities on the lock screen and in the Dynamic Island
- Android 16+: promoted Live Updates (status-bar chip, lock screen, top of the shade); older Android versions get a regular ongoing notification
Activities update the same way on both: locally from JS, or by push from your server while the app is backgrounded or killed (APNs on iOS, FCM on Android).
const activity = await startActivity({
id: 'order-1042',
template: 'progress',
tintColor: '#FF6B00',
state: { title: 'Order #1042', status: 'Preparing', progress: 0.2, progressLabel: '25 min' },
});
await updateActivity('order-1042', { title: 'Order #1042', status: 'On the way', progress: 0.7, progressLabel: '8 min' });
await endActivity('order-1042', { title: 'Order #1042', status: 'Delivered', progress: 1 });Status: early development (v0.1). The API may still change.
Installation
npm install rn-live-activitiesExpo (development builds)
{
"expo": {
"ios": { "bundleIdentifier": "com.example.app" },
"plugins": [["rn-live-activities", { "apsEnvironment": "development" }]]
}
}Then npx expo prebuild. The plugin adds the widget extension, NSSupportsLiveActivities and the push entitlement. Options:
| Option | Default | |
|---|---|---|
| apsEnvironment | "development" | "production" for release builds, or false to manage the entitlement yourself |
| frequentUpdates | false | Sets NSSupportsLiveActivitiesFrequentUpdates |
Expo Go is not supported, because the library ships native code.
Bare React Native
npx rn-live-activities-setup-ios --bundle-id com.example.app
cd ios && pod installThis adds a LiveActivitiesWidget extension target to your Xcode project and enables NSSupportsLiveActivities. It is safe to re-run after upgrading, since it refreshes the library's widget sources. LiveActivitiesWidget/LiveActivitiesWidgetBundle.swift belongs to you; add your own widgets there. For push updates, enable the Push Notifications capability on the app target.
Android needs no setup. The manifest entries (POST_NOTIFICATIONS, POST_PROMOTED_NOTIFICATIONS) merge in automatically.
Templates
| Template | Uses | iOS | Android 16 |
|---|---|---|---|
| progress | progress (0–1), progressLabel | Progress bar; circular progress when minimal | ProgressStyle bar, label in the status chip |
| status | status, subtitle | Status on the trailing side | Status in the chip, subtitle as big text |
| countdown | endsAt (epoch ms) | Live timer | Chronometer counting down |
Every template uses title, subtitle, status, icon and tintColor. icon is an SF Symbol name on iOS and a drawable resource name on Android; if it's missing, the app icon is used.
API
requestPermission(): Promise<boolean> // Android 13+ asks for POST_NOTIFICATIONS
areActivitiesEnabled(): Promise<boolean>
startActivity(options): Promise<LiveActivity>
updateActivity(id, state, { alert?, staleDate?, relevanceScore? }): Promise<void>
endActivity(id, finalState?, { dismissal?: 'default' | 'immediate' | Date | number }): Promise<void>
endAllActivities(): Promise<void>
getActivities(): Promise<LiveActivity[]>
addActivityStateListener(({ activityId, state }) => …) // active | ended | dismissed | stale
addPushTokenListener(({ activityId, token }) => …) // iOS, per activity
addPushToStartTokenListener((token) => …) // iOS 17.2+
getPushToStartToken(): Promise<string | null> // iOS 17.2+
handlePushMessage(remoteMessage.data): Promise<boolean> // Android, from JSUpdates replace the whole state, the same way ActivityKit content-state works, so always send every field you want shown.
Push updates
Both platforms use the same state object. Only the transport differs.
iOS (APNs)
- Start with
enablePushUpdates: trueand send the token fromaddPushTokenListenerto your server. - Send to APNs with
apns-push-type: liveactivityandapns-topic: <bundle-id>.push-type.liveactivity:
{
"aps": {
"timestamp": 1761120000,
"event": "update",
"content-state": {
"title": "Order #1042",
"status": "On the way",
"progress": 0.7,
"progressLabel": "8 min"
}
}
}Use "event": "end" (optionally with "dismissal-date") to end the activity. To start one by push (iOS 17.2+), send to the push-to-start token with "event": "start", "attributes-type": "LiveActivityAttributes" and "attributes": { "activityId": "order-1042", "template": "progress", "tintColor": "#FF6B00" }.
Android (FCM)
Send a data-only message (no notification block). Its liveactivity field holds the same event as a JSON string:
{
"message": {
"token": "<fcm device token>",
"android": { "priority": "high" },
"data": {
"liveactivity": "{\"event\":\"update\",\"activityId\":\"order-1042\",\"timestamp\":1761120000,\"state\":{\"title\":\"Order #1042\",\"status\":\"On the way\",\"progress\":0.7,\"progressLabel\":\"8 min\"}}"
}
}
}event is start (which also takes template and tintColor), update or end (which takes an optional dismissalDate in seconds). As on iOS, timestamp is used to drop out-of-order deliveries.
Hand the message to the library from your FirebaseMessagingService. This applies the update without starting the JS runtime:
override fun onMessageReceived(message: RemoteMessage) {
if (LiveActivitiesPush.handleMessage(this, message.data)) return
// ...your other messages
}If you use @react-native-firebase/messaging, you can call handlePushMessage(message.data) from setBackgroundMessageHandler and onMessage instead.
How it works
| | iOS | Android |
|---|---|---|
| Rendering | SwiftUI ActivityConfiguration in a widget extension | Notification with setRequestPromotedOngoing and ProgressStyle |
| State | Owned by ActivityKit | Persisted in SharedPreferences, so FCM updates work with the app cold |
| Push transport | APNs, applied by the OS | FCM data message, applied by LiveActivitiesPush |
| User dismissal | dismissed state | Swipe → dismissed state (via the delete intent) |
Contributing
License
MIT
