first commit
This commit is contained in:
53
siro_driver/lib/controller/firebase/access_token.dart
Executable file
53
siro_driver/lib/controller/firebase/access_token.dart
Executable 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
18
siro_driver/lib/controller/firebase/bring_app_foreground.dart
Executable file
18
siro_driver/lib/controller/firebase/bring_app_foreground.dart
Executable 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
373
siro_driver/lib/controller/firebase/firbase_messge.dart
Executable file
373
siro_driver/lib/controller/firebase/firbase_messge.dart
Executable 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
548
siro_driver/lib/controller/firebase/local_notification.dart
Executable file
548
siro_driver/lib/controller/firebase/local_notification.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
35
siro_driver/lib/controller/firebase/order_lay.dart
Executable file
35
siro_driver/lib/controller/firebase/order_lay.dart
Executable 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());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user