206 lines
6.5 KiB
Dart
206 lines
6.5 KiB
Dart
import 'dart:convert';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
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'; // NEW
|
|
|
|
/// 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 Firebase and FCM
|
|
Future<void> initialize() async {
|
|
await Firebase.initializeApp();
|
|
_logger.i('✅ Firebase initialized');
|
|
|
|
await _requestNotificationPermission();
|
|
_registerMessageListeners();
|
|
_registerTokenRefreshListener();
|
|
|
|
// Fetch token on app start
|
|
await getFcmToken();
|
|
}
|
|
|
|
/// 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 UI updates
|
|
NotificationActionHandler.handle(message.data);
|
|
|
|
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 register token after login or refresh.');
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Get current token
|
|
Future<String?> getFcmToken() async {
|
|
try {
|
|
final token = await _firebaseMessaging.getToken();
|
|
_logger.i('🔑 FCM token: $token');
|
|
|
|
if (token?.isNotEmpty ?? false) {
|
|
await LocalStorage.setFcmToken(token!);
|
|
final success = await AuthService.registerDeviceToken(token);
|
|
_logger.i(success
|
|
? '✅ Device token registered on server.'
|
|
: '⚠️ Failed to register device token on server.');
|
|
}
|
|
return token;
|
|
} catch (e, s) {
|
|
_logger.e('❌ Failed to get FCM token', error: e, stackTrace: s);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
await Firebase.initializeApp();
|
|
final logger = Logger();
|
|
logger
|
|
..i('⚡ Handling background notification...')
|
|
..i('📦 Data: ${message.data}');
|
|
|
|
// Pass to helper
|
|
NotificationActionHandler.handle(message.data);
|
|
}
|