Skip to content

Commit

Permalink
🔀 Merge pull request #119 from Clon1998/main
Browse files Browse the repository at this point in the history
✨Ability to get the "pushToStartToken"
  • Loading branch information
istornz authored Mar 3, 2025
2 parents f1ffc45 + a182404 commit 3f14dcc
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 86 deletions.
108 changes: 92 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,24 +296,100 @@ That's it 😇
<br />
## Push-to-Start Live Activities (iOS 17.2+) 🚀
iOS 17.2 introduces the ability
to [create Live Activities remotely](https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications#Start-new-Live-Activities-with-ActivityKit-push-notifications)
via push notifications before the user has even
opened your app. This is called "push-to-start" functionality.
### Check Support
First, check if the device supports push-to-start:
```dart
final isPushToStartSupported = await _liveActivitiesPlugin.allowsPushStart();
if (isPushToStartSupported) {
// Device supports push-to-start (iOS 17.2+)
}
```

### Listen for Push-to-Start Tokens

To use push-to-start, you need to listen for push-to-start tokens:

```dart
_liveActivitiesPlugin.pushToStartTokenUpdateStream.listen((token) {
// Send this token to your server
print('Received push-to-start token: $token');
// Your server can use this token to create a Live Activity
// without the user having to open your app first
});
```

### Server Implementation

On your server, you'll need to send a push notification with the following
payload [structure](https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications#Construct-the-payload-that-starts-a-Live-Activity):

```json
{
"aps": {
"timestamp": 1234,
"event": "start",
"content-state": {
"currentHealthLevel": 100,
"eventDescription": "Adventure has begun!"
},
"attributes-type": "AdventureAttributes",
"attributes": {
"currentHealthLevel": 100,
"eventDescription": "Adventure has begun!"
},
"alert": {
"title": {
"loc-key": "%@ is on an adventure!",
"loc-args": [
"Power Panda"
]
},
"body": {
"loc-key": "%@ found a sword!",
"loc-args": [
"Power Panda"
]
},
"sound": "chime.aiff"
}
}
}
```

The push notification should be sent to the push-to-start token you received from the pushToStartTokenUpdateStream. Your
server needs to use Apple's APNs with the appropriate authentication to deliver these notifications.

## 📘 Documentation

| Name | Description | Returned value |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `.init()` | Initialize the Plugin by providing an App Group Id (see above) | `Future` When the plugin is ready to create/update an activity |
| `.createActivity()` | Create an iOS live activity | `String` The activity identifier |
| `.createOrUpdateActivity()` | Create or updates an (existing) live activity based on the provided `UUID` via `customId` | `String` The activity identifier |
| `.updateActivity()` | Update the live activity data by using the `activityId` provided | `Future` When the activity was updated |
| `.endActivity()` | End the live activity by using the `activityId` provided | `Future` When the activity was ended |
| `.getAllActivitiesIds()` | Get all activities ids created | `Future<List<String>>` List of all activities ids |
| `.getAllActivities()` | Get a Map of activitiyIds and the `ActivityState` | `Future<Map<String, LiveActivityState>>` Map of all activitiyId -> `LiveActivityState` |
| `.endAllActivities()` | End all live activities of the app | `Future` When all activities was ended |
| `.areActivitiesEnabled()` | Check if live activities feature are supported & enabled | `Future<bool>` Live activities supported or not |
| `.getActivityState()` | Get the activity current state | `Future<LiveActivityState?>` An enum to know the status of the activity (`active`, `dismissed` or `ended`) |
| `.getPushToken()` | Get the activity push token synchronously (prefer using `activityUpdateStream` instead to keep push token up to date) | `String?` The activity push token (can be null) |
| `.urlSchemeStream()` | Subscription to handle every url scheme (ex: when the app is opened from a live activity / dynamic island button, you can pass data) | `Future<UrlSchemeData>` Url scheme data which handle `scheme` `url` `host` `path` `queryItems` |
| `.dispose()` | Remove all pictures passed in the AppGroups directory in the current session, you can use the `force` parameters to remove **all** pictures | `Future` Picture removed |
| `.activityUpdateStream` | Get notified with a stream about live activity push token & status | `Stream<ActivityUpdate>` Status updates for new push tokens or when the activity ends |
| Name | Description | Returned value |
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| `.init()` | Initialize the Plugin by providing an App Group Id (see above) | `Future` When the plugin is ready to create/update an activity |
| `.createActivity()` | Create an iOS live activity | `String` The activity identifier |
| `.createOrUpdateActivity()` | Create or updates an (existing) live activity based on the provided `UUID` via `customId` | `String` The activity identifier |
| `.updateActivity()` | Update the live activity data by using the `activityId` provided | `Future` When the activity was updated |
| `.endActivity()` | End the live activity by using the `activityId` provided | `Future` When the activity was ended |
| `.getAllActivitiesIds()` | Get all activities ids created | `Future<List<String>>` List of all activities ids |
| `.getAllActivities()` | Get a Map of activitiyIds and the `ActivityState` | `Future<Map<String, LiveActivityState>>` Map of all activitiyId -> `LiveActivityState` |
| `.endAllActivities()` | End all live activities of the app | `Future` When all activities was ended |
| `.areActivitiesEnabled()` | Check if live activities feature are supported & enabled | `Future<bool>` Live activities supported or not |
| `.allowsPushStart()` | Check if device supports push-to-start for Live Activities (iOS 17.2+) | `Future<bool>` Whether push-to-start is supported |
| `.getActivityState()` | Get the activity current state | `Future<LiveActivityState?>` An enum to know the status of the activity (`active`, `dismissed` or `ended`) |
| `.getPushToken()` | Get the activity push token synchronously (prefer using `activityUpdateStream` instead to keep push token up to date) | `String?` The activity push token (can be null) |
| `.urlSchemeStream()` | Subscription to handle every url scheme (ex: when the app is opened from a live activity / dynamic island button, you can pass data) | `Future<UrlSchemeData>` Url scheme data which handle `scheme` `url` `host` `path` `queryItems` |
| `.dispose()` | Remove all pictures passed in the AppGroups directory in the current session, you can use the `force` parameters to remove **all** pictures | `Future` Picture removed |
| `.activityUpdateStream` | Get notified with a stream about live activity push token & status | `Stream<ActivityUpdate>` Status updates for new push tokens or when the activity ends |
| `.pushToStartTokenUpdateStream` | Stream of push-to-start tokens for creating Live Activities remotely (iOS 17.2+) | `Stream<String>` Stream of tokens for push-to-start capability |

<br />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,66 +26,27 @@ public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
private var sharedDefault: UserDefaults?
private var appLifecycleLiveActivityIds = [String]()
private var activityEventSink: FlutterEventSink?
private var pushToStartTokenEventSink: FlutterEventSink?

public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "live_activities", binaryMessenger: registrar.messenger())
let urlSchemeChannel = FlutterEventChannel(name: "live_activities/url_scheme", binaryMessenger: registrar.messenger())
let activityStatusChannel = FlutterEventChannel(name: "live_activities/activity_status", binaryMessenger: registrar.messenger())
let pushToStartTokenUpdatesChannel = FlutterEventChannel(name: "live_activities/push_to_start_token_updates", binaryMessenger: registrar.messenger())

let instance = LiveActivitiesPlugin()

registrar.addMethodCallDelegate(instance, channel: channel)
urlSchemeChannel.setStreamHandler(instance)
activityStatusChannel.setStreamHandler(instance)
pushToStartTokenUpdatesChannel.setStreamHandler(instance)
registrar.addApplicationDelegate(instance)

if #available(iOS 17.2, *) {
Task {
for await data in Activity<LiveActivitiesAppAttributes>.pushToStartTokenUpdates {
let token = data.map {String(format: "%02x", $0)}.joined()
print("Activity PushToStart Token: \(token)")
// send this token to your notification server

DispatchQueue.main.async {
channel.invokeMethod("onPushTokenReceived", arguments: token)
}
}

for await activity in Activity<LiveActivitiesAppAttributes>.activityUpdates {
// Upon finding one, listen for its push token (it is not available immediately!)
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
print("New activity detected with push token: \(pushTokenString)")
}
}
}
}
}

