14 Commits
Author SHA1 Message Date
Hamza-Ayed 16b4829a6d Update: 2026-08-06 22:54:35 2026-08-06 22:54:36 +03:00
Hamza-Ayed a31242f947 chore: update app icons, bump version, and refine iOS privacy usage descriptions 2026-08-04 19:54:34 +03:00
Hamza-Ayed cc9c5885b3 refactor: implement single-flight JWT renewal with rate-limiting cooldown and fix storage token handling in auth controllers 2026-08-02 01:39:21 +03:00
Hamza-Ayed c5f8c6cd8e Update: 2026-08-02 00:56:07 2026-08-02 00:56:07 +03:00
Hamza-Ayed cbaf638abf Update: 2026-08-01 23:53:33 2026-08-01 23:53:34 +03:00
Hamza-Ayed 940b69a554 Add seed_kazan.php script for pricing table 2026-08-01 23:01:16 +03:00
Hamza-Ayed 53158598e7 Update: 2026-08-01 19:24:16 2026-08-01 19:24:17 +03:00
Hamza-Ayed 86b5666595 Update: 2026-08-01 18:52:11 2026-08-01 18:52:12 +03:00
Hamza-Ayed b30fe7e96d Update: 2026-08-01 18:37:02 2026-08-01 18:37:02 +03:00
Hamza-Ayed f7df080b27 Fix payment server: remove global conRide to prevent crashing non-ride DB queries 2026-08-01 18:28:30 +03:00
Hamza-Ayed 998cad6756 Fix hardcoded jordan-siro URL in location server and origins 2026-08-01 18:27:52 +03:00
Hamza-Ayed ff91d95dba Fix database name overriding to prevent intaleq-ridesDB crash on server 2026-08-01 18:07:29 +03:00
Hamza-Ayed 77d7d9faf8 Fix JWT validation: use user_id instead of sub in socket servers, revert statusDriverLocation from toggleActive 2026-08-01 17:57:47 +03:00
Hamza-Ayed 1ddc6f8aa7 Fix payment server: remove global conRide to prevent crashing non-ride DB queries 2026-08-01 17:17:33 +03:00
87 changed files with 1223 additions and 360 deletions
+185
View File
@@ -0,0 +1,185 @@
<?php
// backend/bot/seed_kazan.php
// Populates the `kazan` table with default pricing for Jordan, Syria, and Egypt if missing
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try {
$con = Database::get('main');
echo "Connected to database.\n";
} catch (Exception $e) {
die("Database connection failed: " . $e->getMessage() . "\n");
}
$defaultPricing = [
[
'country' => 'Jordan',
'currency' => 'JOD',
'kazanPercent' => '10',
'fuelPrice' => '1.0',
'startPrice' => '0.35',
'speedPrice' => '0.22',
'comfortPrice' => '0.29',
'ladyPrice' => '0.25',
'electricPrice' => '0.26',
'vanPrice' => '0.33',
'deliveryPrice' => '0.20',
'mishwarVipPrice' => '0.31',
'fixedPrice' => '0.22',
'awfarPrice' => '0.19',
'normalMinPrice' => '0.05',
'peakMinPrice' => '0.06',
'lateMinPrice' => '0.05',
'adminId' => 'system'
],
[
'country' => 'JO',
'currency' => 'JOD',
'kazanPercent' => '10',
'fuelPrice' => '1.0',
'startPrice' => '0.35',
'speedPrice' => '0.22',
'comfortPrice' => '0.29',
'ladyPrice' => '0.25',
'electricPrice' => '0.26',
'vanPrice' => '0.33',
'deliveryPrice' => '0.20',
'mishwarVipPrice' => '0.31',
'fixedPrice' => '0.22',
'awfarPrice' => '0.19',
'normalMinPrice' => '0.05',
'peakMinPrice' => '0.06',
'lateMinPrice' => '0.05',
'adminId' => 'system'
],
[
'country' => 'Syria',
'currency' => 'SYP',
'kazanPercent' => '10',
'fuelPrice' => '12000',
'startPrice' => '1000',
'speedPrice' => '500',
'comfortPrice' => '650',
'ladyPrice' => '550',
'electricPrice' => '600',
'vanPrice' => '750',
'deliveryPrice' => '450',
'mishwarVipPrice' => '700',
'fixedPrice' => '500',
'awfarPrice' => '425',
'normalMinPrice' => '100',
'peakMinPrice' => '115',
'lateMinPrice' => '100',
'adminId' => 'system'
],
[
'country' => 'Egypt',
'currency' => 'EGP',
'kazanPercent' => '10',
'fuelPrice' => '15',
'startPrice' => '10',
'speedPrice' => '3.0',
'comfortPrice' => '4.0',
'ladyPrice' => '3.3',
'electricPrice' => '3.6',
'vanPrice' => '4.5',
'deliveryPrice' => '2.7',
'mishwarVipPrice' => '4.2',
'fixedPrice' => '3.0',
'awfarPrice' => '2.5',
'normalMinPrice' => '0.5',
'peakMinPrice' => '0.6',
'lateMinPrice' => '0.5',
'adminId' => 'system'
],
[
'country' => 'EG',
'currency' => 'EGP',
'kazanPercent' => '10',
'fuelPrice' => '15',
'startPrice' => '10',
'speedPrice' => '3.0',
'comfortPrice' => '4.0',
'ladyPrice' => '3.3',
'electricPrice' => '3.6',
'vanPrice' => '4.5',
'deliveryPrice' => '2.7',
'mishwarVipPrice' => '4.2',
'fixedPrice' => '3.0',
'awfarPrice' => '2.5',
'normalMinPrice' => '0.5',
'peakMinPrice' => '0.6',
'lateMinPrice' => '0.5',
'adminId' => 'system'
]
];
$sqlInsert = "INSERT INTO kazan (
country, currency, kazanPercent, fuelPrice, startPrice,
speedPrice, comfortPrice, ladyPrice, electricPrice, vanPrice,
deliveryPrice, mishwarVipPrice, fixedPrice, awfarPrice,
normalMinPrice, peakMinPrice, lateMinPrice, adminId
) VALUES (
:country, :currency, :kazanPercent, :fuelPrice, :startPrice,
:speedPrice, :comfortPrice, :ladyPrice, :electricPrice, :vanPrice,
:deliveryPrice, :mishwarVipPrice, :fixedPrice, :awfarPrice,
:normalMinPrice, :peakMinPrice, :lateMinPrice, :adminId
) ON DUPLICATE KEY UPDATE
currency = VALUES(currency),
startPrice = VALUES(startPrice),
speedPrice = VALUES(speedPrice),
comfortPrice = VALUES(comfortPrice),
ladyPrice = VALUES(ladyPrice),
electricPrice = VALUES(electricPrice),
vanPrice = VALUES(vanPrice),
deliveryPrice = VALUES(deliveryPrice),
mishwarVipPrice = VALUES(mishwarVipPrice),
fixedPrice = VALUES(fixedPrice),
awfarPrice = VALUES(awfarPrice),
normalMinPrice = VALUES(normalMinPrice),
peakMinPrice = VALUES(peakMinPrice),
lateMinPrice = VALUES(lateMinPrice)";
$stmt = $con->prepare($sqlInsert);
foreach ($defaultPricing as $row) {
try {
$stmt->execute($row);
echo "Successfully seeded/updated kazan pricing for: {$row['country']}\n";
} catch (Exception $e) {
echo "Error seeding kazan for {$row['country']}: " . $e->getMessage() . "\n";
}
}
echo "Kazan seeding completed.\n";
// Also ensure ride_chat_logs table exists in main DB and ride DB
$sqlCreateChat = "CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`ride_id` int NOT NULL,
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ride_id` (`ride_id`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;";
try {
$con->exec($sqlCreateChat);
echo "Successfully created/verified `ride_chat_logs` table in main DB.\n";
} catch (Exception $e) {
echo "Error creating `ride_chat_logs` in main DB: " . $e->getMessage() . "\n";
}
try {
$con_ride = Database::get('ride');
$con_ride->exec($sqlCreateChat);
echo "Successfully created/verified `ride_chat_logs` table in ride DB.\n";
} catch (Exception $e) {
echo "Error creating `ride_chat_logs` in ride DB: " . $e->getMessage() . "\n";
}
+5
View File
@@ -57,6 +57,11 @@ class Database
$user = getenv($cfg['user']); $user = getenv($cfg['user']);
$pass = getenv($cfg['pass']); $pass = getenv($cfg['pass']);
// Fallback for missing ride table if intaleq-ridesDB is still configured
if ($name === 'ride' && $dbname === 'intaleq-ridesDB') {
$dbname = 'rideDB';
}
if (!$dbname || !$user) { if (!$dbname || !$user) {
error_log("[FATAL] Database config missing for: $name (Check ENV keys: {$cfg['name']}, {$cfg['user']})"); error_log("[FATAL] Database config missing for: $name (Check ENV keys: {$cfg['name']}, {$cfg['user']})");
throw new RuntimeException("Database configuration error."); throw new RuntimeException("Database configuration error.");
+1 -1
View File
@@ -43,7 +43,7 @@ header("X-XSS-Protection: 1; mode=block");
// CORS مع التحقق من المصدر المسموح // CORS مع التحقق من المصدر المسموح
$envOrigins = array_map('trim', explode(',', getenv('CORS_ALLOWED_ORIGINS') ?: '')); $envOrigins = array_map('trim', explode(',', getenv('CORS_ALLOWED_ORIGINS') ?: ''));
$defaultOrigins = ['https://siromove.com', 'https://admin.siromove.com', 'https://jordan-siro.intaleqapp.com', 'http://localhost', 'http://127.0.0.1']; $defaultOrigins = ['https://siromove.com', 'https://admin.siromove.com', 'https://api.intaleqapp.com', 'http://localhost', 'http://127.0.0.1'];
$allowedOrigins = array_unique(array_merge($envOrigins, $defaultOrigins)); $allowedOrigins = array_unique(array_merge($envOrigins, $defaultOrigins));
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) { if (in_array($origin, $allowedOrigins)) {
+1 -1
View File
@@ -9,7 +9,7 @@ header('Content-Type: application/json');
// ✅ FIX H-03: allowlist صارم للـ Admin Origins // ✅ FIX H-03: allowlist صارم للـ Admin Origins
$allowedOrigins = array_filter([ $allowedOrigins = array_filter([
getenv('ALLOWED_ORIGIN') ?: 'https://siromove.com', getenv('ALLOWED_ORIGIN') ?: 'https://siromove.com',
'https://jordan-siro.intaleqapp.com', 'https://api.intaleqapp.com',
'http://localhost', 'http://localhost',
'http://127.0.0.1', 'http://127.0.0.1',
]); ]);
+71 -37
View File
@@ -53,48 +53,82 @@ try {
$stmt->execute([':id' => $id]); $stmt->execute([':id' => $id]);
$driver = $stmt->fetch(); $driver = $stmt->fetch();
if (!$driver || empty($driver['password'])) { if (!$driver) {
unauthorizedDriver(); unauthorizedDriver();
} }
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null; // ── مسار السائقين المسجلين عبر OTP (بدون password في DB) ──
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null; if (empty($driver['password'])) {
// التحقق من هوية السائق عبر fingerprint المخزن في driverToken
if (empty($decPhone)) { if (empty($fingerprint)) {
securityLog("LoginDriver failed: phone decryption returned null", [ securityLog("LoginDriver(OTP): no fingerprint sent", [
'driver_id' => $driver['id'] ?? 'unknown', 'driver_id' => $driver['id'],
]); ]);
unauthorizedDriver();
}
// ── المحاولة الأولى: طريقة جديدة (قيم خام) ─────────────
$newParts = [
$driver['id'],
trim($decPhone),
];
if (!empty($decNat)) {
$newParts[] = trim($decNat);
}
$newString = implode('|', $newParts);
$newSecret = hash_hmac('sha256', $newString, $pepper, true);
if (password_verify($newSecret, $driver['password'])) {
// ✅ صح - طريقة جديدة
} else {
// ── المحاولة الثانية: طريقة قديمة (قيم مشفرة) للتوافق ─
$oldParts = [
$driver['id'],
$driver['phone'],
];
if (!empty($driver['national_number'])) {
$oldParts[] = $driver['national_number'];
}
$oldString = implode('|', $oldParts);
$oldSecret = hash_hmac('sha256', $oldString, $pepper, true);
if (!password_verify($oldSecret, $driver['password'])) {
unauthorizedDriver(); unauthorizedDriver();
} }
$stmtFp = $con->prepare('SELECT fingerPrint FROM driverToken WHERE captain_id = :id LIMIT 1');
$stmtFp->execute([':id' => $driver['id']]);
$fpRow = $stmtFp->fetch();
if (!$fpRow || empty($fpRow['fingerPrint'])) {
securityLog("LoginDriver(OTP): no stored fingerprint found", [
'driver_id' => $driver['id'],
]);
unauthorizedDriver();
}
$fpPepper = getenv('FP_PEPPER') ?: '';
$expectedFp = !empty($fpPepper) ? hash('sha256', $fingerprint . $fpPepper) : $fingerprint;
if (!hash_equals($fpRow['fingerPrint'], $expectedFp)) {
securityLog("LoginDriver(OTP): fingerprint mismatch", [
'driver_id' => $driver['id'],
]);
unauthorizedDriver();
}
// ✅ تم التحقق عبر fingerprint — نتابع لتوليد JWT
} else {
// ── المسار التقليدي: التحقق عبر password + HMAC ──────────
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null;
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null;
if (empty($decPhone)) {
securityLog("LoginDriver failed: phone decryption returned null", [
'driver_id' => $driver['id'] ?? 'unknown',
]);
unauthorizedDriver();
}
// ── المحاولة الأولى: طريقة جديدة (قيم خام) ─────────────
$newParts = [
$driver['id'],
trim($decPhone),
];
if (!empty($decNat)) {
$newParts[] = trim($decNat);
}
$newString = implode('|', $newParts);
$newSecret = hash_hmac('sha256', $newString, $pepper, true);
if (password_verify($newSecret, $driver['password'])) {
// ✅ صح - طريقة جديدة
} else {
// ── المحاولة الثانية: طريقة قديمة (قيم مشفرة) للتوافق ─
$oldParts = [
$driver['id'],
$driver['phone'],
];
if (!empty($driver['national_number'])) {
$oldParts[] = $driver['national_number'];
}
$oldString = implode('|', $oldParts);
$oldSecret = hash_hmac('sha256', $oldString, $pepper, true);
if (!password_verify($oldSecret, $driver['password'])) {
unauthorizedDriver();
}
}
} }
$limiter->reset(RateLimiter::identifier(), 'login'); $limiter->reset(RateLimiter::identifier(), 'login');
+3 -3
View File
@@ -19,9 +19,9 @@ function resolveServiceAccountPath(): string {
if (!empty($envPath) && file_exists($envPath)) { if (!empty($envPath) && file_exists($envPath)) {
return $envPath; return $envPath;
} }
// if (file_exists('/keys/firebase_service_account.json')) { if (file_exists('/keys/firebase_service_account.json')) {
// return '/keys/firebase_service_account.json'; return '/keys/firebase_service_account.json';
// } }
if (file_exists('/keys/service-account.json')) { if (file_exists('/keys/service-account.json')) {
return '/keys/service-account.json'; return '/keys/service-account.json';
} }
+7 -1
View File
@@ -32,7 +32,7 @@ try {
// 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API) // 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API)
// — يرجع cache hits لو موجودة، وإلا يرجع IDs فقط // — يرجع cache hits لو موجودة، وإلا يرجع IDs فقط
// ========================================== // ==========================================
$locationServerUrl = getenv('LOCATION_API_URL') ?: 'https://jordan-siro.intaleqapp.com/loction_server/api_get_nearby.php'; $locationServerUrl = getenv('LOCATION_API_URL') ?: 'https://api.intaleqapp.com/loction_server/api_get_nearby.php';
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/keys/.internal_socket_key'; $keyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/keys/.internal_socket_key';
$INTERNAL_KEY = getenv('INTERNAL_SOCKET_KEY') ?: (file_exists($keyPath) ? trim((string)@file_get_contents($keyPath)) : (file_exists(__DIR__ . '/../../../loction-keys/.internal_socket_key') ? trim((string)@file_get_contents(__DIR__ . '/../../../loction-keys/.internal_socket_key')) : (file_exists('/home/location/.internal_socket_key') ? trim((string)@file_get_contents('/home/location/.internal_socket_key')) : ''))); $INTERNAL_KEY = getenv('INTERNAL_SOCKET_KEY') ?: (file_exists($keyPath) ? trim((string)@file_get_contents($keyPath)) : (file_exists(__DIR__ . '/../../../loction-keys/.internal_socket_key') ? trim((string)@file_get_contents(__DIR__ . '/../../../loction-keys/.internal_socket_key')) : (file_exists('/home/location/.internal_socket_key') ? trim((string)@file_get_contents('/home/location/.internal_socket_key')) : '')));
@@ -52,13 +52,19 @@ try {
$response = curl_exec($ch); $response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
error_log("[get.php] api_get_nearby HTTP $httpCode | Response: $response");
$redisDrivers = []; $redisDrivers = [];
if ($httpCode == 200 && $response) { if ($httpCode == 200 && $response) {
$json = json_decode($response, true); $json = json_decode($response, true);
if (isset($json['status']) && $json['status'] === true) { if (isset($json['status']) && $json['status'] === true) {
$redisDrivers = $json['drivers'] ?? $json['data'] ?? []; $redisDrivers = $json['drivers'] ?? $json['data'] ?? [];
} else {
error_log("[get.php] api_get_nearby returned error or false status. JSON: " . print_r($json, true));
} }
} else {
error_log("[get.php] api_get_nearby failed. HTTP Code: $httpCode");
} }
if (empty($redisDrivers)) { if (empty($redisDrivers)) {
+2 -2
View File
@@ -100,7 +100,7 @@ try {
try { try {
// Fetch ride data from remote/local DB for server-side calculation // Fetch ride data from remote/local DB for server-side calculation
$stmtRideData = $con->prepare(" $stmtRideData = $con->prepare("
SELECT id, price AS quoted_price, car_type, SELECT id, price AS quoted_price, carType,
distance AS planned_distance, passenger_id, driver_id, price_for_driver distance AS planned_distance, passenger_id, driver_id, price_for_driver
FROM ride WHERE id = ? AND driver_id = ? FROM ride WHERE id = ? AND driver_id = ?
LIMIT 1 LIMIT 1
@@ -115,7 +115,7 @@ try {
$quotedPrice = floatval($rideData['quoted_price'] ?? 0); $quotedPrice = floatval($rideData['quoted_price'] ?? 0);
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد) $kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
$carType = $rideData['car_type'] ?? 'Fixed Price'; $carType = $rideData['carType'] ?? 'Fixed Price';
// 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة // 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة
// خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts) // خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts)
@@ -90,6 +90,6 @@ try {
} catch (Exception $e) { } catch (Exception $e) {
error_log("[getRideStatusFromStartApp] Error: " . $e->getMessage()); error_log("[getRideStatusFromStartApp] Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]); echo json_encode(["status" => "failure", "message" => $e->getMessage()]);
} }
?> ?>
+2 -2
View File
@@ -38,12 +38,12 @@ try {
// 🆕 Immutable Fare Lock: Save agreed fixed price in Redis // 🆕 Immutable Fare Lock: Save agreed fixed price in Redis
try { try {
$stmtRideInfo = $con->prepare("SELECT price, car_type FROM ride WHERE id = ? LIMIT 1"); $stmtRideInfo = $con->prepare("SELECT price, carType FROM ride WHERE id = ? LIMIT 1");
$stmtRideInfo->execute([$ride_id]); $stmtRideInfo->execute([$ride_id]);
$rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC); $rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC);
if ($rideInfo) { if ($rideInfo) {
$agreedPrice = floatval($rideInfo['price']); $agreedPrice = floatval($rideInfo['price']);
$carTypeStr = $rideInfo['car_type'] ?? 'Fixed Price'; $carTypeStr = $rideInfo['carType'] ?? 'Fixed Price';
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car']; $fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
if (in_array($carTypeStr, $fixedPriceTypes)) { if (in_array($carTypeStr, $fixedPriceTypes)) {
global $redis; global $redis;
+15
View File
@@ -2188,3 +2188,18 @@ CREATE TABLE IF NOT EXISTS `competitor_surge_insights` (
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`) UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. Chat Logs Table
CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`ride_id` int NOT NULL,
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ride_id` (`ride_id`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+19
View File
@@ -1696,6 +1696,25 @@ CREATE TABLE `write_argument_after_applied_from_background` (
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ride_chat_logs`
--
DROP TABLE IF EXISTS `ride_chat_logs`;
CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`ride_id` int NOT NULL,
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ride_id` (`ride_id`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --
-- Dumping events for database 'intaleq-ridesDB' -- Dumping events for database 'intaleq-ridesDB'
-- --
+10
View File
@@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/connect.php';
try {
$con_ride = Database::get('ride');
$stmt = $con_ride->query("DESCRIBE ride");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($columns);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
+99
View File
@@ -0,0 +1,99 @@
# ══════════════════════════════════════════════════════════════════
# ‏إنهاء TLS أمام سوكيتات Workerman — تطبيق intaleq
# ══════════════════════════════════════════════════════════════════
# ‏يُنسَخ على **المضيف** لا داخل الحاويات:
# /etc/nginx/sites-enabled/intaleq-sockets-tls.conf
#
# ‏نفس منطق siro-sockets-tls.conf تماماً، لكن هذا التطبيق (intaleq) يطلب
# ‏‎https://api.intaleqapp.com:2020‎ و ‎:3030‎ — دومين مختلف عن دومين سيرو
# ‏(‏jordan-siro.intaleqapp.com‎). بما أن كلا التطبيقين على نفس السيرفر
# ‏ويستخدمان نفس المنفذين الخارجيين 2020/3030، فالفصل بينهما يتم عبر
# ‏SNI: كل server{} هنا يطابق دومينه فقط، ونجينكس يختار الحاوية الصحيحة
# ‏من اسم الدومين في مصافحة TLS — بلا أي حاجة لتغيير المنفذين الخارجيين.
#
# ‏الحاويتان تُنشران على 127.0.0.1 فقط (12032 / 13032 — انظر
# ‏docker-compose.yml، DRIVER_SOCKET_PORT / PASSENGER_SOCKET_PORT)، وهذان
# ‏المنفذان مختلفان فعلاً عن منفذي سيرو (12020 / 13030) فلا تصادم بينهما.
#
# ‏تحقّق أولاً من مسار شهادة api.intaleqapp.com الصحيح:
# grep -rh ssl_certificate /etc/nginx/sites-enabled/ | grep -v key | sort -u
# ══════════════════════════════════════════════════════════════════
map $http_upgrade $intaleq_connection_upgrade {
default upgrade;
'' close;
}
# ── سوكيت السائقين — GPS ─────────────────────────────────────────
server {
listen 2020 ssl;
listen [::]:2020 ssl;
server_name api.intaleqapp.com;
ssl_certificate /etc/nginx/ssl-certificates/api.intaleqapp.com.crt;
ssl_certificate_key /etc/nginx/ssl-certificates/api.intaleqapp.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:IntaleqSock:10m;
ssl_session_timeout 1d;
access_log off;
error_log /var/log/nginx/intaleq-socket-driver-error.log warn;
location / {
proxy_pass http://127.0.0.1:12032;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $intaleq_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}
# ── سوكيت الركاب — حالة الرحلة وموقع السائق ──────────────────────
server {
listen 3030 ssl;
listen [::]:3030 ssl;
server_name api.intaleqapp.com;
ssl_certificate /etc/nginx/ssl-certificates/api.intaleqapp.com.crt;
ssl_certificate_key /etc/nginx/ssl-certificates/api.intaleqapp.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:IntaleqSock:10m;
ssl_session_timeout 1d;
access_log off;
error_log /var/log/nginx/intaleq-socket-passenger-error.log warn;
location / {
proxy_pass http://127.0.0.1:13032;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $intaleq_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}
@@ -4,5 +4,5 @@
تشغيل التطبيق على هذا الجهاز.</string> تشغيل التطبيق على هذا الجهاز.</string>
<string name="exit_button">إغلاق التطبيق</string> <string name="exit_button">إغلاق التطبيق</string>
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string> <string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
<string name="label">سيرو درايفر</string> <string name="label">انطلق كابتن</string>
</resources> </resources>
+1 -1
View File
@@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:android:492d9bc1df6b40c51632ca","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:a86f1a54f0b1aaa21632ca","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"siro-a6957","configurations":{"android":"1:825988584191:android:492d9bc1df6b40c51632ca","ios":"1:825988584191:ios:a86f1a54f0b1aaa21632ca"}}}}}} {"flutter":{"platforms":{"android":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:android:e3daebe53bf691de77a35f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:ios:6d2c7f479c82ba7077a35f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"intaleq-d48a7","configurations":{"android":"1:1086900987150:android:e3daebe53bf691de77a35f","ios":"1:1086900987150:ios:6d2c7f479c82ba7077a35f"}}}}}}
@@ -512,6 +512,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -522,7 +523,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver; PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -537,6 +538,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -555,6 +557,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -571,6 +574,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -698,6 +702,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -708,7 +713,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver; PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -724,6 +729,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -734,7 +740,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver; PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

+9 -16
View File
@@ -84,32 +84,25 @@
<string>_dartobservatory._tcp</string> <string>_dartobservatory._tcp</string>
</array> </array>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images <string>Intaleq Driver uses your camera to capture required registration documents (such as driver's license and vehicle registration) and take a profile photo.</string>
for uploading and access to connect to a call.</string>
<key>NSContactsUsageDescription</key> <key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string> <string>Intaleq Driver accesses your contacts so you can quickly select emergency contact details and share trip status updates with trusted contacts.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string> <string>Intaleq Driver uses Face ID to securely authenticate driver sign-in and authorize earnings payout requests.</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string></string> <string></string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Allow debugging over the local network.</string> <string>Intaleq Driver uses local network access for local testing and debugging network connections during development.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>Intaleq Driver continuously accesses your location to receive nearby trip requests, navigate to pickup and drop-off locations, calculate trip fares, and share real-time position with passengers even when the app is running in the background.</string>
Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string> <string>Intaleq Driver requires background location access to keep your vehicle status active, route you to rider locations, and track completed mileage even when the app is minimized.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>Intaleq Driver uses your location while using the app to present available pickup requests nearby and provide turn-by-turn navigation during active trips.</string>
Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add <string>Intaleq Driver uses your microphone to enable in-app voice calls with passengers for pickup coordination and safety communication.</string>
voice recordings to your photos and videos and access to connect to a call.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to allow you to upload and manage <string>Intaleq Driver accesses your photo library to allow uploading vehicle registration documents, driver identification cards, and profile pictures from your photo gallery.</string>
images.</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
@@ -129,4 +129,5 @@ class BoxName {
static const String isBusMode = 'isBusMode'; static const String isBusMode = 'isBusMode';
static const String busModeTripId = 'busModeTripId'; static const String busModeTripId = 'busModeTripId';
static const String busModeRouteId = 'busModeRouteId'; static const String busModeRouteId = 'busModeRouteId';
static const String isGmsAvailable = 'isGmsAvailable';
} }
@@ -141,7 +141,9 @@ class LoginDriverController extends GetxController {
} }
var dev = ''; var dev = '';
Future<String>? _walletJwtFuture; // static: لأن CRUD ينشئ LoginDriverController() جديد بكل طلب،
// فلو كانت instance field ما بيشتغل الـ single-flight أبداً.
static Future<String>? _walletJwtFuture;
getJwtWallet() async { getJwtWallet() async {
if (_walletJwtFuture != null) { if (_walletJwtFuture != null) {
@@ -228,7 +230,41 @@ class LoginDriverController extends GetxController {
return ''; return '';
} }
getJWT() async { // ═══════════════════════════════════════════════════════════════
// getJWT — تجديد توكن السائق
// • single-flight: طلب تجديد واحد فقط بنفس اللحظة (static)
// • بيرجّع true/false حتى المستدعي يعرف نجح ولا لأ
// • cooldown بعد الفشل: يمنع ضرب /loginJwtDriver.php (حده 5/دقيقة)
// بحلقة لا نهائية تنتهي بـ 429 "Please slow down"
// ═══════════════════════════════════════════════════════════════
static Future<bool>? _jwtFuture;
static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
/// الوقت المتبقّي على انتهاء الـ cooldown (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) {
// أي استثناء (timeout/socket) لازم يتحوّل لـ false + cooldown،
// مش يطلع للمستدعي ويكسر الطلب اللي فوق
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal() async {
await EncryptionHelper.initialize(); await EncryptionHelper.initialize();
// 1. Check secure storage first to avoid redundant API calls // 1. Check secure storage first to avoid redundant API calls
@@ -259,14 +295,31 @@ class LoginDriverController extends GetxController {
if (isTokenValid) { if (isTokenValid) {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.'); 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'; dev = Platform.isAndroid ? 'android' : 'ios';
Log.print( Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}'); 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { // ⚠️ loginFirstTimeDriver.php بيرجّع توكن نوعه "registration" (صالح ساعة
// وone-time)، وباقي الـ endpoints بترفضه بـ 401. فما بنستخدمه إلا لما ما
// يكون عنا driverID أصلاً (سائق لسا ما تسجّل). أي سائق عنده ID → تجديد عادي
// عبر loginJwtDriver.php حتى لو firstTimeLoadKey ما انكتب.
final driverId = box.read(BoxName.driverID);
final isRegistering =
driverId == null || driverId.toString().isEmpty;
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = { var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId, 'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger, 'password': AK.passnpassenger,
@@ -276,12 +329,21 @@ class LoginDriverController extends GetxController {
}; };
// Log.print('payload: ${payload}'); // Log.print('payload: ${payload}');
var response0 = await http.post( var response0 = await http
Uri.parse(AppLink.loginFirstTimeDriver), .post(
body: payload, Uri.parse(AppLink.loginFirstTimeDriver),
); body: payload,
)
.timeout(const Duration(seconds: 30));
Log.print('response0: ${response0.body}'); Log.print('response0: ${response0.body}');
Log.print('request: ${response0.request}'); Log.print('request: ${response0.request}');
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) { if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body); final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}'); Log.print('decodedResponse1: ${decodedResponse1}');
@@ -297,31 +359,46 @@ class LoginDriverController extends GetxController {
if (jwt != null) { if (jwt != null) {
// box.write(BoxName.jwt, c(jwt)); // box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt); await storage.write(key: BoxName.jwt, value: jwt);
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
return _onJwtSuccess();
} }
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
// ✅ بعد التأكد أن كل المفاتيح موجودة }
await EncryptionHelper.initialize(); return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
// await AppInitializer().getKey();
} else {}
} else { } else {
await EncryptionHelper.initialize(); await EncryptionHelper.initialize();
// بدون driverID التجديد مضمون الفشل — نوقف بدون ما نضرب السيرفر
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد driverID');
}
var payload = { var payload = {
'id': box.read(BoxName.driverID), 'id': driverId,
'password': box.read(BoxName.emailDriver),
'aud': '${AK.allowed}$dev', 'aud': '${AK.allowed}$dev',
'fingerPrint': box.read(BoxName.deviceFingerprint) ?? 'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(), await DeviceHelper.getDeviceFingerprint(),
}; };
// print(payload); // print(payload);
var response1 = await http.post( var response1 = await http
Uri.parse(AppLink.loginJwtDriver), .post(
body: payload, Uri.parse(AppLink.loginJwtDriver),
); body: payload,
)
.timeout(const Duration(seconds: 30));
Log.print('response1.request: ${response1.request}'); Log.print('response1.request: ${response1.request}');
Log.print('response1.body: ${response1.body}'); Log.print('response1.body: ${response1.body}');
// 429 → السيرفر عامل rate limit؛ نحترم Retry-After ولا نعيد المحاولة
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) { if (response1.statusCode == 200) {
final decodedResponse1 = jsonDecode(response1.body); final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}'); // Log.print('decodedResponse1: ${decodedResponse1}');
@@ -337,13 +414,32 @@ class LoginDriverController extends GetxController {
if (jwt != null) { if (jwt != null) {
// await box.write(BoxName.jwt, c(jwt)); // await box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt); await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
} }
return _onJwtFailure('renew: لا يوجد jwt بالرد');
// await AppInitializer().getKey();
} }
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
} }
} }
// نجاح التجديد → تصفير العدّاد والـ cooldown
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد.');
return true;
}
// فشل التجديد → backoff تصاعدي (2,4,8,16,32,60ث كحد أقصى)
// هذا هو اللي بيمنع الحلقة اللانهائية اللي بتوصل لـ rate limit
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 { Future<void> getLocationPermission() async {
var status = await Permission.locationAlways.status; var status = await Permission.locationAlways.status;
if (!status.isGranted) { if (!status.isGranted) {
@@ -653,10 +749,8 @@ class LoginDriverController extends GetxController {
var d = jsonDecoeded['data'][0]; var d = jsonDecoeded['data'][0];
var jwt = jsonDecoeded['jwt']; var jwt = jsonDecoeded['jwt'];
// حفظ التوكن أولاً
if (jwt != null) { if (jwt != null) {
box.write(BoxName.jwt, c(jwt)); await storage.write(key: BoxName.jwt, value: jwt.toString());
await storage.write(key: BoxName.jwt, value: c(jwt));
} }
box.write(BoxName.emailDriver, (d['email'])); box.write(BoxName.emailDriver, (d['email']));
@@ -371,8 +371,7 @@ class RegistrationController extends GetxController {
final uri = Uri.parse(currentUrl); final uri = Uri.parse(currentUrl);
final request = http.MultipartRequest('POST', uri); final request = http.MultipartRequest('POST', uri);
final _jwt = box.read(BoxName.jwt); final String _token = await storage.read(key: BoxName.jwt) ?? '';
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
String timestamp = DateTime.now().millisecondsSinceEpoch.toString(); String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
String nonce = timestamp; String nonce = timestamp;
@@ -581,8 +580,8 @@ class RegistrationController extends GetxController {
try { try {
// ترويسات مشتركة // ترويسات مشتركة
final _jwt = box.read(BoxName.jwt); final String _jwtToken = await storage.read(key: BoxName.jwt) ?? '';
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}'; final bearer = 'Bearer $_jwtToken';
final hmac = '${box.read(BoxName.hmac)}'; final hmac = '${box.read(BoxName.hmac)}';
String fingerPrint = String fingerPrint =
@@ -233,7 +233,7 @@ class FirebaseMessagesController extends GetxController {
case 'token change': case 'token change':
case 'TOKEN_CHANGE': case 'TOKEN_CHANGE':
box.remove(BoxName.jwt); await storage.delete(key: BoxName.jwt);
break; break;
case 'face detect': case 'face detect':
@@ -22,7 +22,6 @@ import 'ssl_pinning.dart';
class CRUD { class CRUD {
final NetGuard _netGuard = NetGuard(); final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient(); final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = ''; static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000); static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1); static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -107,6 +106,21 @@ class CRUD {
} }
} }
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// • getJWT() نفسها single-flight + فيها cooldown بعد الفشل،
// فما في داعي لأي flag هون — ولا في خطر حلقة لا نهائية.
// • بترجع التوكن الصالح أو '' لو التجديد فشل/بـ cooldown.
// ═══════════════════════════════════════════════════════════════
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 — دالة مركزية لكل الطلبات // _makeRequest — دالة مركزية لكل الطلبات
// ─────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────
@@ -119,6 +133,7 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
required Map<String, String> headers, required Map<String, String> headers,
bool allowRefresh = true,
}) async { }) async {
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا // timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
const totalTimeout = Duration(seconds: 60); const totalTimeout = Duration(seconds: 60);
@@ -186,19 +201,33 @@ class CRUD {
} }
} }
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية) // 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) { if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء) // تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php'); final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) { if (isNonCritical || !allowRefresh) return 'token_expired';
_isRefreshingJWT = true;
try { final refreshed = await Get.put(LoginDriverController()).getJWT();
await Get.put(LoginDriverController()).getJWT(); if (!refreshed) return 'token_expired';
} finally {
_isRefreshingJWT = false; final newToken = await _getJwt();
} if (newToken.isEmpty) return 'token_expired';
}
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 // 5xx
@@ -218,18 +247,9 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
String token = await _getJwt();
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض // فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) { final String token = await _ensureJwt();
_isRefreshingJWT = true; if (token.isEmpty) return 'token_expired';
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final headers = { final headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -250,16 +270,8 @@ class CRUD {
}) async { }) async {
try { try {
// فحص صلاحية التوكن قبل الإرسال // فحص صلاحية التوكن قبل الإرسال
String token = await _getJwt(); final String token = await _ensureJwt();
if (!_isJwtValid(token) && !_isRefreshingJWT) { if (token.isEmpty) return 'token_expired';
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
var url = Uri.parse(link); var url = Uri.parse(link);
var response = await _client.post( var response = await _client.post(
@@ -279,15 +291,12 @@ class CRUD {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') return response.body; if (jsonData['status'] == 'success') return response.body;
return jsonData['status']; return jsonData['status'];
} else if (response.statusCode == 429) {
Log.print('🛑 get: 429 rate limited — $link');
return 'rate_limited';
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
if (!_isRefreshingJWT) { // تجديد واحد فقط؛ getJWT فيها single-flight + cooldown
_isRefreshingJWT = true; await Get.put(LoginDriverController()).getJWT();
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired'; return 'token_expired';
} else if (response.statusCode >= 500) { } else if (response.statusCode >= 500) {
addError('Non-200: ${response.statusCode}', 'crud().get - Other', addError('Non-200: ${response.statusCode}', 'crud().get - Other',
@@ -572,12 +581,8 @@ class CRUD {
// ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ───────── // ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ─────────
Future<void> sendEmail(String link, Map<String, String>? payload) async { Future<void> sendEmail(String link, Map<String, String>? payload) async {
// r() هي نفس دالة فك التشفير الثلاثي المختصرة // r() هي نفس دالة فك التشفير الثلاثي المختصرة
String token = await _getJwt(); final String token = await _ensureJwt();
if (token.isEmpty) return;
if (!_isJwtValid(token)) {
await LoginDriverController().getJWT();
token = await _getJwt();
}
final headers = { final headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -479,8 +479,7 @@ class AI extends GetxController {
bool isLoadingVehicleFrontSy = false; bool isLoadingVehicleFrontSy = false;
bool isLoadingVehicleBackSy = false; bool isLoadingVehicleBackSy = false;
Future<void> sendToAI(String type, {required File imageFile}) async { Future<void> sendToAI(String type, {required File imageFile}) async {
final _jwt = box.read(BoxName.jwt); final String _token = await storage.read(key: BoxName.jwt) ?? '';
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
final headers = { final headers = {
'Authorization': 'Bearer $_token', 'Authorization': 'Bearer $_token',
'X-HMAC-Auth': '${box.read(BoxName.hmac)}', 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo; import 'package:geolocator/geolocator.dart' as geo;
import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:intaleq_maps/intaleq_maps.dart';
@@ -25,6 +26,37 @@ import 'background_service.dart';
import 'crud.dart'; import 'crud.dart';
import '../transit/transit_driver_controller.dart'; import '../transit/transit_driver_controller.dart';
/// إعدادات تتبّع الموقع لحالة معيّنة من حالات السائق.
/// الهدف: عدم تشغيل الـ GPS بأقصى طاقته إلا أثناء الرحلة الفعلية.
class _LocationProfile {
final String name;
final LocationAccuracy accuracy;
final int interval; // ميلي ثانية
final double distanceFilter; // متر
const _LocationProfile(
this.name, this.accuracy, this.interval, this.distanceFilter);
_LocationProfile copyWith(
{String? name,
LocationAccuracy? accuracy,
int? interval,
double? distanceFilter}) =>
_LocationProfile(
name ?? this.name,
accuracy ?? this.accuracy,
interval ?? this.interval,
distanceFilter ?? this.distanceFilter,
);
/// بصمة تُستخدم لتفادي استدعاء changeSettings بلا داعٍ.
String get signature => '$accuracy|$interval|$distanceFilter';
@override
String toString() =>
'$name(acc: ${accuracy.name}, ${interval}ms, ${distanceFilter}m)';
}
class LocationController extends GetxController with WidgetsBindingObserver { class LocationController extends GetxController with WidgetsBindingObserver {
// =================================================================== // ===================================================================
// ====== Tunables ====== // ====== Tunables ======
@@ -38,6 +70,29 @@ class LocationController extends GetxController with WidgetsBindingObserver {
static const int powerSaveTriggerLevel = 20; static const int powerSaveTriggerLevel = 20;
static const int powerSaveExitLevel = 25; static const int powerSaveExitLevel = 25;
// ===================================================================
// ====== ملفات التتبّع التكيّفية (أ) ======
// ===================================================================
/// السائق غير متاح (off/blocked): أرخص إعداد ممكن مع إبقاء آخر موقع معروف.
static const _profileIdle =
_LocationProfile('idle', LocationAccuracy.balanced, 30000, 100);
/// متصل وينتظر طلباً: لا حاجة لدقة الملاحة — يكفي أن يعرفه السيرفر بالحي.
static const _profileWaiting =
_LocationProfile('waiting', LocationAccuracy.high, 15000, 50);
/// قَبِل الطلب وفي طريقه للراكب: الراكب يراقب السهم، نحتاج تحديثاً معقولاً.
static const _profileEnRoute =
_LocationProfile('enRoute', LocationAccuracy.high, 5000, 15);
/// رحلة جارية (أو وضع الباص): أعلى دقة — هنا تُحتسب المسافة والأجرة.
static const _profileOnTrip =
_LocationProfile('onTrip', LocationAccuracy.navigation, 4000, 10);
/// بعد هذه المدة من السكون نضاعف الفترة (سائق واقف في مرآب/إشارة طويلة).
static const Duration stationaryGrace = Duration(seconds: 90);
static const double stationarySpeedThreshold = 1.0; // م/ث ≈ 3.6 كم/س
// =================================================================== // ===================================================================
// ====== Services & Variables ====== // ====== Services & Variables ======
// =================================================================== // ===================================================================
@@ -84,6 +139,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isReady = false; bool _isReady = false;
bool _isPowerSavingMode = false; bool _isPowerSavingMode = false;
/// هل خدمات جوجل متوفرة على الجهاز؟ (ج)
/// null = لم يُفحص بعد. على أجهزة هواوي بلا GMS يسقط الباكيج إلى
/// LocationManager الخام: دقة أقل واستهلاك بطارية أعلى ⇒ نخفّف الإعدادات.
bool? _gmsAvailable;
static const MethodChannel _deviceServicesChannel =
MethodChannel('com.siro.siro_driver/device_services');
/// آخر إعداد طُبّق فعلياً — لتفادي استدعاء changeSettings بلا تغيير.
String? _appliedProfileSignature;
DateTime? _stationarySince;
final List<Map<String, dynamic>> _trackBuffer = []; final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = []; final List<Map<String, dynamic>> _behaviorBuffer = [];
@@ -121,7 +187,13 @@ class LocationController extends GetxController with WidgetsBindingObserver {
}); });
socket!.disconnect(); socket!.disconnect();
} }
return;
} }
// ‏أي تغيّر آخر (متاح ⇄ مشغول) يجب أن يصل السيرفر فوراً: هو الحدث الذي
// ‏ينقل الكابتن بين geo:drivers:available و geo:drivers:busy. وبلا هذا
// ‏ننتظر أول حدث GPS — وفلتر المسافة قد يؤجّله دقائق إن كان واقفاً.
_emitCurrentLocationNow();
}); });
bool deps = await _awaitDependencies(); bool deps = await _awaitDependencies();
@@ -130,6 +202,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_isReady = true; _isReady = true;
initSocket(); initSocket();
await _detectGmsAvailability();
await _initLocationSettings(); await _initLocationSettings();
_listenToBatteryChanges(); _listenToBatteryChanges();
@@ -163,10 +236,15 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// إيقاف خدمة الخلفية // إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService(); BackgroundServiceHelper.stopService();
if (socket == null || (!socket!.connected && !_isInitializingSocket)) { // 🔥 Fix: iOS silently kills WebSockets in background.
Log.print("🔄 Initializing Socket on resume..."); // The socket_io_client might still report 'connected' as true for ~20s.
initSocket(); // We MUST force a disconnect so initSocket() properly creates a fresh connection
// and immediately emits 'get_pending_orders' to catch missed FCM payloads.
if (socket != null) {
socket!.disconnect();
} }
initSocket();
} else if (state == AppLifecycleState.paused || } else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) { state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND"); Log.print("📱 Lifecycle: App is in BACKGROUND");
@@ -277,11 +355,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (sid != null || eid != null) { if (sid != null || eid != null) {
isSocketConnected = true; isSocketConnected = true;
if (myLocation.latitude != 0) { // ‏هذا الـ emit هو ما يُسجّل الكابتن في geo:drivers:available بعد كل
print("🚀 Sending initial location to socket after connect..."); // ‏اتصال جديد، فلا نتخطاه لمجرد أن أول تثبيت GPS لم يصل بعد.
emitLocationToSocket(myLocation, heading, speed); print("🚀 Sending initial location to socket after connect...");
} _emitCurrentLocationNow();
_startHeartbeat(); _startHeartbeat();
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الاتصال // 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الاتصال
@@ -474,6 +552,30 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_socketHeartbeat?.cancel(); _socketHeartbeat?.cancel();
} }
/// يبثّ الموقع الحالي فوراً، ويجلب تثبيتاً واحداً إن لم يكن لدينا موقع بعد.
///
/// ضروري بعد كل اتصال جديد وبعد كل تغيّر حالة: ملفّ التتبّع في وضع الانتظار
/// يستخدم فلتر مسافة (50–100 متر)، فالكابتن الواقف قد لا يُصدر أي حدث
/// onLocationChanged إطلاقاً — ولو اعتمدنا عليه وحده لبقي غير مرئي على
/// خريطة الراكب حتى يتحرّك فعلياً.
Future<void> _emitCurrentLocationNow() async {
if (socket == null || !socket!.connected) return;
if (myLocation.latitude == 0 && myLocation.longitude == 0) {
await getLocation();
}
if (myLocation.latitude == 0 && myLocation.longitude == 0) {
Log.print("⚠️ No GPS fix yet — initial location emit skipped.");
return;
}
if (isBusMode) {
emitBusLocationToSocket(myLocation, heading, speed);
} else {
emitLocationToSocket(myLocation, heading, speed);
}
}
// In LocationController.dart // In LocationController.dart
void emitLocationToSocket(LatLng pos, double head, double spd) { void emitLocationToSocket(LatLng pos, double head, double spd) {
@@ -555,13 +657,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _subscribeLocationStream() async { Future<void> _subscribeLocationStream() async {
_locSub?.cancel(); _locSub?.cancel();
int interval = _isPowerSavingMode ? 10000 : 5000;
await location.enableBackgroundMode(enable: true); await location.enableBackgroundMode(enable: true);
location.changeSettings( // مصدر الحقيقة الوحيد لإعدادات الموقع: _applyProfile.
accuracy: LocationAccuracy.navigation, await _applyProfile(force: true);
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
_locSub = location.onLocationChanged.listen((LocationData loc) async { _locSub = location.onLocationChanged.listen((LocationData loc) async {
if (loc.latitude == null || loc.longitude == null) return; if (loc.latitude == null || loc.longitude == null) return;
@@ -577,6 +675,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
speed = loc.speed ?? 0.0; speed = loc.speed ?? 0.0;
heading = loc.heading ?? 0.0; heading = loc.heading ?? 0.0;
// إعادة تقييم ملف التتبّع مع كل قراءة — لا يستدعي changeSettings
// إلا إذا تغيّرت الحالة فعلاً (حالة السائق/الرحلة/السكون/البطارية).
_trackStationary();
await _applyProfile();
box.write('last_lat', pos.latitude); box.write('last_lat', pos.latitude);
box.write('last_lng', pos.longitude); box.write('last_lng', pos.longitude);
box.write('last_heading', heading); box.write('last_heading', heading);
@@ -635,6 +738,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_locSub?.cancel(); _locSub?.cancel();
_locSub = null; _locSub = null;
_appliedProfileSignature = null;
_stationarySince = null;
_recordTimer?.cancel(); _recordTimer?.cancel();
_uploadBatchTimer?.cancel(); _uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel(); _socketHeartbeat?.cancel();
@@ -746,24 +851,123 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false; if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) { if (previousMode != _isPowerSavingMode) {
_startBatchTimers(); _startBatchTimers();
_updateLocationSettings(); _applyProfile();
} }
}); });
} }
Future<void> _updateLocationSettings() async { // ===================================================================
if (_locSub == null) return; // ====== محرّك الملفات التكيّفية (أ + ج) ======
int interval = _isPowerSavingMode ? 10000 : 5000; // ===================================================================
/// (ج) فحص توفّر Google Play Services مرة واحدة عند الإقلاع.
/// النتيجة تُحفظ في الصندوق ليقرأها الـ background isolate أيضاً،
/// وتُبَث مع بيانات السائق لمعرفة حجم شريحة الأجهزة بلا GMS.
Future<void> _detectGmsAvailability() async {
if (!Platform.isAndroid) {
_gmsAvailable = true; // iOS: CoreLocation دائماً متاح
return;
}
try {
final bool available =
await _deviceServicesChannel.invokeMethod<bool>('isGmsAvailable') ??
true;
_gmsAvailable = available;
box.write(BoxName.isGmsAvailable, available);
Log.print(available
? "✅ GMS available — FusedLocationProvider in use."
: "⚠️ No GMS (Huawei/AOSP) — falling back to raw LocationManager, "
"relaxing location profile to protect battery.");
} catch (e) {
// القناة غير مسجّلة (نسخة قديمة) — نفترض التوفّر ولا نغيّر السلوك.
_gmsAvailable = true;
Log.print("⚠️ GMS check failed, assuming available: $e");
}
}
/// تتبّع السكون: سائق واقف لا يحتاج قراءات متلاحقة.
void _trackStationary() {
if (speed >= stationarySpeedThreshold) {
_stationarySince = null;
} else {
_stationarySince ??= DateTime.now();
}
}
bool get _isStationary =>
_stationarySince != null &&
DateTime.now().difference(_stationarySince!) >= stationaryGrace;
/// يختار الملف المناسب من حالة السائق والرحلة، ثم يطبّق عليه
/// مُعدِّلات البطارية والسكون وغياب GMS.
_LocationProfile _resolveProfile() {
final String driverStatus =
box.read(BoxName.statusDriverLocation) ?? 'off';
final String rideStatus = (box.read(BoxName.rideStatus) ?? '').toString();
_LocationProfile p;
if (isBusMode) {
p = _profileOnTrip;
} else if (driverStatus == 'off' || driverStatus == 'blocked') {
p = _profileIdle;
} else if (rideStatus == 'Begin') {
p = _profileOnTrip;
} else if (rideStatus == 'Apply' || rideStatus == 'Arrived') {
p = _profileEnRoute;
} else {
p = _profileWaiting;
}
// وضع توفير الطاقة: ضاعف الفترة والمسافة، وانزل عن دقة الملاحة.
if (_isPowerSavingMode) {
p = p.copyWith(
name: '${p.name}+save',
accuracy: p.accuracy == LocationAccuracy.navigation
? LocationAccuracy.high
: p.accuracy,
interval: p.interval * 2,
distanceFilter: p.distanceFilter * 2,
);
}
// (ج) بلا GMS: كل قراءة تُشعل شريحة الـ GPS منفردة ⇒ خفّف التردد.
if (_gmsAvailable == false) {
p = p.copyWith(
name: '${p.name}+nogms',
interval: (p.interval * 1.5).round(),
distanceFilter: p.distanceFilter * 2,
);
}
// السكون المطوّل: ضاعف الفترة ثلاثاً — إلا في رحلة جارية (عدّاد الانتظار).
if (_isStationary && p.accuracy != LocationAccuracy.navigation) {
p = p.copyWith(
name: '${p.name}+idle',
interval: p.interval * 3,
);
}
return p;
}
/// يطبّق الملف الحالي. لا يستدعي changeSettings إلا عند تغيّر فعلي،
/// لأن كل استدعاء يعيد تشغيل مزوّد الموقع في الطبقة الأصلية.
Future<void> _applyProfile({bool force = false}) async {
if (_locSub == null && !force) return;
final p = _resolveProfile();
if (!force && p.signature == _appliedProfileSignature) return;
try { try {
await location.changeSettings( await location.changeSettings(
accuracy: LocationAccuracy.navigation, accuracy: p.accuracy,
interval: interval, interval: p.interval,
distanceFilter: _isPowerSavingMode ? 20 : 10, distanceFilter: p.distanceFilter,
); );
Log.print( _appliedProfileSignature = p.signature;
"🔋 Location settings updated. Power Save: $_isPowerSavingMode"); Log.print("📍 Location profile → $p");
} catch (e) { } catch (e) {
Log.print("❌ Failed to update location settings: $e"); Log.print("❌ Failed to apply location profile $p: $e");
} }
} }
@@ -832,10 +1036,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (await _ensureServiceAndPermission()) { if (await _ensureServiceAndPermission()) {
try { try {
await location.enableBackgroundMode(enable: true); await location.enableBackgroundMode(enable: true);
location.changeSettings( // (ب) لا نضبط الإعدادات هنا: كانت interval: 1000 تتعارض مع إعدادات
accuracy: LocationAccuracy.navigation, // _subscribeLocationStream وتُبقي الـ GPS مشتعلاً كل ثانية.
interval: 1000, // مصدر الحقيقة الوحيد الآن هو _applyProfile.
distanceFilter: 10);
} catch (e) { } catch (e) {
Log.print("Warning: $e"); Log.print("Warning: $e");
} }
@@ -488,7 +488,7 @@ class ScanDocumentsByApi extends GetxController {
update(); update();
final String token = final String token =
box.read(BoxName.jwt)?.toString().split(AppInformation.addd)[0] ?? ''; await storage.read(key: BoxName.jwt) ?? '';
final String fingerPrint = final String fingerPrint =
box.read(BoxName.deviceFingerprint)?.toString() ?? ''; box.read(BoxName.deviceFingerprint)?.toString() ?? '';
final String driverID = box.read(BoxName.driverID).toString(); final String driverID = box.read(BoxName.driverID).toString();
@@ -312,8 +312,7 @@ class ImageController extends GetxController {
required String filename, required String filename,
required String methodLabel, required String methodLabel,
}) async { }) async {
final jwt = box.read(BoxName.jwt); final String token = await storage.read(key: BoxName.jwt) ?? '';
final String token = jwt != null ? r(jwt).split(AppInformation.addd)[0] : '';
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? ''; final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
final int fileSizeBytes = await file.length(); final int fileSizeBytes = await file.length();
@@ -32,8 +32,7 @@ class HomeCaptainController extends GetxController {
Timer? activeTimer; Timer? activeTimer;
Map data = {}; Map data = {};
bool isHomeMapActive = true; bool isHomeMapActive = true;
InlqBitmap carIcon = InlqBitmap carIcon = InlqBitmap.fromStyleImage('car_icon', size: 2.3);
InlqBitmap.fromStyleImage('car_icon', size: 2.3);
bool isMapReadyForCommands = false; bool isMapReadyForCommands = false;
bool isLoading = true; bool isLoading = true;
late double kazan = 0; late double kazan = 0;
@@ -374,68 +373,115 @@ class HomeCaptainController extends GetxController {
} }
} }
void onButtonSelected() { /// زرّ «متاح / غير متاح» في الواجهة — يقلب الحالة الحالية.
void onButtonSelected() => setActive(!isActive);
/// تفعيل/إيقاف الكابتن بشكل صريح.
/// مُعامِلة صريحة بدل القلب (toggle) لأن الاستدعاء التلقائي في onInit كان
/// يعتمد على أن isActive تساوي false — فأي إعادة بناء للكونترولر أو
/// استدعاء ثانٍ كانت تُطفئ الكابتن بدل تشغيله. الاستدعاء بنفس القيمة
/// الحالية آمن: يعيد التأكد من أن التتبّع شغّال فقط.
void setActive(bool active) {
if (!Get.isRegistered<CaptainWalletController>()) { if (!Get.isRegistered<CaptainWalletController>()) {
Get.put(CaptainWalletController()); Get.put(CaptainWalletController());
} }
totalPoints = Get.find<CaptainWalletController>().totalPoints; totalPoints = Get.find<CaptainWalletController>().totalPoints;
// Toggle Active State if (!active) {
isActive = !isActive; _goOffline();
return;
if (isActive) {
try {
_checkFatigueBeforeOnline(); // Throws exception if tired
if (double.parse(totalPoints) > minPointsThreshold) {
locationController.startLocationUpdates();
HapticFeedback.heavyImpact();
activeStartTime = DateTime.now();
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
activeDuration = DateTime.now().difference(activeStartTime!);
stringActiveDuration = formatDuration(activeDuration);
// Increment Fatigue Counter (write to box every 30s)
_fatigueSeconds++;
if (_fatigueSeconds % 30 == 0) {
int totalSeconds =
(box.read('fatigue_total_seconds') ?? 0) + _fatigueSeconds;
box.write('fatigue_total_seconds', totalSeconds);
_fatigueSeconds = 0;
if (totalSeconds >= 12 * 3600) {
// 12 hours
_forceOfflineDueToFatigue();
}
}
update();
});
} else {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
savePeriod(activeDuration);
activeDuration = Duration.zero;
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
}
} catch (e) {
// Driver is fatigued, revert state
isActive = false;
update();
}
} else {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
savePeriod(activeDuration);
activeDuration = Duration.zero;
// Save offline time for Fatigue Monitoring reset
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
} }
try {
_checkFatigueBeforeOnline(); // يرمي استثناءً إذا تجاوز 12 ساعة
} catch (_) {
isActive = false;
update();
return;
}
// الرصيد غير كافٍ: نُطفئ فعلياً بدل إبقاء isActive = true بلا تتبّع،
// فقد كانت الواجهة تعرض «متاح» بينما لا يُبَث أي موقع.
final double? points = double.tryParse(totalPoints);
if (points == null || points <= minPointsThreshold) {
_goOffline();
return;
}
final bool wasActive = isActive;
isActive = true;
// آمنة للاستدعاء المتكرر: تتجاهل الاشتراك إن كان قائماً بالفعل.
locationController.startLocationUpdates();
if (!wasActive) {
HapticFeedback.heavyImpact();
activeStartTime = DateTime.now();
activeTimer?.cancel();
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
activeDuration = DateTime.now().difference(activeStartTime!);
stringActiveDuration = formatDuration(activeDuration);
// Increment Fatigue Counter (write to box every 30s)
_fatigueSeconds++;
if (_fatigueSeconds % 30 == 0) {
int totalSeconds =
(box.read('fatigue_total_seconds') ?? 0) + _fatigueSeconds;
box.write('fatigue_total_seconds', totalSeconds);
_fatigueSeconds = 0;
if (totalSeconds >= 12 * 3600) {
// 12 hours
_forceOfflineDueToFatigue();
}
}
update();
});
}
update();
}
void _goOffline() {
isActive = false;
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
if (activeDuration.inSeconds > 0) savePeriod(activeDuration);
activeDuration = Duration.zero;
// Save offline time for Fatigue Monitoring reset
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
}
/// يضبط حالة الكابتن المخزَّنة عند إقلاع التطبيق.
/// تذكير بمصطلحات التطبيق: 'off' = متاح، 'on' = مشغول برحلة،
/// 'blocked' = محظور، وهي القيمة التي يقرأها emitLocationToSocket ويبني
/// عليها السيرفر عضوية geo:drivers:available.
///
/// كانت 'off' تُكتب دون شرط في آخر onInit، فتُلغي حظراً ساري المفعول
/// كتبه checkAndShowBlockDialog قبلها بسطر، وتُعيد كابتناً في رحلة نشطة
/// إلى حوض المتاحين بعد إعادة فتح التطبيق.
void _applyInitialDriverStatus() {
final String? blockStr = box.read(BoxName.blockUntilDate);
if (blockStr != null && blockStr.isNotEmpty) {
final DateTime? blockExpiry = DateTime.tryParse(blockStr);
if (blockExpiry != null && DateTime.now().isBefore(blockExpiry)) {
box.write(BoxName.statusDriverLocation, 'blocked');
return;
}
}
// رحلة نجت من إغلاق التطبيق: نُبقيه مشغولاً حتى لا يُوزَّع عليه طلب ثانٍ.
final String? rideStatus = box.read(BoxName.rideStatus);
if (rideStatus == 'Apply' || rideStatus == 'Arrived' ||
rideStatus == 'Begin') {
box.write(BoxName.statusDriverLocation, 'on');
return;
}
box.write(BoxName.statusDriverLocation, 'off'); // متاح
} }
// متغيرات العداد للحظر // متغيرات العداد للحظر
@@ -816,8 +862,16 @@ class HomeCaptainController extends GetxController {
Get.put(FirebaseMessagesController()); Get.put(FirebaseMessagesController());
addToken(); addToken();
// ⚠️ الترتيب مقصود: الحالة تُكتب قبل تشغيل التتبّع.
// ‏أول update_location يُرسَل بعد اتصال السوكيت مباشرة، وهو الذي يُدخل
// ‏الكابتن إلى geo:drivers:available على السيرفر. وكتابة الحالة بعد
// ‏تشغيل التتبّع (كما كانت في آخر السطور) تعني أن ذلك الـ emit قد يحمل
// ‏حالة قديمة ('on' من رحلة سابقة لم تُغلق مثلاً).
_applyInitialDriverStatus();
await getlocation(); await getlocation();
onButtonSelected(); setActive(true);
getDriverRate(); getDriverRate();
addCustomCarIcon(); addCustomCarIcon();
getKazanPercent(); getKazanPercent();
@@ -830,8 +884,8 @@ class HomeCaptainController extends GetxController {
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString(); // totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
// getRefusedOrderByCaptain(); // getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق // 🔥 الفحص عند تشغيل التطبيق
// ‏(يكتب 'blocked' عند وجود حظر ساري — ولم تعد أي كتابة بعده تدهسه)
checkAndShowBlockDialog(); checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً // 2. عدل الليسنر ليصبح مشروطاً
// Camera follow timer — only moves when the driver has // Camera follow timer — only moves when the driver has
// actually moved > 15 meters, saving GPU/battery on idle. // actually moved > 15 meters, saving GPU/battery on idle.
@@ -87,8 +87,7 @@ class ComplaintController extends GetxController {
var uri = Uri.parse(AppLink.uploadAudio); var uri = Uri.parse(AppLink.uploadAudio);
var request = http.MultipartRequest('POST', uri); var request = http.MultipartRequest('POST', uri);
final jwt = box.read(BoxName.jwt); String token = await storage.read(key: BoxName.jwt) ?? '';
String token = jwt != null ? r(jwt).toString().split(Env.addd)[0] : '';
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? ''; final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
var mimeType = lookupMimeType(audioFile.path); var mimeType = lookupMimeType(audioFile.path);
+13 -11
View File
@@ -50,18 +50,20 @@ class DefaultFirebaseOptions {
} }
static const FirebaseOptions android = FirebaseOptions( static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8', apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
appId: '1:825988584191:android:492d9bc1df6b40c51632ca', appId: '1:1086900987150:android:e3daebe53bf691de77a35f',
messagingSenderId: '825988584191', messagingSenderId: '1086900987150',
projectId: 'siro-a6957', projectId: 'intaleq-d48a7',
storageBucket: 'siro-a6957.firebasestorage.app', storageBucket: 'intaleq-d48a7.firebasestorage.app',
); );
static const FirebaseOptions ios = FirebaseOptions( static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA', apiKey: 'AIzaSyAwG09AeehwBfktpKKJwCKQOtEUpHtr-p0',
appId: '1:825988584191:ios:a86f1a54f0b1aaa21632ca', appId: '1:1086900987150:ios:6d2c7f479c82ba7077a35f',
messagingSenderId: '825988584191', messagingSenderId: '1086900987150',
projectId: 'siro-a6957', projectId: 'intaleq-d48a7',
storageBucket: 'siro-a6957.firebasestorage.app', storageBucket: 'intaleq-d48a7.firebasestorage.app',
iosBundleId: 'com.siro.siro-driver', androidClientId: '1086900987150-060srlmdjocdcav377rbur4ka14m90b7.apps.googleusercontent.com',
iosClientId: '1086900987150-6mpaq0ookehlvljtsp2aes26dqpukok1.apps.googleusercontent.com',
iosBundleId: 'com.Intaleq.driver',
); );
} }
+3 -2
View File
@@ -2,7 +2,7 @@ name: intaleq_driver
description: "A new Flutter project." description: "A new Flutter project."
publish_to: "none" # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.1+6 version: 2.0.0+66
environment: environment:
sdk: ">=3.0.5 <4.0.0" sdk: ">=3.0.5 <4.0.0"
@@ -18,7 +18,7 @@ dependencies:
path: ./packages/get path: ./packages/get
get_storage: get_storage:
path: ./packages/get_storage path: ./packages/get_storage
intaleq_maps: ^2.2.1 intaleq_maps: ^2.3.0
secure_string_operations: secure_string_operations:
path: ./secure_string_operations path: ./secure_string_operations
trip_overlay_plugin: trip_overlay_plugin:
@@ -104,6 +104,7 @@ dev_dependencies:
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true
remove_alpha_ios: true
image_path: "assets/images/logo.png" image_path: "assets/images/logo.png"
min_sdk_android: 21 min_sdk_android: 21
web: web:
@@ -4,5 +4,5 @@
تشغيل التطبيق على هذا الجهاز.</string> تشغيل التطبيق على هذا الجهاز.</string>
<string name="exit_button">إغلاق التطبيق</string> <string name="exit_button">إغلاق التطبيق</string>
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string> <string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
<string name="label">سيرو</string> <string name="label">انطلق</string>
</resources> </resources>
+1 -1
View File
@@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:android:7e9088b719dcb7061632ca","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:4f60966dd0a69b3f1632ca","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"siro-a6957","configurations":{"android":"1:825988584191:android:7e9088b719dcb7061632ca","ios":"1:825988584191:ios:4f60966dd0a69b3f1632ca","macos":"1:825988584191:ios:7b48f585862d5cfc1632ca"}}},"macos":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:7b48f585862d5cfc1632ca","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}}}}} {"flutter":{"platforms":{"android":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:android:b7231956aa6d3b3377a35f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:ios:a60973accd7f3dc777a35f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"intaleq-d48a7","configurations":{"android":"1:1086900987150:android:b7231956aa6d3b3377a35f","ios":"1:1086900987150:ios:a60973accd7f3dc777a35f"}}},"macos":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:7b48f585862d5cfc1632ca","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}}}}}
@@ -662,6 +662,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -851,6 +852,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -877,6 +879,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8; DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+8 -14
View File
@@ -58,27 +58,21 @@
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images <string>Intaleq Rider uses your camera so you can take a profile picture, scan QR codes for quick ride check-ins, or attach photos when reporting trip issues to support.</string>
for uploading and access to connect to a call.</string>
<key>NSContactsUsageDescription</key> <key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string> <string>Intaleq Rider accesses your contacts so you can easily select emergency contacts and share live trip status with family or friends.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string> <string>Intaleq Rider uses Face ID to securely authenticate payment transactions and confirm your login.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>Intaleq Rider uses your location to show available drivers nearby, automatically set pickup points, provide turn-by-turn tracking, and update driver arrival status even when the app is in the background.</string>
Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string> <string>Intaleq Rider uses your location in the background to maintain accurate live tracking for your active trips and notify you when your driver arrives.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>Intaleq Rider uses your location while using the app to locate nearby drivers, calculate trip fare estimates, and set your pickup and drop-off points.</string>
Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add <string>Intaleq Rider uses your microphone to allow direct in-app voice calls with your driver for pickup coordination and to record audio notes for support tickets.</string>
voice recordings to your photos and videos and access to connect to a call.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string> <string>Intaleq Rider accesses your photo library so you can select and upload a profile avatar or attach screenshots when submitting support requests.</string>
<key>NSSupportsLiveActivities</key> <key>NSSupportsLiveActivities</key>
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
@@ -82,26 +82,55 @@ class LoginController extends GetxController {
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime // • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider // • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
Future<void> getJWT({bool force = false}) async { static Future<bool>? _jwtFuture;
// إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
static Duration get jwtCooldownRemaining {
final d = _jwtCooldownUntil.difference(DateTime.now());
return d.isNegative ? Duration.zero : d;
}
Future<bool> getJWT({bool force = false}) async {
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal(force: force).catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal({bool force = false}) async {
if (!force && isTokenValid()) { if (!force && isTokenValid()) {
Log.print("JWT is still valid. Skipping request."); Log.print("JWT is still valid. Skipping request.");
return; _jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
} }
try { try {
dev = Platform.isAndroid ? 'android' : 'ios'; dev = Platform.isAndroid ? 'android' : 'ios';
// تأكد إن البصمة محدّثة قبل أي طلب
await DeviceHelper.getDeviceFingerprint(); await DeviceHelper.getDeviceFingerprint();
final String fp = box.read(BoxName.deviceFpEncrypted) ?? ''; final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
final passengerId = box.read(BoxName.passengerID);
final isRegistering = passengerId == null || passengerId.toString().isEmpty;
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
// ── أول تسجيل ─────────────────────────────────────────
// نرسل البصمة المشفرة مع باقي البيانات
// السيرفر سيعمل hash لها ويخزنها في JWT payload
var payload = { var payload = {
'id': box.read(BoxName.passengerID) ?? AK.newId, 'id': passengerId ?? AK.newId,
'password': AK.passnpassenger, 'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev', 'aud': '${AK.allowed}$dev',
'fingerPrint': fp, 'fingerPrint': fp,
@@ -110,12 +139,14 @@ class LoginController extends GetxController {
var response = await http.post( var response = await http.post(
Uri.parse(AppLink.loginFirstTime), Uri.parse(AppLink.loginFirstTime),
body: payload, body: payload,
); ).timeout(const Duration(seconds: 30));
Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}');
if (response.statusCode == 429) {
Log.print('payload: $payload'); final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
Log.print('response code: ${response.statusCode}'); _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('response body: ${response.body}'); Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response.statusCode == 200) { if (response.statusCode == 200) {
final decoded = jsonDecode(response.body); final decoded = jsonDecode(response.body);
@@ -126,18 +157,20 @@ class LoginController extends GetxController {
: decoded['jwt']); : decoded['jwt']);
if (jwt != null) { if (jwt != null) {
// نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage await storage.write(key: BoxName.jwt, value: jwt);
box.write(BoxName.jwt, c(jwt)); await EncryptionHelper.initialize();
storage.write(key: BoxName.jwt, value: c(jwt)); return _onJwtSuccess();
} }
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
await EncryptionHelper.initialize();
} }
return _onJwtFailure('firstTime: HTTP ${response.statusCode}');
} else { } else {
// ── مستخدم موجود: تجديد التوكن if (isRegistering) {
return _onJwtFailure('renew: لا يوجد passengerID');
}
var payload = { var payload = {
'id': box.read(BoxName.passengerID), 'id': passengerId,
'fingerPrint': fp, 'fingerPrint': fp,
'aud': '${AK.allowed}$dev', 'aud': '${AK.allowed}$dev',
}; };
@@ -145,11 +178,15 @@ class LoginController extends GetxController {
var response = await http.post( var response = await http.post(
Uri.parse(AppLink.loginJwtRider), Uri.parse(AppLink.loginJwtRider),
body: payload, body: payload,
); ).timeout(const Duration(seconds: 30));
Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}');
if (response.statusCode == 429) {
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
Log.print('payload: $payload');
Log.print('response: ${response.body}');
if (response.statusCode == 200) { if (response.statusCode == 200) {
final decoded = jsonDecode(response.body); final decoded = jsonDecode(response.body);
final String? jwt = decoded['data'] != null final String? jwt = decoded['data'] != null
@@ -159,16 +196,33 @@ class LoginController extends GetxController {
: decoded['jwt']); : decoded['jwt']);
if (jwt != null) { if (jwt != null) {
box.write(BoxName.jwt, c(jwt)); await storage.write(key: BoxName.jwt, value: jwt);
storage.write(key: BoxName.jwt, value: c(jwt)); return _onJwtSuccess();
} }
return _onJwtFailure('renew: لا يوجد jwt بالرد');
} }
return _onJwtFailure('renew: HTTP ${response.statusCode}');
} }
} catch (e) { } catch (e) {
Log.print('Error in getJWT: $e'); return _onJwtFailure('Error: $e');
} }
} }
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
return true;
}
bool _onJwtFailure(String reason) {
_jwtFailures++;
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
return false;
}
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية) // التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
@@ -24,7 +24,6 @@ class CRUD {
final NetGuard _netGuard = NetGuard(); final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient(); final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = ''; static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000); static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1); static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -98,31 +97,50 @@ class CRUD {
Future<String> _getJwt() async { Future<String> _getJwt() async {
try { try {
final String? encryptedJwt = await storage.read(key: BoxName.jwt); final jwt = await storage.read(key: BoxName.jwt);
if (encryptedJwt == null || encryptedJwt.isEmpty) { if (jwt == null || jwt.toString().isEmpty) {
final String? fallback = box.read(BoxName.jwt); // إذا كان التخزين الآمن فارغاً، نحاول استخراج التوكن القديم من GetStorage للركاب القدامى
final fallback = box.read(BoxName.jwt);
if (fallback != null) { if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0]; try {
return r(fallback).toString().split(Env.addd)[0]; // فك تشفير القديم
} catch (_) {
return fallback.toString(); // ربما تم تخزينه بدون تشفير
}
} }
return ''; return '';
} }
return r(encryptedJwt).toString().split(Env.addd)[0]; // التحقق السريع إذا كان التوكن لا يزال مشفراً (يبدأ برموز غريبة وليس ey)
} catch (e) { if (!jwt.startsWith('ey')) {
Log.print('Error reading JWT from SecureStorage: $e'); try {
final String? fallback = box.read(BoxName.jwt); return r(jwt).toString().split(Env.addd)[0];
if (fallback != null) { } catch (_) {}
return r(fallback).toString().split(Env.addd)[0];
} }
return jwt;
} catch (_) {
return ''; return '';
} }
} }
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
/// Centralized request handler with retry for weak networks. /// Centralized request handler with retry for weak networks.
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff. /// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
Future<dynamic> _makeRequest({ Future<dynamic> _makeRequest({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
required Map<String, String> headers, required Map<String, String> headers,
bool allowRefresh = true,
}) async { }) async {
const totalTimeout = Duration(seconds: 60); const totalTimeout = Duration(seconds: 60);
@@ -180,17 +198,33 @@ class CRUD {
} }
} }
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) { if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php'); final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) { if (isNonCritical || !allowRefresh) return 'token_expired';
_isRefreshingJWT = true;
try { final refreshed = await Get.put(LoginController()).getJWT();
await Get.put(LoginController()).getJWT(); if (!refreshed) return 'token_expired';
} finally {
_isRefreshingJWT = false; final newToken = await _getJwt();
} if (newToken.isEmpty) return 'token_expired';
}
return 'token_expired'; // إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
} }
if (sc >= 500) { if (sc >= 500) {
@@ -206,7 +240,14 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
String token = await _getJwt(); String token = await _ensureJwt();
if (token.isEmpty) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
// باستثناء تسجيل الدخول لأنه لا يحتاج توكن
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = { final headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -221,7 +262,12 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
String token = await _getJwt(); String token = await _ensureJwt();
if (token.isEmpty) {
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = { final headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -86,7 +86,7 @@ class MapSocketController extends GetxController {
io_client.OptionBuilder() io_client.OptionBuilder()
.setTransports(['websocket']) .setTransports(['websocket'])
.disableAutoConnect() .disableAutoConnect()
.setQuery({'id': passengerId, 'jwt': jwt}) .setQuery({'id': passengerId, 'jwt': jwt, 'EIO': '3'})
// محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart). // محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart).
// كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق // كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق
// من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال // من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال
@@ -173,18 +173,6 @@ class MapSocketController extends GetxController {
socket.on('connect_error', (error) { socket.on('connect_error', (error) {
Log.print("❌ Socket Connect Error: $error"); Log.print("❌ Socket Connect Error: $error");
isSocketConnected = false; isSocketConnected = false;
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
// نتأكد يدوياً من إعادة الاتصال
Future.delayed(const Duration(seconds: 3), () {
if (!isSocketConnected && _isSocketInitialized) {
Log.print("🔄 Manual reconnect after connect_error...");
try {
socket.connect();
} catch (e) {
Log.print("Manual reconnect error: $e");
}
}
});
}); });
socket.on('ride_status_change', (data) { socket.on('ride_status_change', (data) {
+13 -11
View File
@@ -47,19 +47,21 @@ class DefaultFirebaseOptions {
} }
static const FirebaseOptions android = FirebaseOptions( static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8', apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
appId: '1:825988584191:android:7e9088b719dcb7061632ca', appId: '1:1086900987150:android:b7231956aa6d3b3377a35f',
messagingSenderId: '825988584191', messagingSenderId: '1086900987150',
projectId: 'siro-a6957', projectId: 'intaleq-d48a7',
storageBucket: 'siro-a6957.firebasestorage.app', storageBucket: 'intaleq-d48a7.firebasestorage.app',
); );
static const FirebaseOptions ios = FirebaseOptions( static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA', apiKey: 'AIzaSyDzGO9a-1IDMLD2FxhmOO9ONL1gMssFa9g',
appId: '1:825988584191:ios:4f60966dd0a69b3f1632ca', appId: '1:1086900987150:ios:a60973accd7f3dc777a35f',
messagingSenderId: '825988584191', messagingSenderId: '1086900987150',
projectId: 'siro-a6957', projectId: 'intaleq-d48a7',
storageBucket: 'siro-a6957.firebasestorage.app', storageBucket: 'intaleq-d48a7.firebasestorage.app',
iosBundleId: 'com.siroapp.rider', androidClientId: '1086900987150-060srlmdjocdcav377rbur4ka14m90b7.apps.googleusercontent.com',
iosClientId: '1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d.apps.googleusercontent.com',
iosBundleId: 'com.Intaleq.intaleq',
); );
static const FirebaseOptions macos = FirebaseOptions( static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA', apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
+3 -2
View File
@@ -2,7 +2,7 @@ name: intaleq_rider
description: "A new Flutter project." description: "A new Flutter project."
publish_to: "none" # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.6+6 version: 34.0.0+67
environment: environment:
sdk: ">=3.0.5 <4.0.0" sdk: ">=3.0.5 <4.0.0"
@@ -79,7 +79,7 @@ dependencies:
internet_connection_checker: ^3.0.1 internet_connection_checker: ^3.0.1
connectivity_plus: ^6.1.5 connectivity_plus: ^6.1.5
app_links: ^7.0.0 app_links: ^7.0.0
intaleq_maps: ^2.2.1 intaleq_maps: ^2.3.0
socket_io_client: 1.0.2 socket_io_client: 1.0.2
# home_widget: ^0.7.0+1 # home_widget: ^0.7.0+1
@@ -94,6 +94,7 @@ dev_dependencies:
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true
remove_alpha_ios: true
image_path: "assets/images/logo.png" image_path: "assets/images/logo.png"
min_sdk_android: 21 min_sdk_android: 21
web: web:
+12 -1
View File
@@ -17,6 +17,8 @@ function getInternalKey(): string {
} }
function getRedisPass(): ?string { function getRedisPass(): ?string {
$envPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
if ($envPass) return trim($envPass);
$path = getenv('REDIS_PASS_KEY_PATH'); $path = getenv('REDIS_PASS_KEY_PATH');
if ($path && file_exists($path)) return trim((string)@file_get_contents($path)); if ($path && file_exists($path)) return trim((string)@file_get_contents($path));
if (file_exists('/keys/.reds_pass_key')) return trim((string)@file_get_contents('/keys/.reds_pass_key')); if (file_exists('/keys/.reds_pass_key')) return trim((string)@file_get_contents('/keys/.reds_pass_key'));
@@ -31,6 +33,7 @@ $headers = getallheaders();
$receivedKey = $headers['x-internal-key'] ?? $_SERVER['HTTP_X_INTERNAL_KEY'] ?? ''; $receivedKey = $headers['x-internal-key'] ?? $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
if (!empty($INTERNAL_KEY) && $receivedKey !== $INTERNAL_KEY) { if (!empty($INTERNAL_KEY) && $receivedKey !== $INTERNAL_KEY) {
error_log("[api_get_nearby] Unauthorized: receivedKey ($receivedKey) != expectedKey ($INTERNAL_KEY)");
http_response_code(403); http_response_code(403);
echo json_encode(['status' => false, 'msg' => 'Unauthorized']); echo json_encode(['status' => false, 'msg' => 'Unauthorized']);
exit; exit;
@@ -79,7 +82,15 @@ try {
$profile = $redis->hgetall("driver:profile:$d_id"); $profile = $redis->hgetall("driver:profile:$d_id");
$lastUpdate = isset($profile['updated_at']) ? (int)$profile['updated_at'] : 0; $lastUpdate = isset($profile['updated_at']) ? (int)$profile['updated_at'] : 0;
if (empty($profile) || ($currentTime - $lastUpdate) > $max_silence) { if (empty($profile)) {
error_log("[api_get_nearby] Driver $d_id skipped: profile is empty in Redis.");
$redis->zrem('geo:drivers:available', $d_id);
$redis->zrem('geo:drivers:busy', $d_id);
continue;
}
if (($currentTime - $lastUpdate) > $max_silence) {
error_log("[api_get_nearby] Driver $d_id skipped: lastUpdate ($lastUpdate) is older than max_silence ($max_silence).");
$redis->zrem('geo:drivers:available', $d_id); $redis->zrem('geo:drivers:available', $d_id);
$redis->zrem('geo:drivers:busy', $d_id); $redis->zrem('geo:drivers:busy', $d_id);
continue; continue;
+35 -7
View File
@@ -90,19 +90,24 @@ function getJwtSecret(): string {
// Redis password: try env var, then Docker keys, then legacy path, then null (Docker Redis has no password) // Redis password: try env var, then Docker keys, then legacy path, then null (Docker Redis has no password)
$redisPass = null; $redisPass = null;
$redisPassPaths = [ $redisPassPaths = [
getenv('REDIS_PASS_KEY_PATH') ?: '', __DIR__ . '/../loction-keys/.reds_pass_key',
'/keys/.reds_pass_key', '/keys/.reds_pass_key',
'/home/location/.reds_pass_key', '/home/location/.reds_pass_key'
]; ];
foreach ($redisPassPaths as $rpp) { foreach ($redisPassPaths as $rpp) {
if (!empty($rpp) && file_exists($rpp)) { if (file_exists($rpp)) {
$redisPass = trim((string) @file_get_contents($rpp)); $redisPass = trim((string) @file_get_contents($rpp));
break; break;
} }
} }
if (empty($redisPass)) {
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
}
if (empty($INTERNAL_KEY)) logMsg('⚠️ Internal key not found (non-critical in Docker)'); if (empty($INTERNAL_KEY)) logMsg('⚠️ Internal key not found (non-critical in Docker)');
if (empty($redisPass)) logMsg('ℹ️ Redis password not set (OK for Docker — no auth required)'); if (empty($redisPass)) logMsg('ℹ️ Redis password not set (OK for Docker — no auth required)');
else logMsg('✅ Redis password loaded successfully');
// ============================================================ // ============================================================
// 🗄️ Redis Singleton // 🗄️ Redis Singleton
@@ -368,8 +373,10 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
if ($st === 'off') { if ($st === 'off') {
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId); $pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
logMsg("[REDIS BATCH] Added Driver $driverId to geo:drivers:available at $lat, $lng");
} elseif ($st === 'on') { } elseif ($st === 'on') {
$pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId); $pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId);
logMsg("[REDIS BATCH] Added Driver $driverId to geo:drivers:busy at $lat, $lng");
} }
} }
} }
@@ -731,7 +738,7 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
} else { } else {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256')); $decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
// Validate that the token belongs to this driver // Validate that the token belongs to this driver
if ((string)$decoded->sub !== (string)$driverId || $decoded->role !== 'driver') { if ((string)$decoded->user_id !== (string)$driverId || $decoded->role !== 'driver') {
logMsg("🚫 Connection Rejected: Invalid JWT for driver_id=$driverId"); logMsg("🚫 Connection Rejected: Invalid JWT for driver_id=$driverId");
$socket->disconnect(); $socket->disconnect();
return; return;
@@ -761,13 +768,27 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$driverState[$driverId] = [ $driverState[$driverId] = [
'lat' => 0.0, 'lat' => 0.0,
'lng' => 0.0, 'lng' => 0.0,
'speed' => -999.0, 'speed' => -999.0,
'heading' => -999.0, 'heading' => -999.0,
'status' => '', 'status' => '',
'expire_ts' => 0, 'expire_ts' => 0,
]; ];
} else {
// ‏اتصال جديد لا يضمن أن السائق ما زال عضواً في geo:drivers:* —
// ‏api_get_nearby.php يحذف العضوية كلّما وجد driver:profile منتهياً
// ‏(TTL = 900 ثانية)، بينما تبقى هذه الحالة في ذاكرة الـ worker لأن
// ‏سوكيت خدمة الخلفية (background_service) يظل مسجَّلاً فيمنع تنظيفها
// ‏عند إغلاق سوكيت الواجهة.
// ‏وبلا تصفيرها يجد أول update_location بعد فتح التطبيق أن status و
// ‏lat/lng بلا تغيير ⇒ لا GEOADD ⇒ يبقى الكابتن غير مرئي على خريطة
// ‏الراكب حتى يتحرك 10 أمتار أو يضغط زر التوفّر (الذي يقطع السوكيت).
// ‏نصفّر الجزء المتطاير فقط ليُجبَر أول تحديث على إعادة تسجيله.
$driverState[$driverId]['lat'] = 0.0;
$driverState[$driverId]['lng'] = 0.0;
$driverState[$driverId]['status'] = '';
$driverState[$driverId]['expire_ts'] = 0;
} }
logMsg("✅ Driver Connected: #$driverId ($platform)"); logMsg("✅ Driver Connected: #$driverId ($platform)");
// 🆕 Deliver any pending orders missed during disconnection // 🆕 Deliver any pending orders missed during disconnection
@@ -862,6 +883,8 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$state = &$driverState[$driverId]; $state = &$driverState[$driverId];
$now = time(); $now = time();
logMsg("[SOCKET] update_location received for Driver $driverId | status=$status, lat=$lat, lng=$lng");
// 1. Forward للراكب (ASYNC + throttle) // 1. Forward للراكب (ASYNC + throttle)
if (!empty($passengerId)) { if (!empty($passengerId)) {
@@ -941,7 +964,12 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
} }
} }
if ($needGeoadd || $statusChanged) { // ‏نُعيد تأكيد العضوية في geo:drivers:* مع كل تجديد صلاحية (كل
// ‏EXPIRE_REFRESH_SECONDS) لا عند الحركة فقط: الكابتن المتاح والواقف
// ‏لا يُصدر didMove ولا statusChanged أبداً، فلو حُذف من المجموعة
// ‏(انقطاع، إعادة تشغيل السيرفر، تنظيف api_get_nearby) لما عاد إليها
// ‏قط. التكلفة: عملية GEOADD واحدة لكل كابتن كل دقيقتين.
if ($needGeoadd || $statusChanged || $needExpireRefresh) {
$eventBuffer[$driverId]['geoadd'] = [ $eventBuffer[$driverId]['geoadd'] = [
'status' => $status, 'status' => $status,
'lng' => $lng, 'lng' => $lng,
+1 -1
View File
@@ -209,7 +209,7 @@ $io->on('connection', function ($socket) {
socket_log("[WARNING] JWT Secret is not configured on the server!"); socket_log("[WARNING] JWT Secret is not configured on the server!");
} else { } else {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256')); $decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
if ((string)$decoded->sub !== (string)$passengerId || $decoded->role !== 'passenger') { if ((string)$decoded->user_id !== (string)$passengerId || $decoded->role !== 'passenger') {
socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp"); socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp");
$socket->disconnect(); $socket->disconnect();
return; return;
+7 -4
View File
@@ -75,9 +75,12 @@ function getLocationRedis(): ?\Redis {
catch (\Exception $_) { $_locationRedis = null; } catch (\Exception $_) { $_locationRedis = null; }
} }
try { try {
$redisPass = ''; $redisPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
foreach ([getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'] as $p) { $keys = [getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'];
if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; } if (!$redisPass) {
foreach ($keys as $p) {
if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; }
}
} }
$host = getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1'); $host = getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1');
$r = new \Redis(); $r = new \Redis();
@@ -221,7 +224,7 @@ $io->on('connection', function ($socket) {
socket_log("[WARNING] JWT Secret is not configured on the server!"); socket_log("[WARNING] JWT Secret is not configured on the server!");
} else { } else {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256')); $decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
if ((string)$decoded->sub !== (string)$passengerId || $decoded->role !== 'passenger') { if ((string)$decoded->user_id !== (string)$passengerId || $decoded->role !== 'passenger') {
socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp"); socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp");
$socket->disconnect(); $socket->disconnect();
return; return;
+5
View File
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/backend/core/bootstrap.php';
$jwtService = new JwtService($redis);
$token = $jwtService->generateAccessToken('34feffd3fa72d6bee56b', 'driver', 'wallet-app:ios');
echo $token;