From a26472ed9f1ee874b2af7de64788a7a20ea67412 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 27 Jul 2026 00:07:27 +0300 Subject: [PATCH] =?UTF-8?q?=D8=A5=D8=B5=D9=84=D8=A7=D8=AD=20=D9=85=D8=B3?= =?UTF-8?q?=D8=A7=D8=B1=20=D8=A7=D9=84=D8=A5=D8=B4=D8=B9=D8=A7=D8=B1?= =?UTF-8?q?=E2=86=92=D8=A7=D9=84=D8=B7=D9=84=D8=A8=20=D8=B9=D9=86=D8=AF=20?= =?UTF-8?q?=D8=A7=D9=84=D9=83=D8=A7=D8=A8=D8=AA=D9=86=20+=20=D8=AA=D9=88?= =?UTF-8?q?=D8=AD=D9=8A=D8=AF=20=D8=A5=D8=B9=D8=A7=D8=AF=D8=A9=20=D8=A7?= =?UTF-8?q?=D8=AA=D8=B5=D8=A7=D9=84=20=D8=B3=D9=88=D9=83=D9=8A=D8=AA=20?= =?UTF-8?q?=D8=A7=D9=84=D8=B1=D8=A7=D9=83=D8=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit سوكيت الراكب: - setReconnectionAttempts: من 20 إلى لا نهائي، توحيداً مع تطبيق السائق. بعد 20 محاولة كان يستسلم **نهائياً** فيصمت للأبد بعد دقائق من شبكة سيئة ويبقى الراكب على الـ polling بلا أن يدري — وخطره أكبر الآن لأن الاتصال صار يعيش من فتح الخريطة لا من إنشاء الرحلة. - الـ heartbeat من 15ث إلى 30ث: Socket.IO يعمل ping/pong على مستوى البروتوكول ومستمع 'heartbeat' في passenger_socket.php فارغ أصلاً. البدء البارد على iOS (سبب "التطبيق يغلق ونرجع من أول"): - didChangeAppLifecycleState لا يُنفَّذ عند إطلاق التطبيق (يبدأ في resumed بلا انتقال حالة)، وهو المُنادي الوحيد لـ _checkPendingTrip ⇒ الضغط على الإشعار والتطبيق مقتول لا يوجّه لصفحة الطلب إطلاقاً. - وحتى لو نُفِّذ، كان يستسلم على '/' ويحوّل المسؤولية لـ HomeCaptainController الذي لا يملك أي منطق كهذا ⇒ الطلب يبقى في التخزين للأبد. صار addPostFrameCallback + مُجدوِل يعيد المحاولة حتى خروج التطبيق من شاشة البداية (سقف ~30ث)، ولا يحذف الطلب قبل التوجيه. - حرس إضافي: لا اقتحام لرحلة نشطة بطلب قديم، لا توجيه مكرر إذا كنا على OrderRequestPage، وتنظيف الحمولة التالفة. TripOverlayPlugin أندرويد فقط بلا أي حرس منصّة: - كل دالة كانت تنادي invokeMethod مباشرة ⇒ MissingPluginException على iOS (لا يوجد ios/ ولا مدخل في pubspec.plugin.platforms). - الأخطر: backgroundMessageHandler كان ينادي showOverlay **قبل** حفظ pending_driver_list، فيسقط الاستثناء ويُلغي الحفظ كلياً على iOS. عُكس الترتيب — الحفظ أولاً دائماً، وهو الحدّ الأدنى الذي لا يتوقف على منصّة أو صلاحية. - أُضيف isSupported وحُصِّنت كل الدوال (ترجع false/no-op ولا ترمي أبداً)، فلا يُقطع أيضاً _initApp قبل _listenToOverlayEvents. أخطاء index في حمولة add_ride.php (أربع نقاط قبول): - 'Duration' كان index 4 وهو end_lng لا المدة ⇒ صار 15 (duration_text). أحد المواضع كان فيه تعليق "انتبه: تأكد من الإندكس الصحيح للوقت". - 'passengerWalletBurc' كان 26 وهو price_for_driver (أرباح السائق) لا رصيد الراكب ⇒ صار 27 (passenger_wallet). - الموضعان مكرران في main.dart و local_notification.dart و order_over_lay.dart و order_request_controller.dart — وُحِّدت كلها. - حرس الطول في backgroundMessageHandler: كان length > 29 مع قراءة index 30 ⇒ RangeError على قائمة طولها 30 بالضبط. Co-Authored-By: Claude Opus 5 --- .../firebase/local_notification.dart | 4 +- .../home/captin/order_request_controller.dart | 2 +- siro_driver/lib/main.dart | 118 ++++++++++++++---- .../Captin/orderCaptin/order_over_lay.dart | 4 +- .../lib/trip_overlay_plugin.dart | 75 +++++++++-- .../home/map/map_socket_controller.dart | 13 +- 6 files changed, 176 insertions(+), 40 deletions(-) diff --git a/siro_driver/lib/controller/firebase/local_notification.dart b/siro_driver/lib/controller/firebase/local_notification.dart index 9ba4e000..7ad6a180 100755 --- a/siro_driver/lib/controller/firebase/local_notification.dart +++ b/siro_driver/lib/controller/firebase/local_notification.dart @@ -359,7 +359,7 @@ class NotificationController extends GetxController { return { 'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}', 'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}', - 'Duration': _getVal(data, 4), // انتبه: تأكد من الإندكس الصحيح للوقت + 'Duration': _getVal(data, 15), // 15 = duration_text في حمولة add_ride.php (كان 4 = end_lng) 'totalCost': _getVal(data, 26), 'Distance': _getVal(data, 5), 'name': _getVal(data, 8), @@ -382,7 +382,7 @@ class NotificationController extends GetxController { 'step2': _getVal(data, 23), 'step3': _getVal(data, 24), 'step4': _getVal(data, 25), - 'passengerWalletBurc': _getVal(data, 26), + 'passengerWalletBurc': _getVal(data, 27), // 27 = passenger_wallet (كان 26 = price_for_driver) 'timeOfOrder': DateTime.now().toString(), 'totalPassenger': _getVal(data, 2), 'carType': _getVal(data, 31), diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index aef6e3fe..ce4af3e1 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -755,7 +755,7 @@ class OrderRequestController extends GetxController 'step2': _safeGet(23), 'step3': _safeGet(24), 'step4': _safeGet(25), - 'passengerWalletBurc': _safeGet(26), + 'passengerWalletBurc': _safeGet(27), // 27 = passenger_wallet (كان 26 = price_for_driver) 'timeOfOrder': DateTime.now().toString(), 'totalPassenger': _safeGet(2), 'carType': _safeGet(31), diff --git a/siro_driver/lib/main.dart b/siro_driver/lib/main.dart index b390082b..b62832fe 100755 --- a/siro_driver/lib/main.dart +++ b/siro_driver/lib/main.dart @@ -207,7 +207,9 @@ Future backgroundMessageHandler(RemoteMessage message) async { double pLat = 0.0; double pLng = 0.0; - if (myList.isNotEmpty && myList.length > 29) { + // نقرأ حتى index 30، فالطول المطلوب 31 لا 30 (كان > 29 ⇒ RangeError + // على قائمة طولها 30 بالضبط). + if (myList.length > 30) { fare = double.tryParse(myList[26].toString()) ?? 0.0; distance = double.tryParse(myList[11].toString()) ?? 0.0; passengerName = myList[8].toString(); @@ -238,11 +240,15 @@ Future backgroundMessageHandler(RemoteMessage message) async { currentRideStatus == 'Arrived'); if (!hasActiveRide) { - if (!isAppInForeground) { + // 🔴 الحفظ **أولاً** ثم النافذة. كان العكس: showOverlay ترمي + // MissingPluginException على iOS (الإضافة أندرويد فقط) فيُلغى الحفظ + // تماماً ⇒ يضغط الكابتن الإشعار فلا يجد طلباً ويبدأ التطبيق من الصفر. + // الحفظ هو الحدّ الأدنى الذي لا يجوز أن يتوقف على أي منصّة أو صلاحية. + await storage.write(key: 'pending_driver_list', value: myListString); + + if (!isAppInForeground && TripOverlayPlugin.isSupported) { await TripOverlayPlugin.showOverlay(tripData, autoCloseSeconds: 15); } - // 🔴 الكتابة على القرص مباشرة بدلاً من الرام لضمان انتقالها للتطبيق 🔴 - await storage.write(key: 'pending_driver_list', value: myListString); } else { print("⛔ [Background FCM] Ignoring new order overlay - driver is in active ride ($currentRideStatus)"); } @@ -299,6 +305,15 @@ class _MyAppState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); _initApp(); + + // 🔴 البدء البارد: didChangeAppLifecycleState **لا يُنفَّذ** عند إطلاق + // التطبيق (يبدأ في resumed بلا انتقال حالة)، فكان _checkPendingTrip لا + // يُنادى إطلاقاً حين يفتح الكابتن التطبيق من الإشعار وهو مقتول. والتعليق + // القديم كان يُحوّل المسؤولية لـ HomeCaptainController وهو لا يملك أي + // منطق كهذا ⇒ يبقى الطلب في التخزين للأبد ويهبط الكابتن على الرئيسية. + WidgetsBinding.instance.addPostFrameCallback((_) { + _schedulePendingTripCheck(); + }); // 🔥 التقاط رسالة القبول من الأندرويد بشكل مباشر 100% 🔥 const platformAppControl = MethodChannel('com.siro_driver/app_control'); platformAppControl.setMethodCallHandler((call) async { @@ -405,28 +420,83 @@ class _MyAppState extends State with WidgetsBindingObserver { } } - void _checkPendingTrip() async { - // 🔴 القراءة من القرص مباشرة 🔴 - String? savedTrip = await storage.read(key: 'pending_driver_list'); + /// ينتظر خروج التطبيق من شاشة البداية ثم يفحص الطلب المعلّق. + /// كان الفحص يستسلم فوراً إذا كان على '/' فيُهدر الطلب — الآن يعيد المحاولة. + bool _pendingCheckScheduled = false; - if (savedTrip != null && savedTrip.isNotEmpty) { - if (Get.currentRoute == '/') { - print( - '⏳ App is still on Splash screen. Postponing notification trip navigation to HomeCaptainController.'); + Future _schedulePendingTripCheck() async { + if (_pendingCheckScheduled) return; + _pendingCheckScheduled = true; + try { + // سقف ~30 ثانية: يكفي لأبطأ إقلاع بلا أن يدور للأبد + for (int attempt = 0; attempt < 60; attempt++) { + final String? savedTrip = await storage.read(key: 'pending_driver_list'); + if (savedTrip == null || savedTrip.isEmpty) { + // لا شيء معلّق بعد — الإشعار قد يكون ما زال يُحفَظ من + // getInitialMessage، فنكمل الانتظار قليلاً. + if (attempt > 10) return; + await Future.delayed(const Duration(milliseconds: 500)); + continue; + } + if (Get.currentRoute == '/' || Get.currentRoute.isEmpty) { + await Future.delayed(const Duration(milliseconds: 500)); + continue; + } + await _checkPendingTrip(); return; } - await storage.delete(key: 'pending_driver_list'); - List driverList = jsonDecode(savedTrip); - - print('👀 التطبيق فتح من الإشعار أو يدوياً، توجيه لصفحة الطلب...'); - Get.toNamed('/OrderRequestPage', arguments: { - 'myListString': savedTrip, - 'DriverList': driverList, - 'body': 'طلب جديد' - }); + print('⚠️ Pending trip check timed out waiting for app to leave splash.'); + } finally { + _pendingCheckScheduled = false; } } + Future _checkPendingTrip() async { + // 🔴 القراءة من القرص مباشرة 🔴 + String? savedTrip = await storage.read(key: 'pending_driver_list'); + if (savedTrip == null || savedTrip.isEmpty) return; + + if (Get.currentRoute == '/' || Get.currentRoute.isEmpty) { + // ما زلنا على شاشة البداية — لا نحذف الطلب، ندع المُجدوِل يعيد المحاولة + print('⏳ Still on splash — deferring pending trip navigation (kept).'); + _schedulePendingTripCheck(); + return; + } + + // لا نقتحم رحلة نشطة بطلب قديم + final String currentRideStatus = box.read(BoxName.rideStatus)?.toString() ?? ''; + if (currentRideStatus == 'Begin' || + currentRideStatus == 'Apply' || + currentRideStatus == 'Arrived') { + print('⛔ Active ride ($currentRideStatus) — discarding stale pending order.'); + await storage.delete(key: 'pending_driver_list'); + return; + } + + if (Get.currentRoute == '/OrderRequestPage') { + print('⚠️ Already on OrderRequestPage — skipping duplicate navigation.'); + return; + } + + List driverList; + try { + driverList = jsonDecode(savedTrip) as List; + } catch (e) { + print('❌ Corrupt pending_driver_list, discarding: $e'); + await storage.delete(key: 'pending_driver_list'); + return; + } + + await storage.delete(key: 'pending_driver_list'); + + print('👀 التطبيق فتح من الإشعار أو يدوياً، توجيه لصفحة الطلب...'); + Get.toNamed('/OrderRequestPage', arguments: { + 'myListString': savedTrip, + 'DriverList': driverList, + 'body': 'طلب جديد' + }); + } + // ============================================================================== // 🟢 دوال معالجة قبول الطلب وتجهيز المتغيرات 🟢 // ============================================================================== @@ -521,7 +591,9 @@ class _MyAppState extends State with WidgetsBindingObserver { return { 'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}', 'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}', - 'Duration': _getVal(data, 4), + // كان index 4 وهو end_lng لا المدة! ترتيب الحمولة في add_ride.php: + // 15 = duration_text. (راجع $payload هناك قبل تعديل أي index) + 'Duration': _getVal(data, 15), 'totalCost': _getVal(data, 26), 'Distance': _getVal(data, 5), 'name': _getVal(data, 8), @@ -544,7 +616,9 @@ class _MyAppState extends State with WidgetsBindingObserver { 'step2': _getVal(data, 23), 'step3': _getVal(data, 24), 'step4': _getVal(data, 25), - 'passengerWalletBurc': _getVal(data, 26), + // كان index 26 وهو price_for_driver (أرباح السائق) لا رصيد الراكب. + // 27 = passenger_wallet في حمولة add_ride.php. + 'passengerWalletBurc': _getVal(data, 27), 'timeOfOrder': DateTime.now().toString(), 'totalPassenger': _getVal(data, 2), 'carType': _getVal(data, 31), diff --git a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart index 615a1439..4a32ba96 100755 --- a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -230,7 +230,7 @@ class _OrderOverlayState extends State 'status': 'Apply', 'passengerLocation': '${_getData(0)},${_getData(1)}', 'passengerDestination': '${_getData(3)},${_getData(4)}', - 'Duration': _getData(4), + 'Duration': _getData(15), // 15 = duration_text (كان 4 = end_lng) 'totalCost': _getData(26), 'Distance': _getData(5), 'name': _getData(8), @@ -251,7 +251,7 @@ class _OrderOverlayState extends State 'step2': myList[23].toString(), 'step3': myList[24].toString(), 'step4': myList[25].toString(), - 'passengerWalletBurc': myList[26].toString(), + 'passengerWalletBurc': myList[27].toString(), // 27 = passenger_wallet (كان 26 = price_for_driver) 'carType': myList[31].toString(), 'kazan': myList[32].toString(), 'startNameLocation': myList[29].toString(), diff --git a/siro_driver/trip_overlay_plugin/lib/trip_overlay_plugin.dart b/siro_driver/trip_overlay_plugin/lib/trip_overlay_plugin.dart index db991506..83be6d47 100644 --- a/siro_driver/trip_overlay_plugin/lib/trip_overlay_plugin.dart +++ b/siro_driver/trip_overlay_plugin/lib/trip_overlay_plugin.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io' show Platform; +import 'package:flutter/foundation.dart' show debugPrint; import 'package:flutter/services.dart'; /// Model for trip data passed to the overlay @@ -119,45 +121,96 @@ class TripOverlayPlugin { } } + /// هذه الإضافة **أندرويد فقط** — لا يوجد تنفيذ iOS ولا مجلد ios/ ولا مدخل + /// في pubspec.plugin.platforms. iOS لا يسمح بنوافذ فوق التطبيقات أصلاً. + /// + /// بلا هذا الحرس كان أي نداء يرمي MissingPluginException على iOS، وأخطر + /// موضع: backgroundMessageHandler ينادي showOverlay **قبل** حفظ + /// pending_driver_list، فيسقط الاستثناء ويُلغي الحفظ ⇒ يفتح الكابتن الإشعار + /// فلا يجد طلباً وتبدأ الشاشة من الصفر. + static bool get isSupported => Platform.isAndroid; + /// Check if SYSTEM_ALERT_WINDOW permission is granted static Future isPermissionGranted() async { - final result = await _channel.invokeMethod('isPermissionGranted'); - return result ?? false; + if (!isSupported) return false; + try { + final result = await _channel.invokeMethod('isPermissionGranted'); + return result ?? false; + } on PlatformException catch (e) { + debugPrint('[TripOverlay] isPermissionGranted failed: ${e.message}'); + return false; + } on MissingPluginException { + return false; + } } /// Open system settings to grant SYSTEM_ALERT_WINDOW permission static Future requestPermission() async { - await _channel.invokeMethod('requestPermission'); + if (!isSupported) return; + try { + await _channel.invokeMethod('requestPermission'); + } on PlatformException catch (e) { + debugPrint('[TripOverlay] requestPermission failed: ${e.message}'); + } on MissingPluginException { + // لا شيء — غير مدعوم على هذه المنصة + } } /// Show the trip overlay with the given [tripData] /// [autoCloseSeconds] — how long before auto-dismiss (default 30s) + /// + /// يرجع false ولا يرمي أبداً: المُنادي يجب أن يكمل مساره (حفظ الطلب المعلّق، + /// الإشعار المحلي) حتى لو تعذّرت النافذة. static Future showOverlay( TripData tripData, { int autoCloseSeconds = 30, }) async { + if (!isSupported) return false; + final granted = await isPermissionGranted(); if (!granted) { await requestPermission(); return false; } - final result = await _channel.invokeMethod('showOverlay', { - 'tripData': tripData.toJson(), - 'autoCloseSeconds': autoCloseSeconds, - }); - return result ?? false; + try { + final result = await _channel.invokeMethod('showOverlay', { + 'tripData': tripData.toJson(), + 'autoCloseSeconds': autoCloseSeconds, + }); + return result ?? false; + } on PlatformException catch (e) { + debugPrint('[TripOverlay] showOverlay failed: ${e.message}'); + return false; + } on MissingPluginException { + return false; + } } /// Programmatically close the overlay (e.g. if trip was cancelled) static Future hideOverlay() async { - await _channel.invokeMethod('hideOverlay'); + if (!isSupported) return; + try { + await _channel.invokeMethod('hideOverlay'); + } on PlatformException catch (e) { + debugPrint('[TripOverlay] hideOverlay failed: ${e.message}'); + } on MissingPluginException { + // لا شيء + } } /// Check if the overlay is currently visible static Future isOverlayActive() async { - final result = await _channel.invokeMethod('isOverlayActive'); - return result ?? false; + if (!isSupported) return false; + try { + final result = await _channel.invokeMethod('isOverlayActive'); + return result ?? false; + } on PlatformException catch (e) { + debugPrint('[TripOverlay] isOverlayActive failed: ${e.message}'); + return false; + } on MissingPluginException { + return false; + } } /// Dispose streams — call in app's dispose() diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart index fc4daa58..2f7a20fd 100644 --- a/siro_rider/lib/controller/home/map/map_socket_controller.dart +++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart @@ -87,7 +87,11 @@ class MapSocketController extends GetxController { .setTransports(['websocket']) .disableAutoConnect() .setQuery({'id': passengerId, 'jwt': jwt}) - .setReconnectionAttempts(20) + // محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart). + // كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق + // من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال + // يعيش من فتح الخريطة لا من إنشاء الرحلة، فالسقف الثابت خطر أكبر الآن. + .setReconnectionAttempts(double.infinity) .setReconnectionDelay(2000) .setReconnectionDelayMax(10000) .enableReconnection() @@ -266,9 +270,14 @@ class MapSocketController extends GetxController { } } + /// ⚠️ زائدة عن الحاجة عملياً: Socket.IO يعمل ping/pong على مستوى البروتوكول، + /// ومستمع 'heartbeat' في passenger_socket.php فارغ ولا يفعل شيئاً. أبقيناها + /// بفاصل 30ث بدل 15ث لتنصيف حركتها — الاتصال صار يعيش من فتح الخريطة لا من + /// إنشاء الرحلة، فعدد الاتصالات الخاملة أعلى بكثير. حذفها كاملاً ممكن لاحقاً + /// بعد التأكد أن لا شيء على السيرفر يعتمد عليها لقياس الحضور. void _startHeartbeat() { _heartbeatTimer?.cancel(); - _heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) { + _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) { if (isSocketConnected && socket.connected) { socket.emit('heartbeat', {'passenger_id': box.read(BoxName.passengerID).toString()});