marco.pms.mobileapp/lib/helpers/services/firebase/firebase_messaging_service.dart

221 lines
7.3 KiB
Dart

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<void> 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<void> _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<String?> 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<void> 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<void> 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<http.Client?> _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<String?> _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<String, dynamic> _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<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final logger = Logger();
logger
..i('⚡ Handling background notification...')
..i('📦 Data: ${message.data}');
NotificationActionHandler.handle(message.data);
}