first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import 'dart:convert';
import 'package:googleapis_auth/auth_io.dart';
import '../../print.dart';
class AccessTokenManager {
static final AccessTokenManager _instance = AccessTokenManager._internal();
late final String serviceAccountJsonKey;
AccessToken? _accessToken;
DateTime? _expiryDate;
AccessTokenManager._internal();
factory AccessTokenManager(String jsonKey) {
if (_instance._isServiceAccountKeyInitialized()) {
// Prevent re-initialization
return _instance;
}
_instance.serviceAccountJsonKey = jsonKey;
return _instance;
}
bool _isServiceAccountKeyInitialized() {
try {
serviceAccountJsonKey; // Access to check if initialized
return true;
} catch (e) {
return false;
}
}
Future<String> getAccessToken() async {
if (_accessToken != null && DateTime.now().isBefore(_expiryDate!)) {
return _accessToken!.data;
}
try {
final serviceAccountCredentials = ServiceAccountCredentials.fromJson(
json.decode(serviceAccountJsonKey));
final client = await clientViaServiceAccount(
serviceAccountCredentials,
['https://www.googleapis.com/auth/firebase.messaging'],
);
_accessToken = client.credentials.accessToken;
_expiryDate = client.credentials.accessToken.expiry;
client.close();
// Log.print('_accessToken!.data: ${_accessToken!.data}');
return _accessToken!.data;
} catch (e) {
throw Exception('Failed to obtain access token');
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppLifecycleManager {
static const platform = MethodChannel('com.sefer_driver/app_lifecycle');
static Future<void> bringAppToForeground() async {
try {
debugPrint('Attempting to bring app to foreground');
await platform.invokeMethod('bringAppToForeground');
debugPrint('Method invocation completed');
} on PlatformException catch (e) {
debugPrint("Failed to bring app to foreground: '${e.message}'.");
} catch (e) {
debugPrint("Unexpected error: $e");
}
}
}

View File

@@ -0,0 +1,373 @@
import 'dart:convert';
import 'dart:io';
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
import 'package:siro_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:siro_driver/controller/voice_call_controller.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../main.dart';
import '../../print.dart';
import '../../views/auth/captin/criminal_documents_page.dart';
import '../../views/home/Captin/home_captain/home_captin.dart';
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
import '../../views/home/Captin/orderCaptin/vip_order_page.dart';
import '../auth/google_sign.dart';
import '../functions/face_detect.dart';
import '../home/captin/map_driver_controller.dart';
import 'local_notification.dart';
class FirebaseMessagesController extends GetxController {
final fcmToken = FirebaseMessaging.instance;
List<String> tokens = [];
List dataTokens = [];
late String driverID;
late String driverToken;
NotificationSettings? notificationSettings;
NotificationController notificationController =
Get.put(NotificationController());
Future<void> getNotificationSettings() async {
// Get the current notification settings
NotificationSettings? notificationSettings =
await FirebaseMessaging.instance.getNotificationSettings();
'Notification authorization status: ${notificationSettings.authorizationStatus}';
// Call the update function if needed
update();
}
Future<void> requestFirebaseMessagingPermission() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
// Check if the platform is Android
if (Platform.isAndroid) {
// Request permission for Android
await messaging.requestPermission();
} else if (Platform.isIOS) {
// Request permission for iOS
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: false,
sound: true,
);
messaging.setForegroundNotificationPresentationOptions(
alert: true, badge: true, sound: true);
}
}
Future getToken() async {
fcmToken.getToken().then((token) {
Log.print('token fcm driver: ${token}');
box.write(BoxName.tokenDriver, (token!));
});
// 🔹 الاشتراك في topic
await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم
print("Subscribed to 'drivers' topic ✅");
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async {
if (message != null && message.data.isNotEmpty) {
Log.print("🔔 FCM getInitialMessage payload: ${message.data}");
String? category = message.data['category'] ?? message.data['type'];
if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) {
String? myListString = message.data['DriverList'];
if (myListString != null && myListString.isNotEmpty) {
await storage.write(key: 'pending_driver_list', value: myListString);
Log.print("💾 Saved pending driver list to secure storage from getInitialMessage");
}
} else {
Future.delayed(const Duration(milliseconds: 1500), () {
fireBaseTitles(message);
});
}
}
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// If the app is in the background or terminated, show a system tray message
RemoteNotification? notification = message.notification;
AndroidNotification? android = notification?.android;
// if (notification != null && android != null) {
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
// if (message.data.isNotEmpty && message.notification != null) {
// fireBaseTitles(message);
// }
});
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
});
}
Future<void> fireBaseTitles(RemoteMessage message) async {
// [!! تعديل جوهري !!]
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
String category = message.data['category'] ?? '';
// اقرأ العنوان والنص (للعرض)
String title = message.notification?.title ?? '';
String body = message.notification?.body ?? '';
// استخدم switch لسهولة القراءة والصيانة
switch (category) {
case 'ORDER':
case 'Order': // Handle both cases for backward compatibility
// if (Platform.isAndroid) {
// notificationController.showNotification(title, body, 'order', '');
// }
// 🔥 [Fix FCM-Guard] منع إعاقة الرحلة النشطة بطلبات جديدة عبر FCM
String currentRideStatus = box.read(BoxName.rideStatus) ?? '';
if (currentRideStatus == 'Begin' ||
currentRideStatus == 'Apply' ||
currentRideStatus == 'Arrived') {
Log.print(
"⛔ [FCM] Ignoring ORDER notification — driver has active ride ($currentRideStatus)");
break;
}
var myListString = message.data['DriverList'];
if (myListString != null) {
var myList = jsonDecode(myListString) as List<dynamic>;
driverToken = myList[14].toString();
Get.put(HomeCaptainController(), permanent: true).changeRideId();
update();
Get.toNamed('/OrderRequestPage', arguments: {
'myListString': myListString,
'DriverList': myList,
'body': body
});
}
break;
case 'OrderVIP':
var myListString = message.data['DriverList'];
if (myListString != null) {
var myList = jsonDecode(myListString) as List<dynamic>;
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'order', '');
}
Get.to(VipOrderPage(), arguments: {
'myListString': myListString,
'DriverList': myList,
'body': body
});
}
break;
case 'Cancel Trip':
case 'TRIP_CANCELLED':
if (Platform.isAndroid) {
notificationController.showNotification(
title, 'Passenger Cancel Trip'.tr, 'cancel', '');
}
Log.print("🔔 FCM: Ride Cancelled by Passenger received.");
// 1. استخراج السبب (أرسلناه من PHP باسم 'reason')
String reason = message.data['reason'] ?? 'No reason provided';
// 2. توجيه الأمر للكنترولر
if (Get.isRegistered<MapDriverController>()) {
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
Get.find<MapDriverController>()
.processRideCancelledByPassenger(reason, source: "FCM");
}
break;
case 'VIP Order Accepted':
// This seems to be a notification for the passenger, but if the driver needs to see it:
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'order', '');
}
// Maybe show a simple snackbar confirmation
mySnackbarSuccess('You accepted the VIP order.'.tr);
break;
case 'message From passenger':
case 'MSG_FROM_PASSENGER':
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding', '');
}
MyDialog().getDialog(title, body, () {
// Empty callback, MyDialog already closes itself using pop().
});
break;
case 'token change':
case 'TOKEN_CHANGE':
GoogleSignInHelper.signOut();
break;
case 'face detect':
case 'FACE_DETECT':
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone2', '');
}
String result0 = await faceDetector();
var result = jsonDecode(result0);
MyDialogContent().getDialog(
'Face Detection Result'.tr,
Text(
result['similar'].toString() == 'true'
? 'similar'.tr
: 'not similar'.tr,
style: AppStyle.title,
),
() {
// Navigator.pop(Get.context!);
},
);
update();
break;
case 'Hi ,I will go now':
case 'PASSENGER_COMING':
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone2', '');
}
update();
break;
case 'Criminal Document Required':
case 'DOC_REQUIRED':
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone2', '');
}
MyDialog().getDialog(title, 'You should have upload it .'.tr, () {
Get.to(() => const CriminalDocumemtPage());
});
break;
case 'Order Applied':
case 'ORDER_TAKEN':
mySnackbarSuccess("The order has been accepted by another driver.".tr);
break;
case 'incoming_call':
case 'INCOMING_CALL':
final sessionId = message.data['session_id'];
final callerName = message.data['caller_name'];
final rideId = message.data['ride_id'];
if (sessionId != null && callerName != null && rideId != null) {
Get.find<VoiceCallController>().receiveCall(
sessionIdVal: sessionId.toString(),
remoteNameVal: callerName.toString(),
rideIdVal: rideId.toString(),
);
}
break;
default:
Log.print('Received unhandled notification category: $category');
// Optionally show a generic notification
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'default', '');
}
break;
}
}
SnackbarController driverAppliedTripSnakBar() {
return Get.snackbar(
'Driver Applied the Ride for You'.tr,
'',
colorText: AppColor.greenColor,
duration: const Duration(seconds: 3),
snackPosition: SnackPosition.TOP,
titleText: Text(
'Applied'.tr,
style: const TextStyle(color: AppColor.redColor),
),
messageText: Text(
'Driver Applied the Ride for You'.tr,
style: AppStyle.title,
),
icon: const Icon(Icons.approval),
shouldIconPulse: true,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
);
}
Future<dynamic> cancelTripDialog() {
return Get.defaultDialog(
barrierDismissible: false,
title: 'Passenger Cancel Trip'.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () {
box.write(BoxName.rideStatus, 'Cancel');
box.write(BoxName.statusDriverLocation, 'off');
Log.print(
'rideStatus from 347 : ${box.read(BoxName.rideStatus)}');
Get.offAll(HomeCaptain());
}));
}
Future<dynamic> cancelTripDialog1() {
return Get.defaultDialog(
barrierDismissible: false,
title: 'Passenger Cancel Trip'.tr,
middleText:
'Trip Cancelled. The cost of the trip will be added to your wallet.'
.tr,
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () {
box.write(BoxName.rideStatus, 'Cancel');
Log.print(
'rideStatus from 364 : ${box.read(BoxName.rideStatus)}');
Get.offAll(HomeCaptain());
}));
}
}
class OverlayContent extends StatelessWidget {
final String title;
final String body;
OverlayContent(this.title, this.body);
@override
Widget build(BuildContext context) {
return Material(
child: Container(
padding: const EdgeInsets.all(16.0),
color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8.0),
Text(
body,
style: const TextStyle(fontSize: 16),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,548 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui'; // للألوان
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 '../../constant/links.dart';
import '../../main.dart'; // للوصول لـ box
import '../../print.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');
// إعدادات أزرار الآيفون (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,
notificationCategories: darwinNotificationCategories, // تسجيل الأزرار
);
InitializationSettings initializationSettings =
InitializationSettings(android: android, iOS: ios);
tz.initializeTimeZones();
print('✅ Notifications initialized with Action Buttons Support');
await _flutterLocalNotificationsPlugin.initialize(
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
settings: initializationSettings,
);
// إنشاء قناة الأندرويد ذات الأهمية القصوى
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'high_importance_channel',
'High Importance Notifications',
description: 'This channel is used for important notifications.',
importance: Importance.max, // أقصى أهمية
playSound: true,
);
await _flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
// ==============================================================================
// 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);
String isHaveSteps = _getVal(data, 20);
// تنسيق النص ليكون 4 أسطر واضحة
formattedBigText = "👤 $paxName\n"
"💰 $price ${'SYP'.tr} | 🛣️ $distance كم\n"
"🟢 من: $startLoc\n"
"🏁 إلى: $endLoc";
if (isHaveSteps == 'true') {
formattedBigText += "\n🛑 هذه الرحلة تحتوي على نقاط توقف!";
}
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);
String briefBody = "$price - مسافة $formattedBigText";
if (_getVal(jsonDecode(myListString), 20) == 'true') {
briefBody = "🛑 (متعددة التوقفات) $price - مسافة $formattedBigText";
}
// عرض الإشعار
await _flutterLocalNotificationsPlugin.show(
id: 1001, // ID ثابت لاستبدال الإشعار القديم
title: title,
body: briefBody, // نص مختصر يظهر في البار العلوي
notificationDetails: 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(id: 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(id: 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(), permanent: true);
} 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,
htmlFormatContent: true,
htmlFormatContentTitle: true,
);
AndroidNotificationDetails android = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
);
DarwinNotificationDetails ios = const DarwinNotificationDetails(
sound: 'default',
presentAlert: true,
presentBadge: true,
presentSound: true,
);
NotificationDetails details =
NotificationDetails(android: android, iOS: ios);
await _flutterLocalNotificationsPlugin.show(
id: 0,
title: title,
body: message,
notificationDetails: details,
payload: jsonEncode({'title': title, 'data': payLoad}));
}
void scheduleNotificationsForSevenDays(
String title, String message, String tone) async {
final AndroidNotificationDetails android = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
);
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) {
await Permission.scheduleExactAlarm.request();
}
}
for (int day = 0; day < 7; day++) {
final notificationTimes = [
{'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;
bool isScheduled = box.read('notification_$notificationId') ?? false;
if (!isScheduled) {
await _scheduleNotificationForTime(
day,
time['hour'] as int,
time['minute'] as int,
title,
message,
details,
notificationId,
);
box.write('notification_$notificationId', true);
}
}
}
}
// ==============================================================================
// دالة حذف إشعار الطلب (تستدعى عند أخذ الطلب من سائق آخر)
// ==============================================================================
Future<void> cancelOrderNotification() async {
// 1001 هو نفس الآيدي الذي استخدمناه عند عرض الإشعار
await _flutterLocalNotificationsPlugin.cancel(id: 1001);
print("🗑️ Order Notification Cancelled (Taken by another driver)");
}
Future<void> _scheduleNotificationForTime(
int dayOffset,
int hour,
int minute,
String title,
String message,
NotificationDetails details,
int notificationId,
) async {
tz.initializeTimeZones();
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,
hour,
minute,
);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
await _flutterLocalNotificationsPlugin.zonedSchedule(
id: notificationId,
title: title,
body: message,
scheduledDate: scheduledDate,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.exact, // أو exactAllowWhileIdle
matchDateTimeComponents: null,
);
}
}

