Update: 2026-08-02 17:52:28

This commit is contained in:
Hamza-Ayed
2026-08-02 17:52:28 +03:00
parent b78a6797d5
commit 4620e84d34
96 changed files with 6250 additions and 476 deletions
@@ -81,26 +81,55 @@ class LoginController extends GetxController {
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
// ─────────────────────────────────────────────────────────────
Future<void> getJWT({bool force = false}) async {
// إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد
static Future<bool>? _jwtFuture;
static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
static Duration get jwtCooldownRemaining {
final d = _jwtCooldownUntil.difference(DateTime.now());
return d.isNegative ? Duration.zero : d;
}
Future<bool> getJWT({bool force = false}) async {
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal(force: force).catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal({bool force = false}) async {
if (!force && isTokenValid()) {
Log.print("JWT is still valid. Skipping request.");
return;
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
}
try {
dev = Platform.isAndroid ? 'android' : 'ios';
// تأكد إن البصمة محدّثة قبل أي طلب
await DeviceHelper.getDeviceFingerprint();
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
final passengerId = box.read(BoxName.passengerID);
final isRegistering = passengerId == null || passengerId.toString().isEmpty;
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
// ── أول تسجيل ─────────────────────────────────────────
// نرسل البصمة المشفرة مع باقي البيانات
// السيرفر سيعمل hash لها ويخزنها في JWT payload
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.passengerID) ?? AK.newId,
'id': passengerId ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
'fingerPrint': fp,
@@ -109,12 +138,14 @@ class LoginController extends GetxController {
var response = await http.post(
Uri.parse(AppLink.loginFirstTime),
body: payload,
);
Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}');
Log.print('payload: $payload');
Log.print('response code: ${response.statusCode}');
Log.print('response body: ${response.body}');
).timeout(const Duration(seconds: 30));
if (response.statusCode == 429) {
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
@@ -125,18 +156,20 @@ class LoginController extends GetxController {
: decoded['jwt']);
if (jwt != null) {
// نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage
box.write(BoxName.jwt, c(jwt));
storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
await EncryptionHelper.initialize();
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response.statusCode}');
} else {
// ── مستخدم موجود: تجديد التوكن
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد passengerID');
}
var payload = {
'id': box.read(BoxName.passengerID),
'id': passengerId,
'fingerPrint': fp,
'aud': '${AK.allowed}$dev',
};
@@ -144,11 +177,15 @@ class LoginController extends GetxController {
var response = await http.post(
Uri.parse(AppLink.loginJwtRider),
body: payload,
);
Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}');
).timeout(const Duration(seconds: 30));
if (response.statusCode == 429) {
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
Log.print('payload: $payload');
Log.print('response: ${response.body}');
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final String? jwt = decoded['data'] != null
@@ -158,16 +195,33 @@ class LoginController extends GetxController {
: decoded['jwt']);
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
}
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
return _onJwtFailure('renew: HTTP ${response.statusCode}');
}
} catch (e) {
Log.print('Error in getJWT: $e');
return _onJwtFailure('Error: $e');
}
}
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
return true;
}
bool _onJwtFailure(String reason) {
_jwtFailures++;
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
return false;
}
// ─────────────────────────────────────────────────────────────
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
// ─────────────────────────────────────────────────────────────
@@ -233,6 +287,9 @@ class LoginController extends GetxController {
Future<String?> getJwtWallet() async {
dev = Platform.isAndroid ? 'android' : 'ios';
// نعيد حساب البصمة أولاً كي لا نرسل قيمة GCM قديمة عالقة في التخزين
// من نسخة سابقة من التطبيق (مثل getJWT تماماً).
await DeviceHelper.getDeviceFingerprint();
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
var payload = {
@@ -0,0 +1,270 @@
// food_controller.dart — حالة تبويب الطعام (تصفح، سلة، تتبّع الطلب)
import 'dart:async';
import 'dart:math';
import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../../views/widgets/error_snakbar.dart';
import 'food_models.dart';
import 'food_service.dart';
class FoodController extends GetxController {
// ── تصفح ──
bool isLoadingMerchants = false;
List<FoodMerchant> merchants = [];
String city = _defaultCityForCountry();
// ── تفاصيل مطعم ──
bool isLoadingMenu = false;
FoodMerchant? selectedMerchant;
List<FoodMenuCategory> categories = [];
// ── السلة (مقيّدة بمطعم واحد فقط) ──
int? cartMerchantId;
final Map<String, FoodCartLine> _cart = {};
List<FoodCartLine> get cartLines => _cart.values.toList();
int get cartItemsCount => _cart.values.fold(0, (sum, l) => sum + l.quantity);
int get cartTotal => _cart.values.fold(0, (sum, l) => sum + l.lineTotal);
// ── عرض السعر الحالي ──
FoodQuote? currentQuote;
bool isQuoting = false;
// ── الطلب النشط (تتبّع) ──
FoodOrder? activeOrder;
Timer? _statusPollTimer;
static String _defaultCityForCountry() {
final country = box.read(BoxName.countryCode);
switch (country) {
case 'SY':
return 'دمشق';
case 'EG':
return 'القاهرة';
default:
return 'عمان';
}
}
void setCity(String newCity) {
if (newCity.trim().isEmpty) return;
city = newCity.trim();
fetchMerchants();
}
Future<void> fetchMerchants({String? category}) async {
isLoadingMerchants = true;
update();
final res = await FoodService.browseMerchants(city: city, category: category);
isLoadingMerchants = false;
if (res.success) {
merchants = res.data ?? [];
} else {
merchants = [];
mySnackbarWarning(res.message);
}
update();
}
Future<void> searchMerchants(String query) async {
if (query.trim().length < 2) return;
isLoadingMerchants = true;
update();
final res = await FoodService.searchMerchants(city: city, query: query.trim());
isLoadingMerchants = false;
if (res.success) merchants = res.data ?? [];
update();
}
Future<void> openMerchant(int merchantId) async {
isLoadingMenu = true;
selectedMerchant = null;
categories = [];
update();
final res = await FoodService.merchantDetails(merchantId);
isLoadingMenu = false;
if (res.success && res.data != null) {
selectedMerchant = res.data!['merchant'] as FoodMerchant;
categories = res.data!['categories'] as List<FoodMenuCategory>;
} else {
mySnackbarWarning(res.message);
}
update();
}
// ── إدارة السلة ──
bool addToCart(FoodMenuItem item, {int quantity = 1, Map<int, List<String>>? options}) {
final line = FoodCartLine(item: item, quantity: quantity, selectedOptions: options ?? {});
final existing = _cart[line.lineKey];
if (existing != null) {
existing.quantity += quantity;
} else {
_cart[line.lineKey] = line;
}
currentQuote = null; // أي تعديل بالسلة يُبطل العرض الموقّع القديم
update();
return true;
}
// يُستدعى من صفحة المطعم مع فحص أن السلة إما فارغة أو لنفس المطعم
bool addToCartForMerchant(int merchantId, FoodMenuItem item,
{int quantity = 1, Map<int, List<String>>? options}) {
if (_cart.isNotEmpty && cartMerchantId != null && cartMerchantId != merchantId) {
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
? 'سلتك تحتوي أصنافاً من مطعم آخر. أفرغها أولاً لإضافة من هذا المطعم.'
: 'Your cart has items from another restaurant. Clear it first.');
return false;
}
cartMerchantId = merchantId;
return addToCart(item, quantity: quantity, options: options);
}
void removeFromCart(String lineKey) {
_cart.remove(lineKey);
if (_cart.isEmpty) cartMerchantId = null;
currentQuote = null;
update();
}
void updateQuantity(String lineKey, int quantity) {
final line = _cart[lineKey];
if (line == null) return;
if (quantity <= 0) {
removeFromCart(lineKey);
return;
}
line.quantity = quantity;
currentQuote = null;
update();
}
void clearCart() {
_cart.clear();
cartMerchantId = null;
currentQuote = null;
update();
}
Future<bool> refreshQuote() async {
if (cartMerchantId == null || cartLines.isEmpty) return false;
isQuoting = true;
update();
final res = await FoodService.getQuote(merchantId: cartMerchantId!, lines: cartLines);
isQuoting = false;
if (res.success && res.data != null) {
currentQuote = res.data;
update();
return true;
}
currentQuote = null;
mySnackbarWarning(res.message);
update();
return false;
}
bool _placingOrder = false;
bool get isPlacingOrder => _placingOrder;
Future<int?> placeOrder({
required String deliveryAddress,
required double deliveryLat,
required double deliveryLng,
required String paymentMethod,
String? customerNote,
}) async {
if (currentQuote == null) {
final ok = await refreshQuote();
if (!ok || currentQuote == null) return null;
}
_placingOrder = true;
update();
final res = await FoodService.createOrder(
quoteToken: currentQuote!.quoteToken,
clientOrderUuid: _generateUuidV4(),
deliveryAddress: deliveryAddress,
deliveryLat: deliveryLat,
deliveryLng: deliveryLng,
paymentMethod: paymentMethod,
customerNote: customerNote,
);
_placingOrder = false;
update();
if (res.success && res.data != null) {
clearCart();
startTrackingOrder(res.data!);
return res.data;
}
mySnackbarWarning(res.message);
return null;
}
// ── تتبّع الطلب (polling — السوكيت ليس مصدر الحقيقة، انظر ملاحظة backend) ──
void startTrackingOrder(int orderId) {
_statusPollTimer?.cancel();
_pollOrderStatus(orderId);
_statusPollTimer = Timer.periodic(const Duration(seconds: 5), (_) => _pollOrderStatus(orderId));
}
Future<void> _pollOrderStatus(int orderId) async {
final res = await FoodService.getOrderStatus(orderId);
if (res.success && res.data != null) {
activeOrder = res.data;
update();
if (activeOrder!.status == 'delivered' || foodOrderIsCancelled(activeOrder!.status)) {
_statusPollTimer?.cancel();
}
}
}
void stopTracking() {
_statusPollTimer?.cancel();
activeOrder = null;
}
Future<bool> rateActiveOrder(int rating, {String? comment}) async {
if (activeOrder == null) return false;
final res = await FoodService.rateOrder(activeOrder!.id, rating, comment: comment);
if (res.success) {
await _pollOrderStatus(activeOrder!.id);
return true;
}
mySnackbarWarning(res.message);
return false;
}
Future<bool> cancelActiveOrder({String? reason}) async {
if (activeOrder == null) return false;
final res = await FoodService.cancelOrder(activeOrder!.id, reason: reason);
if (res.success) {
await _pollOrderStatus(activeOrder!.id);
return true;
}
mySnackbarWarning(res.message);
return false;
}
static String _generateUuidV4() {
final rnd = Random.secure();
final bytes = List<int>.generate(16, (_) => rnd.nextInt(256));
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
String hex(int start, int end) =>
bytes.sublist(start, end).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return '${hex(0, 4)}-${hex(4, 6)}-${hex(6, 8)}-${hex(8, 10)}-${hex(10, 16)}';
}
@override
void onClose() {
_statusPollTimer?.cancel();
super.onClose();
}
}
@@ -0,0 +1,325 @@
// food_models.dart — نماذج بيانات وحدة الطعام (جهة الراكب)
class FoodMerchant {
final int id;
final String nameAr;
final String? nameEn;
final String? logoUrl;
final String? coverUrl;
final String? descriptionAr;
final String city;
final String? address;
final String? category;
final int minOrderAmount;
final int avgPrepMinutes;
final double ratingAvg;
final int ratingCount;
final bool isOpen;
FoodMerchant({
required this.id,
required this.nameAr,
required this.city,
this.nameEn,
this.logoUrl,
this.coverUrl,
this.descriptionAr,
this.address,
this.category,
this.minOrderAmount = 0,
this.avgPrepMinutes = 20,
this.ratingAvg = 0,
this.ratingCount = 0,
this.isOpen = true,
});
factory FoodMerchant.fromJson(Map<String, dynamic> j) => FoodMerchant(
id: int.tryParse(j['id'].toString()) ?? 0,
nameAr: j['name_ar']?.toString() ?? '',
nameEn: j['name_en']?.toString(),
logoUrl: j['logo_url']?.toString(),
coverUrl: j['cover_url']?.toString(),
descriptionAr: j['description_ar']?.toString(),
city: j['city']?.toString() ?? '',
address: j['address']?.toString(),
category: j['category']?.toString(),
minOrderAmount: int.tryParse(j['min_order_amount']?.toString() ?? '0') ?? 0,
avgPrepMinutes: int.tryParse(j['avg_prep_minutes']?.toString() ?? '20') ?? 20,
ratingAvg: double.tryParse(j['rating_avg']?.toString() ?? '0') ?? 0,
ratingCount: int.tryParse(j['rating_count']?.toString() ?? '0') ?? 0,
isOpen: j['is_open'] == true || j['is_open']?.toString() == '1',
);
}
class FoodOptionChoice {
final String id;
final String labelAr;
final int price;
FoodOptionChoice({required this.id, required this.labelAr, required this.price});
factory FoodOptionChoice.fromJson(Map<String, dynamic> j) => FoodOptionChoice(
id: j['id'].toString(),
labelAr: j['label_ar']?.toString() ?? '',
price: int.tryParse(j['price']?.toString() ?? '0') ?? 0,
);
}
class FoodItemOptionGroup {
final int id;
final String groupNameAr;
final bool isRequired;
final int maxSelect;
final List<FoodOptionChoice> choices;
FoodItemOptionGroup({
required this.id,
required this.groupNameAr,
required this.isRequired,
required this.maxSelect,
required this.choices,
});
factory FoodItemOptionGroup.fromJson(Map<String, dynamic> j) => FoodItemOptionGroup(
id: int.tryParse(j['id'].toString()) ?? 0,
groupNameAr: j['group_name_ar']?.toString() ?? '',
isRequired: j['is_required']?.toString() == '1' || j['is_required'] == true,
maxSelect: int.tryParse(j['max_select']?.toString() ?? '1') ?? 1,
choices: (j['choices'] is List)
? (j['choices'] as List)
.map((c) => FoodOptionChoice.fromJson(Map<String, dynamic>.from(c)))
.toList()
: <FoodOptionChoice>[],
);
}
class FoodMenuItem {
final int id;
final int categoryId;
final String nameAr;
final String? nameEn;
final String? descriptionAr;
final String? imageUrl;
final int price;
final bool isAvailable;
final List<FoodItemOptionGroup> options;
FoodMenuItem({
required this.id,
required this.categoryId,
required this.nameAr,
required this.price,
this.nameEn,
this.descriptionAr,
this.imageUrl,
this.isAvailable = true,
this.options = const [],
});
factory FoodMenuItem.fromJson(Map<String, dynamic> j) => FoodMenuItem(
id: int.tryParse(j['id'].toString()) ?? 0,
categoryId: int.tryParse(j['category_id'].toString()) ?? 0,
nameAr: j['name_ar']?.toString() ?? '',
nameEn: j['name_en']?.toString(),
descriptionAr: j['description_ar']?.toString(),
imageUrl: j['image_url']?.toString(),
price: int.tryParse(j['price']?.toString() ?? '0') ?? 0,
isAvailable: j['is_available']?.toString() != '0' && j['is_available'] != false,
options: (j['options'] is List)
? (j['options'] as List)
.map((o) => FoodItemOptionGroup.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodItemOptionGroup>[],
);
}
class FoodMenuCategory {
final int id;
final String nameAr;
final String? nameEn;
final List<FoodMenuItem> items;
FoodMenuCategory({
required this.id,
required this.nameAr,
this.nameEn,
this.items = const [],
});
factory FoodMenuCategory.fromJson(Map<String, dynamic> j) => FoodMenuCategory(
id: int.tryParse(j['id'].toString()) ?? 0,
nameAr: j['name_ar']?.toString() ?? '',
nameEn: j['name_en']?.toString(),
items: (j['items'] is List)
? (j['items'] as List)
.map((i) => FoodMenuItem.fromJson(Map<String, dynamic>.from(i)))
.toList()
: <FoodMenuItem>[],
);
}
// ── سطر في السلة (محلي فقط قبل الإرسال للخادم) ──
class FoodCartLine {
final FoodMenuItem item;
int quantity;
// key: option_group_id, value: قائمة choice_id المختارة
final Map<int, List<String>> selectedOptions;
FoodCartLine({
required this.item,
this.quantity = 1,
Map<int, List<String>>? selectedOptions,
}) : selectedOptions = selectedOptions ?? {};
int get unitPrice {
int total = item.price;
for (final group in item.options) {
final chosen = selectedOptions[group.id] ?? [];
for (final choiceId in chosen) {
final choice = group.choices.firstWhere(
(c) => c.id == choiceId,
orElse: () => FoodOptionChoice(id: '', labelAr: '', price: 0),
);
total += choice.price;
}
}
return total;
}
int get lineTotal => unitPrice * quantity;
Map<String, dynamic> toQuotePayload() => {
'item_id': item.id,
'quantity': quantity,
'options': selectedOptions.entries
.map((e) => {'option_group_id': e.key, 'choice_ids': e.value})
.toList(),
};
// مفتاح تمييز محلي: نفس الصنف بخيارات مختلفة = سطر مختلف في السلة
String get lineKey {
final optKeys = selectedOptions.entries.map((e) => '${e.key}:${e.value.join(",")}').join('|');
return '${item.id}_$optKeys';
}
}
class FoodQuote {
final String quoteToken;
final int itemsTotal;
final int deliveryFee;
final int serviceFee;
final int grandTotal;
final int expiresIn;
FoodQuote({
required this.quoteToken,
required this.itemsTotal,
required this.deliveryFee,
required this.serviceFee,
required this.grandTotal,
required this.expiresIn,
});
factory FoodQuote.fromJson(Map<String, dynamic> j) => FoodQuote(
quoteToken: j['quote_token']?.toString() ?? '',
itemsTotal: int.tryParse(j['items_total']?.toString() ?? '0') ?? 0,
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
serviceFee: int.tryParse(j['service_fee']?.toString() ?? '0') ?? 0,
grandTotal: int.tryParse(j['grand_total']?.toString() ?? '0') ?? 0,
expiresIn: int.tryParse(j['expires_in']?.toString() ?? '600') ?? 600,
);
}
class FoodOrderItemLine {
final String nameArSnapshot;
final int unitPrice;
final int quantity;
final int lineTotal;
FoodOrderItemLine({
required this.nameArSnapshot,
required this.unitPrice,
required this.quantity,
required this.lineTotal,
});
factory FoodOrderItemLine.fromJson(Map<String, dynamic> j) => FoodOrderItemLine(
nameArSnapshot: j['name_ar_snapshot']?.toString() ?? '',
unitPrice: int.tryParse(j['unit_price']?.toString() ?? '0') ?? 0,
quantity: int.tryParse(j['quantity']?.toString() ?? '1') ?? 1,
lineTotal: int.tryParse(j['line_total']?.toString() ?? '0') ?? 0,
);
}
class FoodOrder {
final int id;
final String status;
final int itemsTotal;
final int deliveryFee;
final int grandTotal;
final String? deliveryAddress;
final int? rating;
final DateTime? createdAt;
final DateTime? deliveredAt;
final String? merchantNameAr;
final String? merchantLogoUrl;
final List<FoodOrderItemLine> items;
FoodOrder({
required this.id,
required this.status,
required this.itemsTotal,
required this.deliveryFee,
required this.grandTotal,
this.deliveryAddress,
this.rating,
this.createdAt,
this.deliveredAt,
this.merchantNameAr,
this.merchantLogoUrl,
this.items = const [],
});
factory FoodOrder.fromJson(Map<String, dynamic> j) => FoodOrder(
id: int.tryParse(j['id'].toString()) ?? 0,
status: j['status']?.toString() ?? 'pending',
itemsTotal: int.tryParse(j['items_total']?.toString() ?? '0') ?? 0,
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
grandTotal: int.tryParse(j['grand_total']?.toString() ?? '0') ?? 0,
deliveryAddress: j['delivery_address']?.toString(),
rating: j['rating'] == null ? null : int.tryParse(j['rating'].toString()),
createdAt: DateTime.tryParse(j['created_at']?.toString() ?? ''),
deliveredAt: j['delivered_at'] == null ? null : DateTime.tryParse(j['delivered_at'].toString()),
merchantNameAr: j['merchant_name_ar']?.toString(),
merchantLogoUrl: j['merchant_logo_url']?.toString(),
items: (j['items'] is List)
? (j['items'] as List)
.map((i) => FoodOrderItemLine.fromJson(Map<String, dynamic>.from(i)))
.toList()
: <FoodOrderItemLine>[],
);
}
// ترتيب حالات الطلب لعرض خط زمني تصاعدي في شاشة التتبع
const List<String> foodOrderStatusFlow = [
'pending',
'merchant_accepted',
'preparing',
'ready',
'courier_assigned',
'picked_up',
'delivered',
];
bool foodOrderIsCancelled(String status) =>
status.startsWith('cancelled') || status == 'rejected';
// المبالغ في backend/food مخزّنة بأصغر وحدة نقدية (fils) — العامل هنا يطابق
// FOOD_CURRENCY_DIVISOR الافتراضي في docker/.env.example (1000). إن غُيّر
// هناك يجب تغييره هنا أيضاً — القيمتان يجب أن تبقيا متطابقتين دائماً.
const int foodCurrencyDivisor = 1000;
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
final decimal = smallestUnit / foodCurrencyDivisor;
return '${decimal.toStringAsFixed(3)} $currencySymbol';
}
@@ -0,0 +1,199 @@
// food_service.dart — طبقة الاتصال بـ backend/food (جهة الراكب)
import 'dart:convert';
import '../functions/crud.dart';
import '../../constant/links.dart';
import 'food_models.dart';
class FoodApiResult<T> {
final bool success;
final T? data;
final String message;
final int? code;
FoodApiResult(this.success, this.data, this.message, {this.code});
}
class FoodService {
static String get _base => '${AppLink.server}/food';
static Future<FoodApiResult<List<FoodMerchant>>> browseMerchants({
required String city,
String? category,
}) async {
final payload = <String, dynamic>{'city': city};
if (category != null && category.isNotEmpty) payload['category'] = category;
final res = await CRUD().post(link: '$_base/merchant/browse.php', payload: payload);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['merchants'] is List)
? (msg['merchants'] as List)
.map((m) => FoodMerchant.fromJson(Map<String, dynamic>.from(m)))
.toList()
: <FoodMerchant>[];
return FoodApiResult(true, list, 'ok');
}
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<List<FoodMerchant>>> searchMerchants({
required String city,
required String query,
}) async {
final res = await CRUD().post(
link: '$_base/merchant/search.php',
payload: {'city': city, 'q': query},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['merchants'] is List)
? (msg['merchants'] as List)
.map((m) => FoodMerchant.fromJson(Map<String, dynamic>.from(m)))
.toList()
: <FoodMerchant>[];
return FoodApiResult(true, list, 'ok');
}
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<Map<String, dynamic>>> merchantDetails(int merchantId) async {
final res = await CRUD().post(
link: '$_base/merchant/details.php',
payload: {'merchant_id': merchantId.toString()},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
if (msg is Map) {
final merchant = FoodMerchant.fromJson(Map<String, dynamic>.from(msg['merchant']));
final categories = (msg['categories'] is List)
? (msg['categories'] as List)
.map((c) => FoodMenuCategory.fromJson(Map<String, dynamic>.from(c)))
.toList()
: <FoodMenuCategory>[];
return FoodApiResult(true, {'merchant': merchant, 'categories': categories}, 'ok');
}
}
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<FoodQuote>> getQuote({
required int merchantId,
required List<FoodCartLine> lines,
}) async {
final res = await CRUD().post(
link: '$_base/cart/quote.php',
payload: {
'merchant_id': merchantId.toString(),
'items': jsonEncode(lines.map((l) => l.toQuotePayload()).toList()),
},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
if (msg is Map) return FoodApiResult(true, FoodQuote.fromJson(Map<String, dynamic>.from(msg)), 'ok');
}
return FoodApiResult(false, null, _errMsg(res), code: _errCode(res));
}
static Future<FoodApiResult<int>> createOrder({
required String quoteToken,
required String clientOrderUuid,
required String deliveryAddress,
required double deliveryLat,
required double deliveryLng,
required String paymentMethod, // wallet | cash
String? customerNote,
}) async {
final res = await CRUD().post(
link: '$_base/order/create.php',
payload: {
'quote_token': quoteToken,
'client_order_uuid': clientOrderUuid,
'delivery_address': deliveryAddress,
'delivery_lat': deliveryLat.toString(),
'delivery_lng': deliveryLng.toString(),
'payment_method': paymentMethod,
if (customerNote != null && customerNote.isNotEmpty) 'customer_note': customerNote,
},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
if (msg is Map && msg['order_id'] != null) {
return FoodApiResult(true, int.tryParse(msg['order_id'].toString()) ?? 0, 'ok');
}
}
return FoodApiResult(false, null, _errMsg(res), code: _errCode(res));
}
static Future<FoodApiResult<FoodOrder>> getOrderStatus(int orderId) async {
final res = await CRUD().post(
link: '$_base/order/status.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
if (msg is Map && msg['order'] is Map) {
return FoodApiResult(true, FoodOrder.fromJson(Map<String, dynamic>.from(msg['order'])), 'ok');
}
}
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<void>> cancelOrder(int orderId, {String? reason}) async {
final res = await CRUD().post(
link: '$_base/order/cancel.php',
payload: {'order_id': orderId.toString(), if (reason != null) 'reason': reason},
);
if (res is Map && res['status'] == 'success') return FoodApiResult(true, null, 'ok');
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<void>> rateOrder(int orderId, int rating, {String? comment}) async {
final res = await CRUD().post(
link: '$_base/order/rate.php',
payload: {
'order_id': orderId.toString(),
'rating': rating.toString(),
if (comment != null && comment.isNotEmpty) 'comment': comment,
},
);
if (res is Map && res['status'] == 'success') return FoodApiResult(true, null, 'ok');
return FoodApiResult(false, null, _errMsg(res));
}
static Future<FoodApiResult<List<FoodOrder>>> getHistory({int page = 1}) async {
final res = await CRUD().post(
link: '$_base/order/history.php',
payload: {'page': page.toString()},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['orders'] is List)
? (msg['orders'] as List)
.map((o) => FoodOrder.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodOrder>[];
return FoodApiResult(true, list, 'ok');
}
return FoodApiResult(false, null, _errMsg(res));
}
static int? _errCode(dynamic res) {
if (res is Map && res['code'] != null) return int.tryParse(res['code'].toString());
return null;
}
static String _errMsg(dynamic res) {
if (res == 'no_internet') return 'تحقق من اتصالك بالإنترنت';
if (res == 'token_expired') return 'انتهت الجلسة، حاول مجدداً';
if (res is Map && res['message'] is String) return res['message'];
return 'حدث خطأ، حاول مجدداً';
}
}
+68 -22
View File
@@ -24,7 +24,6 @@ class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -98,31 +97,50 @@ class CRUD {
Future<String> _getJwt() async {
try {
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
if (encryptedJwt == null || encryptedJwt.isEmpty) {
final String? fallback = box.read(BoxName.jwt);
final jwt = await storage.read(key: BoxName.jwt);
if (jwt == null || jwt.toString().isEmpty) {
// إذا كان التخزين الآمن فارغاً، نحاول استخراج التوكن القديم من GetStorage للركاب القدامى
final fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
try {
return r(fallback).toString().split(Env.addd)[0]; // فك تشفير القديم
} catch (_) {
return fallback.toString(); // ربما تم تخزينه بدون تشفير
}
}
return '';
}
return r(encryptedJwt).toString().split(Env.addd)[0];
} catch (e) {
Log.print('Error reading JWT from SecureStorage: $e');
final String? fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
// التحقق السريع إذا كان التوكن لا يزال مشفراً (يبدأ برموز غريبة وليس ey)
if (!jwt.startsWith('ey')) {
try {
return r(jwt).toString().split(Env.addd)[0];
} catch (_) {}
}
return jwt;
} catch (_) {
return '';
}
}
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
/// Centralized request handler with retry for weak networks.
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
const totalTimeout = Duration(seconds: 60);
@@ -180,17 +198,33 @@ class CRUD {
}
}
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginController()).getJWT();
if (!refreshed) return 'token_expired';
final newToken = await _getJwt();
if (newToken.isEmpty) return 'token_expired';
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
}
if (sc >= 500) {
@@ -206,7 +240,14 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
String token = await _ensureJwt();
if (token.isEmpty) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
// باستثناء تسجيل الدخول لأنه لا يحتاج توكن
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -221,7 +262,12 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
String token = await _ensureJwt();
if (token.isEmpty) {
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -42,6 +42,15 @@ class EncryptionHelper {
debugPrint("EncryptionHelper initialized successfully.");
}
/// Encrypts a string using AES-256-CBC with constant IV (deterministic)
/// Same input always produces the same output
String encryptDataCbc(String plainText) {
final cbcEncrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = cbcEncrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
}
/// ✅ FIX H-04: Encrypts a string using AES-256-GCM (new) with random IV
String encryptData(String plainText) {
try {
@@ -190,15 +190,11 @@ void showUpdateDialog(BuildContext context) {
class DeviceHelper {
static Future<String> getDeviceFingerprint() async {
// ── التحقق من وجود بصمة مخزّنة مسبقاً ──────────────────────
// AES-GCM يستخدم IV عشوائي كل مرة، فالتشفير ينتج نتيجة مختلفة
// حتى لو النص الأصلي نفسه. لذلك نخزّن البصمة المشفرة أول مرة
// ونرجعها من التخزين في كل مرة بعدها لضمان الثبات.
final String? cachedFp = box.read(BoxName.deviceFpEncrypted);
if (cachedFp != null && cachedFp.isNotEmpty) {
return cachedFp;
}
// ── البصمة حتمية: AES-CBC بـ IV ثابت ────────────────────────
// نفس الجهاز ⇒ نفس الناتج دائماً، حتى بعد مسح بيانات التطبيق أو
// إعادة التثبيت. لذلك لا نعتمد على الكاش كمصدر للثبات (كان ضرورياً
// أيام AES-GCM بالـ IV العشوائي)، بل نعيد الحساب في كل مرة ونكتب
// القيمة في التخزين فقط لتقرأها بقية الشاشات.
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
var deviceData;
@@ -231,7 +227,7 @@ class DeviceHelper {
// Generate and return the encrypted fingerprint
final String fingerprint = '${deviceId}_$deviceModel';
final String encryptedFp =
EncryptionHelper.instance.encryptData(fingerprint);
EncryptionHelper.instance.encryptDataCbc(fingerprint);
box.write(BoxName.deviceFpEncrypted, encryptedFp);
//Log.print(EncryptionHelper.instance.encryptData(fingerprint));
return encryptedFp;
@@ -163,6 +163,7 @@ class LocationSearchController extends GetxController {
];
readyWayPoints();
getLocation();
_listenForDeepLink();
}
void readyWayPoints() {
@@ -567,6 +567,17 @@ class MapEngineController extends GetxController {
update();
}
/// إغلاق القائمة الجانبية بشكل مباشر (idempotent) — بعكس [getDrawerMenu]
/// التي تعمل كمفتاح تبديل، هذه تُغلق فقط ولا تفتح إن كانت مغلقة أصلاً.
void closeDrawerMenu() {
if (!heightMenuBool) return;
heightMenuBool = false;
widthMapTypeAndTraffic = 50;
heightMenu = 0;
widthMenu = 0;
update();
}
void changeMainBottomMenuMap() {
if (isWayPointStopsSheetUtilGetMap == true) {
changeWayPointSheet();
@@ -796,6 +796,19 @@ final Map<String, String> ar_eg = {
"Open Settings": "افتح الإعدادات",
"Open destination search": "فتح بحث الوجهات",
"Open in Google Maps": "فتح في خرائط جوجل",
"Canceled by you": "إنت لغيتها",
"Canceled by driver": "السواق لغاها",
"Canceled by driver after accepting": "السواق لغاها بعد الموافقة",
"Searching for a driver": "بندور على سواق",
"Driver on the way": "السواق في الطريق",
"Trip in progress": "الرحلة شغالة",
"Not completed": "ما اكتملتش",
"To": "إلى",
"Locating": "جاري التحديد",
"km": "كم",
"Rebook": "إعادة الحجز",
"Reverse trip": "عكس الرحلة",
"Please open the map first": "افتح الخريطة الأول",
"Or pay with Cash instead": "أو ادفع كاش",
"Order": "طلب",
"Order Accepted": "تم قبول الطلب",
@@ -795,6 +795,19 @@ final Map<String, String> ar_jo = {
"Open Settings": "افتح الإعدادات",
"Open destination search": "فتح بحث الوجهات",
"Open in Google Maps": "فتح في خرائط جوجل",
"Canceled by you": "ألغيتها أنت",
"Canceled by driver": "ألغاها السائق",
"Canceled by driver after accepting": "ألغاها السائق بعد القبول",
"Searching for a driver": "البحث عن سائق",
"Driver on the way": "السائق في الطريق",
"Trip in progress": "الرحلة جارية",
"Not completed": "لم تكتمل",
"To": "إلى",
"Locating": "جارٍ التحديد",
"km": "كم",
"Rebook": "إعادة الحجز",
"Reverse trip": "عكس الرحلة",
"Please open the map first": "افتح الخريطة أولاً",
"Or pay with Cash instead": "أو ادفع نقداً",
"Order": "طلب",
"Order Accepted": "تم قبول الطلب",
@@ -796,6 +796,19 @@ final Map<String, String> ar_sy = {
"Open Settings": "افتح الإعدادات",
"Open destination search": "فتح بحث الوجهات",
"Open in Google Maps": "فتح في خرائط جوجل",
"Canceled by you": "ألغيتها أنت",
"Canceled by driver": "ألغاها السائق",
"Canceled by driver after accepting": "ألغاها السائق بعد القبول",
"Searching for a driver": "البحث عن سائق",
"Driver on the way": "السائق في الطريق",
"Trip in progress": "الرحلة جارية",
"Not completed": "لم تكتمل",
"To": "إلى",
"Locating": "جارٍ التحديد",
"km": "كم",
"Rebook": "إعادة الحجز",
"Reverse trip": "عكس الرحلة",
"Please open the map first": "افتح الخريطة أولاً",
"Or pay with Cash instead": "أو ادفع كاش",
"Order": "طلب",
"Order Accepted": "تم قبول الطلب",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> de = {
"Open Settings": "Einstellungen öffnen",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Von dir storniert",
"Canceled by driver": "Vom Fahrer storniert",
"Canceled by driver after accepting": "Vom Fahrer nach Annahme storniert",
"Searching for a driver": "Fahrersuche",
"Driver on the way": "Fahrer unterwegs",
"Trip in progress": "Fahrt läuft",
"Not completed": "Nicht abgeschlossen",
"To": "Nach",
"Locating": "Wird ermittelt",
"km": "km",
"Rebook": "Erneut buchen",
"Reverse trip": "Fahrt umkehren",
"Please open the map first": "Bitte zuerst die Karte öffnen",
"Or pay with Cash instead": "Oder zahlen Sie stattdessen bar",
"Order": "Bestellung",
"Order Accepted": "Bestellung angenommen",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> el = {
"Open Settings": "Ρυθμίσεις",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Ακυρώθηκε από εσάς",
"Canceled by driver": "Ακυρώθηκε από τον οδηγό",
"Canceled by driver after accepting": "Ακυρώθηκε από τον οδηγό μετά την αποδοχή",
"Searching for a driver": "Αναζήτηση οδηγού",
"Driver on the way": "Ο οδηγός είναι καθ' οδόν",
"Trip in progress": "Διαδρομή σε εξέλιξη",
"Not completed": "Δεν ολοκληρώθηκε",
"To": "Προς",
"Locating": "Εντοπισμός",
"km": "χλμ",
"Rebook": "Νέα κράτηση",
"Reverse trip": "Αντιστροφή διαδρομής",
"Please open the map first": "Ανοίξτε πρώτα τον χάρτη",
"Or pay with Cash instead": "Ή πληρώστε με Μετρητά",
"Order": "Αίτημα",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> es = {
"Open Settings": "Abrir configuración",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Cancelado por ti",
"Canceled by driver": "Cancelado por el conductor",
"Canceled by driver after accepting": "Cancelado por el conductor tras aceptar",
"Searching for a driver": "Buscando conductor",
"Driver on the way": "Conductor en camino",
"Trip in progress": "Viaje en curso",
"Not completed": "No completado",
"To": "Hasta",
"Locating": "Localizando",
"km": "km",
"Rebook": "Reservar de nuevo",
"Reverse trip": "Invertir el viaje",
"Please open the map first": "Abre el mapa primero",
"Or pay with Cash instead": "O pague en efectivo en su lugar",
"Order": "Pedido",
"Order Accepted": "Pedido aceptado",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> fa = {
"Open Settings": "باز کردن تنظیمات",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "توسط شما لغو شد",
"Canceled by driver": "توسط راننده لغو شد",
"Canceled by driver after accepting": "پس از پذیرش توسط راننده لغو شد",
"Searching for a driver": "جستجوی راننده",
"Driver on the way": "راننده در راه است",
"Trip in progress": "سفر در جریان است",
"Not completed": "تکمیل نشده",
"To": "به",
"Locating": "در حال تعیین",
"km": "کیلومتر",
"Rebook": "رزرو مجدد",
"Reverse trip": "معکوس کردن سفر",
"Please open the map first": "ابتدا نقشه را باز کنید",
"Or pay with Cash instead": "یا به صورت نقدی پرداخت کنید",
"Order": "درخواست",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> fr = {
"Open Settings": "Ouvrir les paramètres",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Annulé par vous",
"Canceled by driver": "Annulé par le chauffeur",
"Canceled by driver after accepting": "Annulé par le chauffeur après acceptation",
"Searching for a driver": "Recherche d'un chauffeur",
"Driver on the way": "Chauffeur en route",
"Trip in progress": "Trajet en cours",
"Not completed": "Non terminé",
"To": "À",
"Locating": "Localisation",
"km": "km",
"Rebook": "Réserver à nouveau",
"Reverse trip": "Inverser le trajet",
"Please open the map first": "Veuillez d'abord ouvrir la carte",
"Or pay with Cash instead": "Ou payez en espèces",
"Order": "Commande",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> hi = {
"Open Settings": "सेटिंग्स खोलें",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "आपने रद्द किया",
"Canceled by driver": "चालक ने रद्द किया",
"Canceled by driver after accepting": "स्वीकार करने के बाद चालक ने रद्द किया",
"Searching for a driver": "चालक खोजा जा रहा है",
"Driver on the way": "चालक रास्ते में है",
"Trip in progress": "यात्रा जारी है",
"Not completed": "पूर्ण नहीं हुआ",
"To": "तक",
"Locating": "पता लगाया जा रहा है",
"km": "किमी",
"Rebook": "फिर से बुक करें",
"Reverse trip": "यात्रा उलटें",
"Please open the map first": "पहले मानचित्र खोलें",
"Or pay with Cash instead": "या नकद भुगतान करें",
"Order": "ऑर्डर",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> it = {
"Open Settings": "Impostazioni",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Annullato da te",
"Canceled by driver": "Annullato dall'autista",
"Canceled by driver after accepting": "Annullato dall'autista dopo l'accettazione",
"Searching for a driver": "Ricerca autista",
"Driver on the way": "Autista in arrivo",
"Trip in progress": "Viaggio in corso",
"Not completed": "Non completato",
"To": "A",
"Locating": "Localizzazione",
"km": "km",
"Rebook": "Prenota di nuovo",
"Reverse trip": "Inverti il viaggio",
"Please open the map first": "Apri prima la mappa",
"Or pay with Cash instead": "O paga in contanti",
"Order": "Ordine",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> ru = {
"Open Settings": "Настройки",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Отменено вами",
"Canceled by driver": "Отменено водителем",
"Canceled by driver after accepting": "Отменено водителем после принятия",
"Searching for a driver": "Поиск водителя",
"Driver on the way": "Водитель в пути",
"Trip in progress": "Поездка выполняется",
"Not completed": "Не завершено",
"To": "Куда",
"Locating": "Определение",
"km": "км",
"Rebook": "Заказать снова",
"Reverse trip": "Обратный маршрут",
"Please open the map first": "Сначала откройте карту",
"Or pay with Cash instead": "Или оплатите наличными",
"Order": "Заказ",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> tr = {
"Open Settings": "Ayarları Aç",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "Sizin tarafınızdan iptal edildi",
"Canceled by driver": "Sürücü iptal etti",
"Canceled by driver after accepting": "Sürücü kabul ettikten sonra iptal etti",
"Searching for a driver": "Sürücü aranıyor",
"Driver on the way": "Sürücü yolda",
"Trip in progress": "Yolculuk sürüyor",
"Not completed": "Tamamlanmadı",
"To": "Nereye",
"Locating": "Belirleniyor",
"km": "km",
"Rebook": "Yeniden rezerve et",
"Reverse trip": "Yolculuğu ters çevir",
"Please open the map first": "Lütfen önce haritayı açın",
"Or pay with Cash instead": "Veya Nakit öde",
"Order": "Sipariş",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> ur = {
"Open Settings": "ترتیبات کھولیں",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "آپ نے منسوخ کیا",
"Canceled by driver": "ڈرائیور نے منسوخ کیا",
"Canceled by driver after accepting": "قبول کرنے کے بعد ڈرائیور نے منسوخ کیا",
"Searching for a driver": "ڈرائیور تلاش کیا جا رہا ہے",
"Driver on the way": "ڈرائیور راستے میں ہے",
"Trip in progress": "سفر جاری ہے",
"Not completed": "مکمل نہیں ہوا",
"To": "تک",
"Locating": "تعین ہو رہا ہے",
"km": "کلومیٹر",
"Rebook": "دوبارہ بک کریں",
"Reverse trip": "سفر الٹا کریں",
"Please open the map first": "پہلے نقشہ کھولیں",
"Or pay with Cash instead": "یا اس کے بجائے نقد ادائیگی کریں",
"Order": "آرڈر",
"Order Accepted": "Order Accepted",
+13
View File
@@ -762,6 +762,19 @@ final Map<String, String> zh = {
"Open Settings": "افتح الإعدادات",
"Open destination search": "Open destination search",
"Open in Google Maps": "Open in Google Maps",
"Canceled by you": "您已取消",
"Canceled by driver": "司机已取消",
"Canceled by driver after accepting": "司机接单后取消",
"Searching for a driver": "正在寻找司机",
"Driver on the way": "司机正在赶来",
"Trip in progress": "行程进行中",
"Not completed": "未完成",
"To": "到",
"Locating": "定位中",
"km": "公里",
"Rebook": "重新预订",
"Reverse trip": "反向行程",
"Please open the map first": "请先打开地图",
"Or pay with Cash instead": "أو ادفع كاش",
"Order": "طلب",
"Order Accepted": "Order Accepted",