feat: implement splash and onboarding flows, update app assets, and add project design documentation
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 734 B |
|
Before Width: | Height: | Size: 853 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 78 KiB |
@@ -38,7 +38,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="500" height="500"/>
|
||||
<image name="LaunchImage" width="512" height="279"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../config.dart';
|
||||
|
||||
/// واجهة مباشرة مع انطلق (map-saas) — لا مرور عبر الباك إند.
|
||||
///
|
||||
/// كل نداء يمر عبر هذه النافذة: البحث، التفكيك العكسي، المسار.
|
||||
/// الباك إند يبقى مسؤولاً عن المصادقة والرحلات والمدفوعات فقط.
|
||||
class AntlaqApi {
|
||||
final String apiKey;
|
||||
final Dio _dio;
|
||||
|
||||
AntlaqApi({String? apiKey})
|
||||
: apiKey = apiKey ?? AppConfig.mapApiKey,
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: 'https://map-saas.intaleqapp.com',
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
// المفتاح يمر كترويسة `x-api-key` — map-saas يرفض أي مفتاح في
|
||||
// query (تحقّق صارم: `property api_key should not exist`).
|
||||
// المفتاح مقيّد ببصمة التطبيق (bundle id) على map-saas نفسه.
|
||||
headers: {'x-api-key': apiKey ?? AppConfig.mapApiKey},
|
||||
));
|
||||
|
||||
/// بحث أماكن — GET /api/geocoding/search.
|
||||
Future<List<dynamic>> search(String query) async {
|
||||
final r = await _dio.get('/api/geocoding/search', queryParameters: {
|
||||
'q': query,
|
||||
});
|
||||
return (r.data['results'] ?? r.data) as List;
|
||||
}
|
||||
|
||||
/// تفكيك عنوان عكسي — GET /api/geocoding/reverse.
|
||||
/// يرجّع map-saas **مصفوفة** مرتّبة بالأقرب؛ نأخذ العنصر الأول.
|
||||
Future<Map<String, dynamic>> reverse(double lat, double lng) async {
|
||||
final r = await _dio.get('/api/geocoding/reverse', queryParameters: {
|
||||
'lat': lat,
|
||||
'lng': lng,
|
||||
});
|
||||
final data = r.data;
|
||||
if (data is List) {
|
||||
if (data.isEmpty) return {'latitude': lat, 'longitude': lng};
|
||||
return Map<String, dynamic>.from(data.first as Map);
|
||||
}
|
||||
return Map<String, dynamic>.from(data as Map);
|
||||
}
|
||||
|
||||
/// حساب مسار بين نقطتين — GET /api/maps/route.
|
||||
Future<Map<String, dynamic>> route({
|
||||
required double fromLat,
|
||||
required double fromLng,
|
||||
required double toLat,
|
||||
required double toLng,
|
||||
}) async {
|
||||
final r = await _dio.get('/api/maps/route', queryParameters: {
|
||||
'fromLat': fromLat,
|
||||
'fromLng': fromLng,
|
||||
'toLat': toLat,
|
||||
'toLng': toLng,
|
||||
'alternatives': true,
|
||||
'steps': true,
|
||||
'locale': 'ar',
|
||||
});
|
||||
return r.data as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import '../config.dart';
|
||||
import '../storage/token_store.dart';
|
||||
import 'logging_interceptor.dart';
|
||||
|
||||
/// عميل HTTP لكل نداءات الـAPI (docs/23 §4).
|
||||
///
|
||||
@@ -21,15 +22,22 @@ class ApiClient {
|
||||
ApiClient(this._store)
|
||||
: dio = Dio(BaseOptions(
|
||||
baseUrl: AppConfig.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
)) {
|
||||
// Resolve device ID once in background — don't block requests
|
||||
_resolveDeviceId();
|
||||
|
||||
dio.interceptors.add(AppLoggingInterceptor());
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
options.headers['x-tenant-id'] = AppConfig.tenantSlug;
|
||||
// راكب أم سائق — هويتان منفصلتان لنفس الرقم (قرار المالك 2026-07-20 — ب).
|
||||
options.headers['x-app-role'] = AppConfig.appRole;
|
||||
|
||||
final device = await _resolveDeviceId();
|
||||
// Device ID is cached after first call — non-blocking
|
||||
final device = _deviceId;
|
||||
if (device != null) options.headers['x-device-id'] = device;
|
||||
|
||||
final token = await _store.access();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// مساطر طباعة كل طلب HTTP — يظهر فقط في وضع التطوير.
|
||||
class AppLoggingInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final method = options.method;
|
||||
final uri = options.uri;
|
||||
final payload = options.data;
|
||||
|
||||
debugPrint('┌─────── REQUEST ───────');
|
||||
debugPrint('│ $method $uri');
|
||||
if (payload != null) {
|
||||
debugPrint('└────── Payload ───────');
|
||||
try {
|
||||
final json = jsonDecode(payload.toString());
|
||||
debugPrint('${const JsonEncoder.withIndent(' ').convert(json)}');
|
||||
} catch (_) {
|
||||
debugPrint('$payload');
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final status = response.statusCode;
|
||||
final uri = response.requestOptions.uri;
|
||||
final body = response.data;
|
||||
|
||||
debugPrint('┌─────── RESPONSE [$status] ───────');
|
||||
debugPrint('│ $uri');
|
||||
if (body != null) {
|
||||
try {
|
||||
final json = jsonDecode(body.toString());
|
||||
debugPrint('${const JsonEncoder.withIndent(' ').convert(json)}');
|
||||
} catch (_) {
|
||||
debugPrint('$body');
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final status = err.response?.statusCode;
|
||||
final uri = err.requestOptions.uri;
|
||||
final message = err.message;
|
||||
|
||||
debugPrint('┌─────── ERROR [$status] ───────');
|
||||
debugPrint('│ $uri');
|
||||
debugPrint('│ $message');
|
||||
if (err.response?.data != null) {
|
||||
debugPrint('│ ${err.response?.data}');
|
||||
}
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/// مرجع معلومات التطبيق ورقم الإصدار الموحد.
|
||||
abstract final class AppInfo {
|
||||
static const String name = 'Tripz';
|
||||
static const String version = '1.0.0';
|
||||
static const int buildNumber = 1;
|
||||
|
||||
/// الصيغة المنسقة لرقم الإصدار (مثل v1.0.0).
|
||||
static String get formattedVersion => 'v$version';
|
||||
}
|
||||
@@ -10,6 +10,7 @@ class BuildConfig {
|
||||
// ألوان المستأجر (البذرة)
|
||||
static const int primaryColorValue = 0xFF0E7C66;
|
||||
static const int secondaryColorValue = 0xFF0E7C66;
|
||||
static const String defaultCountry = 'JO';
|
||||
|
||||
// أعلام الميزات — const: tree-shaking يحذف الكود المطفأ في release
|
||||
// docs/22 §1.5, docs/23 §5
|
||||
|
||||
@@ -22,9 +22,12 @@ class AppConfig {
|
||||
/// دور هذا التطبيق — يميّز تطبيق الراكب عن السائق عند التسجيل.
|
||||
static const String appRole = 'rider';
|
||||
|
||||
/// مفتاح الخريطة — يُمرَّر من البيئة. لا يُكتب في الكود (docs/23 §6).
|
||||
/// مفتاح الخريطة — يُضمَّن في التطبيق ويُقيَّد ببصمة التطبيق (bundle id
|
||||
/// للأندرويد/iOS) على map-saas نفسه، فلا يعمل خارج هذا التطبيق (قرار المالك
|
||||
/// 2026-07-20: خرائط مباشرة إلى map-saas بلا مرور عبر الباك إند). يمكن
|
||||
/// تجاوزه ببيئة البناء `--dart-define=MAP_API_KEY=...` لكل مستأجر.
|
||||
static const String mapApiKey = String.fromEnvironment(
|
||||
'MAP_API_KEY',
|
||||
defaultValue: '',
|
||||
defaultValue: 'in_9478b32836d19cff73db3063',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../build_config.dart';
|
||||
import 'tripz_colors.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
@@ -6,7 +7,7 @@ import 'typography.dart';
|
||||
/// docs/26 §2, §4
|
||||
ThemeData buildTheme({required Brightness brightness, required Locale locale}) {
|
||||
final bool isDark = brightness == Brightness.dark;
|
||||
final seed = const Color(0xFF0E7C66); // أخضر تركوازي — بذرة المستأجر
|
||||
final seed = Color(BuildConfig.primaryColorValue); // أخضر تركوازي — بذرة المستأجر
|
||||
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
|
||||
@@ -11,6 +11,7 @@ abstract final class T {
|
||||
static const double s24 = 24;
|
||||
static const double s32 = 32;
|
||||
static const double s48 = 48;
|
||||
static const double s64 = 64;
|
||||
|
||||
/// حشوة الصفحة الافتراضية
|
||||
static const double pagePadding = s16;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'api/api_client.dart';
|
||||
import 'api/antlaq_api.dart';
|
||||
import 'storage/token_store.dart';
|
||||
import 'storage/saved_places_store.dart';
|
||||
import '../features/auth/data/auth_repository.dart';
|
||||
import '../features/home/data/maps_repository.dart';
|
||||
import '../features/home/data/tariff_repository.dart';
|
||||
@@ -21,8 +23,9 @@ Future<void> setupDi() async {
|
||||
);
|
||||
|
||||
// ميزات Phase 1: الخرائط + الأسعار
|
||||
getIt.registerLazySingleton<AntlaqApi>(() => AntlaqApi());
|
||||
getIt.registerLazySingleton<MapsRepository>(
|
||||
() => MapsRepository(getIt<ApiClient>()),
|
||||
() => MapsRepository(getIt<AntlaqApi>(), getIt<ApiClient>()),
|
||||
);
|
||||
getIt.registerLazySingleton<TariffRepository>(
|
||||
() => TariffRepository(getIt<ApiClient>()),
|
||||
@@ -44,4 +47,6 @@ Future<void> setupDi() async {
|
||||
getIt.registerLazySingleton<FcmService>(
|
||||
() => FcmService(getIt<TripEventMerger>()),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton<SavedPlacesStore>(() => SavedPlacesStore());
|
||||
}
|
||||
|
||||
@@ -172,6 +172,58 @@
|
||||
"distance": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"edit": "تعديل",
|
||||
"resendIn": "إعادة إرسال خلال {seconds}ث",
|
||||
"@resendIn": {
|
||||
"placeholders": {
|
||||
"seconds": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"resendCode": "إعادة إرسال الرمز",
|
||||
"nameTitle": "ما اسمك؟",
|
||||
"nameSubtitle": "ساعدنا في تخصيص تجربتك",
|
||||
"nameHint": "اسمك",
|
||||
"firstNameHint": "الاسم الأول",
|
||||
"lastNameHint": "اسم العائلة",
|
||||
"nameButton": "متابعة",
|
||||
"reconnect": "جارٍ إعادة الاتصال...",
|
||||
"tripExpired": "انتهت صلاحية الرحلة"
|
||||
"tripExpired": "انتهت صلاحية الرحلة",
|
||||
"loginTitle": "مرحباً بك في Tripz",
|
||||
"phoneLoginSubtitle": "أدخل رقم هاتفك لتسجيل الدخول",
|
||||
"sendOtpButton": "إرسال رمز التحقق",
|
||||
"nameEntryTitle": "ما اسمك؟",
|
||||
"homeTitle": "الرئيسية",
|
||||
"destinationTitle": "الوجهة",
|
||||
"routeTitle": "المسار",
|
||||
"whereToHint": "إلى أين تريد الذهاب؟",
|
||||
"currentLocation": "الموقع الحالي",
|
||||
"savedPlaces": "الأماكن المحفوظة",
|
||||
"home": "المنزل",
|
||||
"work": "العمل",
|
||||
"pickFromMap": "اختيار من الخريطة",
|
||||
"searchHint": "ابحث عن مكان...",
|
||||
"noResultsFound": "لا توجد نتائج",
|
||||
"connecting": "جارٍ الاتصال...",
|
||||
"tripSummary": "ملخص الرحلة",
|
||||
"totalDistance": "المسافة الإجمالية",
|
||||
"estimatedTime": "الوقت المقدر",
|
||||
"totalFare": "الأجرة الإجمالية",
|
||||
"chooseRide": "اختر نوع الرحلة",
|
||||
"promoCode": "كود الترويج",
|
||||
"errorTitle": "حدث خطأ",
|
||||
"errorSubtitle": "يرجى المحاولة مرة أخرى",
|
||||
"emptyTitle": "لا توجد بيانات",
|
||||
"emptySubtitle": "لم يتم العثور على محتوى حالياً",
|
||||
"signIn": "تسجيل الدخول",
|
||||
"confirmPhoneNumber": "تأكيد رقم الهاتف",
|
||||
"driverRatingValue": "التقييم",
|
||||
"missingDataError": "بيانات مفقودة",
|
||||
"onboardingTitle1": "اطلب بلمسة واحدة",
|
||||
"onboardingSubtitle1": "أدخل وجهتك واحصل على سائق في ثوانٍ",
|
||||
"onboardingTitle2": "تتبع رحلتك مباشرة",
|
||||
"onboardingSubtitle2": "شاهد سائقك وهو يقترب في الوقت الفعلي",
|
||||
"onboardingTitle3": "ادفع كما يناسبك",
|
||||
"onboardingSubtitle3": "اختر الدفع نقدًا أو بالبطاقة أو عبر المحفظة",
|
||||
"getStarted": "ابدأ الآن",
|
||||
"skip": "تخطي"
|
||||
}
|
||||