ios/Podfile.lock
.. .. @@ -42,6 +42,8 @@ 42 42 - DKImagePickerController/PhotoGallery 43 43 - Flutter 44 44 - Flutter (1.0.0) 45 + - flutter_app_badger (1.3.0):46 + - Flutter45 47 - flutter_secure_storage (6.0.0): 46 48 - Flutter 47 49 - image_picker_ios (0.0.1): .. .. @@ -71,6 +73,7 @@ 71 73 - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 72 74 - file_picker (from `.symlinks/plugins/file_picker/ios`) 73 75 - Flutter (from `Flutter`) 76 + - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`)74 77 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) 75 78 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 76 79 - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) .. .. @@ -98,6 +101,8 @@ 98 101 :path: ".symlinks/plugins/file_picker/ios" 99 102 Flutter: 100 103 :path: Flutter 104 + flutter_app_badger:105 + :path: ".symlinks/plugins/flutter_app_badger/ios"101 106 flutter_secure_storage: 102 107 :path: ".symlinks/plugins/flutter_secure_storage/ios" 103 108 image_picker_ios: .. .. @@ -123,6 +128,7 @@ 123 128 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 124 129 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be 125 130 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 131 + flutter_app_badger: 16b371e989d04cd265df85be2c3851b49cb68d18126 132 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 127 133 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 128 134 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ios/Runner/AppDelegate.swift
.. .. @@ -8,8 +8,6 @@ 8 8 _ application: UIApplication, 9 9 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 10 ) -> Bool { 11 - application.applicationIconBadgeNumber = 012 - if #available(iOS 16.0, *) { UNUserNotificationCenter.current().setBadgeCount(0) }13 11 return super.application(application, didFinishLaunchingWithOptions: launchOptions) 14 12 } 15 13 .. .. @@ -17,13 +15,18 @@ 17 15 GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) 18 16 } 19 17 20 - // Clear badge when app becomes active21 - override func applicationDidBecomeActive(_ application: UIApplication) {22 - super.applicationDidBecomeActive(application)23 - application.applicationIconBadgeNumber = 024 - if #available(iOS 16.0, *) { UNUserNotificationCenter.current().setBadgeCount(0) }18 + // Read badge count from Flutter's SharedPreferences (UserDefaults) and update icon19 + private func updateBadgeFromPrefs() {20 + // Flutter SharedPreferences stores ints with "flutter." prefix21 + let prefs = UserDefaults.standard22 + let count = prefs.integer(forKey: "flutter.badgeCount")23 + UIApplication.shared.applicationIconBadgeNumber = count25 24 } 26 25 26 + // Don't touch badge on resume — APNs sets it, Flutter decrements it on session view27 +28 + // Badge handled by Flutter via platform channel on session switch and background29 +27 30 // Forward APNs token registration to the push plugin 28 31 override func application( 29 32 _ application: UIApplication, lib/screens/chat_screen.dart
.. .. @@ -67,6 +67,7 @@ 67 67 final Map<String, List<Message>> _catchUpPending = {}; 68 68 List<String>? _cachedSessionOrder; 69 69 Timer? _typingTimer; 70 + bool _unreadCountsLoaded = false;70 71 71 72 @override 72 73 void initState() { .. .. @@ -80,6 +81,16 @@ 80 81 // Load persisted state BEFORE connecting 81 82 final prefs = await SharedPreferences.getInstance(); 82 83 _lastSeq = prefs.getInt('lastSeq') ?? 0; 84 + // Restore persisted unread counts85 + final savedUnreads = prefs.getString('unreadCounts');86 + if (savedUnreads != null && mounted) {87 + try {88 + final map = (jsonDecode(savedUnreads) as Map<String, dynamic>)89 + .map((k, v) => MapEntry(k, v as int));90 + ref.read(unreadCountsProvider.notifier).state = map;91 + } catch (_) {}92 + }93 + _unreadCountsLoaded = true;83 94 // Restore saved session order and active session 84 95 _cachedSessionOrder = prefs.getStringList('sessionOrder'); 85 96 final savedSessionId = prefs.getString('activeSessionId'); .. .. @@ -133,7 +144,17 @@ 133 144 if (_ws != null && !_ws!.isConnected) { 134 145 _ws!.connect(); 135 146 } 147 + // Don't update badge here — provider might not have loaded persisted counts yet.148 + // Native applicationDidBecomeActive reads correct value from UserDefaults.149 + } else if (state == AppLifecycleState.paused && _unreadCountsLoaded) {150 + // Set badge to total unread count when going to background151 + _updateBadgeFromUnreads();136 152 } 153 + }154 +155 + void _updateBadgeFromUnreads() {156 + final counts = ref.read(unreadCountsProvider);157 + _persistUnreadCounts(counts);137 158 } 138 159 139 160 bool _isLoadingMore = false; .. .. @@ -675,6 +696,18 @@ 675 696 final counts = Map<String, int>.from(ref.read(unreadCountsProvider)); 676 697 counts[sessionId] = (counts[sessionId] ?? 0) + 1; 677 698 ref.read(unreadCountsProvider.notifier).state = counts; 699 + _persistUnreadCounts(counts);700 + }701 +702 + void _persistUnreadCounts(Map<String, int> counts) {703 + final total = counts.values.fold<int>(0, (sum, v) => sum + v);704 + // Set badge immediately via platform channel (synchronous native call)705 + PushService.setBadge(total);706 + // Also persist to SharedPreferences for app restart707 + SharedPreferences.getInstance().then((prefs) {708 + prefs.setString('unreadCounts', jsonEncode(counts));709 + prefs.setInt('badgeCount', total);710 + });678 711 } 679 712 680 713 Future<void> _switchSession(String sessionId) async { .. .. @@ -691,6 +724,10 @@ 691 724 final counts = Map<String, int>.from(ref.read(unreadCountsProvider)); 692 725 counts.remove(sessionId); 693 726 ref.read(unreadCountsProvider.notifier).state = counts; 727 + _persistUnreadCounts(counts);728 +729 + // Update badge to reflect remaining unreads730 + _updateBadgeFromUnreads();694 731 695 732 _sendCommand('switch', {'sessionId': sessionId}); 696 733 _scrollToBottom(); lib/services/push_service.dart
.. .. @@ -1,5 +1,5 @@ 1 1 import 'package:flutter/foundation.dart'; 2 -import 'package:flutter/services.dart';2 +import 'package:flutter_app_badger/flutter_app_badger.dart';3 3 import 'package:push/push.dart'; 4 4 5 5 import 'mqtt_service.dart'; .. .. @@ -95,14 +95,16 @@ 95 95 96 96 /// Clear the app icon badge number. 97 97 static void clearBadge() { 98 - try {99 - // Use UIApplication.shared.applicationIconBadgeNumber = 0 via platform channel100 - const platform = MethodChannel('com.tekmidian.pailot/badge');101 - platform.invokeMethod('clearBadge').catchError((_) {102 - // Fallback: UNUserNotificationCenter approach103 - debugPrint('[Push] clearBadge via platform channel failed, using Push API');104 - });105 - } catch (_) {}98 + FlutterAppBadger.removeBadge();99 + }100 +101 + /// Set the app icon badge to a specific count.102 + static void setBadge(int count) {103 + if (count <= 0) {104 + FlutterAppBadger.removeBadge();105 + } else {106 + FlutterAppBadger.updateBadgeCount(count);107 + }106 108 } 107 109 108 110 /// Publish the device token to the daemon via MQTT. macos/Flutter/GeneratedPluginRegistrant.swift
.. .. @@ -10,6 +10,7 @@ 10 10 import device_info_plus 11 11 import file_picker 12 12 import file_selector_macos 13 +import flutter_app_badger13 14 import flutter_secure_storage_macos 14 15 import push 15 16 import record_macos .. .. @@ -22,6 +23,7 @@ 22 23 DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 23 24 FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 24 25 FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 26 + FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))25 27 FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) 26 28 PushPlugin.register(with: registry.registrar(forPlugin: "PushPlugin")) 27 29 RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) pubspec.lock
.. .. @@ -294,6 +294,14 @@ 294 294 description: flutter 295 295 source: sdk 296 296 version: "0.0.0" 297 + flutter_app_badger:298 + dependency: "direct main"299 + description:300 + name: flutter_app_badger301 + sha256: "64d4a279bab862ed28850431b9b446b9820aaae0bf363322d51077419f930fa8"302 + url: "https://pub.dev"303 + source: hosted304 + version: "1.5.0"297 305 flutter_lints: 298 306 dependency: "direct dev" 299 307 description: pubspec.yaml
.. .. @@ -32,6 +32,7 @@ 32 32 bonsoir: ^6.0.2 33 33 crypto: ^3.0.7 34 34 push: ^3.3.3 35 + flutter_app_badger: ^1.5.035 36 36 37 dev_dependencies: 37 38 flutter_test: