import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'firebase_options.dart'; import 'providers/profile_provider.dart'; import 'models/profile.dart'; import 'app.dart'; import 'providers/notifications_provider.dart'; import 'utils/app_time.dart'; import 'utils/notification_permission.dart'; import 'services/notification_service.dart'; import 'services/notification_bridge.dart'; /// Handle messages received while the app is terminated or in background. Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // initialize plugin in background isolate await NotificationService.initialize(); final notification = message.notification; if (notification != null) { NotificationService.show( id: DateTime.now().millisecondsSinceEpoch ~/ 1000, title: notification.title ?? 'Notification', body: notification.body ?? '', payload: message.data['payload'], ); } } Future main() async { WidgetsFlutterBinding.ensureInitialized(); // initialize Firebase before anything that uses messaging await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); await dotenv.load(fileName: '.env'); AppTime.initialize(location: 'Asia/Manila'); final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? ''; final supabaseAnonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? ''; if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) { runApp(const _MissingConfigApp()); return; } await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey); // ensure token saved right away if already signed in final supaClient = Supabase.instance.client; String? initialToken; if (!kIsWeb) { try { initialToken = await FirebaseMessaging.instance.getToken(); } catch (e) { debugPrint('FCM getToken failed: $e'); initialToken = null; } if (initialToken != null && supaClient.auth.currentUser != null) { debugPrint('initial FCM token for signed-in user: $initialToken'); final ctrl = NotificationsController(supaClient); await ctrl.registerFcmToken(initialToken); } } // listen for auth changes to register/unregister token accordingly supaClient.auth.onAuthStateChange.listen((data) async { final event = data.event; if (kIsWeb) { debugPrint( 'auth state change $event on web: skipping FCM token handling', ); return; } String? token; try { token = await FirebaseMessaging.instance.getToken(); } catch (e) { debugPrint('FCM getToken failed during auth change: $e'); token = null; } debugPrint('auth state change $event, token=$token'); if (token == null) return; final ctrl = NotificationsController(supaClient); if (event == AuthChangeEvent.signedIn) { await ctrl.registerFcmToken(token); } else if (event == AuthChangeEvent.signedOut) { await ctrl.unregisterFcmToken(token); } }); if (!kIsWeb) { // on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it // notifications are automatically denied and cannot be re‑enabled from the // system settings. The helper uses `permission_handler`. final granted = await ensureNotificationPermission(); if (!granted) { // we don’t block startup, but it’s worth logging so developers notice. // A real app might show a dialog pointing the user to settings. // debugPrint('notification permission not granted'); } // request FCM permission (iOS/Android13+) and handle foreground messages await FirebaseMessaging.instance.requestPermission(); } FirebaseMessaging.onMessage.listen((RemoteMessage message) { final notification = message.notification; if (notification != null) { NotificationService.show( id: DateTime.now().millisecondsSinceEpoch ~/ 1000, title: notification.title ?? 'Notification', body: notification.body ?? '', payload: message.data['payload'], ); } }); // initialize the local notifications plugin so we can post alerts later await NotificationService.initialize( onDidReceiveNotificationResponse: (response) { // handle user tapping a notification; the payload format is up to us final payload = response.payload; if (payload != null && payload.startsWith('ticket:')) { // ignore if context not mounted; we might use a navigator key in real // app, but keep this simple for now // TODO: navigate to ticket/task as appropriate } }, ); // global navigator key used for snackbars/navigation from notification final navigatorKey = GlobalKey(); runApp( ProviderScope( observers: [NotificationSoundObserver()], child: NotificationBridge( navigatorKey: navigatorKey, child: const TasqApp(), ), ), ); } class NotificationSoundObserver extends ProviderObserver { static final AudioPlayer _player = AudioPlayer(); StreamSubscription? _tokenSub; @override void didUpdateProvider( ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container, ) { // play sound + show OS notification on unread-count increase if (provider == unreadNotificationsCountProvider) { final prev = previousValue as int?; final next = newValue as int?; if (prev != null && next != null && next > prev) { _player.play(AssetSource('tasq_notification.wav')); NotificationService.show( id: DateTime.now().millisecondsSinceEpoch ~/ 1000, title: 'New notifications', body: 'You have $next unread notifications.', ); } } // when profile changes, register or unregister tokens if (provider == currentProfileProvider) { final profile = newValue as Profile?; final controller = container.read(notificationsControllerProvider); if (profile != null) { // signed in: save current token and keep listening for refreshes if (!kIsWeb) { FirebaseMessaging.instance .getToken() .then((token) { if (token != null) { debugPrint('profile observer registering token: $token'); controller.registerFcmToken(token); } }) .catchError((e) => debugPrint('getToken error: $e')); _tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) { debugPrint('token refreshed: $token'); controller.registerFcmToken(token); }); } } else { // signed out: unregister whatever token we currently have _tokenSub?.cancel(); if (!kIsWeb) { FirebaseMessaging.instance .getToken() .then((token) { if (token != null) { debugPrint('profile observer unregistering token: $token'); controller.unregisterFcmToken(token); } }) .catchError((e) => debugPrint('getToken error: $e')); } } } } } class _MissingConfigApp extends StatelessWidget { const _MissingConfigApp(); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Center( child: Padding( padding: EdgeInsets.all(24), child: Text( 'Missing SUPABASE_URL or SUPABASE_ANON_KEY. ' 'Provide them in the .env file.', textAlign: TextAlign.center, ), ), ), ), ); } }