198 lines
6.1 KiB
Dart
198 lines
6.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:real_estate_mobile/features/notifications/data/notification_repository.dart';
|
|
|
|
/// Top-level handler for background messages (must be top-level function).
|
|
@pragma('vm:entry-point')
|
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
await Firebase.initializeApp();
|
|
debugPrint('[FCM] Background message: ${message.messageId}');
|
|
}
|
|
|
|
class PushNotificationService {
|
|
static final PushNotificationService _instance =
|
|
PushNotificationService._internal();
|
|
factory PushNotificationService() => _instance;
|
|
PushNotificationService._internal();
|
|
|
|
FirebaseMessaging? _messaging;
|
|
FlutterLocalNotificationsPlugin? _localNotifications;
|
|
final NotificationRepository _repo = NotificationRepository();
|
|
|
|
String? _currentToken;
|
|
bool _initialized = false;
|
|
|
|
/// Callback when user taps a notification — set by the app to navigate.
|
|
void Function(String? actionUrl)? onNotificationTap;
|
|
|
|
/// Initialize Firebase Messaging and local notifications.
|
|
Future<void> initialize() async {
|
|
if (_initialized) return;
|
|
_initialized = true;
|
|
|
|
_messaging = FirebaseMessaging.instance;
|
|
_localNotifications = FlutterLocalNotificationsPlugin();
|
|
|
|
// Register background handler
|
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
|
|
// Request permission
|
|
final settings = await _messaging!.requestPermission(
|
|
alert: true,
|
|
badge: true,
|
|
sound: true,
|
|
provisional: false,
|
|
);
|
|
debugPrint('[FCM] Permission: ${settings.authorizationStatus}');
|
|
|
|
if (settings.authorizationStatus == AuthorizationStatus.denied) {
|
|
return;
|
|
}
|
|
|
|
// Setup local notifications for foreground display
|
|
await _setupLocalNotifications();
|
|
|
|
// Get token and register with backend
|
|
await _getAndRegisterToken();
|
|
|
|
// Listen for token refresh
|
|
_messaging!.onTokenRefresh.listen(_onTokenRefresh);
|
|
|
|
// Handle foreground messages
|
|
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
|
|
|
// Handle notification taps (app was in background)
|
|
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
|
|
|
|
// Check if app was opened from a terminated state notification
|
|
final initialMessage = await _messaging!.getInitialMessage();
|
|
if (initialMessage != null) {
|
|
_onMessageOpenedApp(initialMessage);
|
|
}
|
|
|
|
// iOS: set foreground notification presentation options
|
|
if (Platform.isIOS) {
|
|
await _messaging!.setForegroundNotificationPresentationOptions(
|
|
alert: true,
|
|
badge: true,
|
|
sound: true,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _setupLocalNotifications() async {
|
|
const androidChannel = AndroidNotificationChannel(
|
|
'high_importance_channel',
|
|
'High Importance Notifications',
|
|
description: 'Used for important notifications',
|
|
importance: Importance.high,
|
|
);
|
|
|
|
await _localNotifications!
|
|
.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin>()
|
|
?.createNotificationChannel(androidChannel);
|
|
|
|
const androidSettings =
|
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
const iosSettings = DarwinInitializationSettings(
|
|
requestAlertPermission: false,
|
|
requestBadgePermission: false,
|
|
requestSoundPermission: false,
|
|
);
|
|
|
|
await _localNotifications!.initialize(
|
|
const InitializationSettings(
|
|
android: androidSettings,
|
|
iOS: iosSettings,
|
|
),
|
|
onDidReceiveNotificationResponse: (response) {
|
|
final payload = response.payload;
|
|
onNotificationTap?.call(payload);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _getAndRegisterToken() async {
|
|
try {
|
|
final token = await _messaging!.getToken();
|
|
if (token != null) {
|
|
_currentToken = token;
|
|
debugPrint('[FCM] Token: $token');
|
|
await _registerTokenWithBackend(token);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('[FCM] Failed to get token: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _onTokenRefresh(String token) async {
|
|
debugPrint('[FCM] Token refreshed: $token');
|
|
_currentToken = token;
|
|
await _registerTokenWithBackend(token);
|
|
}
|
|
|
|
Future<void> _registerTokenWithBackend(String token) async {
|
|
try {
|
|
await _repo.registerFcmToken(token, Platform.isIOS ? 'ios' : 'android');
|
|
debugPrint('[FCM] Token registered with backend');
|
|
} catch (e) {
|
|
debugPrint('[FCM] Failed to register token: $e');
|
|
}
|
|
}
|
|
|
|
void _onForegroundMessage(RemoteMessage message) {
|
|
debugPrint('[FCM] Foreground message: ${message.notification?.title}');
|
|
|
|
final notification = message.notification;
|
|
if (notification == null) return;
|
|
|
|
// Show local notification on Android (iOS handles it natively)
|
|
if (Platform.isAndroid) {
|
|
_localNotifications?.show(
|
|
notification.hashCode,
|
|
notification.title,
|
|
notification.body,
|
|
const NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
'high_importance_channel',
|
|
'High Importance Notifications',
|
|
channelDescription: 'Used for important notifications',
|
|
importance: Importance.high,
|
|
priority: Priority.high,
|
|
icon: '@mipmap/ic_launcher',
|
|
),
|
|
),
|
|
payload: message.data['actionUrl'],
|
|
);
|
|
}
|
|
}
|
|
|
|
void _onMessageOpenedApp(RemoteMessage message) {
|
|
debugPrint('[FCM] Notification tap: ${message.data}');
|
|
final actionUrl = message.data['actionUrl'] as String?;
|
|
onNotificationTap?.call(actionUrl);
|
|
}
|
|
|
|
/// Unregister FCM token from backend (call on logout).
|
|
Future<void> unregister() async {
|
|
if (_currentToken == null) return;
|
|
try {
|
|
await _repo.removeFcmToken(_currentToken!);
|
|
debugPrint('[FCM] Token unregistered from backend');
|
|
} catch (e) {
|
|
debugPrint('[FCM] Failed to unregister token: $e');
|
|
}
|
|
_currentToken = null;
|
|
}
|
|
|
|
/// Re-register token after login.
|
|
Future<void> registerAfterLogin() async {
|
|
await _getAndRegisterToken();
|
|
}
|
|
}
|