import 'package:flutter/foundation.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:push/push.dart'; import 'mqtt_service.dart'; /// Handles APNs push notification registration and token delivery to the daemon. /// /// Flow: /// 1. [initialize] requests permission and registers for remote notifications. /// 2. On new token, the token is published to the daemon via MQTT on /// `pailot/device/token`. /// 3. On notification tap (app was killed), the [onNotificationTap] callback /// is called so the UI can navigate to the right session. class PushService { PushService({required this.mqttService}); final MqttService mqttService; /// Called when the user taps a push notification. /// The [data] map contains any custom data from the notification payload /// (e.g. `sessionId`). void Function(Map data)? onNotificationTap; String? _lastToken; /// Initialize APNs: request permission and listen for tokens. /// Safe to call multiple times — subsequent calls are no-ops if already done. Future initialize() async { try { // Request permission (returns bool on iOS) final granted = await Push.instance.requestPermission( alert: true, badge: true, sound: true, ); debugPrint('[Push] permission granted: $granted'); // Register for remote notifications Push.instance.registerForRemoteNotifications(); // Listen for new/refreshed tokens Push.instance.addOnNewToken((token) { debugPrint('[Push] new token: ${token.substring(0, 16)}...'); _lastToken = token; _sendTokenToDaemon(token); }); // If we already have a token (from a previous session), fetch and send it final existingToken = await Push.instance.token; if (existingToken != null) { debugPrint('[Push] existing token: ${existingToken.substring(0, 16)}...'); _lastToken = existingToken; _sendTokenToDaemon(existingToken); } // Handle notification tap that launched the app from terminated state. // Returns Map? — extract custom data from it. final terminatedPayload = await Push.instance.notificationTapWhichLaunchedAppFromTerminated; if (terminatedPayload != null) { debugPrint('[Push] app launched from notification tap'); onNotificationTap?.call(_toStringMap(terminatedPayload)); } // Handle notification taps while app is in background (suspended). Push.instance.addOnNotificationTap((Map payload) { debugPrint('[Push] notification tapped (background)'); onNotificationTap?.call(_toStringMap(payload)); }); debugPrint('[Push] initialized'); } catch (e) { debugPrint('[Push] initialization error: $e'); } } /// Convert Map to Map for easier use. Map _toStringMap(Map src) { return { for (final entry in src.entries) if (entry.key != null) entry.key!: entry.value, }; } /// Re-send the last known token when MQTT reconnects, and clear badge. void onMqttConnected() { final token = _lastToken; if (token != null) { debugPrint('[Push] re-registering token after MQTT reconnect'); _sendTokenToDaemon(token); } clearBadge(); } /// Clear the app icon badge number. static void clearBadge() { FlutterAppBadger.removeBadge(); } /// Set the app icon badge to a specific count. static void setBadge(int count) { if (count <= 0) { FlutterAppBadger.removeBadge(); } else { FlutterAppBadger.updateBadgeCount(count); } } /// Publish the device token to the daemon via MQTT. void _sendTokenToDaemon(String token) { if (!mqttService.isConnected) { debugPrint('[Push] MQTT not connected, token will be sent on next reconnect'); return; } try { mqttService.sendDeviceToken(token); debugPrint('[Push] token sent to daemon: ${token.substring(0, 16)}...'); } catch (e) { debugPrint('[Push] failed to send token: $e'); } } }