Files
tripz-llc/docs/23-flutter-conventions.md
T
Hamza-AyedandClaude Fable 5 de4bbd00ac feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- 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>
2026-07-18 13:06:46 +03:00

12 KiB

23 — قانون تطبيقات فلاتر (الراكب والسائق)

هذا المستند مُلزِم. كل كود Dart في apps/rider وapps/driver يتبعه. أي انحراف يُصحَّح لا يُبرَّر. اقرأه كاملاً قبل كتابة أول سطر في أي جلسة جديدة — هو العقد الذي يجعل جلستين مختلفتين تُنتجان نفس البنية.

آخر تحديث: 2026-07-18. ذو صلة: 22 §1.5 (الطبقات) · 19 (الاستحقاقات) · tripz-mobile-store-identity.


0. المبادئ الخمسة (إن تعارض شيء معها فهي تُرجَّح)

  1. التطبيقان توأمان في البنية، مختلفان في الميزات. نفس المجلدات ونفس التسميات ونفس طبقة الشبكة. من يعرف الراكب يعرف السائق فوراً.
  2. السيرفر هو مصدر الحقيقة. التطبيق يعرض ما يُعطى. لا منطق تسعير ولا حساب عمولة ولا قرار مطابقة في Dart — أبداً.
  3. الحالة صريحة، لا سحر. Cubit افتراضاً، Bloc عند تعدّد الأحداث. لا GetX ولا متغيّرات عامة قابلة للتغيير.
  4. الميزة تُقفل من نقطة واحدة (تسجيل الموديول)، لا بـif مبعثرة في الشاشات.
  5. الأصيل مُعاد استعماله لا مُعاد بناؤه، وهو موحّد عبر كل المستأجرين (شرط 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 في أي مكان آخر.

الاعتراضات المطلوبة، بهذا الترتيب:

  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)

// ✅ الصحيح — 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 وبتسجيل مشروط
  • النصوص عربية وعبر طبقة الترجمة
  • لا استيراد متقاطع بين الميزات
  • السيرفر يفرض ما يخفيه التطبيق (لا حماية بالواجهة وحدها)