taxgilde/lib/services/notification_service.dart
2026-04-15 12:32:30 +05:30

339 lines
13 KiB
Dart
Raw Permalink 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/download_helper.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,
);
/// Android notification channel for download updates.
const AndroidNotificationChannel _downloadChannel = AndroidNotificationChannel(
'taxglide_downloads',
'TaxGlide Downloads',
description: 'Notifications for file downloads',
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 action: ${response.actionId} payload=${response.payload}');
if (response.actionId == 'location') {
final payload = response.payload;
if (payload != null && payload.startsWith('download:')) {
final filePath = payload.replaceFirst('download:', '');
final String dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
debugPrint('📂 Opening folder from notification: $dirPath');
// Open the system file manager at this location
DownloadHelper.openFolder(dirPath);
return;
}
}
Get.find<NotificationService>().handleNavigationFromPayload(
response.payload,
);
},
);
// Create the channels
final androidPlugin = flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
await androidPlugin?.createNotificationChannel(_channel);
await androidPlugin?.createNotificationChannel(_downloadChannel);
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,
);
}
/// Shows a notification when a file is successfully downloaded.
/// [filename] is the base name of the file.
/// [filePath] is the absolute path to the local file.
void showDownloadNotification(String filename, String filePath) {
debugPrint('📩 NotificationService: showing download success: $filename');
flutterLocalNotificationsPlugin.show(
filename.hashCode.abs() % 100000,
'Download Complete',
'File saved: $filename',
NotificationDetails(
android: AndroidNotificationDetails(
_downloadChannel.id,
_downloadChannel.name,
channelDescription: _downloadChannel.description,
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/launcher_icon',
playSound: true,
styleInformation: BigTextStyleInformation(
'The file **$filename** has been saved to your downloads folder:\n\n$filePath',
contentTitle: 'Download Complete',
summaryText: 'TaxGlide Download',
htmlFormatContent: true,
htmlFormatTitle: true,
),
actions: <AndroidNotificationAction>[
const AndroidNotificationAction(
'view',
'View File',
showsUserInterface: true,
),
const AndroidNotificationAction(
'location',
'Show in Folder',
showsUserInterface: true,
),
],
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: 'download:$filePath',
);
}
// ── 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;
}
// ⭐ Handle file download notifications
if (payload.startsWith('download:')) {
final filePath = payload.replaceFirst('download:', '');
debugPrint('📂 Opening downloaded file from notification: $filePath');
DownloadHelper.openDownloadedFile(filePath);
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;
});
}
}