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>
This commit is contained in:
Hamza-Ayed
2026-07-18 13:06:46 +03:00
co-authored by Claude Fable 5
parent 3ab74a13a8
commit de4bbd00ac
271 changed files with 1333 additions and 81068 deletions
@@ -23,7 +23,7 @@
<application
android:label="Tripz"
android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:usesCleartextTraffic="false"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
+79 -3
View File
@@ -1,12 +1,24 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../config.dart';
import '../storage/token_store.dart';
/// عميل HTTP لكل نداءات الـ API — يحقن x-tenant-id و Bearer تلقائياً.
/// عميل HTTP لكل نداءات الـAPI (docs/23 §4).
///
/// **نقطة الشبكة الوحيدة** في التطبيق: لا `http.get` ولا `Dio` آخر في أي
/// مكان. السبب أن كل ما يفرضه السيرفر (المستأجر · التوكن · بصمة الجهاز ·
/// التوقيع) يُحقن هنا مرة واحدة؛ نداءٌ يلتفّ حول هذا الملف هو نداء بلا
/// مصادقة يفشل عند تفعيل الحراسات على السيرفر.
class ApiClient {
final Dio dio;
final TokenStore _store;
ApiClient(TokenStore store)
/// بصمة الجهاز (docs/17 — D2). تُحسب مرة وتُخزَّن: قراءتها من النظام في كل
/// طلب استدعاءُ قناة أصيلة بلا داعٍ.
static String? _deviceId;
ApiClient(this._store)
: dio = Dio(BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
@@ -16,12 +28,76 @@ class ApiClient {
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
options.headers['x-tenant-id'] = AppConfig.tenantSlug;
final token = await store.access();
final device = await _resolveDeviceId();
if (device != null) options.headers['x-device-id'] = device;
final token = await _store.access();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (e, handler) async {
// تجديد التوكن عند 401 — **مرة واحدة**. بلا علامة `_retried` يعيد
// الطلبُ الفاشل تجديداً يفشل فيعيد… حلقة لانهائية تستنزف البطارية
// وتغرق السيرفر عند انتهاء الجلسة فعلاً.
final isAuthError = e.response?.statusCode == 401;
final alreadyRetried = e.requestOptions.extra['_retried'] == true;
final isRefreshCall = e.requestOptions.path.contains('/auth/refresh');
if (!isAuthError || alreadyRetried || isRefreshCall) {
return handler.next(e);
}
final refreshed = await _tryRefresh();
if (!refreshed) {
await _store.clear(); // الجلسة انتهت فعلاً — لا نُبقي توكناً ميتاً
return handler.next(e);
}
try {
e.requestOptions.extra['_retried'] = true;
final retry = await dio.fetch(e.requestOptions);
return handler.resolve(retry);
} on DioException catch (err) {
return handler.next(err);
}
},
));
}
Future<bool> _tryRefresh() async {
final rt = await _store.refresh();
if (rt == null || rt.isEmpty) return false;
try {
// Dio نظيف: المرور بالعميل نفسه يُدخل التوكن المنتهي في الترويسة.
final plain = Dio(BaseOptions(baseUrl: AppConfig.baseUrl));
final r = await plain.post('/auth/refresh', data: {'refresh_token': rt});
await _store.save(
r.data['access_token'] as String,
(r.data['refresh_token'] ?? rt) as String,
);
return true;
} catch (_) {
return false;
}
}
static Future<String?> _resolveDeviceId() async {
if (_deviceId != null) return _deviceId;
try {
final info = DeviceInfoPlugin();
if (Platform.isAndroid) {
_deviceId = (await info.androidInfo).id;
} else if (Platform.isIOS) {
_deviceId = (await info.iosInfo).identifierForVendor;
}
} catch (_) {
// تعذّر قراءة الجهاز: نُرسل بلا الترويسة بدل إسقاط الطلب. السيرفر
// يرفض عند تفعيل الربط، وهذا أوضح من فشل صامت هنا.
_deviceId = null;
}
return _deviceId;
}
}
+12 -3
View File
@@ -1,15 +1,24 @@
/// إعداد التطبيق. يُمرَّر عبر --dart-define للنكهات (flavor) لاحقاً.
/// إعداد وقت التشغيل — يُمرَّر عبر --dart-define عند البناء.
///
/// ما يخصّ **المستأجر** (اسمه، ألوانه، أعلام ميزاته) لا يوضع هنا: مكانه
/// `build_config.dart` المُولَّد من المانيفست (docs/22 §1.5). هذا الملف
/// للإعداد التقني المشترك بين كل المستأجرين.
class AppConfig {
/// الـAPI عبر TLS حصراً (docs/20). المنفذ الخام مغلق على مستوى الشبكة،
/// فأي عودة إلى http تعني تطبيقاً لا يتصل أصلاً.
static const String baseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'http://194.163.173.157:4010/api',
defaultValue: 'https://tripz-api.intaleqapp.com/api',
);
/// slug المستأجر (يُبنى لكل نكهة). حالياً: siro.
/// slug المستأجر. الافتراض للتطوير؛ البناء المولَّد يمرّره صراحةً.
static const String tenantSlug = String.fromEnvironment(
'TENANT',
defaultValue: 'siro',
);
static const String appName = 'Tripz';
/// دور هذا التطبيق — يميّز تطبيق الراكب عن السائق عند التسجيل.
static const String appRole = 'rider';
}
+9 -7
View File
@@ -5,7 +5,7 @@ publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.0.5 <4.0.0"
sdk: ">=3.5.0 <4.0.0"
dependencies:
flutter:
@@ -14,15 +14,17 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.8
# الحالة والتوجيه والحقن (بديل GetX)
flutter_bloc: ^8.1.6
equatable: ^2.0.5
get_it: ^7.7.0
go_router: ^14.2.7
# الحالة والتوجيه والحقن (بديل GetX) — الإصدارات موحّدة مع تطبيق السائق
# عمداً (docs/23 §9): تباعدهما يعني سلوكاً مختلفاً في كود مشترك حرفياً.
flutter_bloc: ^9.1.1
equatable: ^2.0.7
get_it: ^8.0.3
go_router: ^14.6.2
# الشبكة والتخزين
dio: ^5.4.3+1
dio: ^5.7.0
flutter_secure_storage: ^9.2.2
device_info_plus: ^11.2.0 # بصمة الجهاز (x-device-id — docs/17 D2)
# الخرائط (flutter_map + انطلق/OSM — بلا Google)
flutter_map: ^7.0.2