Files
mywhatsapp/whatsapp_app/lib/services/firebase_service.dart

221 lines
7.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import '../models/conversation_model.dart';
import '../screens/chat_screen.dart';
import 'whatsapp_service.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// We can't access GetX services easily here since it's a separate isolate,
// but Firebase automatically shows the notification.
print("Handling a background message: ${message.messageId}");
}
class FirebaseService extends GetxService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Future<FirebaseService> init() async {
// 1. Request Permission
await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
// 2. Initialize Local Notifications (for foreground)
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings();
const initSettings =
InitializationSettings(android: androidInit, iOS: iosInit);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTap,
);
// 3. Background Handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 4. Foreground Message Handler
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final chatId = message.data['chatId'];
final activeChatId = Get.find<WhatsAppService>().activeChatId.value;
// Smart Notification: Only show if we are NOT currently in this chat
if (chatId != null && activeChatId == chatId) {
print('[FCM] Silent notification, user is currently in chat $chatId');
return; // Silent
}
_showLocalNotification(message);
});
// 5. Handle tapping on a background notification when app opens
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleNotificationClick(message.data);
});
// 6. Check if app was opened from a terminated state via a notification
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
Future.delayed(const Duration(seconds: 1), () {
_handleNotificationClick(initialMessage.data);
});
}
// 7. Request and register FCM Token on the server
_setupFcmTokenRegistration();
return this;
}
void _setupFcmTokenRegistration() async {
try {
// For iOS, wait until the APNS token is successfully initialized by the OS before calling getToken()
if (Platform.isIOS) {
String? apnsToken = await _messaging.getAPNSToken();
int retries = 0;
while (apnsToken == null && retries < 15) {
print('[FCM] APNS token not set yet. Waiting 1 second... (Attempt ${retries + 1}/15)');
await Future.delayed(const Duration(seconds: 1));
apnsToken = await _messaging.getAPNSToken();
retries++;
}
if (apnsToken != null) {
print('[FCM] APNS Token successfully obtained: $apnsToken');
} else {
print('[FCM WARNING] Failed to obtain APNS token after 15 attempts. FCM registration might fail.');
}
}
final token = await _messaging.getToken();
if (token != null) {
final displayToken = token.length > 15 ? token.substring(0, 15) : token;
print('[FCM] Token obtained: $displayToken...');
_registerTokenOnServer(token);
}
} catch (e) {
print('[FCM ERROR] Could not get token: $e');
}
_messaging.onTokenRefresh.listen((newToken) {
print('[FCM] Token refreshed');
_registerTokenOnServer(newToken);
});
}
void _registerTokenOnServer(String token) {
final wsSvc = Get.find<WhatsAppService>();
if (wsSvc.status.value == WsStatus.waReady ||
wsSvc.status.value == WsStatus.connected) {
print('[FCM] Sending token to server...');
wsSvc.sendFcmToken(token);
} else {
// Listen to status changes and send once connected
late StreamSubscription sub;
sub = wsSvc.status.listen((status) {
if (status == WsStatus.waReady || status == WsStatus.connected) {
print('[FCM] WebSocket connected, sending token to server...');
wsSvc.sendFcmToken(token);
sub.cancel();
}
});
}
}
void _showLocalNotification(RemoteMessage message) {
final notification = message.notification;
if (notification == null) return;
const androidDetails = AndroidNotificationDetails(
'whatsapp_channel',
'WhatsApp Messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails();
const details =
NotificationDetails(android: androidDetails, iOS: iosDetails);
_localNotifications.show(
notification.hashCode,
notification.title,
notification.body,
details,
payload: jsonEncode(message.data),
);
}
void showLocalNotificationFromData(Map<String, dynamic> data) {
final chatId = data['chatId'];
final name = data['name'] ?? 'WhatsApp';
final body = data['body'] ?? 'New Message';
// Only show local notifications when the app is actively in the foreground (resumed).
// If the app is in the background or suspended, the native FCM notification will handle it natively.
// This fully prevents duplicate notifications!
if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) {
print('[FCM] Skipping WebSocket local notification: app is in background.');
return;
}
// Smart Notification: Only show if we are NOT currently in this chat
final activeChatId = Get.find<WhatsAppService>().activeChatId.value;
if (chatId != null && activeChatId == chatId) {
return; // Silent
}
const androidDetails = AndroidNotificationDetails(
'whatsapp_channel',
'WhatsApp Messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
_localNotifications.show(
DateTime.now().microsecond,
name,
body,
details,
payload: jsonEncode({'chatId': chatId, 'name': name}),
);
}
void _onNotificationTap(NotificationResponse response) {
if (response.payload != null) {
final data = jsonDecode(response.payload!);
_handleNotificationClick(data);
}
}
void _handleNotificationClick(Map<String, dynamic> data) {
final chatId = data['chatId'];
final name = data['name'] ?? 'Chat';
if (chatId != null) {
final dummyChat = ConversationModel(
id: chatId,
name: name,
isGroup: false,
unreadCount: 0,
timestamp: 0,
pinned: false,
isMuted: false,
);
Get.to(() => ChatScreen(conversation: dummyChat), preventDuplicates: false);
}
}
}