245 lines
8.3 KiB
Dart
245 lines
8.3 KiB
Dart
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 'package:taxglide/consts/notification_webscoket.dart';
|
||
|
||
import 'firebase_options.dart';
|
||
import 'package:taxglide/router/router.dart';
|
||
import 'package:taxglide/services/notification_service.dart';
|
||
|
||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey =
|
||
GlobalKey<ScaffoldMessengerState>();
|
||
|
||
/// ---------------------------------------------------------------------------
|
||
/// 🚨 BACKGROUND HANDLER - Must be top-level function (outside main)
|
||
/// This handles notifications when app is CLOSED/TERMINATED
|
||
/// ---------------------------------------------------------------------------
|
||
@pragma('vm:entry-point')
|
||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||
debugPrint("📩 Background Notification Received!");
|
||
}
|
||
|
||
Future<void> 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>(
|
||
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<void> _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<NotificationService>().showForegroundNotification(
|
||
message,
|
||
tag: dedupTag,
|
||
);
|
||
|
||
// 🔄 Trigger live refresh for detail screens/badge counts
|
||
NotificationWebSocket().triggerRefresh();
|
||
}
|
||
});
|
||
|
||
// Background tap
|
||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||
debugPrint("📥 Notification tapped from background!");
|
||
Get.find<NotificationService>().handleNavigation(message.data);
|
||
});
|
||
}
|
||
|
||
/// ---------------------------------------------------------------------------
|
||
/// Non-blocking Messaging Setup
|
||
/// ---------------------------------------------------------------------------
|
||
Future<void> _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<void> _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<TaxglideApp> createState() => _TaxglideAppState();
|
||
}
|
||
|
||
class _TaxglideAppState extends State<TaxglideApp> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GetMaterialApp(
|
||
title: 'Taxglide App',
|
||
debugShowCheckedModeBanner: false,
|
||
scaffoldMessengerKey: rootScaffoldMessengerKey,
|
||
initialRoute: '/splash',
|
||
getPages: AppRoutes.routes,
|
||
theme: ThemeData(
|
||
useMaterial3: true,
|
||
fontFamily: 'Gilroy',
|
||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||
),
|
||
);
|
||
}
|
||
}
|