255 lines
9.9 KiB
Dart
255 lines
9.9 KiB
Dart
import 'package:firebase_messaging/firebase_messaging.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:taxglide/consts/local_store.dart';
|
||
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
|
||
import 'package:taxglide/view/Main_controller/main_controller.dart';
|
||
import 'package:taxglide/view/screens/history/detail_screen.dart';
|
||
import 'package:taxglide/view/screens/notification_screen.dart';
|
||
import 'package:taxglide/router/consts_routers.dart';
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Local Notifications setup (singleton plugin instance)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||
FlutterLocalNotificationsPlugin();
|
||
|
||
/// Android notification channel used for all foreground chat/app notifications.
|
||
const AndroidNotificationChannel _channel = AndroidNotificationChannel(
|
||
'taxglide_foreground', // id
|
||
'TaxGlide Notifications', // name shown in system settings
|
||
description: 'Foreground notifications for TaxGlide',
|
||
importance: Importance.high,
|
||
playSound: true,
|
||
enableVibration: true,
|
||
);
|
||
|
||
/// Call once in main() after Firebase.initializeApp().
|
||
Future<void> initLocalNotifications() async {
|
||
const initSettings = InitializationSettings(
|
||
android: AndroidInitializationSettings('@mipmap/launcher_icon'),
|
||
iOS: DarwinInitializationSettings(),
|
||
);
|
||
|
||
await flutterLocalNotificationsPlugin.initialize(
|
||
initSettings,
|
||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||
// Notification tapped while app is open – handled inside NotificationService.
|
||
debugPrint('🔔 Local notification tapped: payload=${response.payload}');
|
||
Get.find<NotificationService>().handleNavigationFromPayload(
|
||
response.payload,
|
||
);
|
||
},
|
||
);
|
||
|
||
// Create the Android channel (no-op on iOS).
|
||
await flutterLocalNotificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin
|
||
>()
|
||
?.createNotificationChannel(_channel);
|
||
|
||
debugPrint('✅ LocalNotifications initialized');
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// NotificationService
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
class NotificationService extends GetxController {
|
||
final RemoteMessage? initialMessage;
|
||
NotificationService(this.initialMessage);
|
||
|
||
/// Running notification ID counter so multiple notifications can stack.
|
||
int _notifId = 0;
|
||
|
||
/// Shows a native Android system notification for a foreground message.
|
||
/// Works from any context – WebSocket callbacks, FCM listeners, etc.
|
||
///
|
||
/// [tag] is used as a deduplication key: if two sources (FCM + WebSocket)
|
||
/// fire for the same chat/event, passing the same tag means Android will
|
||
/// *replace* the first notification instead of showing a second one.
|
||
void showForegroundNotification(RemoteMessage message, {String? tag}) {
|
||
final String title =
|
||
message.notification?.title ??
|
||
message.data['title']?.toString() ??
|
||
'New Notification';
|
||
final String body =
|
||
message.notification?.body ??
|
||
message.data['body']?.toString() ??
|
||
'You have a new update';
|
||
|
||
// Build a simple payload string so tapping routes correctly.
|
||
final String? type = message.data['type']?.toString();
|
||
final String? id =
|
||
message.data['page_id']?.toString() ?? message.data['id']?.toString();
|
||
final String payload = (type != null && id != null) ? '$type:$id' : '';
|
||
|
||
// Use the tag as dedup key; fall back to an incrementing ID so unrelated
|
||
// notifications still stack correctly.
|
||
final int notifId = tag != null ? tag.hashCode.abs() % 100000 : _notifId++;
|
||
|
||
debugPrint(
|
||
'🔔 NotificationService: showing local notification '
|
||
'[id=$notifId tag=$tag title=$title]',
|
||
);
|
||
|
||
flutterLocalNotificationsPlugin.show(
|
||
notifId,
|
||
title,
|
||
body,
|
||
NotificationDetails(
|
||
android: AndroidNotificationDetails(
|
||
_channel.id,
|
||
_channel.name,
|
||
channelDescription: _channel.description,
|
||
importance: Importance.high,
|
||
priority: Priority.high,
|
||
icon: '@mipmap/launcher_icon',
|
||
playSound: true,
|
||
enableVibration: true,
|
||
styleInformation: BigTextStyleInformation(body),
|
||
tag: tag, // same tag → replaces instead of duplicating
|
||
),
|
||
iOS: const DarwinNotificationDetails(
|
||
presentAlert: true,
|
||
presentBadge: true,
|
||
presentSound: true,
|
||
),
|
||
),
|
||
payload: payload,
|
||
);
|
||
}
|
||
|
||
// ── Navigation helpers ──────────────────────────────────────────────────────
|
||
|
||
/// Called when a local notification is tapped (payload = "type:id").
|
||
void handleNavigationFromPayload(String? payload) {
|
||
if (payload == null || payload.isEmpty) {
|
||
_safeNavigate(() => Get.offAll(() => MainController()));
|
||
return;
|
||
}
|
||
final parts = payload.split(':');
|
||
final type = parts.isNotEmpty ? parts[0] : null;
|
||
final idStr = parts.length > 1 ? parts[1] : null;
|
||
handleNavigation({'type': type, 'id': idStr});
|
||
}
|
||
|
||
Future<void> handleNavigation(Map<String, dynamic> data) async {
|
||
final LocalStore localStore = LocalStore();
|
||
|
||
try {
|
||
// ── Auth check ──────────────────────────────────────────────────────────
|
||
final String? token = await localStore.getToken();
|
||
if (token == null || token.isEmpty || token == 'null') {
|
||
debugPrint('🔒 No valid token → Login');
|
||
_safeNavigate(() => Get.offAllNamed(ConstRouters.login));
|
||
return;
|
||
}
|
||
|
||
// Support both 'page'+'page_id' and 'type'+'id' payload formats.
|
||
final String? type = data['page']?.toString() ?? data['type']?.toString();
|
||
final String? idStr =
|
||
data['page_id']?.toString() ??
|
||
data['pageId']?.toString() ??
|
||
data['id']?.toString();
|
||
|
||
if (type == null || type.isEmpty) {
|
||
debugPrint('⚠️ Invalid notification data: type is empty');
|
||
_safeNavigate(() => Get.offAll(() => MainController()));
|
||
return;
|
||
}
|
||
|
||
debugPrint('📍 Notification nav → type=$type id=$idStr');
|
||
|
||
if (type == 'chat') {
|
||
final int chatId = int.tryParse(idStr ?? '') ?? 0;
|
||
if (chatId == 0) {
|
||
_safeNavigate(() => Get.offAll(() => MainController()));
|
||
return;
|
||
}
|
||
debugPrint('💬 Navigating to Chat $chatId');
|
||
_safeNavigate(() {
|
||
try {
|
||
if (Get.isDialogOpen ?? false) Get.back();
|
||
if (Get.isBottomSheetOpen ?? false) Get.back();
|
||
Navigator.of(Get.context!).popUntil((route) {
|
||
return route.isFirst ||
|
||
route.settings.name == '/splash' ||
|
||
route.settings.name == '/MainController';
|
||
});
|
||
} catch (_) {}
|
||
Future.delayed(const Duration(milliseconds: 200), () {
|
||
if (Get.currentRoute == '/LiveChatScreen') {
|
||
Get.off(
|
||
() => LiveChatScreen(chatid: chatId, fileid: '0'),
|
||
preventDuplicates: false,
|
||
);
|
||
} else {
|
||
Get.to(
|
||
() => LiveChatScreen(chatid: chatId, fileid: '0'),
|
||
preventDuplicates: false,
|
||
);
|
||
}
|
||
});
|
||
});
|
||
} else if (type == 'service') {
|
||
final int serviceId = int.tryParse(idStr ?? '') ?? 0;
|
||
if (serviceId == 0) {
|
||
_safeNavigate(() => Get.offAll(() => MainController()));
|
||
return;
|
||
}
|
||
debugPrint('📋 Navigating to Service $serviceId');
|
||
_safeNavigate(
|
||
() => Get.offAll(
|
||
() => MainController(
|
||
initialIndex: 2,
|
||
child: DetailScreen(id: serviceId, sourceTabIndex: 2),
|
||
),
|
||
),
|
||
);
|
||
} else {
|
||
debugPrint("❓ Unknown type '$type' → NotificationScreen");
|
||
_safeNavigate(() {
|
||
Get.offAll(() => const MainController());
|
||
Future.delayed(
|
||
const Duration(milliseconds: 300),
|
||
() => Get.to(() => const NotificationScreen()),
|
||
);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
debugPrint('❌ Error handling notification nav: $e');
|
||
try {
|
||
final String? token = await localStore.getToken();
|
||
if (token != null && token.isNotEmpty && token != 'null') {
|
||
_safeNavigate(() => Get.offAll(() => MainController()));
|
||
} else {
|
||
_safeNavigate(() => Get.offAllNamed(ConstRouters.login));
|
||
}
|
||
} catch (_) {
|
||
_safeNavigate(() => Get.offAllNamed(ConstRouters.login));
|
||
}
|
||
}
|
||
}
|
||
|
||
void _safeNavigate(Function action) {
|
||
if (Get.key.currentState != null) {
|
||
action();
|
||
return;
|
||
}
|
||
int attempts = 0;
|
||
Future.doWhile(() async {
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
attempts++;
|
||
if (Get.key.currentState != null) {
|
||
action();
|
||
return false;
|
||
}
|
||
return attempts < 20;
|
||
});
|
||
}
|
||
}
|