import 'dart:convert'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:injectable/injectable.dart'; /// Background message handler — must be a top-level function. @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { debugPrint('[FCM] Background message: ${message.messageId}'); } @lazySingleton class FcmService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); static const _androidChannel = AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications.', importance: Importance.high, ); /// Call this once during app startup (after Firebase.initializeApp). Future initialize({ void Function(RemoteMessage message)? onMessageTap, }) async { // 1. Request permission (iOS + Android 13+) await _requestPermission(); // 2. Setup local notifications (needed to show heads-up on Android) await _setupLocalNotifications(); // 3. Register background handler FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); // 4. Foreground message handler FirebaseMessaging.onMessage.listen((message) { debugPrint('[FCM] Foreground message: ${message.messageId}'); _showLocalNotification(message); }); // 5. App opened from notification (background → foreground) FirebaseMessaging.onMessageOpenedApp.listen((message) { debugPrint('[FCM] Notification tapped (background): ${message.messageId}'); onMessageTap?.call(message); }); // 6. App launched from terminated state via notification final initialMessage = await _messaging.getInitialMessage(); if (initialMessage != null) { debugPrint('[FCM] App launched from notification: ${initialMessage.messageId}'); onMessageTap?.call(initialMessage); } // 7. Print FCM token for debugging final token = await getToken(); debugPrint('[FCM] Token: $token'); } Future _requestPermission() async { final settings = await _messaging.requestPermission( alert: true, badge: true, sound: true, ); debugPrint('[FCM] Permission status: ${settings.authorizationStatus}'); } Future _setupLocalNotifications() async { // Android init const androidInit = AndroidInitializationSettings('@mipmap/launcher_icon'); // iOS init const iosInit = DarwinInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, requestSoundPermission: false, ); const initSettings = InitializationSettings( android: androidInit, iOS: iosInit, ); await _localNotifications.initialize( initSettings, onDidReceiveNotificationResponse: (details) { debugPrint('[FCM] Local notification tapped: ${details.payload}'); }, ); // Create Android notification channel if (Platform.isAndroid) { await _localNotifications .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(_androidChannel); } // iOS: show notification even when app is in foreground await _messaging.setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); } void _showLocalNotification(RemoteMessage message) { final notification = message.notification; if (notification == null) return; _localNotifications.show( notification.hashCode, notification.title, notification.body, NotificationDetails( android: AndroidNotificationDetails( _androidChannel.id, _androidChannel.name, channelDescription: _androidChannel.description, icon: '@mipmap/launcher_icon', importance: Importance.high, priority: Priority.high, ), iOS: const DarwinNotificationDetails(), ), payload: jsonEncode(message.data), ); } /// Returns the FCM registration token for this device. Future getToken() => _messaging.getToken(); /// Subscribe to a topic (e.g. 'all', 'promo'). Future subscribeToTopic(String topic) => _messaging.subscribeToTopic(topic); /// Unsubscribe from a topic. Future unsubscribeFromTopic(String topic) => _messaging.unsubscribeFromTopic(topic); /// Listen for token refresh. Stream get onTokenRefresh => _messaging.onTokenRefresh; }