2026-04-11
This commit is contained in:
commit
27e27fb70b
270 files changed
+30296
No files matched your search
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user