Skip to content

Commit

Permalink
✨Ability to get the "pushToStartToken" to support
Browse files Browse the repository at this point in the history
  • Loading branch information
Clon1998 committed Mar 2, 2025
1 parent f1ffc45 commit 3107c61
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 70 deletions.
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;
}
}
54 changes: 29 additions & 25 deletions lib/live_activities_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<String>('createActivity', {
'data': data,
'removeWhenAppIsKilled': removeWhenAppIsKilled,
Expand All @@ -49,13 +49,9 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform {
}

@override
Future updateActivity(String activityId, Map<String, dynamic> data,
[AlertConfig? alertConfig]) async {
return methodChannel.invokeMethod('updateActivity', {
'activityId': activityId,
'data': data,
'alertConfig': alertConfig?.toMap()
});
Future updateActivity(String activityId, Map<String, dynamic> data, [AlertConfig? alertConfig]) async {
return methodChannel
.invokeMethod('updateActivity', {'activityId': activityId, 'data': data, 'alertConfig': alertConfig?.toMap()});
}

@override
Expand All @@ -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,
Expand All @@ -89,18 +84,15 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform {

@override
Future<List<String>> getAllActivitiesIds() async {
final result =
await methodChannel.invokeListMethod<String>('getAllActivitiesIds');
final result = await methodChannel.invokeListMethod<String>('getAllActivitiesIds');
return result ?? [];
}

@override
Future<Map<String, LiveActivityState>> getAllActivities() async {
final result =
await methodChannel.invokeMapMethod<String, String>('getAllActivities');
final result = await methodChannel.invokeMapMethod<String, String>('getAllActivities');

return result?.map((key, value) =>
MapEntry(key, LiveActivityState.values.byName(value))) ??
return result?.map((key, value) => MapEntry(key, LiveActivityState.values.byName(value))) ??
<String, LiveActivityState>{};
}

Expand All @@ -110,16 +102,24 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform {
return false;
}

final result =
await methodChannel.invokeMethod<bool>('areActivitiesEnabled');
final result = await methodChannel.invokeMethod<bool>('areActivitiesEnabled');
return result ?? false;
}

@override
Future<bool> allowsPushStart() async {
if (!Platform.isIOS) {
return false;
}

final result = await methodChannel.invokeMethod<bool>('allowsPushStart');
return result ?? false;
}

@override
Stream<UrlSchemeData> urlSchemeStream() {
return urlSchemeChannel.receiveBroadcastStream('urlSchemeStream').map(
(dynamic event) =>
UrlSchemeData.fromMap(Map<String, dynamic>.from(event)),
(dynamic event) => UrlSchemeData.fromMap(Map<String, dynamic>.from(event)),
);
}

Expand Down Expand Up @@ -150,4 +150,8 @@ class MethodChannelLiveActivities extends LiveActivitiesPlatform {
.receiveBroadcastStream('activityUpdateStream')
.distinct()
.map((event) => ActivityUpdate.fromMap(Map<String, dynamic>.from(event)));

@override
Stream<String> get pushToStartTokenUpdateStream =>
pushToStartTokenUpdatesChannel.receiveBroadcastStream('pushToStartTokenUpdateStream').distinct().cast<String>();
}
8 changes: 8 additions & 0 deletions lib/live_activities_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ abstract class LiveActivitiesPlatform extends PlatformInterface {
'areActivitiesEnabled() has not been implemented.');
}

Future<bool> allowsPushStart() {
throw UnimplementedError(
'supportsStartActivities() has not been implemented.');
}

Stream<UrlSchemeData> urlSchemeStream() {
throw UnimplementedError('urlSchemeStream() has not been implemented.');
}
Expand All @@ -91,4 +96,7 @@ abstract class LiveActivitiesPlatform extends PlatformInterface {

Stream<ActivityUpdate> get activityUpdateStream =>
throw UnimplementedError('pushTokenUpdates has not been implemented');

Stream<String> get pushToStartTokenUpdateStream =>
throw UnimplementedError('pushToStartTokenUpdateStream has not been implemented');
}

0 comments on commit 3107c61

Please sign in to comment.