public func getPushToStartToken() {
if #available(iOS 17.2, *) {
Task {
for await data in Activity<LiveActivitiesAppAttributes>.pushToStartTokenUpdates {
let token = data.map {String(format: "%02x", $0)}.joined()
print("Activity PushToStart Token: \(token)")
//send this token to your notification server
}

for await activity in Activity<LiveActivitiesAppAttributes>.activityUpdates {
// Upon finding one, listen for its push token (it is not available immediately!)
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
print("New activity detected with push token: \(pushTokenString)")
}
}
}
}
}


public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
urlSchemeSink = nil
activityEventSink = nil
pushToStartTokenEventSink = nil
}

public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
Expand All @@ -94,6 +55,9 @@ public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
urlSchemeSink = events
} else if (args == "activityUpdateStream") {
activityEventSink = events
} else if (args == "pushToStartTokenUpdateStream") {
pushToStartTokenEventSink = events
startObservingPushToStartTokens()
}
}

Expand All @@ -106,7 +70,9 @@ public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
urlSchemeSink = nil
} else if (args == "activityUpdateStream") {
activityEventSink = nil
}
} else if (args == "pushToStartTokenUpdateStream") {
pushToStartTokenEventSink = nil
}
}
return nil
}
Expand All @@ -127,6 +93,17 @@ public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
result(ActivityAuthorizationInfo().areActivitiesEnabled)
return
}

