Update: 2026-06-26 04:04:03

This commit is contained in:
Hamza-Ayed
2026-06-26 04:04:04 +03:00
parent aea0c8e44e
commit da9e6eb981
10 changed files with 325 additions and 112 deletions

View File

@@ -373,6 +373,7 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
// 2. Validate Promo Code // 2. Validate Promo Code
$discount = 0; $discount = 0;
$promo_applied = false;
if (!empty($promo_code)) { if (!empty($promo_code)) {
$sqlPromo = "SELECT amount FROM `promos` $sqlPromo = "SELECT amount FROM `promos`
WHERE promo_code = :promo_code WHERE promo_code = :promo_code
@@ -384,10 +385,31 @@ if (!empty($promo_code)) {
':promo_code' => $promo_code, ':promo_code' => $promo_code,
':passenger_id' => $passenger_id ':passenger_id' => $passenger_id
]); ]);
if ($stmtPromo->rowCount() > 0) {
// ✅ FIX P4: إذا لم يُوجد الكود أو كان منتهي الصلاحية → failure فوراً
if ($stmtPromo->rowCount() === 0) {
echo json_encode([
'status' => 'failure',
'message' => 'Promo code not found or has expired',
'applied_discount' => 0,
]);
exit;
}
$promoData = $stmtPromo->fetch(PDO::FETCH_ASSOC); $promoData = $stmtPromo->fetch(PDO::FETCH_ASSOC);
$discount = (float) $promoData['amount']; $discount = (float) $promoData['amount'];
// ✅ FIX P4: إذا كان الخصم صفر → failure مع رسالة واضحة
if ($discount <= 0) {
echo json_encode([
'status' => 'failure',
'message' => 'This promo code has no discount value',
'applied_discount' => 0,
]);
exit;
} }
$promo_applied = true;
} }
// 3. Fetch Passenger Wallet (Negative Balance / Debt) // 3. Fetch Passenger Wallet (Negative Balance / Debt)

View File

@@ -139,18 +139,10 @@ if (!isset($tokenData['prices'][$carType])) {
exit; exit;
} }
// ✅ FIX H-05: التحقق من distance و duration في الـ token أيضاً // ✅ FIX P2: تم حذف التحقق من distance و duration
if (isset($tokenData['distance']) && $tokenData['distance'] != $distance) { // السبب: token['distance'] هو الإحداثيات بينما $distance هو المسافة بالكيلومتر (0.x)
error_log("[add_ride] Security failed — distance mismatch."); // وtoken['duration'] هو الثواني بينما $duration_text هو الدقائق — mismatch دائم يكسر جميع الرحلات
printFailure("Tampered ride data (distance mismatch)"); // الإحداثيات كافية للتحقق من سلامة الطلب عبر coordsMatch() أعلاه
exit;
}
if (isset($tokenData['duration']) && $tokenData['duration'] != $duration_text) {
error_log("[add_ride] Security failed — duration mismatch.");
printFailure("Tampered ride data (duration mismatch)");
exit;
}
// Securely override pricing from the cryptographically signed token // Securely override pricing from the cryptographically signed token
$price = $tokenData['prices'][$carType]['price']; $price = $tokenData['prices'][$carType]['price'];

View File

