import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:googleapis_auth/auth_io.dart'; import 'package:logger/logger.dart'; import 'package:http/http.dart' as http; import 'package:flutter/services.dart' show rootBundle; import 'package:marco/helpers/services/local_notification_service.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/notification_action_handler.dart'; /// Firebase Notification Service class FirebaseNotificationService { final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; final Logger _logger = Logger(); static const _fcmScopes = [ 'https://www.googleapis.com/auth/firebase.messaging', ]; /// Initialize FCM (Firebase.initializeApp() should be called once globally) Future initialize() async { _logger.i('✅ FirebaseMessaging initializing...'); await _requestNotificationPermission(); _registerMessageListeners(); _registerTokenRefreshListener(); // Fetch token on app start (but only register with server if JWT available) await getFcmToken(registerOnServer: true); } /// Request notification permission Future _requestNotificationPermission() async { final settings = await _firebaseMessaging.requestPermission(); _logger.i('📩 Permission Status: ${settings.authorizationStatus}'); } /// Foreground, background, and tap listeners void _registerMessageListeners() { FirebaseMessaging.onMessage.listen((message) { _logger.i('📩 Foreground Notification'); _logNotificationDetails(message); // Handle custom actions NotificationActionHandler.handle(message.data); // Show local notification if (message.notification != null) { LocalNotificationService.showNotification( title: message.notification!.title ?? "No title", body: message.notification!.body ?? "No body", ); } }); FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); } /// Token refresh handler void _registerTokenRefreshListener() { _firebaseMessaging.onTokenRefresh.listen((newToken) async { _logger.i('🔄 Token refreshed: $newToken'); if (newToken.isEmpty) return; await LocalStorage.setFcmToken(newToken); final jwt = await LocalStorage.getJwtToken(); if (jwt?.isNotEmpty ?? false) { final success = await AuthService.registerDeviceToken(newToken); _logger.i(success ? '✅ Device token updated on server after refresh.' : '⚠️ Failed to update device token on server.'); } else { _logger.w('⚠️ JWT not available — will retry after login.'); } }); } /// Get current token (optionally sync to server if logged in) Future getFcmToken({bool registerOnServer = false}) async { try { final token = await _firebaseMessaging.getToken(); _logger.i('🔑 FCM token: $token'); if (token?.isNotEmpty ?? false) { await LocalStorage.setFcmToken(token!); if (registerOnServer) { final jwt = await LocalStorage.getJwtToken(); if (jwt?.isNotEmpty ?? false) { final success = await AuthService.registerDeviceToken(token); _logger.i(success ? '✅ Device token registered on server.' : '⚠️ Failed to register device token on server.'); } else { _logger.w('⚠️ JWT not available — skipping server registration.'); } } } return token; } catch (e, s) { _logger.e('❌ Failed to get FCM token', error: e, stackTrace: s); return null; } } /// Re-register token with server (useful after login) Future registerTokenAfterLogin() async { final token = await LocalStorage.getFcmToken(); if (token?.isNotEmpty ?? false) { final success = await AuthService.registerDeviceToken(token!); _logger.i(success ? "✅ FCM token registered after login." : "⚠️ Failed to register FCM token after login."); } } /// Send a test notification using FCM v1 API Future sendTestNotification(String deviceToken) async { try { final client = await _getAuthenticatedHttpClient(); if (client == null) return; final projectId = await _getProjectId(); if (projectId == null) return; _logger.i('🏗 Firebase Project ID: $projectId'); final url = Uri.parse( 'https://fcm.googleapis.com/v1/projects/$projectId/messages:send'); final payload = _buildNotificationPayload(deviceToken); final response = await client.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(payload), ); if (response.statusCode == 200) { _logger.i('✅ Test notification sent successfully'); } else { _logger.e('❌ Send failed: ${response.statusCode} ${response.body}'); } client.close(); } catch (e, s) { _logger.e('❌ Error sending notification', error: e, stackTrace: s); } } /// Authenticated HTTP client using service account Future _getAuthenticatedHttpClient() async { try { final credentials = ServiceAccountCredentials.fromJson( json.decode(await rootBundle.loadString('assets/service-account.json')), ); return clientViaServiceAccount(credentials, _fcmScopes); } catch (e, s) { _logger.e('❌ Failed to authenticate', error: e, stackTrace: s); return null; } } /// Get Project ID from service account Future _getProjectId() async { try { final jsonMap = json .decode(await rootBundle.loadString('assets/service-account.json')); return jsonMap['project_id']; } catch (e) { _logger.e('❌ Failed to load project_id: $e'); return null; } } /// Build FCM v1 payload Map _buildNotificationPayload(String token) => { "message": { "token": token, "notification": { "title": "Test Notification", "body": "This is a test message from Flutter (v1 API)" }, "data": { "click_action": "FLUTTER_NOTIFICATION_CLICK", "type": "expense_updated", // Example "expense_id": "1234" }, } }; /// Handle tap on notification void _handleNotificationTap(RemoteMessage message) { _logger.i('📌 Notification tapped: ${message.data}'); NotificationActionHandler.handle(message.data); } /// Log notification details void _logNotificationDetails(RemoteMessage message) { _logger ..i('🆔 ID: ${message.messageId}') ..i('📜 Title: ${message.notification?.title}') ..i('📜 Body: ${message.notification?.body}') ..i('📦 Data: ${message.data}'); } } /// Background handler (required by Firebase) Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { final logger = Logger(); logger ..i('⚡ Handling background notification...') ..i('📦 Data: ${message.data}'); NotificationActionHandler.handle(message.data); }