View File

@@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart'; // للترجمة .tr
class NotificationService {
static const String _serverUrl =
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
static Future<void> sendNotification({
required String target,
required String title,
required String body,
required String category, // إلزامي للتصنيف
String? tone,
List<String>? driverList,
bool isTopic = false,
}) async {
try {
// 1. تجهيز البيانات المخصصة (Data Payload)
Map<String, dynamic> customData = {};
customData['category'] = category;
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
if (driverList != null && driverList.isNotEmpty) {
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
customData['driverList'] = jsonEncode(driverList);
}
// 2. تجهيز الطلب الرئيسي للسيرفر
final Map<String, dynamic> requestPayload = {
'target': target,
'title': title,
'body': body,
'isTopic': isTopic,
'data':
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
};
if (tone != null) {
requestPayload['tone'] = tone;
}
final response = await http.post(
Uri.parse(_serverUrl),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(requestPayload),
);
if (response.statusCode == 200) {
print('✅ Notification sent successfully.');
// print('Response: ${response.body}');
} else {
print('❌ Failed to send notification. Code: ${response.statusCode}');
print('Error Body: ${response.body}');
}
} catch (e) {
print('❌ Error sending notification: $e');
}
}
}

View File

@@ -0,0 +1,35 @@
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:flutter/material.dart';
class OverlayContent1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: Container(
padding: EdgeInsets.all(16.0),
color: Colors.white,
child: MyElevatedButton(
title: 'go to order',
onPressed: () async {
var res = await CRUD().post(
link: AppLink.addFeedBack,
payload: {
"passengerId": 'dddddd',
"feedBack": "eeeee",
},
);
print(res);
if (res != 'failure') {
Navigator.push(
context, MaterialPageRoute(builder: (cont) => HomeCaptain()));
// Get.to(OrderRequestPage());
}
},
),
),
);
}
}