الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع. - N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر. N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)، ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird. فلاتر: - docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام). - السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب. - طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط. - الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 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. الخرائط
flutter_map+ بلاطات انطلق — لاgoogle_maps_flutter.- البلاطات: التطبيق → انطلق مباشرة (حجم كبير، بلا أسرار).
- الجيوكودنغ/المسارات/الأماكن: التطبيق → سيرفرنا → انطلق (يحمي المفتاح ويكيّش ويقيس الحصص).
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 |
| الخرائط | flutter_map + latlong2 |
| التخزين الآمن | 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وبتسجيل مشروط - النصوص عربية وعبر طبقة الترجمة
- لا استيراد متقاطع بين الميزات
- السيرفر يفرض ما يخفيه التطبيق (لا حماية بالواجهة وحدها)