import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'delivery_repository.dart'; @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { try { await Firebase.initializeApp(); } catch (_) { // Firebase is optional in local builds until google-services.json is added. } } /// Adapter preparado para FCM. La inicialización de Firebase requiere los /// archivos google-services.json del tenant/entorno antes de activar producción. class PushNotificationService { static bool _initialized = false; static final StreamController _events = StreamController.broadcast(); static final StreamController _openedEvents = StreamController.broadcast(); static final StreamController _openedOrderEvents = StreamController.broadcast(); static final List _pendingOpenedOrderIds = []; static const MethodChannel _nativeChannel = MethodChannel('smartplus/delivery_tracking'); static RemoteMessage? _initialMessage; static Stream get events => _events.stream; static Stream get openedEvents => _openedEvents.stream; static Stream get openedOrderEvents => _openedOrderEvents.stream; static Future initialize() async { try { await Firebase.initializeApp(); _nativeChannel.setMethodCallHandler((call) async { if (call.method == 'notificationOpened') { final orderId = (call.arguments as Map?)?['orderId']?.toString(); if (orderId != null && orderId.isNotEmpty) { _dispatchOpenedOrderId(orderId); } } }); final nativeInitialOrderId = await _nativeChannel.invokeMethod( 'consumePendingNotification', ); if (nativeInitialOrderId != null && nativeInitialOrderId.isNotEmpty) { _dispatchOpenedOrderId(nativeInitialOrderId); } FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); FirebaseMessaging.onMessage.listen(_events.add); FirebaseMessaging.onMessageOpenedApp.listen(_dispatchOpenedMessage); final initialMessage = await FirebaseMessaging.instance .getInitialMessage(); _initialMessage = initialMessage; _initialized = true; } catch (error, stackTrace) { debugPrint('FCM initialization failed: $error'); debugPrintStack(stackTrace: stackTrace); _initialized = false; } } static void _dispatchOpenedMessage(RemoteMessage message) { if (_openedEvents.hasListener) { _openedEvents.add(message); return; } final orderId = message.data['order_id']?.toString(); if (orderId != null && orderId.isNotEmpty) { _pendingOpenedOrderIds.add(orderId); } } static void _dispatchOpenedOrderId(String orderId) { if (_openedOrderEvents.hasListener) { _openedOrderEvents.add(orderId); return; } _pendingOpenedOrderIds.add(orderId); } /// Drains native/FCM open events received before the orders screen mounted. static List takePendingOpenedOrderIds() { final pending = List.from(_pendingOpenedOrderIds); _pendingOpenedOrderIds.clear(); return pending; } static RemoteMessage? takeInitialMessage() { final message = _initialMessage; _initialMessage = null; return message; } static Future showForegroundNotification(RemoteMessage message) async { final orderId = message.data['order_id']?.toString() ?? ''; try { await _nativeChannel.invokeMethod('showForegroundNotification', { 'title': message.notification?.title ?? 'Nuevo pedido asignado', 'body': message.notification?.body ?? 'Tienes un pedido pendiente', 'orderId': orderId, }); } on PlatformException catch (error) { debugPrint('Foreground notification failed: $error'); } } Future deviceToken() async { if (!_initialized) return null; try { return await FirebaseMessaging.instance.getToken(); } on Exception catch (error) { debugPrint('FCM token failed: $error'); return null; } } Future requestPermission() async { if (!_initialized) return; await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, ); } Future registerWithBackend(DeliveryRepository repository) async { if (!_initialized) return; final prefs = await SharedPreferences.getInstance(); // Force one clean token refresh after the local Firebase project/config // changed. SharedPreferences survives APK reinstalls, so changing this // marker prevents reusing a token already rejected by FCM. const refreshMarker = 'fcm_token_force_refreshed_v3'; if (prefs.getBool(refreshMarker) != true) { await FirebaseMessaging.instance.deleteToken(); await prefs.setBool(refreshMarker, true); } final token = await deviceToken(); if (token == null || token.isEmpty) { debugPrint('FCM registration skipped: token unavailable'); return; } final uuid = prefs.getString('delivery_device_uuid') ?? 'android-${DateTime.now().microsecondsSinceEpoch}'; await prefs.setString('delivery_device_uuid', uuid); debugPrint('FCM token obtained; registering device with backend'); try { await repository.registerDevice(deviceUuid: uuid, fcmToken: token); await prefs.remove('delivery_fcm_registration_pending'); } catch (error) { // La próxima apertura de la app volverá a intentar registrar el token. await prefs.setString('delivery_fcm_registration_pending', token); debugPrint('FCM registration pending: $error'); return; } FirebaseMessaging.instance.onTokenRefresh.listen((nextToken) { repository .registerDevice(deviceUuid: uuid, fcmToken: nextToken) .catchError((error) { debugPrint('FCM token refresh registration failed: $error'); }); }); } Future clearSession() async { final prefs = await SharedPreferences.getInstance(); if (_initialized) { try { await FirebaseMessaging.instance.deleteToken(); } on Exception { // Local credentials are still cleared if Firebase is unavailable. } } await prefs.remove('delivery_device_uuid'); await prefs.remove('delivery_fcm_registration_pending'); } }