diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d9c5314..4950264 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + diff --git a/lib/controller/auth/captin/login_captin_controller.dart b/lib/controller/auth/captin/login_captin_controller.dart index 23e88e6..e2e59b0 100755 --- a/lib/controller/auth/captin/login_captin_controller.dart +++ b/lib/controller/auth/captin/login_captin_controller.dart @@ -384,7 +384,7 @@ class LoginDriverController extends GetxController { textConfirm: 'Verify'.tr, confirmTextColor: Colors.white, onConfirm: () { - // Get.back(); + Get.back(); // انتقل لصفحة OTP الجديدة Get.to( () => OtpVerificationPage( diff --git a/lib/controller/auth/captin/opt_token_controller.dart b/lib/controller/auth/captin/opt_token_controller.dart index 931d362..976860e 100644 --- a/lib/controller/auth/captin/opt_token_controller.dart +++ b/lib/controller/auth/captin/opt_token_controller.dart @@ -90,7 +90,7 @@ class OtpVerificationController extends GetxController { if (response != 'failure') { Log.print('response: ${response}'); - Get.back(); // توجه إلى الصفحة التالية + // Get.back(); // توجه إلى الصفحة التالية await CRUD().post( link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php', payload: { @@ -98,17 +98,7 @@ class OtpVerificationController extends GetxController { 'fingerPrint': finger.toString(), 'captain_id': box.read(BoxName.driverID).toString(), }); - final fcm = Get.isRegistered() - ? Get.find() - : Get.put(FirebaseMessagesController()); - // await fcm.sendNotificationToDriverMAP( - // 'token change', - // 'change device'.tr, - // ptoken.toString(), - // [], - // 'cancel.wav', - // ); await NotificationService.sendNotification( target: ptoken.toString(), title: 'token change'.tr, diff --git a/lib/controller/firebase/firbase_messge.dart b/lib/controller/firebase/firbase_messge.dart index 9e44f26..77b448b 100755 --- a/lib/controller/firebase/firbase_messge.dart +++ b/lib/controller/firebase/firbase_messge.dart @@ -110,9 +110,9 @@ class FirebaseMessagesController extends GetxController { switch (category) { case 'ORDER': case 'Order': // Handle both cases for backward compatibility - if (Platform.isAndroid) { - notificationController.showNotification(title, body, 'order', ''); - } + // if (Platform.isAndroid) { + // notificationController.showNotification(title, body, 'order', ''); + // } var myListString = message.data['DriverList']; if (myListString != null) { var myList = jsonDecode(myListString) as List; diff --git a/lib/controller/firebase/local_notification.dart b/lib/controller/firebase/local_notification.dart index 6b8caae..25c7b09 100755 --- a/lib/controller/firebase/local_notification.dart +++ b/lib/controller/firebase/local_notification.dart @@ -1,62 +1,410 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:ui'; // للألوان -import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; - import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; + import '../../constant/box_name.dart'; -import '../../main.dart'; +import '../../constant/links.dart'; +import '../../main.dart'; // للوصول لـ box import '../../print.dart'; -import '../../views/notification/notification_captain.dart'; +import '../../views/home/Captin/driver_map_page.dart'; +import '../../views/home/Captin/orderCaptin/order_request_page.dart'; +import '../functions/crud.dart'; import '../home/captin/home_captain_controller.dart'; class NotificationController extends GetxController { final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + // ============================================================================== + // 1. تهيئة الإشعارات (إعداد القنوات والأزرار للآيفون والأندرويد) + // ============================================================================== Future initNotifications() async { + // إعدادات الأندرويد const AndroidInitializationSettings android = AndroidInitializationSettings('@mipmap/launcher_icon'); - DarwinInitializationSettings ios = DarwinInitializationSettings( + + // إعدادات أزرار الآيفون (Categories) + // هذا الجزء ضروري لظهور الأزرار في iOS + final List darwinNotificationCategories = [ + DarwinNotificationCategory( + 'ORDER_CATEGORY', // المعرف المستخدم لربط الإشعار بالأزرار + actions: [ + DarwinNotificationAction.plain('ACCEPT_ORDER', '✅ قبول'), + DarwinNotificationAction.plain('SHOW_DETAILS', '📄 تفاصيل'), + DarwinNotificationAction.plain( + 'REJECT_ORDER', + '❌ رفض', + options: { + DarwinNotificationActionOption.destructive + }, // يظهر باللون الأحمر + ), + ], + ) + ]; + + // إعدادات الآيفون العامة + final DarwinInitializationSettings ios = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, - // onDidReceiveLocalNotification: - // (int id, String? title, String? body, String? payload) async {}, + notificationCategories: darwinNotificationCategories, // تسجيل الأزرار ); + InitializationSettings initializationSettings = InitializationSettings(android: android, iOS: ios); + tz.initializeTimeZones(); - print('Notifications initialized'); + print('✅ Notifications initialized with Action Buttons Support'); + await _flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); - // Create a notification channel + // إنشاء قناة الأندرويد ذات الأهمية القصوى const AndroidNotificationChannel channel = AndroidNotificationChannel( - 'high_importance_channel', // Use the same ID as in strings.xml + 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications.', - importance: Importance.high, + importance: Importance.max, // أقصى أهمية + playSound: true, ); - // Register the channel with the system await _flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); } - // Displays a notification with the given title and message + // ============================================================================== + // 2. دالة عرض الإشعار المطور (شكل واضح + أزرار + صوت مخصص) + // ============================================================================== + void showOrderNotification( + String title, String body, String tone, String myListString) async { + // أ) تنسيق النص والبيانات بشكل جميل + String formattedBigText = body; + String summaryText = 'طلب جديد'; + String price = ''; + + try { + List data = jsonDecode(myListString); + // استخراج البيانات (تأكد أن الاندكسات مطابقة للباك إند عندك) + price = _getVal(data, 26); + String distance = _getVal(data, 5); + String startLoc = _getVal(data, 29); + String endLoc = _getVal(data, 30); + String paxName = _getVal(data, 8); + // String rating = _getVal(data, 33); + + // تنسيق النص ليكون 4 أسطر واضحة + formattedBigText = "👤 $paxName\n" + "💰 $price ${'SYP'.tr} | 🛣️ $distance كم\n" + "🟢 من: $startLoc\n" + "🏁 إلى: $endLoc"; + + summaryText = 'سعر الرحلة: $price'; + } catch (e) { + print("Error formatting notification text: $e"); + } + + // ب) نمط النص الكبير (BigText) للأندرويد + BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation( + formattedBigText, + contentTitle: '🚖 $title', + summaryText: summaryText, + htmlFormatContent: true, + htmlFormatContentTitle: true, + ); + + // ج) معالجة اسم الصوت (أندرويد بدون امتداد، آيفون مع امتداد) + String soundNameAndroid = tone.contains('.') ? tone.split('.').first : tone; + String soundNameIOS = tone.contains('.') ? tone : "$tone.wav"; + + // د) إعدادات الأندرويد (الأزرار + Full Screen) + final androidDetails = AndroidNotificationDetails( + 'high_importance_channel', + 'High Importance Notifications', + importance: Importance.max, + priority: Priority.max, + fullScreenIntent: true, // يفتح الشاشة وتظهر التفاصيل + category: AndroidNotificationCategory.call, // يعامل كمكالمة (رنين مستمر) + visibility: NotificationVisibility.public, + ongoing: true, // يمنع الحذف بالسحب + sound: RawResourceAndroidNotificationSound(soundNameAndroid), + audioAttributesUsage: AudioAttributesUsage.alarm, // صوت عالٍ كالمنبه + styleInformation: bigTextStyleInformation, + color: const Color(0xFF1A252F), + + // الأزرار الثلاثة + actions: [ + const AndroidNotificationAction( + 'ACCEPT_ORDER', + '✅ قبول فوري', + showsUserInterface: true, + titleColor: Color(0xFF4CAF50), // أخضر + ), + const AndroidNotificationAction( + 'SHOW_DETAILS', + '📄 التفاصيل', + showsUserInterface: true, + titleColor: Color(0xFF2196F3), // أزرق + ), + const AndroidNotificationAction( + 'REJECT_ORDER', + '❌ رفض', + showsUserInterface: false, // لا يفتح التطبيق + cancelNotification: true, + titleColor: Color(0xFFE53935), // أحمر + ), + ], + ); + + // هـ) إعدادات الآيفون + final iosDetails = DarwinNotificationDetails( + sound: soundNameIOS, + presentAlert: true, + presentBadge: true, + presentSound: true, + categoryIdentifier: 'ORDER_CATEGORY', // ربط الأزرار + interruptionLevel: InterruptionLevel.critical, // محاولة لكسر الصامت + ); + + final details = + NotificationDetails(android: androidDetails, iOS: iosDetails); + + // عرض الإشعار + await _flutterLocalNotificationsPlugin.show( + 1001, // ID ثابت لاستبدال الإشعار القديم + title, + "$price - مسافة $formattedBigText", // نص مختصر يظهر في البار العلوي + details, + payload: jsonEncode({ + 'type': 'Order', + 'data': myListString, + }), + ); + } + + // ============================================================================== + // 3. معالجة الاستجابة (عند الضغط على الأزرار) + // ============================================================================== + Future handleNotificationResponse(NotificationResponse response) async { + final payload = response.payload; + if (payload == null) return; + + final payloadData = jsonDecode(payload) as Map; + final rawData = payloadData['data']; + + List listData = []; + if (rawData is String) { + listData = jsonDecode(rawData); + } else if (rawData is List) { + listData = rawData; + } + + print("🔔 Notification Action: ${response.actionId}"); + + // أ) زر القبول + if (response.actionId == 'ACCEPT_ORDER') { + await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار + _processAcceptOrder(listData); + } + + // ب) زر التفاصيل + else if (response.actionId == 'SHOW_DETAILS') { + // await _flutterLocalNotificationsPlugin.cancel(1001); // اختياري: حذف الإشعار + Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData}); + } + + // ج) زر الرفض + else if (response.actionId == 'REJECT_ORDER') { + await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار + _processRejectOrder(listData); + } + + // د) الضغط على الإشعار نفسه (بدون أزرار) + else { + Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData}); + } + } + + // ============================================================================== + // 4. منطق القبول الآمن (Safe Accept Logic) + // ============================================================================== + Future _processAcceptOrder(List data) async { + // إظهار Loading + Get.dialog( + WillPopScope( + onWillPop: () async => false, + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ), + barrierDismissible: false, + ); + + try { + final driverId = box.read(BoxName.driverID); + String orderId = _getVal(data, 16); + String passengerToken = _getVal(data, 9); + + print("🚀 Sending Accept Request for Order: $orderId"); + + var res = await CRUD().post( + link: "${AppLink.ride}/rides/acceptRide.php", + payload: { + 'id': orderId, + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'passengerToken': passengerToken, + 'driver_id': driverId, + }, + ); + + print("📥 Server Response: $res"); + + if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج + + // 🔴 فحص النتيجة بدقة (Map أو String) + bool isFailure = false; + if (res is Map && res['status'] == 'failure') { + isFailure = true; + } else if (res == 'failure') { + isFailure = true; + } + + if (isFailure) { + Get.defaultDialog( + title: "تنبيه", + middleText: "عذراً، الطلب أخذه سائق آخر.", + confirmTextColor: Colors.white, + onConfirm: () => Get.back(), + textConfirm: "حسناً", + ); + return; // توقف هنا ولا تكمل + } + + // ✅ نجاح -> تجهيز الانتقال + + // حماية من الكراش: التأكد من وجود HomeCaptainController + if (!Get.isRegistered()) { + print("♻️ Reviving HomeCaptainController..."); + Get.put(HomeCaptainController()); + } else { + Get.find().changeRideId(); + } + + box.write(BoxName.statusDriverLocation, 'on'); + box.write(BoxName.rideStatus, 'Apply'); + + var rideArgs = _buildRideArgs(data); + box.write(BoxName.rideArguments, rideArgs); + + // استخدام offAll لمنع الرجوع لصفحة الطلب + Get.offAll(() => PassengerLocationMapPage(), arguments: rideArgs); + } catch (e) { + if (Get.isDialogOpen == true) Get.back(); + print("❌ Error in accept process: $e"); + Get.snackbar("خطأ", "حدث خطأ غير متوقع"); + } + } + + // ============================================================================== + // 5. منطق الرفض (يعمل في الخلفية بدون فتح صفحات) + // ============================================================================== + Future _processRejectOrder(List data) async { + try { + final driverId = box.read(BoxName.driverID); + String orderId = _getVal(data, 16); + + if (driverId != null && orderId.isNotEmpty) { + print("📤 Rejecting Order: $orderId"); + await CRUD().post(link: AppLink.addDriverOrder, payload: { + 'driver_id': driverId, + 'order_id': orderId, + 'status': 'Refused' + }); + print("✅ Order Rejected Successfully"); + } + } catch (e) { + print("❌ Error rejecting order: $e"); + } + } + + // ============================================================================== + // 6. دوال مساعدة (Helpers) + // ============================================================================== + + Map _buildRideArgs(List data) { + return { + 'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}', + 'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}', + 'Duration': _getVal(data, 4), // انتبه: تأكد من الإندكس الصحيح للوقت + 'totalCost': _getVal(data, 26), + 'Distance': _getVal(data, 5), + 'name': _getVal(data, 8), + 'phone': _getVal(data, 10), + 'email': _getVal(data, 28), + 'WalletChecked': _getVal(data, 13), + 'tokenPassenger': _getVal(data, 9), + 'direction': + 'https://www.google.com/maps/dir/${_getVal(data, 0)}/${_getVal(data, 1)}/', + 'DurationToPassenger': _getVal(data, 15), + 'rideId': _getVal(data, 16), + 'passengerId': _getVal(data, 7), + 'driverId': _getVal(data, 18), + 'durationOfRideValue': _getVal(data, 19), + 'paymentAmount': _getVal(data, 2), + 'paymentMethod': _getVal(data, 13) == 'true' ? 'visa' : 'cash', + 'isHaveSteps': _getVal(data, 20), + 'step0': _getVal(data, 21), + 'step1': _getVal(data, 22), + 'step2': _getVal(data, 23), + 'step3': _getVal(data, 24), + 'step4': _getVal(data, 25), + 'passengerWalletBurc': _getVal(data, 26), + 'timeOfOrder': DateTime.now().toString(), + 'totalPassenger': _getVal(data, 2), + 'carType': _getVal(data, 31), + 'kazan': _getVal(data, 32), + 'startNameLocation': _getVal(data, 29), + 'endNameLocation': _getVal(data, 30), + }; + } + + String _getVal(List data, int index) { + if (data.length > index && data[index] != null) { + return data[index].toString(); + } + return ''; + } + + // Callbacks + void onDidReceiveNotificationResponse(NotificationResponse response) { + handleNotificationResponse(response); + } + + void onDidReceiveBackgroundNotificationResponse( + NotificationResponse response) { + handleNotificationResponse(response); + } + + // ============================================================================== + // 7. الدوال القديمة (Old Scheduled Notifications) - لم يتم تغييرها + // ============================================================================== + void showNotification( String title, String message, String tone, String payLoad) async { + // هذه الدالة القديمة للإشعارات البسيطة (ليس الطلبات) BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation( message, contentTitle: title.tr, @@ -68,7 +416,7 @@ class NotificationController extends GetxController { 'High Importance Notifications', importance: Importance.max, priority: Priority.high, - sound: RawResourceAndroidNotificationSound(tone), + sound: RawResourceAndroidNotificationSound(tone.split('.').first), ); DarwinNotificationDetails ios = const DarwinNotificationDetails( @@ -84,126 +432,6 @@ class NotificationController extends GetxController { payload: jsonEncode({'title': title, 'data': payLoad})); } - void scheduleNotificationAtSpecificTime( - String title, String message, String tone, int hour, int minute) async { - // Initialize and set Cairo time zone - tz.initializeTimeZones(); - var cairoLocation; - if (box.read(BoxName.countryCode).toString() == 'Egypt') { - cairoLocation = tz.getLocation('Africa/Cairo'); - } else {} // todo get for location country - - final AndroidNotificationDetails android = AndroidNotificationDetails( - 'high_importance_channel', - 'High Importance Notifications', - importance: Importance.max, - priority: Priority.high, - showWhen: false, - sound: RawResourceAndroidNotificationSound(tone), - ); - - const DarwinNotificationDetails ios = DarwinNotificationDetails( - sound: 'default', - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - - final NotificationDetails details = - NotificationDetails(android: android, iOS: ios); - - final now = - tz.TZDateTime.now(cairoLocation); // Use Cairo timezone for current time - tz.TZDateTime scheduledTime = tz.TZDateTime( - cairoLocation, now.year, now.month, now.day, hour, minute); - - // If the scheduled time has already passed for today, schedule it for the next day - if (scheduledTime.isBefore(now)) { - scheduledTime = scheduledTime.add(const Duration(days: 1)); - } - - if (Platform.isAndroid) { - if (await Permission.scheduleExactAlarm.isDenied) { - if (await Permission.scheduleExactAlarm.request().isGranted) { - print('SCHEDULE_EXACT_ALARM permission granted'); - } else { - print('SCHEDULE_EXACT_ALARM permission denied'); - return; - } - } - } - - print('Current time: $now'); - print('Scheduling notification for: $scheduledTime'); - await _flutterLocalNotificationsPlugin.zonedSchedule( - 0, - title, - message, - scheduledTime, - details, - // androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time, - androidScheduleMode: - AndroidScheduleMode.alarmClock, // Triggers daily at the same time - ); - print('Notification scheduled successfully'); - } - - void scheduleNotificationAfter1Minute( - String title, String message, String tone) async { - final AndroidNotificationDetails android = AndroidNotificationDetails( - 'high_importance_channel', - 'High Importance Notifications', - importance: Importance.max, - priority: Priority.high, - showWhen: false, - sound: RawResourceAndroidNotificationSound(tone), - ); - - const DarwinNotificationDetails ios = DarwinNotificationDetails( - sound: 'default', - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - - final NotificationDetails details = - NotificationDetails(android: android, iOS: ios); - -// Schedule the notification to be shown after 1 minute - Timer.periodic(const Duration(seconds: 15), (timer) async { - final now = tz.TZDateTime.now(tz.local); - final scheduledTime = now.add(const Duration(seconds: 10)); - Log.print('scheduledTime: ${scheduledTime}'); - if (Platform.isAndroid) { - if (await Permission.scheduleExactAlarm.isDenied) { - if (await Permission.scheduleExactAlarm.request().isGranted) { - print('SCHEDULE_EXACT_ALARM permission granted'); - } else { - print('SCHEDULE_EXACT_ALARM permission denied'); - return; - } - } - } - - print('Scheduling notification for: $scheduledTime'); - await _flutterLocalNotificationsPlugin.zonedSchedule( - 0, - title, - message, - scheduledTime, - details, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time, - androidScheduleMode: AndroidScheduleMode.alarmClock, - ); - print('Notification scheduled successfully'); - }); - } - void scheduleNotificationsForSevenDays( String title, String message, String tone) async { final AndroidNotificationDetails android = AndroidNotificationDetails( @@ -211,7 +439,7 @@ class NotificationController extends GetxController { 'High Importance Notifications', importance: Importance.max, priority: Priority.high, - sound: RawResourceAndroidNotificationSound(tone), + sound: RawResourceAndroidNotificationSound(tone.split('.').first), ); const DarwinNotificationDetails ios = DarwinNotificationDetails( @@ -224,35 +452,24 @@ class NotificationController extends GetxController { final NotificationDetails details = NotificationDetails(android: android, iOS: ios); - // Check for the exact alarm permission on Android 12 and above if (Platform.isAndroid) { if (await Permission.scheduleExactAlarm.isDenied) { - if (await Permission.scheduleExactAlarm.request().isGranted) { - print('SCHEDULE_EXACT_ALARM permission granted'); - } else { - print('SCHEDULE_EXACT_ALARM permission denied'); - return; - } + await Permission.scheduleExactAlarm.request(); } } - // Schedule notifications for the next 7 days for (int day = 0; day < 7; day++) { - // List of notification times final notificationTimes = [ - {'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, // 8:00 AM - {'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, // 3:00 PM - {'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, // 8:00 PM + {'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, + {'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, + {'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, ]; for (var time in notificationTimes) { final notificationId = time['id'] as int; - - // Check if this notification ID is already stored bool isScheduled = box.read('notification_$notificationId') ?? false; if (!isScheduled) { - // Schedule the notification if not already scheduled await _scheduleNotificationForTime( day, time['hour'] as int, @@ -262,16 +479,10 @@ class NotificationController extends GetxController { details, notificationId, ); - - // Mark this notification ID as scheduled in GetStorage box.write('notification_$notificationId', true); - } else { - print('Notification with ID $notificationId is already scheduled.'); } } } - - print('Notifications scheduled successfully for the next 7 days'); } Future _scheduleNotificationForTime( @@ -283,30 +494,26 @@ class NotificationController extends GetxController { NotificationDetails details, int notificationId, ) async { - // Initialize and set Cairo timezone tz.initializeTimeZones(); - var cairoLocation = tz.getLocation('Africa/Cairo'); + var cairoLocation = + tz.getLocation('Africa/Cairo'); // تأكد من المنطقة الزمنية final now = tz.TZDateTime.now(cairoLocation); tz.TZDateTime scheduledDate = tz.TZDateTime( cairoLocation, now.year, now.month, - now.day + dayOffset, // Add offset to schedule for the next days + now.day + dayOffset, hour, minute, ); - // If the scheduled time is in the past, move it to the next day if (scheduledDate.isBefore(now)) { - scheduledDate = scheduledDate.add(Duration(days: 1)); + scheduledDate = scheduledDate.add(const Duration(days: 1)); } - print('Current time (Cairo): $now'); - print('Scheduling notification for: $scheduledDate'); - await _flutterLocalNotificationsPlugin.zonedSchedule( - notificationId, // Unique ID for each notification + notificationId, title, message, scheduledDate, @@ -314,243 +521,7 @@ class NotificationController extends GetxController { androidScheduleMode: AndroidScheduleMode.exact, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: - null, // Don't repeat automatically; we handle 7 days manually + matchDateTimeComponents: null, ); - - print('Notification scheduled successfully for: $scheduledDate'); - } - - void scheduleNotificationEvery10Hours( - String title, String message, String tone) async { - final AndroidNotificationDetails android = AndroidNotificationDetails( - 'high_importance_channel', - 'High Importance Notifications', - importance: Importance.max, - priority: Priority.high, - showWhen: false, - sound: RawResourceAndroidNotificationSound(tone), - ); - - const DarwinNotificationDetails ios = DarwinNotificationDetails( - sound: 'default', - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - - final NotificationDetails details = - NotificationDetails(android: android, iOS: ios); - - if (Platform.isAndroid) { - if (await Permission.scheduleExactAlarm.isDenied) { - if (await Permission.scheduleExactAlarm.request().isGranted) { - print('SCHEDULE_EXACT_ALARM permission granted'); - } else { - print('SCHEDULE_EXACT_ALARM permission denied'); - return; - } - } - } - - Timer.periodic(const Duration(hours: 10), (timer) async { - final now = tz.TZDateTime.now(tz.local); - final scheduledTime = now.add(const Duration(minutes: 10)); - - print('Scheduling notification for: $scheduledTime'); - await _flutterLocalNotificationsPlugin.zonedSchedule( - 0, - title.tr, - message.tr, - scheduledTime, - details, - // androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time, - androidScheduleMode: AndroidScheduleMode.alarmClock, - ); - }); - - print('Notifications scheduled every 5 seconds'); - } - - void showTimerNotification(String title, String message, String tone) async { - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - - // Setup Android notification - final AndroidNotificationDetails android = AndroidNotificationDetails( - 'high_importance_channel', - 'High Importance Notifications', - importance: Importance.max, - priority: Priority.high, - showWhen: false, - sound: RawResourceAndroidNotificationSound( - tone), // tone without the file extension - ); - - // Setup iOS notification - const DarwinNotificationDetails ios = DarwinNotificationDetails( - sound: 'default', - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - - final NotificationDetails details = - NotificationDetails(android: android, iOS: ios); - - // Request permission on Android - if (Platform.isAndroid) { - if (await Permission.scheduleExactAlarm.isDenied) { - if (await Permission.scheduleExactAlarm.request().isGranted) { - print('SCHEDULE_EXACT_ALARM permission granted'); - } else { - print('SCHEDULE_EXACT_ALARM permission denied'); - return; - } - } - } - - // Timer duration (e.g., 120 seconds countdown) - int countdown = 12; - - // Display the notification initially with the full countdown time - - // Timer to update the notification every second - Timer.periodic(const Duration(seconds: 1), (timer) async { - // if (countdown > 0) { - // Update the existing notification with the updated countdown - - // Decrease the countdown by 1 - countdown--; - // } else { - // // Cancel the timer when the countdown reaches zero - // timer.cancel(); - // } - }); - await flutterLocalNotificationsPlugin.show( - 0, - title, - '$message Remaining: $countdown seconds', // Initial message - details, - ); - print('Notification will update every second'); - } - - // Callback when the notification is tapped - void onDidReceiveNotificationResponse(NotificationResponse response) { - handleNotificationResponse(response); - } - - // Callback when the notification is tapped while the app is in the background - void onDidReceiveBackgroundNotificationResponse( - NotificationResponse response) { - handleNotificationResponse(response); - } - - // Handle notification response for both foreground and background - void handleNotificationResponse(NotificationResponse response) { - print('Notification tapped!'); - Log.print('response.payload: ${response.payload}'); - if (response.payload != null) { - print('Notification payload: ${response.payload}'); - var payloadData = jsonDecode(response.payload.toString()); - - if (payloadData is Map) { - String title = payloadData['title']; - var data = payloadData['data']; - - switch (title) { - case 'Order': - _handleOrderNotification(data); - break; - case 'OrderSpeed': - _handleOrderSpeedNotification(data); - break; - case 'ADS': - _handleADSNotification(); - break; - default: - Log.print('Unknown notification type'); - } - } else { - Log.print('Invalid payload format'); - } - } else { - Log.print('Payload is null'); - } - } - - void _handleOrderNotification(dynamic data) { - if (data is String) { - var orderData = jsonDecode(data); - if (orderData is List && orderData.length == 34) { - //closeOverLay(); - Get.put(HomeCaptainController()).changeRideId(); - Get.to(() => OrderRequestPage(), arguments: {'myListString': data}); - } else { - Log.print('Invalid order data'); - } - } else { - Log.print('Invalid order payload'); - } - } - - void _handleOrderSpeedNotification(dynamic data) { - if (data is String) { - var orderData = jsonDecode(data); - if (orderData is List && orderData.length == 34) { - //closeOverLay(); - Get.put(HomeCaptainController()).changeRideId(); - Get.to(() => OrderRequestPage(), arguments: {'myListString': data}); - } else { - Log.print('Invalid order data'); - } - } else { - Log.print('Invalid order payload'); - } - } - - void _handleADSNotification() { - // var orderData = jsonDecode(data); - //closeOverLay(); - Get.to( - () => const NotificationCaptain(), - ); - } - - void onDidReceiveLocalNotification( - int id, String? title, String? body, String? payload) async { - // display a dialog with the notification details, tap ok to go to another page - } -} - -class NotificationController1 extends GetxController { - final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - - // Initializes the local notifications plugin - Future initNotifications() async { - const AndroidInitializationSettings android = - AndroidInitializationSettings('@mipmap/launcher_icon'); - const InitializationSettings initializationSettings = - InitializationSettings(android: android); - await _flutterLocalNotificationsPlugin.initialize(initializationSettings); - } - - // Displays a notification with the given title and message - void showNotification( - String title, String message, String tone, String payLoad) async { - AndroidNotificationDetails android = AndroidNotificationDetails( - 'your channel id', 'your channel name', - importance: Importance.max, - priority: Priority.high, - showWhen: false, - sound: RawResourceAndroidNotificationSound(tone)); - - NotificationDetails details = NotificationDetails(android: android); - await _flutterLocalNotificationsPlugin.show(0, title, message, details); } } diff --git a/lib/controller/home/captin/order_request_controller.dart b/lib/controller/home/captin/order_request_controller.dart index 73c4aa8..4f0d2f1 100755 --- a/lib/controller/home/captin/order_request_controller.dart +++ b/lib/controller/home/captin/order_request_controller.dart @@ -513,11 +513,12 @@ class OrderRequestController extends GetxController } } - // Accept Order Logic +// Accept Order Logic Future acceptOrder() async { endTimer(); _stopAudio(); + // 1. إرسال الطلب var res = await CRUD() .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { 'id': _safeGet(16), @@ -527,13 +528,35 @@ class OrderRequestController extends GetxController 'driver_id': box.read(BoxName.driverID), }); - if (res == 'failure') { + Log.print('res from orderrequestpage: ${res}'); + + // ============================================================ + // تصحيح: فحص الرد بدقة (Map أو String) + // ============================================================ + bool isFailure = false; + + if (res is Map && res['status'] == 'failure') { + isFailure = true; + } else if (res == 'failure') { + isFailure = true; + } + + if (isFailure) { + // ⛔ حالة الفشل: الطلب مأخوذ MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () { - Get.back(); - Get.back(); + Get.back(); // إغلاق الديالوج + Get.back(); // العودة للصفحة الرئيسية (إغلاق صفحة الطلب) }); } else { - Get.put(HomeCaptainController()).changeRideId(); + // ✅ حالة النجاح + + // حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه + if (!Get.isRegistered()) { + Get.put(HomeCaptainController()); + } else { + Get.find().changeRideId(); + } + box.write(BoxName.statusDriverLocation, 'on'); changeApplied(); @@ -573,6 +596,8 @@ class OrderRequestController extends GetxController }; box.write(BoxName.rideArguments, rideArgs); + + // الانتقال النهائي Get.off(() => PassengerLocationMapPage(), arguments: rideArgs); } } diff --git a/lib/main.dart b/lib/main.dart index 9ad84c2..6b9522f 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -155,68 +155,103 @@ Future createAllNotificationChannels() async { } /// ============ Handlers: Background ============ +// في ملف main.dart (خارج كلاس MyApp) @pragma('vm:entry-point') Future backgroundMessageHandler(RemoteMessage message) async { + // 1. تهيئة بيئة فلاتر في الخلفية WidgetsFlutterBinding.ensureInitialized(); - await initFirebaseIfNeeded(); - await GetStorage.init(); - if (!Get.isRegistered()) { - Get.put(NotificationController()); - } - if (!Get.isRegistered()) { - Get.put(FirebaseMessagesController()); - } + // 2. تهيئة الكونترولر (لأنه isolate منفصل) + // ملاحظة: تأكد أنك لا تعتمد على Context هنا + final NotificationController notificationController = + NotificationController(); - if (!await FlutterOverlayWindow.isPermissionGranted()) { - Log.print("Overlay permission not granted; showing only notification."); - } + // مهم جداً: إعادة تهيئة الإشعارات داخل هذه العملية المنفصلة + await notificationController.initNotifications(); - if (Platform.isAndroid) { - String category = message.data['category'] ?? ''; - if (message.notification != null) { - Log.print('message.notification!.title: ${message.notification!.title}'); + print("🟢 Background Message Received: ${message.data}"); - if (category == 'Order' || category == 'OrderSpeed') { - final myListString = message.data['DriverList'] ?? '[]'; - Log.print('myListString: $myListString'); + // 3. استخراج البيانات (الآن العنوان والنص داخل data وليس notification) + String? title = message.data['title']; + String? body = message.data['body']; + String? tone = message.data['tone'] ?? 'order'; + String? myListString = message.data['DriverList']; - List myList; - try { - myList = jsonDecode(myListString) as List; - } catch (e) { - Log.print('Error decoding JSON: $e'); - myList = []; - } - - final isOverlayActive = await FlutterOverlayWindow.isActive(); - if (isOverlayActive) { - await FlutterOverlayWindow.shareData(myList); - } else { - await FlutterOverlayWindow.showOverlay( - enableDrag: true, - flag: OverlayFlag.focusPointer, - positionGravity: PositionGravity.auto, - height: WindowSize.matchParent, - width: WindowSize.matchParent, - startPosition: const OverlayPosition(0, -30), - ); - await FlutterOverlayWindow.shareData(myList); - } - - NotificationController().showNotification( - message.notification!.title.toString(), - message.notification!.body.toString(), - 'order', - myListString, - ); - } else { - FirebaseMessagesController().fireBaseTitles(message); - } - } + // 4. شرط الأمان: التأكد من وجود البيانات المطلوبة + if (title != null && body != null && myListString != null) { + // 5. عرض الإشعار المحلي + notificationController.showOrderNotification( + title, + body, + 'ding.wav', + myListString, + ); + } else { + print("⚠️ Received empty data message or missing fields."); } } +// @pragma('vm:entry-point') +// Future backgroundMessageHandler(RemoteMessage message) async { +// WidgetsFlutterBinding.ensureInitialized(); +// await initFirebaseIfNeeded(); +// await GetStorage.init(); + +// if (!Get.isRegistered()) { +// Get.put(NotificationController()); +// } +// if (!Get.isRegistered()) { +// Get.put(FirebaseMessagesController()); +// } + +// if (!await FlutterOverlayWindow.isPermissionGranted()) { +// Log.print("Overlay permission not granted; showing only notification."); +// } + +// if (Platform.isAndroid) { +// String category = message.data['category'] ?? ''; +// Log.print('category: ${category}'); +// // if (message.notification != null) { +// // Log.print('message.notification!.title: ${message.notification!.title}'); + +// if (category == 'Order' || category == 'OrderSpeed') { +// final myListString = message.data['DriverList'] ?? '[]'; +// Log.print('myListString: $myListString'); + +// List myList; +// try { +// myList = jsonDecode(myListString) as List; +// } catch (e) { +// Log.print('Error decoding JSON: $e'); +// myList = []; +// } + +// // final isOverlayActive = await FlutterOverlayWindow.isActive(); +// // if (isOverlayActive) { +// // await FlutterOverlayWindow.shareData(myList); +// // } else { +// // await FlutterOverlayWindow.showOverlay( +// // enableDrag: true, +// // flag: OverlayFlag.focusPointer, +// // positionGravity: PositionGravity.auto, +// // height: WindowSize.matchParent, +// // width: WindowSize.matchParent, +// // startPosition: const OverlayPosition(0, -30), +// // ); +// // await FlutterOverlayWindow.shareData(myList); +// // } +// NotificationController().showOrderNotification( +// message.notification?.title ?? "طلب جديد", // العنوان +// message.notification?.body ?? "لديك طلب توصيل جديد", // النص الأساسي +// 'ding', // اسم نغمة الإشعار (تأكد أنها موجودة في raw) +// myListString, // البيانات القادمة من السيرفر (JSON String List) +// ); +// } else { +// FirebaseMessagesController().fireBaseTitles(message); +// } +// // } +// } +// } @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { diff --git a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index 5a11610..d78a971 100755 --- a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -103,7 +103,7 @@ GetBuilder leftMainMenuCaptainIcons() { // Get.snackbar( // '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}', // ''); - NotificationController1().showNotification( + NotificationController().showNotification( 'Intaleq Driver'.tr, '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}', 'ding', diff --git a/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/lib/views/home/Captin/orderCaptin/order_over_lay.dart index a29290b..4c0dc54 100755 --- a/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -216,29 +216,15 @@ class _OrderOverlayState extends State await _closeOverlay(); return; } - var res = await CRUD().post( - link: "${AppLink.ride}/rides/updateStausFromSpeed.php", - payload: { - 'id': orderData!.orderId, - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); - List bodyToPassenger = [ - _getData(6).toString(), - _getData(8).toString(), - _getData(9).toString(), - ]; + var res = await CRUD() + .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { + 'id': orderData!.orderId, + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'driver_id': box.read(BoxName.driverID), + 'passengerToken': _getData(9), + }); - NotificationService.sendNotification( - target: _getData(9).toString(), - title: "Accepted Ride".tr, - body: 'your ride is Accepted'.tr, - isTopic: false, // Important: this is a token - tone: 'start', - driverList: bodyToPassenger, - category: 'Accepted Ride', - ); final payload = { // بيانات أساسية 'driver_id': driverId, @@ -275,7 +261,6 @@ class _OrderOverlayState extends State 'timeOfOrder': DateTime.now().toIso8601String(), 'totalPassenger': _getData(2), }; - Log.print('myList: ${myList}'); Log.print('payload: ${payload}'); CRUD().post( link: AppLink.addOverLayStatus, @@ -283,11 +268,6 @@ class _OrderOverlayState extends State ); if (res != "failure") { // Using rideId (_getData(16)) for order_id consistently - CRUD().post(link: AppLink.addDriverOrder, payload: { - 'driver_id': driverId, // Driver ID from the order data - 'order_id': orderData!.orderId, - 'status': 'Apply' - }); _log("Server update successful. Writing to storage."); notificationController.showNotification(