taxgilde/lib/services/notification_service.dart
2026-04-11 10:21:31 +05:30

255 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
});
}
}