diff --git a/ios/app/VoltraModuleImpl.swift b/ios/app/VoltraModuleImpl.swift index 6b91a1e1..7ef526c0 100644 --- a/ios/app/VoltraModuleImpl.swift +++ b/ios/app/VoltraModuleImpl.swift @@ -63,6 +63,16 @@ public class VoltraModuleImpl { }() let relevanceScore: Double = options?.relevanceScore ?? 0.0 + let pushType: PushType? = { + if let channelId = options?.channelId { + if #available(iOS 18.0, *) { + return .channel(channelId) + } + return nil + } + return pushNotificationsEnabled ? .token : nil + }() + // Create request struct with compressed JSON let createRequest = CreateActivityRequest( activityId: activityName, @@ -70,7 +80,7 @@ public class VoltraModuleImpl { jsonString: compressedJson, staleDate: staleDate, relevanceScore: relevanceScore, - pushType: pushNotificationsEnabled ? .token : nil, + pushType: pushType, endExistingWithSameName: true ) diff --git a/ios/app/VoltraOptions.swift b/ios/app/VoltraOptions.swift index cc115b6f..8e286736 100644 --- a/ios/app/VoltraOptions.swift +++ b/ios/app/VoltraOptions.swift @@ -33,6 +33,10 @@ public struct StartVoltraOptions: Record { @Field public var relevanceScore: Double? + /// Channel ID for broadcast push notifications (iOS 18+). + @Field + public var channelId: String? + public init() {} } diff --git a/src/VoltraModule.ts b/src/VoltraModule.ts index 55a37122..b982ca73 100644 --- a/src/VoltraModule.ts +++ b/src/VoltraModule.ts @@ -27,6 +27,11 @@ export type StartVoltraOptions = { * Double value between 0.0 and 1.0, defaults to 0.0 */ relevanceScore?: number + /** + * Channel ID for broadcast push notifications (iOS 18+). + * When provided, the Live Activity subscribes to broadcast updates on this channel. + */ + channelId?: string } /** diff --git a/src/live-activity/api.ts b/src/live-activity/api.ts index a02cc917..9fca8a78 100644 --- a/src/live-activity/api.ts +++ b/src/live-activity/api.ts @@ -34,6 +34,12 @@ export type StartLiveActivityOptions = { * URL to open when the Live Activity is tapped. */ deepLinkUrl?: string + /** + * Channel ID for broadcast push notifications (iOS 18+). + * When provided, the Live Activity subscribes to broadcast updates on this channel + * instead of receiving individual push tokens. + */ + channelId?: string } & SharedLiveActivityOptions export type UpdateLiveActivityOptions = SharedLiveActivityOptions @@ -60,6 +66,10 @@ export type UseLiveActivityOptions = { * URL to open when the Live Activity is tapped. */ deepLinkUrl?: string + /** + * Channel ID for broadcast push notifications (iOS 18+). + */ + channelId?: string } export type UseLiveActivityResult = { @@ -271,6 +281,7 @@ export const startLiveActivity = async ( target: 'liveActivity', deepLinkUrl: options?.deepLinkUrl, activityId: options?.activityName, + channelId: options?.channelId, ...normalizedSharedOptions, }) return targetId diff --git a/website/docs/ios/api/configuration.md b/website/docs/ios/api/configuration.md index d992294a..dc3d9b0f 100644 --- a/website/docs/ios/api/configuration.md +++ b/website/docs/ios/api/configuration.md @@ -94,6 +94,21 @@ await startLiveActivity(variants, { **Valid range:** 0.0 to 1.0 (default: 0.0) +### Channel ID (broadcast push, iOS 18+) + +The `channelId` option subscribes the Live Activity to a broadcast channel for server-side updates. When provided, the activity receives updates via broadcast push notifications instead of individual device tokens—one server notification updates all activities on that channel. Requires `enablePushNotifications: true` and the Broadcast Capability enabled in your Apple Developer account. + +```typescript +import { startLiveActivity } from 'voltra/client' + +await startLiveActivity(variants, { + activityName: 'match-123', + channelId: 'CTrNsYq/Ee8AALLzHQaVlA==', // From APNs channel management +}) +``` + +For full broadcast setup, see [Server-side updates - Broadcast push notifications](../development/server-side-updates.md#broadcast-push-notifications-ios-18). + These options can be used together with dismissal policy and other configuration options: ```typescript diff --git a/website/docs/ios/development/managing-live-activities-locally.md b/website/docs/ios/development/managing-live-activities-locally.md index dc772bb7..fbce9b58 100644 --- a/website/docs/ios/development/managing-live-activities-locally.md +++ b/website/docs/ios/development/managing-live-activities-locally.md @@ -50,8 +50,9 @@ const variants = { } const activityId = await startLiveActivity(variants, { - activityId: 'order-123', // Optional: for re-binding on app restart + activityName: 'order-123', // Optional: for re-binding on app restart deepLinkUrl: 'myapp://order/123', // Optional: URL to open when tapped + channelId: 'CTrNsYq/Ee8AALLzHQaVlA==', // Optional: broadcast channel (iOS 18+) dismissalPolicy: { after: 30 }, staleDate: Date.now() + 60 * 60 * 1000, // 1 hour relevanceScore: 0.8, @@ -257,6 +258,20 @@ await startLiveActivity(variants, { Live Activities can receive updates even when your app is in the background or terminated, but they cannot execute JavaScript code. For real-time updates from backgrounded apps, use server-side push notifications. +### Channel ID (broadcast push, iOS 18+) + +Subscribes the Live Activity to a broadcast channel for server-side updates. When provided, the activity receives updates via broadcast push notifications instead of individual device tokens—one server notification updates all activities on that channel. + +```typescript +// For shared content (e.g., live sports, flight status) +await startLiveActivity(variants, { + activityName: 'match-123', + channelId: 'CTrNsYq/Ee8AALLzHQaVlA==', // From APNs channel management +}) +``` + +Requires `enablePushNotifications: true` in the Voltra plugin and the Broadcast Capability enabled in your Apple Developer account. See [Server-side updates](./server-side-updates.md#broadcast-push-notifications-ios-18) for details. + ## Best practices ### Activity lifecycle management diff --git a/website/docs/ios/development/server-side-updates.md b/website/docs/ios/development/server-side-updates.md index ed810946..5c329a64 100644 --- a/website/docs/ios/development/server-side-updates.md +++ b/website/docs/ios/development/server-side-updates.md @@ -210,6 +210,53 @@ useEffect(() => { Use only Voltra-provided tokens, which are specialized for Live Activity push notifications and different from regular device tokens. Update tokens are tied to specific Live Activities, while push-to-start tokens are for starting new activities. Update tokens are provided when Live Activities are started and may change during the activity's lifecycle. When sending notifications via APNS, use these push tokens as the target device token to route notifications to the correct Live Activity or device. +## Broadcast push notifications (iOS 18+) + +Starting with iOS 18 and iPadOS 18, you can use **broadcast push notifications** to update many Live Activities with a single push notification. Instead of sending individual notifications to each device token, you send one broadcast to a shared channel—all Live Activities subscribed to that channel receive the update. This is ideal for scenarios like live sports scores or flight status where many users follow the same event. + +### Prerequisites + +1. **Enable Broadcast Capability:** In your [Apple Developer account](https://developer.apple.com/account), go to Certificates, Identifiers & Profiles > Identifiers, select your App ID, and enable **Broadcast Capability** under Push Notifications. + +2. **Create a channel:** Your server creates a channel via APNs and receives a channel ID. You can maintain up to 10,000 channels per app. Use [Apple Push Notification Console](https://icloud.developer.apple.com/dashboard/notifications/) or the [channel management API](https://developer.apple.com/documentation/usernotifications/sending-channel-management-requests-to-apns) to create channels. + +3. **Plugin configuration:** Keep `enablePushNotifications: true` in your Voltra plugin config—the `aps-environment` entitlement is still required for broadcast push. + +### Starting a Live Activity with a channel + +Pass the `channelId` option when starting a Live Activity to subscribe it to a broadcast channel: + +```typescript +import { startLiveActivity } from 'voltra/client' +import { Voltra } from 'voltra' + +const activityId = await startLiveActivity(variants, { + activityName: 'match-123', + channelId: 'CTrNsYq/Ee8AALLzHQaVlA==', // Channel ID from your server +}) +``` + +When `channelId` is provided, the Live Activity subscribes to broadcast updates. On iOS versions before 18, `channelId` is ignored and the activity starts without push support. + +### Sending broadcast updates + +To update all Live Activities on a channel, send a POST request to APNs with: + +- **Path:** `/4/broadcasts/apps/` (bundle ID without the `.push-type.liveactivity` suffix) +- **Header:** `apns-channel-id: ` +- **Payload:** Same structure as individual updates—`event: "update"`, `content-state`, `timestamp`, etc. + +For the full broadcast payload format and headers, see [Apple's broadcast push documentation](https://developer.apple.com/documentation/usernotifications/sending-broadcast-push-notification-requests-to-apns). + +### Broadcast vs. individual tokens + +| Aspect | Individual tokens | Broadcast | +|--------|-------------------|-----------| +| Server sends | One notification per device | One notification per channel | +| `activityTokenReceived` event | Fires for each activity | Does not fire | +| Best for | Per-user content (orders, rides) | Shared content (scores, flights) | +| iOS version | 16.2+ | 18+ | + ## Handling background execution When Live Activity tokens change or need to be refreshed, iOS may wake your app in the background to deliver new tokens. The app has a limited window of time (typically around 30 seconds) to handle the event and communicate with your server before iOS may suspend or terminate the background process.