إصلاح انحدار أدخلتُه: /admin/overview كان يستعلم عن `t.created_at` وهو غير موجود في كيان الرحلة (الاسم `requested_at`)، فترجع النقطة 500 ويُفرَّغ جدول المستأجرين في لوحة السوبر-أدمن. فحص الأنواع لم يمسكه لأن استعلامات QueryBuilder نصوص، والمحكّ الذي كان سيمسكه لم يُشغَّل. قرارات المالك 2026-07-18: - الخرائط: `intaleq_maps` حصراً (SDK انطلق على MapLibre). تصحيح قرار سابق خاطئ: flutter_map + latlong2 طبقة منافسة تعطي نوعَي LatLng متضاربين. السبب ليس عمل جوجل في سوريا — بل قِدَم بياناتها في المنطقة. - docs/24 جديد: فلسفة المال — فصل الإيراد (شحن السائق · رسوم العمليات) عن الأمانة (شحن الراكب)، بمحفظتين لا بحقل حالة، ودفتر مضاف فقط. - P موسّعة (بوابات · تسوية بالرسائل لكليك/شام كاش · رسوم بالدولة · تقارير) و O5 (ترتيب المهام المجدولة)، وتأجيل Android Auto/CarPlay. - الحزم: نسخ سيرو الأحدث + live_activities لشاشة القفل في iOS. تنظيف: إخراج 463 ملف بناء NDK من التتبّع، وإخراج شهادات التوقيع ومفتاح App Store من المستودع (تُولَّد من جديد عند الحاجة) مع تحديث .gitignore. الملفات باقية على القرص — أُزيلت من الفهرس فقط. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
23 — قانون تطبيقات فلاتر (الراكب والسائق)
هذا المستند مُلزِم. كل كود Dart في
apps/riderوapps/driverيتبعه. أي انحراف يُصحَّح لا يُبرَّر. اقرأه كاملاً قبل كتابة أول سطر في أي جلسة جديدة — هو العقد الذي يجعل جلستين مختلفتين تُنتجان نفس البنية.
آخر تحديث: 2026-07-18. ذو صلة: 22 §1.5 (الطبقات) · 19 (الاستحقاقات) · tripz-mobile-store-identity.
0. المبادئ الخمسة (إن تعارض شيء معها فهي تُرجَّح)
- التطبيقان توأمان في البنية، مختلفان في الميزات. نفس المجلدات ونفس التسميات ونفس طبقة الشبكة. من يعرف الراكب يعرف السائق فوراً.
- السيرفر هو مصدر الحقيقة. التطبيق يعرض ما يُعطى. لا منطق تسعير ولا حساب عمولة ولا قرار مطابقة في Dart — أبداً.
- الحالة صريحة، لا سحر. Cubit افتراضاً، Bloc عند تعدّد الأحداث. لا GetX ولا متغيّرات عامة قابلة للتغيير.
- الميزة تُقفل من نقطة واحدة (تسجيل الموديول)، لا بـ
ifمبعثرة في الشاشات. - الأصيل مُعاد استعماله لا مُعاد بناؤه، وهو موحّد عبر كل المستأجرين (شرط Shorebird).
1. بنية المجلدات — مُلزِمة حرفياً
apps/<rider|driver>/lib/
├── main.dart # نقطة الدخول: تهيئة ثم runApp — لا منطق
├── app.dart # MaterialApp + الثيم + الراوتر + المزوّدون العامّون
├── core/
│ ├── build_config.dart # ⚠️ مُولَّد — لا يُحرَّر (N3)
│ ├── config.dart # إعداد وقت التشغيل (--dart-define)
│ ├── di.dart # get_it: تسجيل التبعيات
│ ├── router.dart # go_router: كل المسارات
│ ├── theme.dart # الثيم من ألوان المستأجر
│ ├── api/
│ │ ├── api_client.dart # Dio + الاعتراضات (توكن · مستأجر · توقيع · جهاز)
│ │ └── api_exception.dart
│ └── storage/
│ └── token_store.dart # تخزين آمن للتوكنات
└── features/<feature>/
├── cubit/ # <feature>_cubit.dart + <feature>_state.dart
├── data/ # <feature>_repository.dart + models/
└── view/ # الشاشات + widgets/
قواعد لا تُخالَف:
- الميزة لا تستورد من ميزة أخرى. التشارك يصعد إلى
core/أو إلى موديول مشترك. استيرادfeatures/trip/…من داخلfeatures/auth/…ممنوع. view/لا يستوردdata/مباشرة — يمرّ بالـCubit دائماً.- ملف واحد = صنف عام واحد.
- لا مجلد
utils/عام (مقبرة الشيفرة). الدالة تعيش قرب ما تخدمه.
2. التسمية
| العنصر | القاعدة | مثال |
|---|---|---|
| ملفات | snake_case.dart |
trip_repository.dart |
| أصناف | PascalCase |
TripCubit |
| الميزة (مجلد) | مفرد، إنجليزي | features/trip/ |
| الحالة | <Feature>State بحقل status |
TripState |
| الاستحقاق | Features.<camelCase> |
Features.marketIntel |
النصوص المرئية للمستخدم عربية؛ أسماء الرموز والملفات إنجليزية. لا نصّ عربي مكتوب داخل widget مباشرة — يمرّ بطبقة الترجمة.
3. إدارة الحالة
Cubit افتراضاً. Bloc فقط حين تتعدّد الأحداث الواردة على نفس الحالة (مثل شاشة الرحلة الحيّة: سوكت + موقع + إجراء المستخدم معاً).
قواعد:
- الحالة غير قابلة للتغيير (
copyWith)، وتحملstatusمنenum— لاbool isLoadingمتناثرة. - الـCubit لا يعرف Flutter: لا
BuildContext، لاSnackBar، لا تنقّل. يُصدر حالة، والواجهة تتصرّف. - الـCubit لا ينادي
Dioمباشرة — يمرّ بـRepositoryدائماً. - خطأ الشبكة يتحوّل إلى رسالة عربية داخل الـCubit، والواجهة تعرض
state.errorكما هو.
// النمط المعتمد
enum TripStatus { idle, loading, active, error }
class TripState {
final TripStatus status;
final Trip? trip;
final String? error;
const TripState({this.status = TripStatus.idle, this.trip, this.error});
TripState copyWith({...}) => ...;
}
4. طبقة الشبكة — نقطة واحدة إجبارياً
كل نداء يمرّ بـcore/api/api_client.dart. لا http.get في أي مكان آخر.
الاعتراضات المطلوبة، بهذا الترتيب:
x-tenant-id=BuildConfig.tenantSlug.Authorization: BearerمنTokenStore.x-device-id— بصمة الجهاز (يفرضها السيرفر عندAUTH_REQUIRE_DEVICE_BINDING=true، docs/17 D2).- توقيع HMAC للنقاط المالية (docs/17 I6) — يُحسب على الجسم كما يُرسَل بلا إعادة تسلسل، وإلا فشل توقيع سليم.
- تجديد التوكن عند 401 مرة واحدة ثم تسجيل خروج — لا حلقة تجديد لانهائية.
⚠️ التوقيع وربط الجهاز مطفآن على السيرفر الآن ويُفعَّلان بعد أن يرسلهما التطبيق. رتّب: أرسل أولاً، ثم فعّل السيرفر.
التخزين: التوكنات في flutter_secure_storage حصراً — لا SharedPreferences للتوكن.
5. أعلام الميزات (القلب — راجع docs/22 §1.5)
// ✅ الصحيح — const، يُحذف الكود من الـbinary
if (Features.marketIntel) { ... }
// ❌ ممنوع — يُبقي كود الميزة في الملف فيمكن استخراجه
if (featureMap['market_intel'] == true) { ... }
التسجيل المشروط هو القاعدة، والفحص المتناثر استثناء:
// core/di.dart — نقطة الإقفال الوحيدة
if (Features.chat) {
getIt.registerLazySingleton<ChatRepository>(() => ChatRepository(getIt()));
}
// core/router.dart
if (Features.chat) routes.add(GoRoute(path: '/chat', builder: ...));
القاعدة الحاكمة: الفعّال = BuildConfig ∩ المانيفست الريموت ∩ استحقاقات السيرفر.
التطبيق لا يمنح صلاحية — يخفي فقط. من فكّك التطبيق وفعّل كل شيء يصطدم بـ403 من FeatureGuard.
6. الخرائط — intaleq_maps حصراً
القرار (المالك، 2026-07-18، مصحَّح): تُستعمل حزمة intaleq_maps (SDK انطلق، إصدار 2.2.0، مبنية على MapLibre GL) — وهي حزمة المالك نفسه ومنشورة على pub.dev.
⚠️ تصحيح قرار سابق: كان مكتوباً «
flutter_map+ بلاطات انطلق». هذا خطأ:flutter_mapوlatlong2طبقة منافسة تُعرّفLatLngخاصاً بها، فيجتمع في المشروع نوعان بنفس الاسم.intaleq_mapsتُصدِّرLatLngوMarkerوPolylineوCameraUpdateبنفسها كبديل drop-in لـgoogle_maps_flutter. لا يُضافflutter_mapولاlatlong2إطلاقاً.
لماذا انطلق لا جوجل (تصحيح فهم شائع): خرائط جوجل تعمل في سوريا كـAPI وكرندر؛ غير العامل جزئياً هو تطبيق جوجل الرسمي — وهذا لا يعنينا لأن التوجيه داخل تطبيقنا. السبب الحقيقي أن بيانات جوجل في المنطقة قديمة (لم تُحدَّث منذ 2011)، بينما انطلق مُحدَّثة وبمستوى تجاري وأدقّ ميدانياً.
الخريطة كواجهة تأتي من الحزمة؛ ويبقى ما هو سيرفري على حاله: الجيوكودنغ والمسارات والأماكن تمرّ عبر سيرفرنا (يحمي المفتاح ويكيّش ويقيس الحصص لكل مستأجر).
مفتاح الخريطة يأتي من البيئة/إعداد المستأجر — لا يُكتب في الكود ولا في المستودع.
7. ما يُعاد استعماله من التطبيق القديم — لا يُعاد بناؤه
| المكوّن | لماذا |
|---|---|
android/ · ios/ |
تحمل هوية النشر والصلاحيات والتوقيع |
secure_string_operations · trip_overlay_plugin · bubble-master |
إضافات أصيلة مكتوبة ومختبَرة |
firebase.json · firebase_options.dart · google-services.json |
مشروع FCM القائم |
shorebird.yaml |
app_id منشور — تغييره يقطع تحديثات OTA عن المستخدمين الحاليين |
key/ |
شهادات توقيع — فقدانها يعني تعذّر تحديث التطبيق المنشور نهائياً |
| الخطوط (mohanad · josefin · digit) | هوية بصرية قائمة |
معرّفات المتجر في tripz-mobile-store-identity — تُنسخ ولا تُكتب من الذاكرة.
⚠️ معرّفا السائق متضاربان بين المنصّتين (com.sefer_driver مقابل com.sefer.driver) — يؤكّدهما المالك قبل أي إصدار.
8. الطبقة الأصيلة موحّدة
لا تُفرَّع android/ أو ios/ بين المستأجرين أو الطبقات. المتغيّر الوحيد المسموح آلياً: applicationId · PRODUCT_BUNDLE_IDENTIFIER · android:label · الأيقونات — وكلها يضبطها scripts/generate-tenant-app.sh.
السبب: Shorebird يرقّع Dart فقط. أصيلٌ موحّد ⇒ ترقية الباقة patch فوري. أصيلٌ متفرّع ⇒ كل ترقية إصدار متجر ومراجعة.
9. الحزم المعتمدة
| الغرض | الحزمة |
|---|---|
| الحالة | flutter_bloc |
| الحقن | get_it |
| الشبكة | dio |
| التنقّل | go_router |
| الخرائط | intaleq_maps (لا flutter_map ولا latlong2 ولا google_maps_flutter) |
| التخزين الآمن | flutter_secure_storage |
| الزمن الحقيقي | socket_io_client |
| الإشعارات | firebase_messaging + flutter_local_notifications |
إدخال حزمة خارج هذا الجدول قرارٌ يُوثَّق هنا أولاً. تُثبَّت أحدث الإصدارات المستقرّة، ويُشغَّل flutter pub outdated قبل كل مجموعة عمل.
10. الفروق المشروعة بين التطبيقين
| الراكب | السائق | |
|---|---|---|
| الميزة المحورية | طلب رحلة وتتبّعها | استقبال العروض وتنفيذها |
| الموقع | عند الاستعمال | خلفية دائمة + متحكّم مواقع |
| الأصيل الخاص | — | overlay العرض · bubble · حيوية الوجه |
| المال | الدفع | المحفظة · الرصيد · السحب |
متحكّم موقع السائق (نمط سيرو، مثبت عملياً): تسجيل كل ٣ ثوانٍ · رفع دفعات كل دقيقتين · وعيٌ بالبطارية (تخفيف عند ٢٠٪) · سوكت للبثّ الحي ودفعات للحفظ الدائم. لا تُعِد اختراعه.
11. قواعد العمل اليومية
- لا بناء ولا اختبار على الماك — يُشغّلهما المالك (راجع no-build-or-test-on-mac). الوكيل يكتب فقط.
- التعليقات تشرح لماذا لا ماذا، وبالعربية.
- ملف
build_config.dartمُولَّد: تعديله يُمحى عند أول توليد. - كل ميزة جديدة تبدأ بسؤال: هل لها علم استحقاق؟ إن كانت مدفوعة فالعلم أولاً، ثم الكود.
12. قائمة تحقّق قبل اعتبار ميزة «منجزة»
- الحالة عبر Cubit/Bloc، غير قابلة للتغيير، بـ
statusمن enum - الـCubit بلا
BuildContextوبلاDioمباشر - كل النداءات عبر
ApiClient - الميزة المدفوعة محروسة بـ
Features.xوبتسجيل مشروط - النصوص عربية وعبر طبقة الترجمة
- لا استيراد متقاطع بين الميزات
- السيرفر يفرض ما يخفيه التطبيق (لا حماية بالواجهة وحدها)
13. الاختبار على بناء release لا debug ⚠️
حذف كود الميزة المقفلة يحدث في بناء release/AOT فقط. في debug (JIT) يبقى الكود كله موجوداً ولو كان العلم const false.
فالتحقّق من أن نسخة lite لا تحتوي فعلاً على كود النسخ الأعلى لا يصحّ إلا على بناء release — وأي فحص على debug يعطي نتيجة مضلِّلة. القياس المعتمد: بناء release لكل طبقة ومقارنة الحجم + تفتيش الرموز.
14. مؤجَّل بقرار المالك (2026-07-18)
- Android Auto (موجود في سيرو:
MyCarSession·MyCarScreen·MyCarAppService) وCarPlay — عليهما مشاكل ولا أولوية لهما الآن. يُنظر فيهما لاحقاً.