expo-onesignal-live-activities
v0.2.10
Published
The complete Live Activity setup for Expo + OneSignal. Handles Widget Extension target, entitlements, Info.plist, AppDelegate, push-to-start tokens, EAS credentials, and widget UI scaffolding — you just write SwiftUI.
Maintainers
Readme
expo-onesignal-live-activities
The complete Live Activity setup for Expo + OneSignal. One package handles everything — Widget Extension target, entitlements, Info.plist, AppDelegate, push-to-start token registration, EAS credentials, and widget UI scaffolding. You write SwiftUI, the package handles the rest.
[!IMPORTANT] iOS only. All functions gracefully return no-ops on Android and web. Requires iOS 16.2+ and a physical device for full functionality.
What This Replaces
Setting up OneSignal Live Activities in Expo normally requires 15+ manual steps:
- Create a Widget Extension target in Xcode (File > New > Target > Widget Extension)
- Add
NSSupportsLiveActivitiesto Info.plist - Add
NSSupportsLiveActivitiesFrequentUpdatesto Info.plist - Configure
aps-environmententitlement - Set up App Group entitlements for data sharing
- Edit the Podfile to add
OneSignalXCFrameworkto the widget target - Run
pod install - Write a Swift struct conforming to
OneSignalLiveActivityAttributes - Write the full SwiftUI widget UI (lock screen + Dynamic Island)
- Add the widget target to the main app target's membership
- Call
OneSignal.LiveActivities.setupDefault()in AppDelegate - Set up an async task for
pushToStartTokenUpdates(iOS 17.2+) - Register push-to-start tokens with OneSignal manually
- Add
appExtensionstoextra.eas.build.experimental.iosfor EAS Build signing - Upload
.p8APNs key to OneSignal dashboard - Build server-side API calls with exact property-name parity
With this package:
npx expo install expo-onesignal-live-activities # 1. Install
npx expo-onesignal-live-activities # 2. Scaffold widget UI templates
# 3. Add plugin to app.config.ts, customize SwiftUI, prebuildThe config plugin handles steps 1–14 automatically. You write the SwiftUI widget UI (step 9) and set up your server API (step 16).
Features
Zero-config setup (handled by the config plugin)
- Info.plist — sets
NSSupportsLiveActivitiesandNSSupportsLiveActivitiesFrequentUpdates - Entitlements — configures
aps-environmentand App Group - AppDelegate — injects the Live Activity coordinator before the RN bridge boots
- Widget Extension — creates the Xcode target with correct build settings, signing, versioning, and font registration
- EAS Build — auto-registers the extension for credential provisioning (no manual
appExtensionsconfig) - Push-to-start tokens — registers tokens with OneSignal at app launch, including a synchronous fallback for the iOS 17.2–18.x timing race
- Activity token relay — automatically calls OneSignal
enter()/exit()as activities start and end - Widget UI scaffolding — CLI generates starter SwiftUI templates you can customize
JavaScript API
- Client-side updates — update Live Activity content state from JavaScript without a push round-trip
- End activities — programmatically dismiss activities from JavaScript
- Activity listing — enumerate all active activities with tokens, attributes, and state
- Device support check — check if Live Activities are supported before showing related UI
- React hooks —
useLiveActivityTokenanduseLiveActivitiesfor declarative usage
Prerequisites
- Expo SDK 53+ (requires Swift AppDelegate)
- OneSignal React Native SDK 5.x (
react-native-onesignaloronesignal-expo-plugin) - iOS 16.2+ deployment target
[!WARNING] Do not call
OneSignal.LiveActivities.setupDefault()in your app. This package replaces that functionality. Using both causes duplicate token registrations and sequence races.Not compatible with Expo Go — use EAS Build or
npx expo prebuild.
Installation
npx expo install expo-onesignal-live-activities1. Scaffold the widget UI (recommended)
Run the CLI to generate starter SwiftUI templates for your Live Activity widget:
npx expo-onesignal-live-activitiesThis creates widgets/live-activity/ with four customizable SwiftUI files:
LiveActivityWidget.swift— lock screen and Dynamic Island UILiveActivityWidgetBundle.swift— widget bundle entry pointCachedImageView.swift— App Group image cache helperTheme.swift— colors and layout constants
2. Configure the plugin
Add to your app.json or app.config.ts:
{
"expo": {
"plugins": [
["expo-onesignal-live-activities", {
"mode": "production",
"activityIdKey": "onesignal_activity_id",
"appGroupIdentifier": "group.com.myapp.onesignal",
"enableFrequentUpdates": true,
"widgetTarget": {
"name": "MyAppLiveActivity",
"widgetDir": "./widgets/live-activity",
"fonts": ["./assets/fonts/MyFont-Bold.ttf"]
}
}]
]
}
}The plugin automatically handles:
- Info.plist — sets
NSSupportsLiveActivitiesandNSSupportsLiveActivitiesFrequentUpdates - Entitlements — configures
aps-environmentand App Group - AppDelegate — injects
LiveActivityCoordinator.shared.start()before the RN bridge boots - Widget Extension — creates the Xcode target with correct build settings, signing, and versioning
- EAS Build — auto-registers the extension in
appExtensionsfor credential provisioning (no manual config needed)
3. Rebuild
npx expo prebuild -p ios --cleanPlugin Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| activityIdKey | string | "onesignal_activity_id" | Key in DefaultLiveActivityAttributes.data used to resolve the OneSignal activity ID |
| enableFrequentUpdates | boolean | true | Enable NSSupportsLiveActivitiesFrequentUpdates in Info.plist |
| mode | "development" \| "production" | "development" | Sets the aps-environment entitlement. Use "production" for TestFlight and App Store builds |
| appGroupIdentifier | string | — | App Group ID for sharing data between the app and widget (e.g., "group.com.myapp.onesignal") |
| widgetTarget | object | — | Configure automatic Widget Extension target creation (see below) |
widgetTarget Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| name | string | (required) | Widget Extension target name (e.g., "MyAppLiveActivity") |
| widgetDir | string | (required) | Path to your widget Swift files (e.g., "./widgets/live-activity") |
| deploymentTarget | string | "16.4" | Minimum iOS deployment target for the widget |
| fonts | string[] | — | Font files to bundle with the widget (e.g., ["assets/fonts/Inter-Bold.ttf"]) |
| pods | Array<{name, version?}> | — | Additional CocoaPods for the widget target |
Quick Start
With the config plugin configured, observation is fully automatic — push-to-start token registration, activity token relay to OneSignal, and lifecycle tracking all happen at app launch with zero JavaScript. The only function most apps need is updateLiveActivity for client-side content updates:
import { updateLiveActivity } from 'expo-onesignal-live-activities';
// Update a running Live Activity's content state (matched by attribute key/value)
await updateLiveActivity('orderId', 'order_123', {
status: 'Out for delivery',
eta: '12:30 PM',
progress: 0.75,
});That's it. The config plugin injects LiveActivityCoordinator.shared.start() into your AppDelegate before the React Native bridge boots, so all ActivityKit sequences are observed from the moment the app launches — including background push-to-start launches.
API Reference
Core Functions
updateLiveActivity(matchKey, matchValue, contentState)
Update the content state of a running Live Activity. Finds the activity where attributes.data[matchKey] equals matchValue.
await updateLiveActivity('deliveryId', 'del_456', {
status: 'Arriving soon',
eta: '2 minutes',
driverName: 'Alex',
});| Parameter | Type | Description |
|-----------|------|-------------|
| matchKey | string | Key in the activity's attributes data to match on |
| matchValue | string | Value to match |
| contentState | Record<string, unknown> | New content state fields |
endLiveActivity(matchKey, matchValue)
End a running Live Activity immediately.
await endLiveActivity('deliveryId', 'del_456');listActiveActivities()
Returns all currently active Live Activities with their attributes, content state, resolved OneSignal ID, and push token.
const activities = await listActiveActivities();
// [
// {
// id: "AB12CD34-...",
// resolvedActivityId: "order_123",
// pushToken: "a1b2c3d4...",
// attributes: { orderId: "order_123", storeName: "Pizza Place" },
// contentState: { status: "Preparing", eta: "20 min" }
// }
// ]isLiveActivitiesSupported()
Check if the device supports Live Activities (iOS 16.2+, activities enabled in Settings).
const supported = await isLiveActivitiesSupported();Advanced Functions
These are escape hatches for advanced use cases. Most apps don't need them.
startObserving()
Manually start the Live Activity coordinator. You don't need this if you use the config plugin — it auto-starts from AppDelegate before the React Native bridge boots. Only use this if you've opted out of the config plugin's AppDelegate injection.
Calling this when observation is already running is a no-op (idempotent).
await startObserving();stopObserving()
Stop all Live Activity observation. Cancels push-to-start token registration, activity token relay, and lifecycle tracking. This is destructive — after calling it, no tokens are relayed to OneSignal until startObserving() is called again.
Use case: logout flows where you want to fully disconnect from Live Activity observation.
await stopObserving();Hooks
useLiveActivityToken()
Subscribe to Live Activity token events. Returns the latest token event or null.
import { useLiveActivityToken } from 'expo-onesignal-live-activities';
function MyComponent() {
const tokenEvent = useLiveActivityToken();
useEffect(() => {
if (tokenEvent) {
console.log(`Activity ${tokenEvent.activityId} token: ${tokenEvent.token}`);
}
}, [tokenEvent]);
}useLiveActivities(pollIntervalMs?)
Poll active Live Activities. Returns the list and a manual refresh function.
import { useLiveActivities } from 'expo-onesignal-live-activities';
function ActiveActivities() {
const { activities, refresh } = useLiveActivities(5000); // poll every 5s
return (
<View>
{activities.map((a) => (
<Text key={a.id}>{a.resolvedActivityId}: {a.contentState.status}</Text>
))}
<Button title="Refresh" onPress={refresh} />
</View>
);
}Types
type LiveActivityInfo = {
id: string;
resolvedActivityId: string | null;
pushToken: string | null;
attributes: Record<string, unknown>;
contentState: Record<string, unknown>;
};
type LiveActivityTokenEvent = {
activityId: string;
token: string;
};
type ContentStateUpdate = Record<string, unknown>;How It Works
The package uses a LiveActivityCoordinator singleton that:
- Pre-seeds existing activities from
Activity<DefaultLiveActivityAttributes>.activitieson start - Observes new activities via
Activity<DefaultLiveActivityAttributes>.activityUpdates - Registers push-to-start tokens via
pushToStartTokenUpdates(iOS 17.2+) with a synchronous fallback for the known timing race - Relays tokens to OneSignal via
OneSignalLiveActivitiesManagerImpl.enter()/.exit()/.setPushToStartToken() - Emits events to JavaScript for token updates and activity lifecycle changes
The coordinator starts from AppDelegate.didFinishLaunchingWithOptions (injected by the config plugin) — before the React Native bridge boots — so it catches push-to-start launches that happen in the background.
Widget UI
This package handles both the bridge (token observation, updates, lifecycle) and the widget extension setup (Xcode target, build phases, pods).
Scaffolded templates (recommended)
Run npx expo-onesignal-live-activities to generate starter templates, then customize the SwiftUI in widgets/live-activity/. The generated LiveActivityWidget.swift reads from context.attributes.data and context.state.data — the same dictionaries you populate from your OneSignal push-to-start payload.
Data flows through the DefaultLiveActivityAttributes type from OneSignal:
// In your widget — read attributes (static) and state (dynamic)
let title = context.attributes.data["title"]?.asString() ?? "My Activity"
let status = context.state.data["status"]?.asString() ?? "Loading..."
let value = context.state.data["value"]?.asString() ?? ""Update from JavaScript:
await updateLiveActivity('orderId', 'order_123', {
status: 'Out for delivery',
value: '$42.50',
});Manual widget setup
If you prefer to manage the Widget Extension target yourself (e.g., in Xcode), omit widgetTarget from the plugin config. You'll need to:
- Create a Widget Extension target in Xcode manually
- Use
DefaultLiveActivityAttributesfromOneSignalLiveActivities - Add
OneSignalXCFrameworkpod to the widget target's Podfile - Configure entitlements and Info.plist for the widget target
Server-Side: Starting a Live Activity
Use OneSignal's push-to-start API to create Live Activities. The activityIdKey you configured in the plugin must match a key in your payload's attributes.data:
{
"app_id": "YOUR_ONESIGNAL_APP_ID",
"include_player_ids": ["PLAYER_ID"],
"name": "LIVE_ACTIVITY_NOTIFICATION",
"content_available": true,
"data": {
"activity_type": "default",
"activity_attributes": {
"data": {
"onesignal_activity_id": "order_123",
"storeName": "Pizza Place",
"orderId": "order_123"
}
},
"content_state": {
"data": {
"status": "Order confirmed",
"eta": "25 min"
}
},
"push_to_start_token": "TOKEN_FROM_CLIENT"
}
}Troubleshooting
"No recipients" on push-to-start
The device hasn't registered a push-to-start token with OneSignal. Check:
- The coordinator is starting at app launch (verify
LiveActivityCoordinator.shared.start()in your AppDelegate) - Live Activities are enabled in device Settings > Your App > Live Activities
- The app has been launched at least once after install to register the token
- You're running on a physical device — Simulator doesn't support push-to-start
Token not arriving
- Ensure
startObserving()is called before the Live Activity starts, or use the config plugin for auto-start - Verify
NSSupportsLiveActivitiesistruein your Info.plist - Check that the Widget Extension target uses
DefaultLiveActivityAttributes(from OneSignal) - Run on a physical device — Simulator has limited Live Activity support
Conflict with setupDefault()
This package replaces OneSignal.LiveActivities.setupDefault(). If both are active, you'll get duplicate token registrations and sequence races. Remove the setupDefault() call from your app.
CFBundleVersion missing on install
If you see "does not have a CFBundleVersion key" during device install, ensure you're on v0.2.3+ which resolves widget extension version build settings from the main target.
iOS 18 throttling
iOS 18 may throttle Live Activity updates. Set enableFrequentUpdates: true in the plugin config. Even so, iOS may batch updates during periods of heavy system load.
Activities not ending
If activities persist after calling endLiveActivity(), verify:
- The
matchKey/matchValuecombination matches an active activity - Check
await listActiveActivities()to see what's currently running
Build errors
- "No such module 'OneSignalLiveActivities'": Add
OneSignalXCFrameworkpod to your Widget Extension target's Podfile - "Unsupported deployment target": Ensure your Widget Extension targets iOS 16.2+
Known Limitations
- iOS only — all functions no-op on Android and web
- Requires OneSignal SDK 5.x — not compatible with OneSignal SDK 3.x or 4.x
- No client-side activity creation — activities must be started via OneSignal's push-to-start API (server-initiated)
updateLiveActivitymatches first activity — if multiple activities share the same value for the match key, only the first is updated- Push-to-start token unreliability — Apple's
pushToStartTokenUpdatesis unreliable on ~50% of devices (all iOS versions). The package includes a synchronous fallback but cannot fully work around this Apple bug. - Requires physical device for full functionality (Simulator support is limited)
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'feat: add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
Development
# Install dependencies
bun install
# Build
bun run build
# Run tests
bun run test
# Lint
bun run lintLicense
MIT
