import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get/get.dart'; import 'package:file_picker/file_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'firebase_options.dart'; import 'package:taxglide/router/router.dart'; import 'package:taxglide/services/notification_service.dart'; final GlobalKey rootScaffoldMessengerKey = GlobalKey(); /// --------------------------------------------------------------------------- /// 🚨 BACKGROUND HANDLER - Must be top-level function (outside main) /// This handles notifications when app is CLOSED/TERMINATED /// --------------------------------------------------------------------------- @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); debugPrint("📩 Background Notification Received!"); } Future main() async { try { WidgetsFlutterBinding.ensureInitialized(); // 1. Initialize Firebase first (required) debugPrint("🔥 Initializing Firebase..."); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); debugPrint("✅ Firebase Initialized"); // ✅ Register background handler as early as possible FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // 2. Fetch initial FCM message (terminated → tapped notification) RemoteMessage? initialMessage; try { debugPrint("📩 Fetching initial message..."); initialMessage = await FirebaseMessaging.instance .getInitialMessage() .timeout( const Duration(seconds: 2), onTimeout: () { debugPrint("⚠️ Firebase getInitialMessage timed out!"); return null; }, ); debugPrint("📥 Initial message fetched: ${initialMessage != null}"); } catch (e) { debugPrint("⚠️ Could not fetch initial message: $e"); } Get.put( NotificationService(initialMessage), permanent: true, ); // 3. Initialize local notifications plugin (must happen before runApp) await initLocalNotifications(); // 4. Start the app debugPrint("🚀 Calling runApp..."); runApp(const ProviderScope(child: TaxglideApp())); // 4. Run FCM/permission setup in background (non-blocking) _runBackgroundTasks(); } catch (e, stack) { debugPrint("❌ CRITICAL BOOT ERROR: $e"); debugPrint(stack.toString()); runApp(const ProviderScope(child: TaxglideApp())); } } /// Helper to run non-critical startup tasks without blocking the UI Future _runBackgroundTasks() async { try { FirebaseMessaging messaging = FirebaseMessaging.instance; // Request permissions (Non-blocking) await messaging.requestPermission( alert: true, badge: true, sound: true, provisional: false, ); // Request notification permission for Android 13+ if (Platform.isAndroid) { await Permission.notification.request(); } // Setup messaging (tokens, etc) await _setupMessaging(messaging); // On iOS, set alert to true – we show foreground FCM messages naturally. await messaging.setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); // Set up FCM foreground listener _setupFcmListeners(); // Other plugins await _initializePlugins(); debugPrint("✅ All background initializations complete"); } catch (e) { debugPrint("⚠️ Background task failed: $e"); } } void _setupFcmListeners() { FirebaseMessaging.onMessage.listen((RemoteMessage message) async { debugPrint( "🔔 Foreground FCM received! Notification block: ${message.notification}", ); debugPrint("🔍 Foreground FCM Data block: ${message.data}"); // Show custom in-app notification for Android (since system doesn't show it in foreground) // On iOS, setForegroundNotificationPresentationOptions handles this. if (Platform.isAndroid) { // Pass a dedup tag (chat/page id) so that if the WebSocket fires for the // same event, the second notification replaces the first instead of // showing a double notification. final String? dedupTag = message.data['page_id']?.toString() ?? message.data['chat_id']?.toString() ?? message.data['id']?.toString(); Get.find().showForegroundNotification( message, tag: dedupTag, ); } }); // Background tap FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { debugPrint("📥 Notification tapped from background!"); Get.find().handleNavigation(message.data); }); } /// --------------------------------------------------------------------------- /// Non-blocking Messaging Setup /// --------------------------------------------------------------------------- Future _setupMessaging(FirebaseMessaging messaging) async { if (Platform.isIOS) { debugPrint("🍎 iOS Detected: Requesting notification permissions..."); NotificationSettings settings = await messaging.requestPermission( alert: true, badge: true, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { debugPrint("✅ User granted notification permissions"); } else if (settings.authorizationStatus == AuthorizationStatus.provisional) { debugPrint("✅ User granted provisional notification permissions"); } else { debugPrint( "❌ User declined or has not accepted notification permissions", ); } // Checking for APNS token - essential for FCM on iOS String? apnsToken = await messaging.getAPNSToken(); if (apnsToken == null) { debugPrint("⏳ APNS Token not ready. Retrying (Max 10s)..."); // Simulators will stay null here. Physical devices might take a few seconds. for (int i = 0; i < 10; i++) { await Future.delayed(const Duration(seconds: 1)); apnsToken = await messaging.getAPNSToken(); if (apnsToken != null) { debugPrint("✅ APNS Token received after retry"); break; } } } if (apnsToken != null) { debugPrint("✅ APNS Token: $apnsToken"); } else { debugPrint( "⚠️ APNS Token is NULL. " "IMPORTANT: Push notifications require a PHYSICAL DEVICE and valid Xcode Push Capability. " "Tokens are NOT available on Simulators.", ); return; } } // FCM Token generation is now handled EXCLUSIVELY during OTP verification in ApiRepository. } /// --------------------------------------------------------------------------- /// Plugin Initializer /// --------------------------------------------------------------------------- Future _initializePlugins() async { try { await FilePicker.platform.clearTemporaryFiles(); debugPrint("✅ Plugins initialized"); } catch (e) { debugPrint("⚠️ Plugin init failed: $e"); } } class TaxglideApp extends StatefulWidget { const TaxglideApp({super.key}); @override State createState() => _TaxglideAppState(); } class _TaxglideAppState extends State { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'Taxglide App', debugShowCheckedModeBanner: false, scaffoldMessengerKey: rootScaffoldMessengerKey, initialRoute: '/splash', getPages: AppRoutes.routes, theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), ), ); } }