if (call.method == "allowsPushStart") {
guard #available(iOS 17.2, *), !ProcessInfo.processInfo.isiOSAppOnMac else {
result(false)
return
}

// This is iOS 17.2+ so push-to-start is supported
result(true)
return
}

if #available(iOS 16.1, *) {
switch call.method {
Expand Down Expand Up @@ -403,7 +380,22 @@ public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
result(nil)
}
}


private func startObservingPushToStartTokens() {
if #available(iOS 17.2, *) {
Task {
for await data in Activity<LiveActivitiesAppAttributes>.pushToStartTokenUpdates {
let token = data.map { String(format: "%02x", $0) }.joined()
print("Activity PushToStart Token: \(token)")

DispatchQueue.main.async {
self.pushToStartTokenEventSink?(token)
}
}
}
}
}

@available(iOS 16.1, *)
func getAllActivitiesIds(result: @escaping FlutterResult) {
var activitiesId: [String] = []
Expand Down
30 changes: 30 additions & 0 deletions lib/live_activities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ class LiveActivities {
return LiveActivitiesPlatform.instance.areActivitiesEnabled();
}

/// Checks if iOS 17.2+ which allows push start for live activities.
Future<bool> allowsPushStart() {
return LiveActivitiesPlatform.instance.allowsPushStart();
}

/// Get a stream of url scheme data.
/// Don't forget to add **CFBundleURLSchemes** to your Info.plist file.
/// Return a Future of [scheme] [url] [host] [path] and [queryParameters].
Expand Down Expand Up @@ -147,4 +152,29 @@ class LiveActivities {
/// ```
Stream<ActivityUpdate> get activityUpdateStream =>
LiveActivitiesPlatform.instance.activityUpdateStream;

/// A stream of push-to-start tokens for iOS 17.2+ Live Activities.
/// This stream emits tokens that can be used to start a Live Activity remotely via push notifications.
///
/// When iOS generates or updates a push-to-start token, it will be emitted through this stream.
/// You should send this token to your push notification server to enable remote Live Activity creation.
///
/// Example usage:
/// ```dart
/// liveActivities.pushToStartTokenUpdateStream.listen((token) {
/// // Send token to your server
/// print('Received push-to-start token: $token');
/// });
/// ```
///
/// This feature is only available on iOS 17.2 and later. Use [allowsPushStart] to check support.
Stream<String> get pushToStartTokenUpdateStream async* {
final allowed = await allowsPushStart();

if (!allowed) {
throw Exception('Push-to-start is not allowed on this device');
}

yield* LiveActivitiesPlatform.instance.pushToStartTokenUpdateStream;
}
}
Loading

0 comments on commit 3f14dcc

Please sign in to comment.