Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
Reference in New Issue
Block a user