From 3107c61fd52077e09c9595a6f4180395029971c2 Mon Sep 17 00:00:00 2001 From: Patrick Schmidt Date: Sun, 2 Mar 2025 14:08:59 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8Ability=20to=20get=20the=20"pushTo?= =?UTF-8?q?StartToken"=20to=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveActivitiesPlugin.swift | 82 +++++++++---------- lib/live_activities.dart | 30 +++++++ lib/live_activities_method_channel.dart | 54 ++++++------ lib/live_activities_platform_interface.dart | 8 ++ 4 files changed, 104 insertions(+), 70 deletions(-) diff --git a/ios/live_activities/Sources/live_activities/LiveActivitiesPlugin.swift b/ios/live_activities/Sources/live_activities/LiveActivitiesPlugin.swift index 4960fad..a0c92eb 100644 --- a/ios/live_activities/Sources/live_activities/LiveActivitiesPlugin.swift +++ b/ios/live_activities/Sources/live_activities/LiveActivitiesPlugin.swift @@ -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.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.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.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.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? { @@ -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() } } @@ -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 } @@ -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 { @@ -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.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] = [] diff --git a/lib/live_activities.dart b/lib/live_activities.dart index a9371ea..d86ee0c 100644 --- a/lib/live_activities.dart +++ b/lib/live_activities.dart @@ -105,6 +105,11 @@ class LiveActivities { return LiveActivitiesPlatform.instance.areActivitiesEnabled(); } + /// Checks if iOS 17.2+ which allows push start for live activities. + Future 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]. @@ -147,4 +152,29 @@ class LiveActivities { /// ``` Stream 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 get pushToStartTokenUpdateStream async* { + final allowed = await allowsPushStart(); + + if (!allowed) { + throw Exception('Push-to-start is not allowed on this device'); + } + + yield* LiveActivitiesPlatform.instance.pushToStartTokenUpdateStream; + } } diff --git a/lib/live_activities_method_channel.dart b/lib/live_activities_method_channel.dart index d91f8a7..561698c 100644 --- a/lib/live_activities_method_channel.dart +++ b/lib/live_activities_method_channel.dart @@ -17,12 +17,13 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { final methodChannel = const MethodChannel('live_activities'); @visibleForTesting - final activityStatusChannel = - const EventChannel('live_activities/activity_status'); + final activityStatusChannel = const EventChannel('live_activities/activity_status'); @visibleForTesting - final EventChannel urlSchemeChannel = - const EventChannel('live_activities/url_scheme'); + final EventChannel urlSchemeChannel = const EventChannel('live_activities/url_scheme'); + + @visibleForTesting + final EventChannel pushToStartTokenUpdatesChannel = const EventChannel('live_activities/push_to_start_token_updates'); @override Future init(String appGroupId, {String? urlScheme}) async { @@ -39,8 +40,7 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { Duration? staleIn, }) async { // If the duration is less than 1 minute then pass a null value instead of using 0 minutes - final staleInMinutes = - (staleIn?.inMinutes ?? 0) >= 1 ? staleIn?.inMinutes : null; + final staleInMinutes = (staleIn?.inMinutes ?? 0) >= 1 ? staleIn?.inMinutes : null; return methodChannel.invokeMethod('createActivity', { 'data': data, 'removeWhenAppIsKilled': removeWhenAppIsKilled, @@ -49,13 +49,9 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { } @override - Future updateActivity(String activityId, Map data, - [AlertConfig? alertConfig]) async { - return methodChannel.invokeMethod('updateActivity', { - 'activityId': activityId, - 'data': data, - 'alertConfig': alertConfig?.toMap() - }); + Future updateActivity(String activityId, Map data, [AlertConfig? alertConfig]) async { + return methodChannel + .invokeMethod('updateActivity', {'activityId': activityId, 'data': data, 'alertConfig': alertConfig?.toMap()}); } @override @@ -65,8 +61,7 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { bool removeWhenAppIsKilled = false, Duration? staleIn, }) async { - final staleInMinutes = - (staleIn?.inMinutes ?? 0) >= 1 ? staleIn?.inMinutes : null; + final staleInMinutes = (staleIn?.inMinutes ?? 0) >= 1 ? staleIn?.inMinutes : null; return methodChannel.invokeMethod('createOrUpdateActivity', { 'customId': customId, 'data': data, @@ -89,18 +84,15 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { @override Future> getAllActivitiesIds() async { - final result = - await methodChannel.invokeListMethod('getAllActivitiesIds'); + final result = await methodChannel.invokeListMethod('getAllActivitiesIds'); return result ?? []; } @override Future> getAllActivities() async { - final result = - await methodChannel.invokeMapMethod('getAllActivities'); + final result = await methodChannel.invokeMapMethod('getAllActivities'); - return result?.map((key, value) => - MapEntry(key, LiveActivityState.values.byName(value))) ?? + return result?.map((key, value) => MapEntry(key, LiveActivityState.values.byName(value))) ?? {}; } @@ -110,16 +102,24 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { return false; } - final result = - await methodChannel.invokeMethod('areActivitiesEnabled'); + final result = await methodChannel.invokeMethod('areActivitiesEnabled'); + return result ?? false; + } + + @override + Future allowsPushStart() async { + if (!Platform.isIOS) { + return false; + } + + final result = await methodChannel.invokeMethod('allowsPushStart'); return result ?? false; } @override Stream urlSchemeStream() { return urlSchemeChannel.receiveBroadcastStream('urlSchemeStream').map( - (dynamic event) => - UrlSchemeData.fromMap(Map.from(event)), + (dynamic event) => UrlSchemeData.fromMap(Map.from(event)), ); } @@ -150,4 +150,8 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform { .receiveBroadcastStream('activityUpdateStream') .distinct() .map((event) => ActivityUpdate.fromMap(Map.from(event))); + + @override + Stream get pushToStartTokenUpdateStream => + pushToStartTokenUpdatesChannel.receiveBroadcastStream('pushToStartTokenUpdateStream').distinct().cast(); } diff --git a/lib/live_activities_platform_interface.dart b/lib/live_activities_platform_interface.dart index a8505c0..1c0826c 100644 --- a/lib/live_activities_platform_interface.dart +++ b/lib/live_activities_platform_interface.dart @@ -77,6 +77,11 @@ abstract class LiveActivitiesPlatform extends PlatformInterface { 'areActivitiesEnabled() has not been implemented.'); } + Future allowsPushStart() { + throw UnimplementedError( + 'supportsStartActivities() has not been implemented.'); + } + Stream urlSchemeStream() { throw UnimplementedError('urlSchemeStream() has not been implemented.'); } @@ -91,4 +96,7 @@ abstract class LiveActivitiesPlatform extends PlatformInterface { Stream get activityUpdateStream => throw UnimplementedError('pushTokenUpdates has not been implemented'); + + Stream get pushToStartTokenUpdateStream => + throw UnimplementedError('pushToStartTokenUpdateStream has not been implemented'); } From 1a0ed2714f656b5decb598d96e379f13da41bdac Mon Sep 17 00:00:00 2001 From: Patrick Schmidt Date: Sun, 2 Mar 2025 14:14:36 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20new=20pushT?= =?UTF-8?q?oStart=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/live_activities_test.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/live_activities_test.dart b/test/live_activities_test.dart index fbd2ae5..2676b7c 100644 --- a/test/live_activities_test.dart +++ b/test/live_activities_test.dart @@ -98,6 +98,16 @@ class MockLiveActivitiesPlatform }) { return Future.value(); } + + @override + Future allowsPushStart() { + return Future.value(true); + } + + @override + Stream get pushToStartTokenUpdateStream { + return Stream.value('PUSH_TO_START_TOKEN'); + } } void main() { @@ -190,4 +200,15 @@ void main() { expect(correctMappingNotNull, 'ACTIVITY_TOKEN'); }); + + test('allowsPushStart', () async { + expect(await liveActivitiesPlugin.allowsPushStart(), true); + }); + + test('pushToStartTokenUpdateStream', () async { + expect( + await liveActivitiesPlugin.pushToStartTokenUpdateStream.first, + 'PUSH_TO_START_TOKEN', + ); + }); } From 520c7d0fd6ff04d04f147d6cf416e815e7b0ecd8 Mon Sep 17 00:00:00 2001 From: Patrick Schmidt Date: Sun, 2 Mar 2025 14:24:15 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9D=20Add=20section=20about=20push?= =?UTF-8?q?ToStart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 172 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 5109ee5..9914bc6 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,12 @@ You need to **implement** in your Flutter iOS project a **Widget Extension** & d > ℹī¸ You can check into the [**example repository**](https://github.com/istornz/live_activities/tree/main/example) for a full example app using Live Activities & Dynamic Island - ## 📱 Native - - Open the Xcode workspace project `ios/Runner.xcworkspace`. - - Click on `File` -> `New` -> `Target...` - - Select `Widget Extension` & click on **Next**. + - Open the Xcode workspace project `ios/Runner.xcworkspace`. + - Click on `File` -> `New` -> `Target...` + - Select `Widget Extension` & click on **Next**. - Specify the product name (e.g., `MyAppWidget`) and be sure to select "**Runner**" in "Embed in Application" dropdown. - - Click on **Finish**. - - When selecting Finish, an alert will appear, you will need to click on **Activate**. + - Click on **Finish**. + - When selecting Finish, an alert will appear, you will need to click on **Activate**. create widget extension xcode @@ -124,20 +124,20 @@ struct FootballMatchApp: Widget { - ## 💙 Flutter - - Import the plugin. + - Import the plugin. ```dart import 'package:live_activities/live_activities.dart'; ``` - - Initialize the Plugin by passing the created **App Group Id** (created above). + - Initialize the Plugin by passing the created **App Group Id** (created above). ```dart final _liveActivitiesPlugin = LiveActivities(); _liveActivitiesPlugin.init(appGroupId: "YOUR_GROUP_ID"); ``` - - Create your dynamic activity. + - Create your dynamic activity. ```dart final Map activityModel = { @@ -183,10 +183,10 @@ final Map activityModel = { 'txtFile': LiveActivityFileFromAsset('assets/files/rules.txt'), 'assetKey': LiveActivityFileFromAsset.image('assets/images/pizza_chorizo.png'), 'url': LiveActivityFileFromUrl.image( - 'https://cdn.pixabay.com/photo/2015/10/01/17/17/car-967387__480.png', - imageOptions: LiveActivityImageFileOptions( - resizeFactor: 0.2 - ) + 'https://cdn.pixabay.com/photo/2015/10/01/17/17/car-967387__480.png', + imageOptions: LiveActivityImageFileOptions( + resizeFactor: 0.2 + ) ), }; @@ -270,24 +270,24 @@ You can update live activity directly in your app using the `updateActivity()` m To do this, you can update it using Push Notification on a server. - Get the push token: - - Listen on the activity updates (recommended): - ```dart - _liveActivitiesPlugin.activityUpdateStream.listen((event) { - event.map( - active: (activity) { - // Get the token - print(activity.activityToken); - }, - ended: (activity) {}, - unknown: (activity) {}, - ); - }); - ``` - - Get directly the push token (not recommended, because the token may change in the future): - ```dart - final activityToken = await _liveActivitiesPlugin.getPushToken(_latestActivityId!); - print(activityToken); - ``` + - Listen on the activity updates (recommended): + ```dart + _liveActivitiesPlugin.activityUpdateStream.listen((event) { + event.map( + active: (activity) { + // Get the token + print(activity.activityToken); + }, + ended: (activity) {}, + unknown: (activity) {}, + ); + }); + ``` + - Get directly the push token (not recommended, because the token may change in the future): + ```dart + final activityToken = await _liveActivitiesPlugin.getPushToken(_latestActivityId!); + print(activityToken); + ``` - Update your activity with the token on your server (more information can be [**found here**](https://ohdarling88.medium.com/update-dynamic-island-and-live-activity-with-push-notification-38779803c145)). To set `matchName` for a specific notification, you just need to grab the notification id you want (ex. `35253464632`) and concatenate with your key by adding a `_`, example: `35253464632_matchName`. @@ -296,24 +296,100 @@ That's it 😇
+## 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 of all activities ids | -| `.getAllActivities()` | Get a Map of activitiyIds and the `ActivityState` | `Future>` 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` Live activities supported or not | -| `.getActivityState()` | Get the activity current state | `Future` 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` 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` 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 of all activities ids | +| `.getAllActivities()` | Get a Map of activitiyIds and the `ActivityState` | `Future>` 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` Live activities supported or not | +| `.allowsPushStart()` | Check if device supports push-to-start for Live Activities (iOS 17.2+) | `Future` Whether push-to-start is supported | +| `.getActivityState()` | Get the activity current state | `Future` 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` 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` 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` Stream of tokens for push-to-start capability |
@@ -342,12 +418,12 @@ That's it 😇 > - Supports Live Activities: Be sure to set the `NSSupportsLiveActivities` property to `true` in `Info.plist` files for **BOTH** `Runner` and your `extension`. > - iOS Version Requirement: The device must run **iOS 16.1 or later**. > - Device Activity Check: Confirm that the `areActivitiesEnabled()` method returns true on your device. -> - Minimum Deployment Target: Confirm that the `extensions` deployment target is not set lower than your devices. +> - Minimum Deployment Target: Confirm that the `extensions` deployment target is not set lower than your devices. ### Is Android supported? > Currently, no, but the plugin does not crash when run on Android. This means you can safely install it in a hybrid app. -> +> > Simply call `areActivitiesEnabled()` before creating your activity to ensure it can be displayed on the user's device. 😊 ## đŸ‘Ĩ Contributions From a182404f68b96b8b080513c0ee22027a43a0aaaa Mon Sep 17 00:00:00 2001 From: Patrick Schmidt Date: Sun, 2 Mar 2025 20:09:46 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20Revert=20indent=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9914bc6..76f403b 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,12 @@ You need to **implement** in your Flutter iOS project a **Widget Extension** & d > ℹī¸ You can check into the [**example repository**](https://github.com/istornz/live_activities/tree/main/example) for a full example app using Live Activities & Dynamic Island - ## 📱 Native - - Open the Xcode workspace project `ios/Runner.xcworkspace`. - - Click on `File` -> `New` -> `Target...` - - Select `Widget Extension` & click on **Next**. + - Open the Xcode workspace project `ios/Runner.xcworkspace`. + - Click on `File` -> `New` -> `Target...` + - Select `Widget Extension` & click on **Next**. - Specify the product name (e.g., `MyAppWidget`) and be sure to select "**Runner**" in "Embed in Application" dropdown. - - Click on **Finish**. - - When selecting Finish, an alert will appear, you will need to click on **Activate**. + - Click on **Finish**. + - When selecting Finish, an alert will appear, you will need to click on **Activate**. create widget extension xcode @@ -124,20 +124,20 @@ struct FootballMatchApp: Widget { - ## 💙 Flutter - - Import the plugin. + - Import the plugin. ```dart import 'package:live_activities/live_activities.dart'; ``` - - Initialize the Plugin by passing the created **App Group Id** (created above). + - Initialize the Plugin by passing the created **App Group Id** (created above). ```dart final _liveActivitiesPlugin = LiveActivities(); _liveActivitiesPlugin.init(appGroupId: "YOUR_GROUP_ID"); ``` - - Create your dynamic activity. + - Create your dynamic activity. ```dart final Map activityModel = { @@ -183,10 +183,10 @@ final Map activityModel = { 'txtFile': LiveActivityFileFromAsset('assets/files/rules.txt'), 'assetKey': LiveActivityFileFromAsset.image('assets/images/pizza_chorizo.png'), 'url': LiveActivityFileFromUrl.image( - 'https://cdn.pixabay.com/photo/2015/10/01/17/17/car-967387__480.png', - imageOptions: LiveActivityImageFileOptions( - resizeFactor: 0.2 - ) + 'https://cdn.pixabay.com/photo/2015/10/01/17/17/car-967387__480.png', + imageOptions: LiveActivityImageFileOptions( + resizeFactor: 0.2 + ) ), }; @@ -270,24 +270,24 @@ You can update live activity directly in your app using the `updateActivity()` m To do this, you can update it using Push Notification on a server. - Get the push token: - - Listen on the activity updates (recommended): - ```dart - _liveActivitiesPlugin.activityUpdateStream.listen((event) { - event.map( - active: (activity) { - // Get the token - print(activity.activityToken); - }, - ended: (activity) {}, - unknown: (activity) {}, - ); - }); - ``` - - Get directly the push token (not recommended, because the token may change in the future): - ```dart - final activityToken = await _liveActivitiesPlugin.getPushToken(_latestActivityId!); - print(activityToken); - ``` + - Listen on the activity updates (recommended): + ```dart + _liveActivitiesPlugin.activityUpdateStream.listen((event) { + event.map( + active: (activity) { + // Get the token + print(activity.activityToken); + }, + ended: (activity) {}, + unknown: (activity) {}, + ); + }); + ``` + - Get directly the push token (not recommended, because the token may change in the future): + ```dart + final activityToken = await _liveActivitiesPlugin.getPushToken(_latestActivityId!); + print(activityToken); + ``` - Update your activity with the token on your server (more information can be [**found here**](https://ohdarling88.medium.com/update-dynamic-island-and-live-activity-with-push-notification-38779803c145)). To set `matchName` for a specific notification, you just need to grab the notification id you want (ex. `35253464632`) and concatenate with your key by adding a `_`, example: `35253464632_matchName`. @@ -418,12 +418,12 @@ server needs to use Apple's APNs with the appropriate authentication to deliver > - Supports Live Activities: Be sure to set the `NSSupportsLiveActivities` property to `true` in `Info.plist` files for **BOTH** `Runner` and your `extension`. > - iOS Version Requirement: The device must run **iOS 16.1 or later**. > - Device Activity Check: Confirm that the `areActivitiesEnabled()` method returns true on your device. -> - Minimum Deployment Target: Confirm that the `extensions` deployment target is not set lower than your devices. +> - Minimum Deployment Target: Confirm that the `extensions` deployment target is not set lower than your devices. ### Is Android supported? > Currently, no, but the plugin does not crash when run on Android. This means you can safely install it in a hybrid app. -> +> > Simply call `areActivitiesEnabled()` before creating your activity to ensure it can be displayed on the user's device. 😊 ## đŸ‘Ĩ Contributions