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

سوكيت الراكب:
- 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
@@ -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<bool> isPermissionGranted() async {
final result = await _channel.invokeMethod<bool>('isPermissionGranted');
return result ?? false;
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>('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<void> 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<bool> 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<bool>('showOverlay', {
'tripData': tripData.toJson(),
'autoCloseSeconds': autoCloseSeconds,
});
return result ?? false;
try {
final result = await _channel.invokeMethod<bool>('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<void> 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<bool> isOverlayActive() async {
final result = await _channel.invokeMethod<bool>('isOverlayActive');
return result ?? false;
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>('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()