# 23 — قانون تطبيقات فلاتر (الراكب والسائق) > **هذا المستند مُلزِم.** كل كود Dart في `apps/rider` و`apps/driver` يتبعه. أي انحراف يُصحَّح لا يُبرَّر. > اقرأه كاملاً قبل كتابة أول سطر في أي جلسة جديدة — هو العقد الذي يجعل جلستين مختلفتين تُنتجان نفس البنية. آخر تحديث: 2026-07-18. ذو صلة: [22 §1.5](22-full-product-roadmap.md) (الطبقات) · [19](19-entitlements-licensing.md) (الاستحقاقات) · [[tripz-mobile-store-identity]]. --- ## 0. المبادئ الخمسة (إن تعارض شيء معها فهي تُرجَّح) 1. **التطبيقان توأمان في البنية، مختلفان في الميزات.** نفس المجلدات ونفس التسميات ونفس طبقة الشبكة. من يعرف الراكب يعرف السائق فوراً. 2. **السيرفر هو مصدر الحقيقة.** التطبيق يعرض ما يُعطى. لا منطق تسعير ولا حساب عمولة ولا قرار مطابقة في Dart — أبداً. 3. **الحالة صريحة، لا سحر.** Cubit افتراضاً، Bloc عند تعدّد الأحداث. لا GetX ولا متغيّرات عامة قابلة للتغيير. 4. **الميزة تُقفل من نقطة واحدة** (تسجيل الموديول)، لا بـ`if` مبعثرة في الشاشات. 5. **الأصيل مُعاد استعماله لا مُعاد بناؤه**، وهو **موحّد عبر كل المستأجرين** (شرط Shorebird). --- ## 1. بنية المجلدات — مُلزِمة حرفياً ``` apps//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// ├── cubit/ # _cubit.dart + _state.dart ├── data/ # _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/` | | الحالة | `State` بحقل `status` | `TripState` | | الاستحقاق | `Features.` | `Features.marketIntel` | **النصوص المرئية للمستخدم عربية**؛ أسماء الرموز والملفات إنجليزية. لا نصّ عربي مكتوب داخل widget مباشرة — يمرّ بطبقة الترجمة. ## 3. إدارة الحالة **Cubit افتراضاً.** Bloc فقط حين تتعدّد الأحداث الواردة على نفس الحالة (مثل شاشة الرحلة الحيّة: سوكت + موقع + إجراء المستخدم معاً). قواعد: - الحالة **غير قابلة للتغيير** (`copyWith`)، وتحمل `status` من `enum` — لا `bool isLoading` متناثرة. - الـCubit **لا يعرف Flutter**: لا `BuildContext`، لا `SnackBar`، لا تنقّل. يُصدر حالة، والواجهة تتصرّف. - الـCubit لا ينادي `Dio` مباشرة — يمرّ بـ`Repository` دائماً. - خطأ الشبكة يتحوّل إلى رسالة عربية **داخل الـCubit**، والواجهة تعرض `state.error` كما هو. ```dart // النمط المعتمد 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` في أي مكان آخر. الاعتراضات المطلوبة، بهذا الترتيب: 1. `x-tenant-id` = `BuildConfig.tenantSlug`. 2. `Authorization: Bearer` من `TokenStore`. 3. `x-device-id` — بصمة الجهاز (يفرضها السيرفر عند `AUTH_REQUIRE_DEVICE_BINDING=true`، docs/17 D2). 4. **توقيع HMAC** للنقاط المالية (docs/17 I6) — يُحسب على الجسم **كما يُرسَل** بلا إعادة تسلسل، وإلا فشل توقيع سليم. 5. تجديد التوكن عند 401 **مرة واحدة** ثم تسجيل خروج — لا حلقة تجديد لانهائية. > ⚠️ التوقيع وربط الجهاز مطفآن على السيرفر الآن ويُفعَّلان **بعد** أن يرسلهما التطبيق. رتّب: أرسل أولاً، ثم فعّل السيرفر. **التخزين**: التوكنات في `flutter_secure_storage` حصراً — لا `SharedPreferences` للتوكن. ## 5. أعلام الميزات (القلب — راجع docs/22 §1.5) ```dart // ✅ الصحيح — const، يُحذف الكود من الـbinary if (Features.marketIntel) { ... } // ❌ ممنوع — يُبقي كود الميزة في الملف فيمكن استخراجه if (featureMap['market_intel'] == true) { ... } ``` **التسجيل المشروط هو القاعدة**، والفحص المتناثر استثناء: ```dart // core/di.dart — نقطة الإقفال الوحيدة if (Features.chat) { getIt.registerLazySingleton(() => 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** — عليهما مشاكل ولا أولوية لهما الآن. يُنظر فيهما لاحقاً.