feat(apps): الخريطة وطلب الرحلة ودورة السائق — المرحلة 4

## حسم تعارض الخرائط (docs/38 §7)
لصالح القرار الأحدث 2026-07-20: كل الخرائط — البلاطات والبحث والعكسي
والمسار — مباشرة إلى map-saas بترويسة x-api-key، بلا مرور بباك إند تريبز.
نقاط /maps/* في الباك إند لا تُستعمل، وإحداها (geocode) معطوبة على المنشور.
العقد تُحقّق حيّاً بـcurl قبل كتابة سطر.

## النواة المشتركة
- core/api/antlaq_api.dart: عميل منفصل بلا AuthInterceptor — توكن تريبز لا
  شأن لخادم الخرائط به، وإرساله إليه تسريب بلا مقابل
- core/realtime: Socket.IO واحد (trip:update · driver:location · trip:offer
  · trip:offer_taken)
- core/location: نقطة الموقع الوحيدة بمرشّح 25 متراً
- features/trip/data: سبعة نماذج + MapsRepository + TripRepository متطابق
  حرفياً بين التوأمين

## الراكب
RideCubit بستّ مراحل + RidePage وثمانية ويدجت: الخريطة · دبّوس المنتصف ·
المخطّط · الخط الزمني · اختيار النوع · التأكيد · البحث عن سائق · الرحلة.

## السائق
DutyCubit بأربع مراحل + DutyPage: مفتاح الاتصال · العرض بمؤقّت · الرحلة.

## قرارات تحمي الأداء والصحّة
- GeoPoint مستقلّ عن حزمة الخريطة؛ intaleq_maps مستوردة في ملف واحد فقط
  لكل تطبيق. تبديل محرّك الخريطة لا يلمس منطقاً
- الخريطة خارج BlocBuilder الورقة ولها BlocSelector خاص: نبضة موقع كل 25م
  لا تعيد بناء الشاشة
- مهلة بحث محليّة 90ث + استطلاع كل 5ث: الخادم قد لا يُطلق expired/no_drivers
  أبداً (ثغرة R1)، وانتظار حدث قد لا يصل = شاشة بحث أبدية
- offeredDrivers == 0 جواب نهائي فوري بلا انتظار المهلة
- الأجرة لا تُعرض إلا إن كانت صالحة: /tariff/quote يرجّع 200 بقيم null صامتة
- موقع السائق بمسارين عمداً: REST لمطابقة Redis والسوكت لخريطة الراكب

flutter analyze نظيف · الراكب 70 ملف/5,294 سطر · السائق 67 ملف/4,771 سطر.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-08-04 23:33:56 +03:00
co-authored by Claude Opus 5
parent c52338f3bb
commit 1156299d09
59 changed files with 4652 additions and 181 deletions
+75 -15
View File
@@ -1,32 +1,25 @@
{
"@@locale": "ar",
"appTitle": "Tripz",
"actionRetry": "أعد المحاولة",
"actionCancel": "إلغاء",
"actionContinue": "متابعة",
"actionSave": "حفظ",
"actionOpenSettings": "افتح الإعدادات",
"errorGeneric": "حدث خطأ غير متوقّع",
"errorNetwork": "تعذّر الاتصال بالخادم",
"errorTimeout": "انتهت مهلة الاتصال",
"emptyDefault": "لا يوجد شيء هنا بعد",
"splashRider": "تطبيق الراكب",
"splashDriver": "تطبيق السائق",
"agreementTitle": "شروط الاستخدام",
"agreementLead": "قبل أن نبدأ، اقرأ الشروط ووافق عليها.",
"agreementCheckbox": "قرأت الشروط وأوافق عليها",
"agreementAccept": "أوافق وأتابع",
"permissionTitle": "نحتاج موقعك",
"permissionLead": "الموقع هو ما يجعل الرحلة ممكنة: به نعرف أين أنت وأين السائق.",
"permissionAllow": "السماح بالوصول للموقع",
"permissionDeniedForever": "رفضتَ الإذن نهائياً. افتح الإعدادات وفعّله يدوياً ثم عُد.",
"phoneTitle": "أهلاً بك، كابتن",
"phoneLead": "أدخل رقم هاتفك، ونرسل لك رمز تحقّق.",
"phoneLabel": "رقم الهاتف",
@@ -35,27 +28,35 @@
"phoneErrEmpty": "أدخل رقم هاتفك",
"phoneErrLeadingZero": "أدخل الرقم بلا الصفر في البداية",
"phoneErrShort": "الرقم قصير جداً",
"otpTitle": "أدخل رمز التحقّق",
"otpLead": "أرسلنا رمزاً من أربع خانات إلى {phone}",
"@otpLead": { "placeholders": { "phone": { "type": "String" } } },
"@otpLead": {
"placeholders": {
"phone": {
"type": "String"
}
}
},
"otpVerify": "تحقّق وتابع",
"otpResend": "إعادة إرسال الرمز",
"otpResendIn": "إعادة الإرسال بعد {seconds} ثانية",
"@otpResendIn": { "placeholders": { "seconds": { "type": "int" } } },
"@otpResendIn": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"otpErrIncomplete": "أدخل الرمز كاملاً",
"otpErrInvalid": "الرمز غير صحيح أو انتهت صلاحيته",
"otpErrTooManyAttempts": "محاولات كثيرة. اطلب رمزاً جديداً.",
"otpErrRateLimited": "طلبتَ الرمز مرات كثيرة. انتظر قليلاً ثم أعد المحاولة.",
"otpChangeNumber": "تعديل الرقم",
"profileTitle": "أكمل ملفك",
"profileLead": "اسمك يظهر للراكب عند الرحلة.",
"profileNameLabel": "الاسم",
"profileNameErrEmpty": "أدخل اسمك",
"sessionExpired": "انتهت جلستك. سجّل الدخول من جديد.",
"settingsTitle": "الإعدادات",
"settingsTheme": "المظهر",
"settingsThemeSystem": "حسب النظام",
@@ -63,5 +64,64 @@
"settingsThemeDark": "داكن",
"settingsLanguage": "اللغة",
"settingsLanguageArabic": "العربية",
"settingsLanguageEnglish": "English"
}
"settingsLanguageEnglish": "English",
"rideWhereTo": "إلى أين؟",
"rideOrigin": "من",
"rideDestination": "إلى",
"rideSearchHint": "ابحث عن مكان",
"rideAddStop": "إضافة محطة",
"rideStop": "محطة {n}",
"@rideStop": {
"placeholders": {
"n": {
"type": "int"
}
}
},
"ridePickOnMap": "تحديد من الخريطة",
"rideConfirmPoint": "تأكيد هذه النقطة",
"rideConfirmTrip": "اطلب الرحلة",
"rideSearchingTitle": "نبحث لك عن سائق",
"rideSearchingLead": "عادةً أقل من دقيقة",
"rideCancelSearch": "إلغاء البحث",
"rideNoDrivers": "لا يوجد سائقون متاحون الآن. جرّب بعد قليل.",
"rideQuoteFailed": "تعذّر حساب الأجرة",
"rideRouteFailed": "تعذّر حساب المسار",
"rideRequestFailed": "تعذّر إرسال الطلب",
"rideSurge": "الطلب مرتفع الآن",
"rideCancelTrip": "إلغاء الرحلة",
"rideEta": "{min} دقيقة",
"@rideEta": {
"placeholders": {
"min": {
"type": "String"
}
}
},
"rideDistanceKm": "{km} كم",
"@rideDistanceKm": {
"placeholders": {
"km": {
"type": "String"
}
}
},
"tripAssigned": "السائق في الطريق إليك",
"tripDriverArriving": "السائق يقترب",
"tripDriverArrived": "السائق وصل وينتظرك",
"tripInProgress": "في الطريق إلى وجهتك",
"tripCompleted": "وصلت — رحلة موفّقة",
"driverGoOnline": "ابدأ الاستقبال",
"driverGoOffline": "إيقاف الاستقبال",
"driverOnline": "متصل",
"driverOffline": "غير متصل",
"driverNoOffers": "لا طلبات الآن. ابقَ متصلاً.",
"driverOfferTitle": "طلب رحلة جديد",
"driverAccept": "قبول",
"driverReject": "تجاهل",
"driverOfferTaken": "سبقك سائق آخر لهذا الطلب",
"driverArrivedAction": "وصلت لموقع الراكب",
"driverStartTrip": "بدء الرحلة",
"driverEndTrip": "إنهاء الرحلة",
"driverOnTheWay": "في الطريق للراكب"
}
+75 -15
View File
@@ -1,32 +1,25 @@
{
"@@locale": "en",
"appTitle": "Tripz",
"actionRetry": "Try again",
"actionCancel": "Cancel",
"actionContinue": "Continue",
"actionSave": "Save",
"actionOpenSettings": "Open settings",
"errorGeneric": "Something went wrong",
"errorNetwork": "Could not reach the server",
"errorTimeout": "The connection timed out",
"emptyDefault": "Nothing here yet",
"splashRider": "Rider app",
"splashDriver": "Driver app",
"agreementTitle": "Terms of use",
"agreementLead": "Before we start, read the terms and accept them.",
"agreementCheckbox": "I have read and accept the terms",
"agreementAccept": "Accept and continue",
"permissionTitle": "We need your location",
"permissionLead": "Location is what makes a trip possible: it tells us where you are and where your driver is.",
"permissionAllow": "Allow location access",
"permissionDeniedForever": "You denied the permission permanently. Open settings, enable it, then come back.",
"phoneTitle": "Welcome, captain",
"phoneLead": "Enter your phone number and we'll send you a verification code.",
"phoneLabel": "Phone number",
@@ -35,27 +28,35 @@
"phoneErrEmpty": "Enter your phone number",
"phoneErrLeadingZero": "Enter the number without the leading zero",
"phoneErrShort": "That number is too short",
"otpTitle": "Enter your code",
"otpLead": "We sent a 4-digit code to {phone}",
"@otpLead": { "placeholders": { "phone": { "type": "String" } } },
"@otpLead": {
"placeholders": {
"phone": {
"type": "String"
}
}
},
"otpVerify": "Verify and continue",
"otpResend": "Resend the code",
"otpResendIn": "Resend in {seconds}s",
"@otpResendIn": { "placeholders": { "seconds": { "type": "int" } } },
"@otpResendIn": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"otpErrIncomplete": "Enter the full code",
"otpErrInvalid": "That code is wrong or expired",
"otpErrTooManyAttempts": "Too many attempts. Request a new code.",
"otpErrRateLimited": "You requested the code too many times. Wait a moment and try again.",
"otpChangeNumber": "Change number",
"profileTitle": "Complete your profile",
"profileLead": "Your name is shown to the rider during a trip.",
"profileNameLabel": "Name",
"profileNameErrEmpty": "Enter your name",
"sessionExpired": "Your session ended. Please sign in again.",
"settingsTitle": "Settings",
"settingsTheme": "Appearance",
"settingsThemeSystem": "Follow system",
@@ -63,5 +64,64 @@
"settingsThemeDark": "Dark",
"settingsLanguage": "Language",
"settingsLanguageArabic": "العربية",
"settingsLanguageEnglish": "English"
}
"settingsLanguageEnglish": "English",
"rideWhereTo": "Where to?",
"rideOrigin": "From",
"rideDestination": "To",
"rideSearchHint": "Search for a place",
"rideAddStop": "Add a stop",
"rideStop": "Stop {n}",
"@rideStop": {
"placeholders": {
"n": {
"type": "int"
}
}
},
"ridePickOnMap": "Pick on the map",
"rideConfirmPoint": "Confirm this point",
"rideConfirmTrip": "Request ride",
"rideSearchingTitle": "Finding you a driver",
"rideSearchingLead": "Usually under a minute",
"rideCancelSearch": "Cancel search",
"rideNoDrivers": "No drivers available right now. Try again shortly.",
"rideQuoteFailed": "Could not price this trip",
"rideRouteFailed": "Could not build the route",
"rideRequestFailed": "Could not send the request",
"rideSurge": "Demand is high right now",
"rideCancelTrip": "Cancel ride",
"rideEta": "{min} min",
"@rideEta": {
"placeholders": {
"min": {
"type": "String"
}
}
},
"rideDistanceKm": "{km} km",
"@rideDistanceKm": {
"placeholders": {
"km": {
"type": "String"
}
}
},
"tripAssigned": "Your driver is on the way",
"tripDriverArriving": "Your driver is close",
"tripDriverArrived": "Your driver is waiting",
"tripInProgress": "On the way to your destination",
"tripCompleted": "You've arrived",
"driverGoOnline": "Go online",
"driverGoOffline": "Go offline",
"driverOnline": "Online",
"driverOffline": "Offline",
"driverNoOffers": "No requests yet. Stay online.",
"driverOfferTitle": "New ride request",
"driverAccept": "Accept",
"driverReject": "Dismiss",
"driverOfferTaken": "Another driver took this request",
"driverArrivedAction": "I've arrived",
"driverStartTrip": "Start trip",
"driverEndTrip": "End trip",
"driverOnTheWay": "Heading to rider"
}