26-1-22/1

This commit is contained in:
Hamza-Ayed
2026-01-22 19:30:31 +03:00
parent fbfde115a8
commit 12c5ce31a2
9 changed files with 504 additions and 502 deletions

View File

@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

View File

@@ -384,7 +384,7 @@ class LoginDriverController extends GetxController {
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
// Get.back();
Get.back();
// انتقل لصفحة OTP الجديدة
Get.to(
() => OtpVerificationPage(

View File

@@ -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<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: 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,

View File

@@ -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<dynamic>;

View File

@@ -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<void> initNotifications() async {
// إعدادات الأندرويد
const AndroidInitializationSettings android =
AndroidInitializationSettings('@mipmap/launcher_icon');
DarwinInitializationSettings ios = DarwinInitializationSettings(
// إعدادات أزرار الآيفون (Categories)
// هذا الجزء ضروري لظهور الأزرار في iOS
final List<DarwinNotificationCategory> 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<dynamic> 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: <AndroidNotificationAction>[
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<void> handleNotificationResponse(NotificationResponse response) async {
final payload = response.payload;
if (payload == null) return;
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
final rawData = payloadData['data'];
List<dynamic> 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<void> _processAcceptOrder(List<dynamic> 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<HomeCaptainController>()) {
print("♻️ Reviving HomeCaptainController...");
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().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<void> _processRejectOrder(List<dynamic> 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<String, dynamic> _buildRideArgs(List<dynamic> 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<dynamic> 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<void> _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
);
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, dynamic>) {
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(),
matchDateTimeComponents: null,
);
}
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<void> 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);
}
}

View File

@@ -518,6 +518,7 @@ class OrderRequestController extends GetxController
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<HomeCaptainController>()) {
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().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);
}
}

View File

@@ -155,68 +155,103 @@ Future<void> createAllNotificationChannels() async {
}
/// ============ Handlers: Background ============
// في ملف main.dart (خارج كلاس MyApp)
@pragma('vm:entry-point')
Future<void> backgroundMessageHandler(RemoteMessage message) async {
// 1. تهيئة بيئة فلاتر في الخلفية
WidgetsFlutterBinding.ensureInitialized();
await initFirebaseIfNeeded();
await GetStorage.init();
if (!Get.isRegistered<NotificationController>()) {
Get.put(NotificationController());
}
if (!Get.isRegistered<FirebaseMessagesController>()) {
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<dynamic> myList;
try {
myList = jsonDecode(myListString) as List<dynamic>;
} 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',
// 4. شرط الأمان: التأكد من وجود البيانات المطلوبة
if (title != null && body != null && myListString != null) {
// 5. عرض الإشعار المحلي
notificationController.showOrderNotification(
title,
body,
'ding.wav',
myListString,
);
} else {
FirebaseMessagesController().fireBaseTitles(message);
}
}
print("⚠️ Received empty data message or missing fields.");
}
}
// @pragma('vm:entry-point')
// Future<void> backgroundMessageHandler(RemoteMessage message) async {
// WidgetsFlutterBinding.ensureInitialized();
// await initFirebaseIfNeeded();
// await GetStorage.init();
// if (!Get.isRegistered<NotificationController>()) {
// Get.put(NotificationController());
// }
// if (!Get.isRegistered<FirebaseMessagesController>()) {
// 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<dynamic> myList;
// try {
// myList = jsonDecode(myListString) as List<dynamic>;
// } 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) {

View File

@@ -103,7 +103,7 @@ GetBuilder<HomeCaptainController> 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',

View File

@@ -216,29 +216,15 @@ class _OrderOverlayState extends State<OrderOverlay>
await _closeOverlay();
return;
}
var res = await CRUD().post(
link: "${AppLink.ride}/rides/updateStausFromSpeed.php",
payload: {
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),
});
List<String> bodyToPassenger = [
_getData(6).toString(),
_getData(8).toString(),
_getData(9).toString(),
];
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<OrderOverlay>
'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<OrderOverlay>
);
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(