@@ -1,7 +1,6 @@
<?php <?php
require_once __DIR__ . '/../connect.php'; require_once __DIR__ . '/../connect.php';
// استلام الرقم وتشفيره
$phone = filterRequest("phone"); $phone = filterRequest("phone");
$phoneEncrypted = $encryptionHelper->encryptData($phone); $phoneEncrypted = $encryptionHelper->encryptData($phone);
@@ -26,12 +25,7 @@ $sql = "SELECT
COALESCE(r.rideTimeFinish, '1970-01-01 00:00:00') AS ride_time_finish, COALESCE(r.rideTimeFinish, '1970-01-01 00:00:00') AS ride_time_finish,
COALESCE(r.price_for_driver, 0) AS price_for_driver, COALESCE(r.price_for_driver, 0) AS price_for_driver,
COALESCE(r.price_for_passenger, 0) AS price_for_passenger, COALESCE(r.price_for_passenger, 0) AS price_for_passenger,
COALESCE(r.distance, 0) AS distance, COALESCE(r.distance, 0) AS distance
0 AS passenger_wallet_balance,
0 AS passenger_payment_amount,
'' AS passenger_payment_method,
0 AS driver_payment_amount,
'' AS driver_payment_method
FROM FROM
passengers p passengers p
LEFT JOIN LEFT JOIN
@@ -42,7 +36,6 @@ LEFT JOIN
ORDER BY date DESC, time DESC ORDER BY date DESC, time DESC
LIMIT 1 LIMIT 1
) )
WHERE WHERE
p.phone = :phone"; p.phone = :phone";
@@ -53,7 +46,20 @@ $stmt->execute();
if ($stmt->rowCount() > 0) { if ($stmt->rowCount() > 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك التشفير للحقول الحساسة $stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: ["country" => "Jordan"];
$country = $kazan['country'] ?? 'Jordan';
$walletServer = "https://walletintaleq.intaleq.xyz";
if (strtolower($country) == 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
} elseif (strtolower($country) == 'egypt') {
$walletServer = getenv('WALLET_SERVER_EGYPT') ?: "https://walletintaleq.intaleq.xyz";
} else {
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://walletintaleq.intaleq.xyz";
}
foreach ($rows as &$row) { foreach ($rows as &$row) {
if (isset($row['phone'])) $row['phone'] = $encryptionHelper->decryptData($row['phone']); if (isset($row['phone'])) $row['phone'] = $encryptionHelper->decryptData($row['phone']);
if (isset($row['email'])) $row['email'] = $encryptionHelper->decryptData($row['email']); if (isset($row['email'])) $row['email'] = $encryptionHelper->decryptData($row['email']);
@@ -64,7 +70,37 @@ if ($stmt->rowCount() > 0) {
if (isset($row['last_name'])) $row['last_name'] = $encryptionHelper->decryptData($row['last_name']); if (isset($row['last_name'])) $row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
if (isset($row['employmentType']))$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']); if (isset($row['employmentType']))$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
if (isset($row['maritalStatus'])) $row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']); if (isset($row['maritalStatus'])) $row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
unset($r['password']); unset($row['password']);
$passenger_id = $row['id'] ?? '';
if (!empty($passenger_id)) {
$walletUrl = "$walletServer/v2/main/ride/passengerWallet/get_s2s_wallet.php";
$ch = curl_init($walletUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(["passenger_id" => $passenger_id]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY')
]
]);
$s2sRes = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$totalWallet = 0.0;
if ($httpCode === 200 && $s2sRes) {
$resDecoded = json_decode($s2sRes, true);
if ($resDecoded && isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
$totalWallet = (float)($resDecoded['message']['totalWallet'] ?? 0.0);
}
}
$row['passenger_wallet_balance'] = $totalWallet;
} else {
$row['passenger_wallet_balance'] = 0;
}
} }
jsonSuccess($rows); jsonSuccess($rows);

View File

@@ -482,7 +482,19 @@ class LocationSearchController extends GetxController {
bool _pendingGeocode = true; bool _pendingGeocode = true;
void updateCurrentLocationFromCamera(LatLng target) { void updateCurrentLocationFromCamera(LatLng target) {
Log.print('📍 updateCurrentLocationFromCamera: $target'); // ✅ FIX P3: Guard — تجاهل التحديثات إذا كانت المسافة < 20 متر
final double distanceDelta = Geolocator.distanceBetween(
newMyLocation.latitude,
newMyLocation.longitude,
target.latitude,
target.longitude,
);
if (distanceDelta < 20.0) {
Log.print('📍 updateCurrentLocationFromCamera: Skipped (المسافة ${distanceDelta.toStringAsFixed(1)}م < 20م)');
return;
}
Log.print('📍 updateCurrentLocationFromCamera: $target (تغيير ${distanceDelta.toStringAsFixed(1)}م)');
newMyLocation = target; newMyLocation = target;
if (_pendingGeocode) { if (_pendingGeocode) {
@@ -522,7 +534,16 @@ class LocationSearchController extends GetxController {
void onCameraMoveThrottled(CameraPosition pos) { void onCameraMoveThrottled(CameraPosition pos) {
_camThrottle?.cancel(); _camThrottle?.cancel();
_camThrottle = Timer(const Duration(milliseconds: 160), () { _camThrottle = Timer(const Duration(milliseconds: 160), () {
Log.print('📸 onCameraMoveThrottled: ${pos.target}'); // ✅ FIX P3: Guard — تجاهل التحديث إذا كان التغيير ضئيلاً (< 20م)
final double distanceDelta = Geolocator.distanceBetween(
newMyLocation.latitude,
newMyLocation.longitude,
pos.target.latitude,
pos.target.longitude,
);
if (distanceDelta < 20.0) return;
Log.print('📸 onCameraMoveThrottled: ${pos.target} (تغيير ${distanceDelta.toStringAsFixed(1)}م)');
int waypointsLength = Get.find<WayPointController>().wayPoints.length; int waypointsLength = Get.find<WayPointController>().wayPoints.length;
int index = wayPointIndex; int index = wayPointIndex;
if (waypointsLength > 0 && index < placesCoordinate.length) { if (waypointsLength > 0 && index < placesCoordinate.length) {

View File

@@ -1451,11 +1451,27 @@ class RideLifecycleController extends GetxController {
"has_steps": Get.find<WayPointController>().wayPoints.length > 1 "has_steps": Get.find<WayPointController>().wayPoints.length > 1
? 'true' ? 'true'
: 'false', : 'false',
"step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "", // ✅ FIX P5: إرسال step0-4 فقط عندما has_steps == true
"step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "", "step0": Get.find<WayPointController>().wayPoints.length > 1 &&
"step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "", placesCoordinate.length > 0
"step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "", ? placesCoordinate[0]
"step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "", : "",
"step1": Get.find<WayPointController>().wayPoints.length > 1 &&
placesCoordinate.length > 1
? placesCoordinate[1]
: "",
"step2": Get.find<WayPointController>().wayPoints.length > 1 &&
placesCoordinate.length > 2
? placesCoordinate[2]
: "",
"step3": Get.find<WayPointController>().wayPoints.length > 1 &&
placesCoordinate.length > 3
? placesCoordinate[3]
: "",
"step4": Get.find<WayPointController>().wayPoints.length > 1 &&
placesCoordinate.length > 4
? placesCoordinate[4]
: "",
"price_token": priceToken, "price_token": priceToken,
}; };
@@ -1796,15 +1812,30 @@ class RideLifecycleController extends GetxController {
if (!promoFormKey.currentState!.validate()) return; if (!promoFormKey.currentState!.validate()) return;
try { try {
final String pricingPassengerLat =
mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.first.latitude.toString()
: newMyLocation.latitude.toString();
final String pricingPassengerLng =
mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.first.longitude.toString()
: newMyLocation.longitude.toString();
final String pricingDestLat = mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.last.latitude.toString()
: myDestination.latitude.toString();
final String pricingDestLng = mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.last.longitude.toString()
: myDestination.longitude.toString();
final res = await CRUD().post(link: AppLink.getPrices, payload: { final res = await CRUD().post(link: AppLink.getPrices, payload: {
'distance': distance.toString(), 'distance': distance.toString(),
'durationToRide': durationToRide.toString(), 'durationToRide': durationToRide.toString(),
'startNameAddress': startNameAddress, 'startNameAddress': startNameAddress,
'endNameAddress': endNameAddress, 'endNameAddress': endNameAddress,
'destLat': myDestination.latitude.toString(), 'destLat': pricingDestLat,
'destLng': myDestination.longitude.toString(), 'destLng': pricingDestLng,
'passengerLat': newMyLocation.latitude.toString(), 'passengerLat': pricingPassengerLat,
'passengerLng': newMyLocation.longitude.toString(), 'passengerLng': pricingPassengerLng,
'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0', 'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0',
'activeMenuWaypointCount': activeMenuWaypointCount.toString(), 'activeMenuWaypointCount': activeMenuWaypointCount.toString(),
'promo_code': promo.text, 'promo_code': promo.text,
@@ -1833,6 +1864,56 @@ class RideLifecycleController extends GetxController {
totalPassengerRayehGaiBalash = totalPassengerRayehGaiBalash =
data['totalPassengerRayehGaiBalash']?.toString() ?? '0'; data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
priceToken = res['price_token']?.toString() ?? '';
num appliedDiscount = (res['applied_discount'] ?? 0) is num
? res['applied_discount'] ?? 0
: num.tryParse(res['applied_discount']?.toString() ?? '0') ?? 0;
// ✅ FIX P4: إظهار خطأ إذا كان الخصم صفر (برومو غير صالح)
if (appliedDiscount <= 0) {
Log.print('⚠️ Promo code returned zero discount. Showing error.');
MyDialog().getDialog(
'Invalid Promo Code'.tr,
'This promo code is invalid, expired, or does not apply to your account.'
.tr,
() => Get.back(),
);
return;
}
Log.print('💰 Promo discount applied: $appliedDiscount');
String carType = box.read(BoxName.carType) ?? '';
switch (carType) {
case 'Comfort':
totalPassenger = totalPassengerComfort;
break;
case 'Fixed Price':
totalPassenger = totalPassengerSpeed;
break;
case 'Electric':
totalPassenger = totalPassengerElectric;
break;
case 'Awfar Car':
totalPassenger = totalPassengerBalash;
break;
case 'Scooter':
case 'Pink Bike':
totalPassenger = totalPassengerScooter;
break;
case 'Van':
totalPassenger = totalPassengerVan;
break;
case 'Lady':
totalPassenger = totalPassengerLady;
break;
case 'Rayeh Gai':
totalPassenger = totalPassengerRayehGai;
break;
}
totalCostPassenger = totalPassenger;
promoTaken = true; promoTaken = true;
update(); update();
@@ -1872,15 +1953,30 @@ class RideLifecycleController extends GetxController {
newTime = currentTime.add(durationToAdd); newTime = currentTime.add(durationToAdd);
try { try {
final String pricingPassengerLat =
mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.first.latitude.toString()
: newMyLocation.latitude.toString();
final String pricingPassengerLng =
mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.first.longitude.toString()
: newMyLocation.longitude.toString();
final String pricingDestLat = mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.last.latitude.toString()
: myDestination.latitude.toString();
final String pricingDestLng = mapEngine.polylineCoordinates.isNotEmpty
? mapEngine.polylineCoordinates.last.longitude.toString()
: myDestination.longitude.toString();
final res = await CRUD().post(link: AppLink.getPrices, payload: { final res = await CRUD().post(link: AppLink.getPrices, payload: {
'distance': distance.toString(), 'distance': distance.toString(),
'durationToRide': durationToRide.toString(), 'durationToRide': durationToRide.toString(),
'startNameAddress': startNameAddress, 'startNameAddress': startNameAddress,
'endNameAddress': endNameAddress, 'endNameAddress': endNameAddress,
'destLat': myDestination.latitude.toString(), 'destLat': pricingDestLat,
'destLng': myDestination.longitude.toString(), 'destLng': pricingDestLng,
'passengerLat': newMyLocation.latitude.toString(), 'passengerLat': pricingPassengerLat,
'passengerLng': newMyLocation.longitude.toString(), 'passengerLng': pricingPassengerLng,
'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0', 'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0',
'activeMenuWaypointCount': activeMenuWaypointCount.toString(), 'activeMenuWaypointCount': activeMenuWaypointCount.toString(),
'passenger_id': box.read(BoxName.passengerID) ?? '', 'passenger_id': box.read(BoxName.passengerID) ?? '',
@@ -2140,7 +2236,8 @@ class RideLifecycleController extends GetxController {
if (statusRide == 'Begin' || if (statusRide == 'Begin' ||
currentRideState.value == RideState.inProgress) { currentRideState.value == RideState.inProgress) {
polyLines = { polyLines = {
...polyLines.where((p) => !p.polylineId.value.startsWith('driver_route')), ...polyLines
.where((p) => !p.polylineId.value.startsWith('driver_route')),
Polyline( Polyline(
polylineId: const PolylineId('main_route'), polylineId: const PolylineId('main_route'),
points: remainingPoints, points: remainingPoints,
@@ -2190,8 +2287,7 @@ class RideLifecycleController extends GetxController {
payload: {'driver_id': driverId}); payload: {'driver_id': driverId});
if (res != 'failure' && res is Map) { if (res != 'failure' && res is Map) {
if (res['message'] != null && if (res['message'] != null && (res['message'] as List).isNotEmpty) {
(res['message'] as List).isNotEmpty) {
var _data = (res['message'] as List)[0]; var _data = (res['message'] as List)[0];
LatLng newDriverPos = LatLng( LatLng newDriverPos = LatLng(
@@ -2225,8 +2321,7 @@ class RideLifecycleController extends GetxController {
} }
} }
mapEngine.clearMarkersExceptStartEndAndDriver(); mapEngine.clearMarkersExceptStartEndAndDriver();
reloadMarkerDriverCarsLocationToPassengerAfterApplied( reloadMarkerDriverCarsLocationToPassengerAfterApplied(res);
res);
} }
} }
update(); update();
@@ -3393,18 +3488,39 @@ class RideLifecycleController extends GetxController {
hours = durationToAdd.inHours; hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round(); minutes = (durationToAdd.inMinutes % 60).round();
if (polyLines.isNotEmpty) mapEngine.clearPolyline();
rideConfirm = false;
isMarkersShown = true;
update();
await bottomSheet();
await mapEngine.playRouteAnimation(
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
// ✅ FIX P1: انتظار تحميل الأيقونات قبل إنشاء الـ markers
// intaleq_maps تتطلب صوراً مسجلة عبر addImage() وليس أصولاً مباشرة
if (!mapEngine.isIconsLoaded) {
Log.print('⏳ Waiting for map icons to load before placing markers...');
for (int i = 0; i < 20 && !mapEngine.isIconsLoaded; i++) {
await Future.delayed(const Duration(milliseconds: 200));
}
}
// ✅ FIX P1: استخدام fromStyleImage مع المعرّفات المسجلة مسبقاً
markers = { markers = {
Marker( Marker(
markerId: const MarkerId('start'), markerId: const MarkerId('start'),
position: startLoc, position: startLoc,
icon: InlqBitmap.fromStyleImage('orange_marker'), icon: InlqBitmap.fromStyleImage(mapEngine.startIcon),
infoWindow: const InfoWindow(title: 'A'), infoWindow: const InfoWindow(title: 'A'),
anchor: const Offset(0.5, 1.0), anchor: const Offset(0.5, 1.0),
), ),
Marker( Marker(
markerId: const MarkerId('end'), markerId: const MarkerId('end'),
position: endLoc, position: endLoc,
icon: InlqBitmap.fromStyleImage('violet_marker'), icon: InlqBitmap.fromStyleImage(mapEngine.endIcon),
infoWindow: const InfoWindow(title: 'B'), infoWindow: const InfoWindow(title: 'B'),
anchor: const Offset(0.5, 1.0), anchor: const Offset(0.5, 1.0),
), ),
@@ -3418,7 +3534,7 @@ class RideLifecycleController extends GetxController {
markerId: MarkerId('waypoint_$i'), markerId: MarkerId('waypoint_$i'),
position: wp, position: wp,
icon: InlqBitmap.fromStyleImage( icon: InlqBitmap.fromStyleImage(
isFirstWaypoint ? 'orange_marker' : 'violet_marker'), isFirstWaypoint ? mapEngine.startIcon : mapEngine.endIcon),
infoWindow: infoWindow:
InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'),
anchor: const Offset(0.5, 1.0), anchor: const Offset(0.5, 1.0),
@@ -3426,16 +3542,8 @@ class RideLifecycleController extends GetxController {
} }
} }
if (polyLines.isNotEmpty) mapEngine.clearPolyline(); Log.print('✅ FIX P1: Markers placed — start: $startLoc, end: $endLoc');
rideConfirm = false;
isMarkersShown = true;
update(); update();
await bottomSheet();
await mapEngine.playRouteAnimation(
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
} catch (e, stackTrace) { } catch (e, stackTrace) {
if (isDrawingRoute) { if (isDrawingRoute) {
isDrawingRoute = false; isDrawingRoute = false;
@@ -4041,16 +4149,19 @@ class RideLifecycleController extends GetxController {
// We intentionally DO NOT initialize data here during onInit // We intentionally DO NOT initialize data here during onInit
// because this controller is instantiated globally before the map is opened. // because this controller is instantiated globally before the map is opened.
// Initialization will be triggered by MapPagePassenger calling initializeDataAfterLogin() // Initialization will be triggered by MapPagePassenger calling initializeDataAfterLogin()
Log.print("RideLifecycleController.onInit: Waiting for MapPagePassenger to trigger initialization."); Log.print(
"RideLifecycleController.onInit: Waiting for MapPagePassenger to trigger initialization.");
} }
Future<void> initializeDataAfterLogin() async { Future<void> initializeDataAfterLogin() async {
if (isDataInitializedAfterLogin) { if (isDataInitializedAfterLogin) {
Log.print("RideLifecycleController: Already initialized, skipping duplicate initializeDataAfterLogin."); Log.print(
"RideLifecycleController: Already initialized, skipping duplicate initializeDataAfterLogin.");
return; return;
} }
isDataInitializedAfterLogin = true; isDataInitializedAfterLogin = true;
Log.print("RideLifecycleController: Initializing data after successful login..."); Log.print(
"RideLifecycleController: Initializing data after successful login...");
getLocationArea(passengerLocation.latitude, passengerLocation.longitude); getLocationArea(passengerLocation.latitude, passengerLocation.longitude);
await _stagePricingAndState(); await _stagePricingAndState();
await _stageNiceToHave(); await _stageNiceToHave();

View File

@@ -137,7 +137,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
// ── Car Selection List ─────────────────────────────── // ── Car Selection List ───────────────────────────────
SizedBox( SizedBox(
height: 120, height: 134,
child: ListView.builder( child: ListView.builder(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -303,32 +303,32 @@ class CarDetailsTypeToChoose extends StatelessWidget {
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
width: 104, width: 122,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: isSelected gradient: isSelected
? LinearGradient( ? LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
AppColor.primaryColor.withAlpha(18), AppColor.primaryColor.withAlpha(22),
AppColor.primaryColor.withAlpha(8), AppColor.primaryColor.withAlpha(10),
], ],
) )
: null, : null,
color: isSelected ? null : AppColor.secondaryColor, color: isSelected ? null : AppColor.secondaryColor,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(18),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
? AppColor.primaryColor ? AppColor.primaryColor
: AppColor.grayColor.withOpacity(0.2), : AppColor.grayColor.withOpacity(0.25),
width: isSelected ? 2.0 : 1.0, width: isSelected ? 2.5 : 1.5,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: isSelected color: isSelected
? AppColor.primaryColor.withAlpha(40) ? AppColor.primaryColor.withAlpha(50)
: Colors.black.withAlpha(12), : Colors.black.withAlpha(16),
blurRadius: isSelected ? 12 : 4, blurRadius: isSelected ? 14 : 6,
offset: const Offset(0, 3), offset: const Offset(0, 3),
), ),
], ],
@@ -338,11 +338,11 @@ class CarDetailsTypeToChoose extends StatelessWidget {
// Selected indicator // Selected indicator
if (isSelected) if (isSelected)
Positioned( Positioned(
top: 4, top: 5,
right: 4, right: 5,
child: Container( child: Container(
width: 18, width: 22,
height: 18, height: 22,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
@@ -358,13 +358,13 @@ class CarDetailsTypeToChoose extends StatelessWidget {
), ),
], ],
), ),
child: const Icon(Icons.check, size: 11, color: Colors.white), child: const Icon(Icons.check, size: 13, color: Colors.white),
), ),
), ),
// Card content // Card content
Padding( Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 6, 6), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -374,11 +374,11 @@ class CarDetailsTypeToChoose extends StatelessWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: Image.asset( child: Image.asset(
carType.image, carType.image,
height: 44, height: 54,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 2),
// Car name // Car name
FittedBox( FittedBox(
@@ -388,7 +388,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontWeight: fontWeight:
isSelected ? FontWeight.w800 : FontWeight.w600, isSelected ? FontWeight.w800 : FontWeight.w600,
fontSize: 12, fontSize: 13,
color: isSelected color: isSelected
? AppColor.primaryColor ? AppColor.primaryColor
: AppColor.writeColor, : AppColor.writeColor,
@@ -397,12 +397,12 @@ class CarDetailsTypeToChoose extends StatelessWidget {
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 2),
// Price tag // Price tag
Container( Container(
padding: padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3), const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? AppColor.primaryColor ? AppColor.primaryColor
@@ -417,7 +417,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
child: Text( child: Text(
'${_getPassengerPriceText(carType, controller)} ${CurrencyHelper.currency}', '${_getPassengerPriceText(carType, controller)} ${CurrencyHelper.currency}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: isSelected color: isSelected
? Colors.white ? Colors.white
@@ -653,7 +653,6 @@ class CarDetailsTypeToChoose extends StatelessWidget {
if (controller.promoFormKey.currentState! if (controller.promoFormKey.currentState!
.validate()) { .validate()) {
controller.applyPromoCodeToPassenger(context); controller.applyPromoCodeToPassenger(context);
Navigator.of(context).pop();
} }
}, },
), ),

View File

@@ -65,13 +65,21 @@ class FirebaseMessagesController extends GetxController {
// : Get.put(NotificationController()); // : Get.put(NotificationController());
Future getToken() async { Future getToken() async {
fcmToken.getToken().then((token) { try {
// Log.print('fcmToken: ${token}'); final token = await fcmToken.getToken();
box.write(BoxName.tokenFCM, (token.toString())); if (token != null) {
}); box.write(BoxName.tokenFCM, token);
// 🔹 الاشتراك في topic }
await fcmToken.subscribeToTopic("service"); // أو "users" حسب نوع المستخدم } catch (e) {
Log.print('⚠️ getToken error (APNS not ready yet): $e');
}
try {
await fcmToken.subscribeToTopic("service");
Log.print("Subscribed to 'service' topic ✅"); Log.print("Subscribed to 'service' topic ✅");
} catch (e) {
Log.print('⚠️ subscribeToTopic error (APNS not ready yet): $e');
}
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// If the app is in the background or terminated, show a system tray message // If the app is in the background or terminated, show a system tray message

View File

@@ -221,7 +221,6 @@ class CRUD {
} }
if (sc == 401) { if (sc == 401) {
// استخدام SessionManager لتجديد الجلسة عند 401
if (Get.isRegistered<SessionManager>()) { if (Get.isRegistered<SessionManager>()) {
final sessionManager = Get.find<SessionManager>(); final sessionManager = Get.find<SessionManager>();
if (!sessionManager.isRefreshing.value && if (!sessionManager.isRefreshing.value &&
@@ -229,7 +228,6 @@ class CRUD {
await sessionManager.refreshSession(silent: false); await sessionManager.refreshSession(silent: false);
} }
} else { } else {
// fallback للتجديد القديم
await getJWT(); await getJWT();
} }
return 'token_expired'; return 'token_expired';
@@ -262,12 +260,10 @@ class CRUD {
if (Get.isRegistered<SessionManager>()) { if (Get.isRegistered<SessionManager>()) {
final sessionManager = Get.find<SessionManager>(); final sessionManager = Get.find<SessionManager>();
await sessionManager.refreshSession(silent: true); await sessionManager.refreshSession(silent: true);
token = r(box.read(BoxName.jwt) ?? '').toString().split(Env.addd)[0];
} else { } else {
// fallback: تجديد يدوي
await getJWT(); await getJWT();
token = r(box.read(BoxName.jwt) ?? '').toString().split(Env.addd)[0];
} }
token = r(box.read(BoxName.jwt) ?? '').toString().split(Env.addd)[0];
} }
// Initialize app signature if null // Initialize app signature if null
@@ -302,7 +298,7 @@ class CRUD {
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// getJWT — V1 Login Flow // getJWT — V1 Login Flow
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
Future<void> getJWT() async { Future<bool> getJWT() async {
var payload = { var payload = {
'fingerprint': _getFpHeader(), 'fingerprint': _getFpHeader(),
'password': box.read(BoxName.password) ?? '', 'password': box.read(BoxName.password) ?? '',
@@ -310,7 +306,6 @@ class CRUD {
'aud': 'service', 'aud': 'service',
}; };
// Initialize app signature if null
if (_appSignature == null) { if (_appSignature == null) {
try { try {
_appSignature = await SecurityHelper.getAppSignature(); _appSignature = await SecurityHelper.getAppSignature();
@@ -340,10 +335,11 @@ class CRUD {
await storage.write(key: BoxName.jwt, value: c(jwt)); await storage.write(key: BoxName.jwt, value: c(jwt));
if (hmac != null) { if (hmac != null) {
await box.write(BoxName.hmac, hmac); await box.write(BoxName.hmac, hmac);
final verify = box.read(BoxName.hmac);
Log.print('✅ Verified stored HMAC: $verify');
} }
return true;
} }
Log.print('❌ getJWT failed: $response');
return false;
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────

View File

@@ -79,18 +79,12 @@ class SessionManager extends GetxController {
for (int attempt = 1; attempt <= _maxRetries; attempt++) { for (int attempt = 1; attempt <= _maxRetries; attempt++) {
Log.print('🔄 Session refresh attempt $attempt/$_maxRetries'); Log.print('🔄 Session refresh attempt $attempt/$_maxRetries');
await CRUD().getJWT(); final success = await CRUD().getJWT();
// التحقق من نجاح التجديد if (success) {
final newRawToken = box.read(BoxName.jwt)?.toString() ?? '';
final newToken = newRawToken.isNotEmpty ? r(newRawToken).toString().split(Env.addd)[0] : '';
final isValid = CRUD.isJwtValid(newToken);
if (isValid) {
status.value = SessionStatus.valid; status.value = SessionStatus.valid;
isRefreshing.value = false; isRefreshing.value = false;
// إشعار النجاح
if (!silent) { if (!silent) {
_showSessionRefreshedNotification(); _showSessionRefreshedNotification();
} }
@@ -99,7 +93,6 @@ class SessionManager extends GetxController {
return true; return true;
} }
// إذا فشلت المحاولة، انتظر قبل إعادة المحاولة
if (attempt < _maxRetries) { if (attempt < _maxRetries) {
await Future.delayed(Duration(seconds: attempt)); await Future.delayed(Duration(seconds: attempt));
} }

View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/../../jwtconnect.php';
$providedKey = $_SERVER['HTTP_X_S2S_API_KEY'] ?? '';
if (empty($providedKey) || $providedKey !== getenv('S2S_SHARED_KEY')) {
http_response_code(401);
printFailure("Unauthorized: Invalid or missing X-S2S-Api-Key.");
exit;
}
$passenger_id = filterRequest("passenger_id");
if (empty($passenger_id)) {
printFailure("Missing required parameter: passenger_id");
exit;
}
try {
$stmtTotalWallet = $con->prepare("
SELECT COALESCE(SUM(balance), 0)
FROM `passengerWallet`
WHERE passenger_id = :passenger_id
");
$stmtTotalWallet->execute([':passenger_id' => $passenger_id]);
$totalWallet = (float)($stmtTotalWallet->fetchColumn() ?: 0.0);
printSuccess([
"totalWallet" => $totalWallet
]);
} catch (Exception $e) {
error_log("[get_s2s_passenger_wallet] " . $e->getMessage());
printFailure("An error occurred");
}
?>