feat: implement splash and onboarding flows, update app assets, and add project design documentation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user