You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
car_common_app/lib/services/firebase_service.dart

403 lines
16 KiB
Dart

import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/main.dart';
import 'package:mc_common_app/services/chat_navigation_service.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
abstract class FirebaseMessagingService {
Future<String> getFirebaseToken();
Future<void> initializeNotifications();
}
class FirebaseMessagingServiceImp implements FirebaseMessagingService {
final FlutterLocalNotificationsPlugin flutterLocalNotifications;
final FirebaseMessaging firebaseMessaging;
FirebaseMessagingServiceImp({required this.firebaseMessaging, required this.flutterLocalNotifications});
@override
Future<String> getFirebaseToken() async => await firebaseMessaging.getToken() ?? "";
Future<void> requestPermissions() async {
if (Platform.isIOS) {
await flutterLocalNotifications.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(alert: true, badge: true, sound: true);
} else if (Platform.isAndroid) {
AndroidFlutterLocalNotificationsPlugin? androidImplementation = flutterLocalNotifications.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
bool? granted = await androidImplementation?.requestNotificationsPermission();
if (granted == false) {
await Permission.notification.request();
}
}
}
@override
Future<void> initializeNotifications() async {
await requestPermissions();
AppState().setDeviceToken = await getFirebaseToken();
AppState().setDeviceType = Platform.isAndroid ? "1" : "2";
await Permission.notification.isDenied.then((bool value) {
if (value) {
Permission.notification.request();
}
});
// Create high-priority notification channel for Android (required for terminated state)
if (Platform.isAndroid) {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'mowater_chat_channel', // More descriptive channel ID
'Chat Notifications',
description: 'Notifications for chat messages',
importance: Importance.max,
// Changed to max for Android 14
playSound: true,
enableVibration: true,
showBadge: true,
);
await flutterLocalNotifications.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
log("✅ Android notification channel created: ${channel.id}");
}
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('ic_launcher');
DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestCriticalPermission: true,
requestProvisionalPermission: true,
requestSoundPermission: true,
defaultPresentAlert: true,
defaultPresentBadge: true,
defaultPresentBanner: true,
defaultPresentList: true,
defaultPresentSound: true,
);
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin);
await flutterLocalNotifications.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
// Handle notification tap when app is in foreground/background
log("========== Notification Tapped ==========");
log("Notification ID: ${response.id}");
log("Action ID: ${response.actionId}");
log("Input: ${response.input}");
log("Payload: ${response.payload}");
log("=========================================");
if (response.payload != null && response.payload!.isNotEmpty) {
try {
Map<String, dynamic> data = jsonDecode(response.payload!);
log("Parsed payload data: $data");
_handleNotificationNavigation(data);
} catch (e) {
logger.e("Error parsing notification payload: $e");
}
}
},
);
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) _handleNotification(initialMessage);
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Foreground notification - just display, don't navigate automatically
if (message.notification != null) _handleForegroundNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationOpenedApp);
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
FirebaseMessaging.instance.onTokenRefresh.listen((String token) async {
AppState().setDeviceToken = token;
});
}
void _onDidReceiveLocalNotification(int id, String? title, String? body, String? payload) async {
logger.i(body);
}
void _handleForegroundNotification(RemoteMessage message) async {
log("========== Foreground Notification Received ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("Android config: ${message.toMap()}");
log("======================================================");
// Just display notification - DON'T navigate automatically
// Navigation happens only when user taps the notification
displayNotification(
title: message.notification!.title ?? '',
body: message.notification!.body ?? '',
payload: jsonEncode(message.data),
);
}
void _handleNotificationOpenedApp(RemoteMessage message) async {
log("========== Notification Opened App ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("=============================================");
// User tapped notification - navigate to chat
_handleNotificationNavigation(message.data);
}
void _handleNotification(RemoteMessage message) async {
// This is called when app is terminated and opened by notification
log("========== App Terminated Notification ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("=================================================");
// User opened app from notification - navigate to chat
_handleNotificationNavigation(message.data);
}
void _handleNotificationNavigation(Map<String, dynamic> data) async {
log("========== Handling Notification Navigation ==========");
log("Full data received: $data");
if (data.isEmpty) {
log("Data is empty, skipping navigation");
log("======================================================");
return;
}
// Backend sends NotiType as string: "1" = Request, "2" = Ads, "3" = General
String? notiTypeStr = data['NotiType'];
int? notiType = int.tryParse(notiTypeStr ?? '');
log("NotiType from backend: $notiTypeStr (parsed: $notiType)");
if (notiType == null || notiType < 1 || notiType > 3) {
log("Invalid or missing NotiType, skipping navigation");
log("======================================================");
return;
}
// Map NotiType to chat type
String chatType;
switch (notiType) {
case 1:
chatType = 'request_offer';
break;
case 2:
chatType = 'ad_chat';
break;
case 3:
chatType = 'general_chat';
break;
default:
log("Unknown NotiType: $notiType");
log("======================================================");
return;
}
log("Mapped chat type: $chatType");
// Wait a bit for context to be available
await Future.delayed(Duration(milliseconds: 300));
BuildContext? context = navigatorKey.currentContext;
if (context == null) {
logger.e("Context not available for navigation");
log("======================================================");
return;
}
// Log all available data fields to understand backend format
log("Available data fields:");
data.forEach((key, value) {
log(" $key: $value");
});
switch (chatType) {
case 'request_offer':
log("Navigating to Request Offer Chat...");
await _navigateToRequestOfferChat(context, data);
break;
case 'ad_chat':
log("Navigating to Advertisement Chat...");
await _navigateToAdChat(context, data);
break;
case 'general':
log("Navigating to General Chat...");
await _navigateToGeneralChat(context, data);
break;
default:
logger.e("Unknown chat type: $chatType");
}
log("======================================================");
}
Future<void> _navigateToRequestOfferChat(
BuildContext context,
Map<String, dynamic> data,
) async {
try {
// Try both backend format and our format
int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0;
if (requestId == 0) {
log("❌ Invalid or missing RequestID in notification data");
return;
}
if (AppState().currentAppType == AppType.customer) {
// Customer notification tap: land on this request's offers list, matching
// the normal (non-notification) customer flow in requests_fragment.dart.
// ProvidersChatListPage below is provider-only (it reads the current
// user's own providerId), so routing a customer there fails.
log("✅ Navigating to offers list for request: $requestId");
navigateWithName(context, AppRoutes.offersListPage, arguments: requestId);
} else {
log("✅ Navigating to providers chat list for request: $requestId");
navigateWithName(context, AppRoutes.providersChatListPage);
}
// TODO: If backend sends all required data (ProviderId, CustomerId),
// we can navigate directly to the chat screen
} catch (e) {
logger.e("Error navigating to request chat: $e");
}
}
Future<void> _navigateToAdChat(
BuildContext context,
Map<String, dynamic> data,
) async {
try {
// Try both backend format and our format
int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0;
// The "other participant" for the chat we're about to open/keep messaging in is
// whoever SENT the message that triggered this notification — NOT whatever the
// payload calls the "receiver". Backend has repeatedly mixed up sender/receiver
// fields in this payload (see firebase_notification_payload_format.md); if we
// blindly trust a "receiver" field that turns out to be OUR OWN id, every message
// we send afterwards gets addressed back to ourselves, and we start getting
// pushed notifications for our own messages. Prefer sender_id; fall back to the
// old receiver fields only if sender fields are missing entirely.
String? receiverUserId = data['SenderUserID'] ?? data['sender_id'] ?? data['ReceiverUserID'] ?? data['receiver_id'];
log("📦 Ad chat notification payload fields — "
"AdID: ${data['AdID']}, ad_id: ${data['ad_id']}, "
"SenderUserID: ${data['SenderUserID']}, sender_id: ${data['sender_id']}, "
"ReceiverUserID: ${data['ReceiverUserID']}, receiver_id: ${data['receiver_id']} "
"-> resolved receiverUserId: $receiverUserId");
final String? myUserId = AppState().getUser.data?.userInfo?.userId?.toString();
if (myUserId != null && receiverUserId == myUserId) {
log("⚠️ Resolved receiverUserId ($receiverUserId) matches the current user's own id — "
"the notification payload is likely still misidentifying sender/receiver. "
"Proceeding anyway, but messages sent from this chat may misroute.");
}
if (adId == 0 || receiverUserId == null) {
log("❌ Invalid AdID or sender/receiver id in notification data");
log(" AdID: ${data['AdID']}, resolved receiverUserId: $receiverUserId");
return;
}
// Check if user is already on this specific chat
// Try to get ChatVM from Provider, but don't fail if not available
try {
final chatVM = Provider.of<ChatVM>(context, listen: false);
if (chatVM.isOnAdChat(adId, receiverUserId)) {
log("✅ User already on ad chat (adId=$adId), skipping navigation");
return;
}
} catch (e) {
// ChatVM not available in context, proceed with navigation
log("⚠️ Could not check chat context: $e");
}
log("✅ Navigating to ad chat: adId=$adId");
final chatNavService = injector.get<ChatNavigationService>();
await chatNavService.navigateToAdChat(
context: context,
adsId: adId,
receiverUserId: receiverUserId,
);
} catch (e) {
logger.e("Error navigating to ad chat: $e");
}
}
Future<void> _navigateToGeneralChat(
BuildContext context,
Map<String, dynamic> data,
) async {
try {
// Try both backend format and our format
int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0;
if (requestId == 0) {
log("❌ Invalid or missing RequestID in notification data");
return;
}
log("✅ Navigating to general chat list for request: $requestId");
navigateWithName(
context,
AppRoutes.generalChatsListForProvider,
arguments: requestId,
);
} catch (e) {
logger.e("Error navigating to general chat: $e");
}
}
Future<void> displayNotification({
required String title,
required String body,
String? payload,
}) async {
const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
'mowater_chat_channel', // Match the channel ID we created
'Chat Notifications',
channelDescription: 'Notifications for chat messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: payload ?? 'item x');
}
}
@pragma('vm:entry-point')
Future<dynamic> backgroundMessageHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}