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
+4 -4
View File
@@ -5,8 +5,8 @@ class AppInformation {
static const String phoneNumber = '';
static const String linkedInProfile =
'https://www.linkedin.com/in/hamza-ayed/';
static const String website = 'https://intaleqapp.com';
static const String email = 'support@intaleqapp.com';
static const String website = 'https://siromove.com';
static const String email = 'support@siromove.com';
static const String complaintPrompt =
'for this data for complaint from driver or passenger i collect all data i want you analyze this complaint and show what is reason and what is solution .this data collected from many table to find solution if payment in visa not complete and if ride status is finished it will be paymnet in payment table if ride status is not finished there is no need to pay and payment table is null for this ride and if paymentFromPaymentTable not null and visa type not cash the payment sucssessed . if ratingpassenger is low or passengr rating drivers low grade then dont mine of this passenger ,look at driver too like passengerratingdriver with rating or ratingtopassenger .in json add status of complaint and message to passenger and message to driver and message to call center write in arabic in json output with key in english .for output please just json i want';
static const String addd = 'BlBlNl';
@@ -143,7 +143,7 @@ class AppInformation {
<h2>7. Account Deletion & Contact</h2>
<p>You have the right to request the deletion of your account and personal data. To do so, or for any other questions, please contact us. We will respond to deletion requests within 30 days.</p>
<p><strong>Email:</strong> <a href="mailto:support@intaleqapp.com">support@intaleqapp.com</a></p>
<p><strong>Email:</strong> <a href="mailto:support@siromove.com">support@siromove.com</a></p>
</body>
</html>
@@ -283,7 +283,7 @@ class AppInformation {
<h2>7. حذف الحساب والتواصل</h2>
<p>لديك الحق في طلب حذف حسابك وبياناتك الشخصية. للقيام بذلك، أو لأي استفسارات أخرى، يرجى التواصل معنا. سنرد على طلبات الحذف في غضون 30 يومًا.</p>
<p><strong>البريد الإلكتروني:</strong> <a href="mailto:support@intaleqapp.com">support@intaleqapp.com</a></p>
<p><strong>البريد الإلكتروني:</strong> <a href="mailto:support@siromove.com">support@siromove.com</a></p>
</body>
</html>
+13
View File
@@ -21,6 +21,19 @@ class AppLink {
static const String appDomain = 'siromove.com';
// ── روابط متجر تطبيق السائق (سيرو كابتن) ────────────────────────────────
// تُستخدم في كل رسائل مشاركة/دعوة السائقين.
static const String driverAppStoreIOS =
'https://apps.apple.com/jo/app/id6785282536';
static const String driverAppStoreAndroid =
'https://play.google.com/store/apps/details?id=com.siro.siro_driver';
/// سطرا التحميل الجاهزان للإلصاق في نص المشاركة — نرسل الرابطين معاً
/// لأننا لا نعرف نظام تشغيل المستلم عند مشاركة نص عبر واتساب.
static String get driverDownloadLinks =>
'📱 Android: $driverAppStoreAndroid\n🍏 iPhone: $driverAppStoreIOS';
static String get inviteRedirectUrl {
if (currentCountry == 'Syria') {
return "https://siromove.com/inviteSyria.php";
@@ -45,8 +45,8 @@ class InviteController extends GetxController {
final String shareText =
'''Join Siro as a driver using my referral code!
Use code: $driverCouponCode
Download the Siro Driver app now and earn rewards:
https://siromove.com/invite.php?code=$driverCouponCode&app=driver
Download the Siro Captain app now and earn rewards:
${AppLink.driverDownloadLinks}
''';
await Share.share(shareText);
}
@@ -228,10 +228,33 @@ class LoginDriverController extends GetxController {
return '';
}
getJWT() 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() async {
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal().catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal() async {
await EncryptionHelper.initialize();
// 1. Check secure storage first to avoid redundant API calls
String? secureJwt = await storage.read(key: BoxName.jwt);
if (secureJwt != null && secureJwt.isNotEmpty) {
bool isTokenValid = false;
@@ -250,7 +273,6 @@ class LoginDriverController extends GetxController {
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp != null) {
// Check if token is valid with a 30-second buffer
isTokenValid =
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
}
@@ -259,14 +281,23 @@ class LoginDriverController extends GetxController {
if (isTokenValid) {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
return;
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
}
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
}
dev = Platform.isAndroid ? 'android' : 'ios';
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
final driverId = box.read(BoxName.driverID);
final isRegistering = driverId == null || driverId.toString().isEmpty;
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
@@ -274,18 +305,21 @@ class LoginDriverController extends GetxController {
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// Log.print('payload: ${payload}');
var response0 = await http.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
);
Log.print('response0: ${response0.body}');
Log.print('request: ${response0.request}');
).timeout(const Duration(seconds: 30));
if (response0.statusCode == 429) {
final retryAfter = int.tryParse(response0.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}');
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
@@ -295,18 +329,20 @@ class LoginDriverController extends GetxController {
}
if (jwt != null) {
// box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
// await AppInitializer().getKey();
} else {}
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
} else {
await EncryptionHelper.initialize();
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد driverID');
}
var payload = {
'id': box.read(BoxName.driverID),
'password': box.read(BoxName.emailDriver),
@@ -314,18 +350,21 @@ class LoginDriverController extends GetxController {
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
);
Log.print('response1.request: ${response1.request}');
Log.print('response1.body: ${response1.body}');
).timeout(const Duration(seconds: 30));
if (response1.statusCode == 429) {
final retryAfter = int.tryParse(response1.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
if (response1.statusCode == 200) {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
@@ -335,15 +374,30 @@ class LoginDriverController extends GetxController {
}
if (jwt != null) {
// await box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
}
// await AppInitializer().getKey();
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
}
}
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;
}
Future<void> getLocationPermission() async {
var status = await Permission.locationAlways.status;
if (!status.isGranted) {
@@ -0,0 +1,129 @@
// food_delivery_controller.dart — حالة تبويب التوصيل (وضع التوصيل، العروض، المهام النشطة)
import 'dart:async';
import 'package:get/get.dart';
import '../../views/widgets/error_snakbar.dart';
import 'food_delivery_models.dart';
import 'food_delivery_service.dart';
class FoodDeliveryController extends GetxController {
bool isDeliveryModeEnabled = false;
bool isTogglingMode = false;
List<FoodDeliveryOffer> pendingOffers = [];
List<FoodDeliveryTask> activeTasks = [];
bool isLoadingTasks = false;
final Set<int> _respondingOfferIds = {};
final Set<int> _busyTaskIds = {};
Timer? _pollTimer;
@override
void onInit() {
super.onInit();
_refreshAll();
_pollTimer = Timer.periodic(const Duration(seconds: 4), (_) => _refreshAll());
}
Future<void> _refreshAll() async {
if (isDeliveryModeEnabled) {
await _fetchPendingOffers();
}
await fetchActiveTasks();
}
Future<void> toggleDeliveryMode(bool enable) async {
isTogglingMode = true;
update();
final res = await FoodDeliveryService.toggleAvailability(enable);
isTogglingMode = false;
if (res.success) {
isDeliveryModeEnabled = enable;
if (!enable) pendingOffers = [];
} else {
mySnackbarWarning(res.message);
}
update();
}
Future<void> _fetchPendingOffers() async {
final res = await FoodDeliveryService.getPendingOffers();
if (res.success) {
pendingOffers = res.data ?? [];
update();
}
}
Future<void> fetchActiveTasks() async {
isLoadingTasks = true;
final res = await FoodDeliveryService.getActiveTasks();
isLoadingTasks = false;
if (res.success) activeTasks = res.data ?? [];
update();
}
bool isRespondingToOffer(int orderId) => _respondingOfferIds.contains(orderId);
bool isTaskBusy(int orderId) => _busyTaskIds.contains(orderId);
Future<void> respondToOffer(int orderId, bool accept) async {
if (_respondingOfferIds.contains(orderId)) return;
_respondingOfferIds.add(orderId);
update();
final res = await FoodDeliveryService.respondToOffer(orderId, accept);
_respondingOfferIds.remove(orderId);
pendingOffers.removeWhere((o) => o.orderId == orderId);
if (!res.success) {
mySnackbarWarning(res.message);
} else if (accept) {
mySnackbarSuccess('تم قبول طلب التوصيل');
}
update();
await fetchActiveTasks();
}
Future<void> markPickedUp(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markPickedUp(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم استلام الطلب من المطعم');
await fetchActiveTasks();
} else {
mySnackbarWarning(res.message);
}
update();
}
Future<void> markDelivered(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markDelivered(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم تسليم الطلب بنجاح');
await fetchActiveTasks();
} else {
mySnackbarWarning(res.message);
}
update();
}
@override
void onClose() {
_pollTimer?.cancel();
super.onClose();
}
}
@@ -0,0 +1,92 @@
// food_delivery_models.dart — نماذج بيانات وحدة التوصيل (جهة السائق)
class FoodDeliveryTask {
final int id;
final String status; // courier_assigned | picked_up
final int merchantId;
final String merchantNameAr;
final double? merchantLat;
final double? merchantLng;
final String? merchantAddress;
final int deliveryFee;
final String? deliveryAddress;
final double deliveryLat;
final double deliveryLng;
final DateTime? createdAt;
FoodDeliveryTask({
required this.id,
required this.status,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
required this.deliveryLat,
required this.deliveryLng,
this.merchantLat,
this.merchantLng,
this.merchantAddress,
this.deliveryAddress,
this.createdAt,
});
factory FoodDeliveryTask.fromJson(Map<String, dynamic> j) => FoodDeliveryTask(
id: int.tryParse(j['id'].toString()) ?? 0,
status: j['status']?.toString() ?? '',
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
merchantAddress: j['merchant_address']?.toString(),
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
deliveryAddress: j['delivery_address']?.toString(),
deliveryLat: double.tryParse(j['delivery_lat']?.toString() ?? '0') ?? 0,
deliveryLng: double.tryParse(j['delivery_lng']?.toString() ?? '0') ?? 0,
createdAt: DateTime.tryParse(j['created_at']?.toString() ?? ''),
);
}
// عرض توصيل وارد — من polling على food/courier/pending_offers.php
class FoodDeliveryOffer {
final int orderId;
final int merchantId;
final String merchantNameAr;
final double? merchantLat;
final double? merchantLng;
final int deliveryFee;
final DateTime? offeredAt;
FoodDeliveryOffer({
required this.orderId,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
this.merchantLat,
this.merchantLng,
this.offeredAt,
});
factory FoodDeliveryOffer.fromJson(Map<String, dynamic> j) => FoodDeliveryOffer(
orderId: int.tryParse(j['order_id'].toString()) ?? 0,
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
offeredAt: DateTime.tryParse(j['offered_at']?.toString() ?? ''),
);
// مهلة العرض 20 ثانية من الخادم (SET NX EX 20) — عدّاد تقريبي للعرض فقط،
// الخادم هو الحكم الفعلي (رفض offer_respond إن انتهت المهلة فعلاً).
int get secondsRemaining {
if (offeredAt == null) return 20;
final elapsed = DateTime.now().difference(offeredAt!).inSeconds;
return (20 - elapsed).clamp(0, 20);
}
}
const int foodCurrencyDivisor = 1000;
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
final decimal = smallestUnit / foodCurrencyDivisor;
return '${decimal.toStringAsFixed(3)} $currencySymbol';
}
@@ -0,0 +1,89 @@
// food_delivery_service.dart — طبقة الاتصال بـ backend/food (جهة السائق/الموصّل)
import '../functions/crud.dart';
import '../../constant/links.dart';
import 'food_delivery_models.dart';
class FoodDeliveryApiResult<T> {
final bool success;
final T? data;
final String message;
FoodDeliveryApiResult(this.success, this.data, this.message);
}
class FoodDeliveryService {
static String get _base => '${AppLink.server}/food';
static Future<FoodDeliveryApiResult<bool>> toggleAvailability(bool enabled) async {
final res = await CRUD().post(
link: '$_base/courier/toggle_availability.php',
payload: {'enabled': enabled.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, enabled, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryTask>>> getActiveTasks() async {
final res = await CRUD().post(link: '$_base/courier/active.php');
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) => FoodDeliveryTask.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodDeliveryTask>[];
return FoodDeliveryApiResult(true, list, 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryOffer>>> getPendingOffers() async {
final res = await CRUD().post(link: '$_base/courier/pending_offers.php');
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['offers'] is List)
? (msg['offers'] as List)
.map((o) => FoodDeliveryOffer.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodDeliveryOffer>[];
return FoodDeliveryApiResult(true, list, 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<String>> respondToOffer(int orderId, bool accept) async {
final res = await CRUD().post(
link: '$_base/courier/offer_respond.php',
payload: {'order_id': orderId.toString(), 'response': accept ? 'accept' : 'reject'},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
return FoodDeliveryApiResult(true, (msg is Map ? msg['response']?.toString() : null) ?? '', 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<void>> markPickedUp(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/picked_up.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<void>> markDelivered(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/delivered.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
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 'حدث خطأ، حاول مجدداً';
}
}
@@ -67,8 +67,8 @@ class AppUpdateController extends GetxController {
void _showStoreUpdateDialog() {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.siro_driver'
: 'https://apps.apple.com/jo/app/siro-driver/id6482995159';
? AppLink.driverAppStoreAndroid
: AppLink.driverAppStoreIOS;
Get.defaultDialog(
title: "تحديث جديد متوفر".tr,
+56 -40
View File
@@ -22,7 +22,6 @@ import 'ssl_pinning.dart';
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);
@@ -107,6 +106,18 @@ class CRUD {
}
}
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginDriverController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
// ═══════════════════════════════════════════════════════════════
// _makeRequest — دالة مركزية لكل الطلبات
// ───────────────────────────────────────────────────────────────
@@ -119,6 +130,7 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
const totalTimeout = Duration(seconds: 60);
@@ -186,19 +198,33 @@ class CRUD {
}
}
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES-$requestId] 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(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).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,
);
}
// 5xx
@@ -218,16 +244,11 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
String token = await _ensureJwt();
if (token.isEmpty) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
@@ -247,19 +268,11 @@ class CRUD {
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
bool allowRefresh = true,
}) async {
try {
// فحص صلاحية التوكن قبل الإرسال
String token = await _getJwt();
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final String token = await _ensureJwt();
var url = Uri.parse(link);
var response = await _client.post(
@@ -280,15 +293,18 @@ class CRUD {
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else if (response.statusCode == 401) {
if (!_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
// (allowRefresh: false تمنع أي تكرار إضافي) — نفس سلوك _makeRequest.
if (!allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
return await get(
link: link,
payload: payload,
allowRefresh: false,
);
} else if (response.statusCode >= 500) {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
url.toString());
@@ -48,8 +48,8 @@ Future<String> getPackageInfo() async {
void showUpdateDialog(BuildContext context) {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.siro_driver'
: 'https://apps.apple.com/jo/app/siro-driver/id6482995159';
? AppLink.driverAppStoreAndroid
: AppLink.driverAppStoreIOS;
showGeneralDialog(
context: context,
@@ -137,7 +137,7 @@ class ReferralController extends GetxController {
String get shareMessage {
final appName = 'Siro';
return 'Join $appName as a driver! Use my code: $referralCode\nDownload: https://siro.app/driver?ref=$referralCode';
return 'Join $appName as a driver! Use my code: $referralCode\n${AppLink.driverDownloadLinks}';
}
String get shareMessagePassenger {
@@ -22,10 +22,8 @@ class RatingController extends GetxController {
void _redirectToAppStore() async {
// URLs for App Store and Google Play Store
const appStoreUrl =
'https://apps.apple.com/st/app/siro-driver/id6482995159';
const playStoreUrl =
'https://play.google.com/store/apps/details?id=com.siro_driver';
const appStoreUrl = AppLink.driverAppStoreIOS;
const playStoreUrl = AppLink.driverAppStoreAndroid;
final url = GetPlatform.isIOS ? appStoreUrl : playStoreUrl;
if (await launchUrl(Uri.parse(url))) {
@@ -229,7 +229,7 @@ class InviteScreen extends StatelessWidget {
final appName = 'Siro';
final isDriver = controller.selectedTab == 0;
final shareText = isDriver
? 'Join $appName as a driver! Use my referral code: ${rc.referralCode}\nDownload: ${AppLink.inviteRedirectUrl}?code=${rc.referralCode}&app=driver\n\n💡 Note: If the link is not clickable, save this number or reply to activate links!'
? 'Join $appName as a driver! Use my referral code: ${rc.referralCode}\n${AppLink.driverDownloadLinks}\n\n💡 Note: If the link is not clickable, save this number or reply to activate links!'
: 'Get a ride with $appName! Use my referral code: ${rc.referralCode} for a discount.\nDownload: ${AppLink.inviteRedirectUrl}?code=${rc.referralCode}&app=rider\n\n💡 Note: If the link is not clickable, save this number or reply to activate links!';
Share.share(shareText);
},
@@ -0,0 +1,235 @@
// food_delivery_home_page.dart — تبويب التوصيل: تفعيل وضع التوصيل، عروض واردة، مهام نشطة
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/food_delivery/food_delivery_controller.dart';
import '../../controller/food_delivery/food_delivery_models.dart';
import '../widgets/elevated_btn.dart';
import '../widgets/my_scafold.dart';
class FoodDeliveryHomePage extends StatelessWidget {
const FoodDeliveryHomePage({super.key});
@override
Widget build(BuildContext context) {
Get.put(FoodDeliveryController());
return GetBuilder<FoodDeliveryController>(
builder: (c) => MyScafolld(
title: 'Delivery'.tr,
isleading: true,
body: [
Column(
children: [
_modeToggleBar(c),
if (c.pendingOffers.isNotEmpty) _offersSection(c),
Expanded(
child: RefreshIndicator(
onRefresh: c.fetchActiveTasks,
child: c.isLoadingTasks && c.activeTasks.isEmpty
? const Center(child: CircularProgressIndicator())
: c.activeTasks.isEmpty
? _emptyState(c)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: c.activeTasks.length,
itemBuilder: (_, i) => _taskCard(c, c.activeTasks[i]),
),
),
),
],
),
],
),
);
}
Widget _modeToggleBar(FoodDeliveryController c) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: AppColor.cardColor,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColor.borderColor),
),
child: Row(
children: [
Icon(
c.isDeliveryModeEnabled ? Icons.delivery_dining_rounded : Icons.moped_outlined,
color: c.isDeliveryModeEnabled ? AppColor.greenColor : AppColor.grayColor,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Delivery Mode'.tr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
Text(
c.isDeliveryModeEnabled
? 'You will receive delivery offers'.tr
: 'Turn on to receive delivery offers'.tr,
style: AppStyle.subtitle,
),
],
),
),
c.isTogglingMode
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: Switch(
value: c.isDeliveryModeEnabled,
activeColor: AppColor.greenColor,
onChanged: (v) => c.toggleDeliveryMode(v),
),
],
),
);
}
Widget _offersSection(FoodDeliveryController c) {
return SizedBox(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: c.pendingOffers.length,
itemBuilder: (_, i) => _offerCard(c, c.pendingOffers[i]),
),
);
}
Widget _offerCard(FoodDeliveryController c, FoodDeliveryOffer offer) {
final isResponding = c.isRespondingToOffer(offer.orderId);
return Container(
width: 260,
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColor.primaryColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 12, offset: const Offset(0, 4))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.notifications_active_rounded, color: Colors.white, size: 18),
const SizedBox(width: 6),
Expanded(
child: Text('New Delivery Offer'.tr,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
),
Text('${offer.secondsRemaining}s', style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 6),
Text(offer.merchantNameAr,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15),
maxLines: 1, overflow: TextOverflow.ellipsis),
Text(foodFormatPrice(offer.deliveryFee),
style: const TextStyle(color: Colors.white70, fontSize: 13)),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 36,
child: isResponding
? const Center(child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)))
: OutlinedButton(
style: OutlinedButton.styleFrom(side: const BorderSide(color: Colors.white54)),
onPressed: () => c.respondToOffer(offer.orderId, false),
child: Text('Reject'.tr, style: const TextStyle(color: Colors.white70, fontSize: 12)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 36,
child: isResponding
? const SizedBox.shrink()
: MyElevatedButton(
title: 'Accept'.tr,
kolor: AppColor.greenColor,
onPressed: () => c.respondToOffer(offer.orderId, true),
),
),
),
],
),
],
),
);
}
Widget _emptyState(FoodDeliveryController c) {
return ListView(
children: [
const SizedBox(height: 100),
Icon(Icons.moped_outlined, size: 72, color: AppColor.grayColor),
const SizedBox(height: 16),
Center(child: Text('No delivery tasks right now'.tr, style: AppStyle.title)),
],
);
}
Widget _taskCard(FoodDeliveryController c, FoodDeliveryTask task) {
final isPickedUp = task.status == 'picked_up';
final isBusy = c.isTaskBusy(task.id);
return Card(
margin: const EdgeInsets.only(bottom: 14),
color: AppColor.cardColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: isPickedUp ? AppColor.greenColor : AppColor.borderColor, width: isPickedUp ? 1.5 : 1),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(isPickedUp ? Icons.location_on_rounded : Icons.storefront_rounded,
color: AppColor.accentColor),
const SizedBox(width: 8),
Expanded(
child: Text(
isPickedUp ? (task.deliveryAddress ?? '') : task.merchantNameAr,
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(foodFormatPrice(task.deliveryFee), style: AppStyle.subtitle),
],
),
const SizedBox(height: 4),
Text(
isPickedUp ? 'Deliver to customer'.tr : (task.merchantAddress ?? ''),
style: AppStyle.subtitle,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 44,
child: isBusy
? const Center(child: CircularProgressIndicator())
: MyElevatedButton(
title: isPickedUp ? 'Mark Delivered'.tr : 'Picked Up from Restaurant'.tr,
kolor: isPickedUp ? AppColor.greenColor : AppColor.accentColor,
onPressed: () => isPickedUp ? c.markDelivered(task.id) : c.markPickedUp(task.id),
),
),
],
),
),
);
}
}
@@ -33,6 +33,7 @@ import '../About Us/video_page.dart';
import '../assurance_health_page.dart';
import '../maintain_center_page.dart';
import '../../../transit/transit_driver_home_page.dart';
import '../../../food_delivery/food_delivery_home_page.dart';
// 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة
class DrawerItem {
@@ -61,6 +62,11 @@ class AppDrawer extends StatelessWidget {
// icon: Icons.directions_bus_filled_rounded,
// color: Colors.teal,
// onTap: () => Get.to(() => const TransitDriverHomePage())),
DrawerItem(
title: 'Delivery'.tr,
icon: Icons.delivery_dining_rounded,
color: Colors.deepOrange,
onTap: () => Get.to(() => const FoodDeliveryHomePage())),
DrawerItem(
title: 'Balance'.tr,
icon: Icons.account_balance_wallet,
+6 -6
View File
@@ -1521,10 +1521,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
@@ -1537,10 +1537,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -2125,10 +2125,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.9"
timezone:
dependency: transitive
description: