import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:nearle/helpers/http_overrides.dart'; // Top-level background handler required by Firebase Messaging @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { HttpOverrides.global = MyHttpOverrides(); try { await Firebase.initializeApp(); } catch (_) {} await NotificationServce.display(message); } class NotificationServce { static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; static final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); static final AudioPlayer _player = AudioPlayer(); static String _channelId = 'Nearle'; static bool _isPlaying = false; static const AndroidNotificationChannel channel = AndroidNotificationChannel( 'Nearle', 'Nearle Notification', description: 'Channel for Nearle notifications', importance: Importance.max, playSound: true, enableVibration: true, showBadge: true, ); static Future initialize(BuildContext context) async { try { final prefs = await SharedPreferences.getInstance(); final alreadyInit = prefs.getBool('notifications_init_done') ?? false; if (alreadyInit) { return; } await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, ); final existing = (prefs.getString('order_alert_sound') ?? '').trim(); if (existing.isEmpty) { await prefs.setString('order_alert_sound', 'assets/audio/alert-1.mp3'); } await prefs.setBool('notifications_init_done', true); } catch (_) {} await _notificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); await _applyChannelSoundFromPrefs(); const InitializationSettings initializationSettings = InitializationSettings( android: AndroidInitializationSettings('@mipmap/ic_launcher'), iOS: DarwinInitializationSettings( requestSoundPermission: true, requestBadgePermission: true, requestAlertPermission: true, defaultPresentSound: true, defaultPresentBadge: true, defaultPresentBanner: true, defaultPresentAlert: true, defaultPresentList: true, ), ); await _notificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse response) async {}, ); RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage(); if (initialMessage != null) { await _handleInitialMessage(initialMessage); } FirebaseMessaging.onMessage.listen((RemoteMessage message) async { await _handleMessage(message); }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async { await _handleMessageOpenedApp(message); }); } // ✅ Removed duplicated background handler (this was breaking your notifications) static Future _handleInitialMessage(RemoteMessage message) async { if (message.notification != null) { await display(message); } } static Future _handleMessage(RemoteMessage message) async { if (message.notification != null) { await display(message); } } static Future _handleMessageOpenedApp(RemoteMessage message) async {} static Future _applyChannelSoundFromPrefs() async { try { final prefs = await SharedPreferences.getInstance(); String sel = (prefs.getString('order_alert_sound') ?? '').trim(); if (sel.isEmpty) { sel = 'assets/audio/alert-1.mp3'; } final fileName = sel.split('/').last; final base = fileName.split('.').first; final rawName = base.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); final androidImpl = _notificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); if (androidImpl != null) { _channelId = 'Nearle_$rawName'; final custom = AndroidNotificationChannel( _channelId, 'Nearle Notification', description: 'Channel for Nearle notifications', importance: Importance.max, playSound: true, sound: RawResourceAndroidNotificationSound(rawName), enableVibration: true, showBadge: true, ); await androidImpl.createNotificationChannel(custom); } } catch (_) {} } static Future _downloadAndSaveImage( String imageUrl, String fileName) async { try { final directory = await getTemporaryDirectory(); final filePath = '${directory.path}/$fileName'; final response = await http.get(Uri.parse(imageUrl)); if (response.statusCode == 200) { final file = File(filePath); await file.writeAsBytes(response.bodyBytes); return filePath; } return null; } catch (_) { return null; } } static String? _extractImageUrl(RemoteMessage message) { String? imageUrl = message.data['image'] as String?; imageUrl ??= message.notification?.android?.imageUrl; imageUrl ??= message.notification?.apple?.imageUrl; return imageUrl; } static Future _playSelectedSound({int times = 1}) async { try { if (_isPlaying) return; _isPlaying = true; final prefs = await SharedPreferences.getInstance(); String selected = (prefs.getString('order_alert_sound') ?? '').trim(); if (selected.isEmpty) { selected = 'assets/audio/alert-1.mp3'; } final rel = selected.startsWith('assets/') ? selected.replaceFirst('assets/', '') : selected; await _player.stop(); await _player.setReleaseMode(ReleaseMode.stop); for (int i = 0; i < times; i++) { await _player.play(AssetSource(rel)); try { await _player.onPlayerComplete.first; } catch (_) {} if (i < times - 1) { await Future.delayed(const Duration(milliseconds: 120)); } } } catch (_) {} finally { _isPlaying = false; } } /// Lightweight local notification helper for in-app events (no FCM message). static Future showLocalNotification({ required String title, required String body, bool playSound = true, String? payload, }) async { try { final id = DateTime.now().millisecondsSinceEpoch ~/ 1000; final notificationDetails = NotificationDetails( android: AndroidNotificationDetails( _channelId, 'Nearle Notification', importance: Importance.max, priority: Priority.high, icon: '@mipmap/ic_launcher', playSound: playSound, enableVibration: true, channelShowBadge: true, ongoing: false, autoCancel: true, ), iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: playSound, presentList: true, presentBanner: true, ), ); await _notificationsPlugin.show( id, title, body, notificationDetails, payload: payload, ); } catch (_) {} } static Future display(RemoteMessage message) async { final id = DateTime.now().millisecondsSinceEpoch ~/ 1000; final payload = jsonEncode({'id': id.toString(), 'data': message.data}); final appState = WidgetsBinding.instance.lifecycleState; final bool isForeground = appState == AppLifecycleState.resumed; if (isForeground) { await _playSelectedSound(times: 5); } NotificationDetails notificationDetails; final imageUrl = _extractImageUrl(message); String? persistedImageUrl = imageUrl; String? persistedImagePath; if (imageUrl != null && imageUrl.isNotEmpty) { final imagePath = await _downloadAndSaveImage( imageUrl, 'notification_image.jpg', ); persistedImagePath = imagePath; notificationDetails = NotificationDetails( android: AndroidNotificationDetails( _channelId, 'Nearle Notification', importance: Importance.max, priority: Priority.high, icon: '@mipmap/ic_launcher', playSound: !isForeground, enableVibration: true, fullScreenIntent: true, channelShowBadge: true, ongoing: false, autoCancel: true, styleInformation: imagePath != null ? BigPictureStyleInformation(FilePathAndroidBitmap(imagePath)) : const DefaultStyleInformation(true, true), ), iOS: const DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, presentList: true, presentBanner: true, ), ); } else { notificationDetails = NotificationDetails( android: AndroidNotificationDetails( _channelId, 'Nearle Notification', importance: Importance.max, priority: Priority.high, icon: '@mipmap/ic_launcher', playSound: !isForeground, enableVibration: true, fullScreenIntent: true, channelShowBadge: true, ongoing: false, autoCancel: true, ), iOS: const DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, presentList: true, presentBanner: true, ), ); } if (isForeground) { final ctx = Get.context; if (ctx != null) { final title = message.notification?.title ?? message.data['title'] ?? 'Nearle'; final body = message.notification?.body ?? message.data['body'] ?? ''; ScaffoldMessenger.of(ctx).showSnackBar( SnackBar( content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontWeight: FontWeight.w700)), if (body.isNotEmpty) Text(body), ], ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), duration: const Duration(seconds: 3), ), ); } } else { await _notificationsPlugin.show( id, message.notification?.title ?? message.data['title'] ?? 'Nearle', message.notification?.body ?? message.data['body'] ?? 'Notification', notificationDetails, payload: payload, ); } try { final prefs = await SharedPreferences.getInstance(); final nowIso = DateTime.now().toIso8601String(); final title = message.notification?.title ?? message.data['title'] ?? 'Nearle'; final body = message.notification?.body ?? message.data['body'] ?? ''; final entry = { 'id': id, 'title': title, 'body': body, 'time': nowIso, 'data': message.data, if (persistedImageUrl != null) 'imageUrl': persistedImageUrl, if (persistedImagePath != null) 'imagePath': persistedImagePath, }; final existingRaw = prefs.getString('notifications_log'); List list = []; if (existingRaw != null && existingRaw.isNotEmpty) { try { list = jsonDecode(existingRaw) as List; } catch (_) {} } list.insert(0, entry); if (list.length > 100) list = list.sublist(0, 100); await prefs.setString('notifications_log', jsonEncode(list)); } catch (_) {} } }