Matthias Nott
4 days ago a526ea4ce4a6da31222f73ca12c8dd9017fb2410
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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<String, dynamic> 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<void> 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<String?, Object?>? — 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<String?, Object?> payload) {
        debugPrint('[Push] notification tapped (background)');
        onNotificationTap?.call(_toStringMap(payload));
      });
      debugPrint('[Push] initialized');
    } catch (e) {
      debugPrint('[Push] initialization error: $e');
    }
  }
  /// Convert Map<String?, Object?> to Map<String, dynamic> for easier use.
  Map<String, dynamic> _toStringMap(Map<String?, Object?> 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');
    }
  }
}