2026-04-11

This commit is contained in:
Sarankumar committed 2026-04-11 10:21:31 +05:30
commit 27e27fb70b
270 files changed
+30296

No files matched your search

+254
View File
@@ -0,0 +1,254 @@
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;
});
}
}