إصلاح مسار الإشعار→الطلب عند الكابتن + توحيد إعادة اتصال سوكيت الراكب

سوكيت الراكب:
- 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 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 00:07:27 +03:00
co-authored by Claude Opus 5
parent ab3be7e2e3
commit a26472ed9f
6 changed files with 176 additions and 40 deletions
+96 -22
View File
@@ -207,7 +207,9 @@ Future<void> 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<void> 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<MyApp> 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<MyApp> 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<void> _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<dynamic> 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<void> _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<dynamic> driverList;
try {
driverList = jsonDecode(savedTrip) as List<dynamic>;
} 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<MyApp> 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<MyApp> 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),