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.
403 lines
16 KiB
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();
|
|
}
|