import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:real_estate_mobile/features/notifications/data/notification_repository.dart'; /// Top-level handler for background messages (must be top-level function). @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); debugPrint('[FCM] Background message: ${message.messageId}'); } class PushNotificationService { static final PushNotificationService _instance = PushNotificationService._internal(); factory PushNotificationService() => _instance; PushNotificationService._internal(); final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); final NotificationRepository _repo = NotificationRepository(); String? _currentToken; bool _initialized = false; /// Callback when user taps a notification — set by the app to navigate. void Function(String? actionUrl)? onNotificationTap; /// Initialize Firebase Messaging and local notifications. Future initialize() async { if (_initialized) return; _initialized = true; // Register background handler FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // Request permission final settings = await _messaging.requestPermission( alert: true, badge: true, sound: true, provisional: false, ); debugPrint('[FCM] Permission: ${settings.authorizationStatus}'); if (settings.authorizationStatus == AuthorizationStatus.denied) { return; } // Setup local notifications for foreground display await _setupLocalNotifications(); // Get token and register with backend await _getAndRegisterToken(); // Listen for token refresh _messaging.onTokenRefresh.listen(_onTokenRefresh); // Handle foreground messages FirebaseMessaging.onMessage.listen(_onForegroundMessage); // Handle notification taps (app was in background) FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp); // Check if app was opened from a terminated state notification final initialMessage = await _messaging.getInitialMessage(); if (initialMessage != null) { _onMessageOpenedApp(initialMessage); } // iOS: set foreground notification presentation options if (Platform.isIOS) { await _messaging.setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); } } Future _setupLocalNotifications() async { const androidChannel = AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'Used for important notifications', importance: Importance.high, ); await _localNotifications .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(androidChannel); const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, requestSoundPermission: false, ); await _localNotifications.initialize( const InitializationSettings( android: androidSettings, iOS: iosSettings, ), onDidReceiveNotificationResponse: (response) { final payload = response.payload; onNotificationTap?.call(payload); }, ); } Future _getAndRegisterToken() async { try { final token = await _messaging.getToken(); if (token != null) { _currentToken = token; debugPrint('[FCM] Token: $token'); await _registerTokenWithBackend(token); } } catch (e) { debugPrint('[FCM] Failed to get token: $e'); } } Future _onTokenRefresh(String token) async { debugPrint('[FCM] Token refreshed: $token'); _currentToken = token; await _registerTokenWithBackend(token); } Future _registerTokenWithBackend(String token) async { try { await _repo.registerFcmToken(token, Platform.isIOS ? 'ios' : 'android'); debugPrint('[FCM] Token registered with backend'); } catch (e) { debugPrint('[FCM] Failed to register token: $e'); } } void _onForegroundMessage(RemoteMessage message) { debugPrint('[FCM] Foreground message: ${message.notification?.title}'); final notification = message.notification; if (notification == null) return; // Show local notification on Android (iOS handles it natively) if (Platform.isAndroid) { _localNotifications.show( notification.hashCode, notification.title, notification.body, const NotificationDetails( android: AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', channelDescription: 'Used for important notifications', importance: Importance.high, priority: Priority.high, icon: '@mipmap/ic_launcher', ), ), payload: message.data['actionUrl'], ); } } void _onMessageOpenedApp(RemoteMessage message) { debugPrint('[FCM] Notification tap: ${message.data}'); final actionUrl = message.data['actionUrl'] as String?; onNotificationTap?.call(actionUrl); } /// Unregister FCM token from backend (call on logout). Future unregister() async { if (_currentToken == null) return; try { await _repo.removeFcmToken(_currentToken!); debugPrint('[FCM] Token unregistered from backend'); } catch (e) { debugPrint('[FCM] Failed to unregister token: $e'); } _currentToken = null; } /// Re-register token after login. Future registerAfterLogin() async { await _getAndRegisterToken(); } }