17 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
Hamza-Ayed 09fa0c4e37 Fix payment server: remove global conRide to prevent crashing non-ride DB queries 2026-08-01 16:24:32 +03:00
Hamza-Ayed 4846109824 Fix payment server: remove global conRide to prevent crashing non-ride DB queries 2026-08-01 16:23:52 +03:00
Hamza-Ayed 0966699070 Add test_hash.php 2026-08-01 16:20:17 +03:00
90 changed files with 1268 additions and 389 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']);
$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) {
error_log("[FATAL] Database config missing for: $name (Check ENV keys: {$cfg['name']}, {$cfg['user']})");
throw new RuntimeException("Database configuration error.");
+1 -1
View File
@@ -43,7 +43,7 @@ header("X-XSS-Protection: 1; mode=block");
// CORS مع التحقق من المصدر المسموح
$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));
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
+1 -1
View File
@@ -9,7 +9,7 @@ header('Content-Type: application/json');
// ✅ FIX H-03: allowlist صارم للـ Admin Origins
$allowedOrigins = array_filter([
getenv('ALLOWED_ORIGIN') ?: 'https://siromove.com',
'https://jordan-siro.intaleqapp.com',
'https://api.intaleqapp.com',
'http://localhost',
'http://127.0.0.1',
]);
+74 -47
View File
@@ -45,63 +45,90 @@ try {
$pepper = getenv('SECRET_KEY_HMAC');
$stmt = $con->prepare('
SELECT d.id, d.phone, d.national_number, d.email, d.password, d.birthdate, c.year
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
WHERE d.id = :id
SELECT id, phone, national_number, email, password, birthdate
FROM driver
WHERE id = :id
LIMIT 1
');
$stmt->execute([':id' => $id]);
$driver = $stmt->fetch();
if (!$driver || empty($driver['password'])) {
if (!$driver) {
unauthorizedDriver();
}
$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);
}
if (!empty($driver['year']) && preg_match('/^\d{4}$/', $driver['year'])) {
$newParts[] = $driver['year'];
}
$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'];
}
if (!empty($driver['year']) && preg_match('/^\d{4}$/', $driver['year'])) {
$oldParts[] = $driver['year'];
}
$oldString = implode('|', $oldParts);
$oldSecret = hash_hmac('sha256', $oldString, $pepper, true);
if (!password_verify($oldSecret, $driver['password'])) {
// ── مسار السائقين المسجلين عبر OTP (بدون password في DB) ──
if (empty($driver['password'])) {
// التحقق من هوية السائق عبر fingerprint المخزن في driverToken
if (empty($fingerprint)) {
securityLog("LoginDriver(OTP): no fingerprint sent", [
'driver_id' => $driver['id'],
]);
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');
+3 -3
View File
@@ -19,9 +19,9 @@ function resolveServiceAccountPath(): string {
if (!empty($envPath) && file_exists($envPath)) {
return $envPath;
}
// if (file_exists('/keys/firebase_service_account.json')) {
// return '/keys/firebase_service_account.json';
// }
if (file_exists('/keys/firebase_service_account.json')) {
return '/keys/firebase_service_account.json';
}
if (file_exists('/keys/service-account.json')) {
return '/keys/service-account.json';
}
+7 -1
View File
@@ -32,7 +32,7 @@ try {
// 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API)
// — يرجع 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';
$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);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("[get.php] api_get_nearby HTTP $httpCode | Response: $response");
$redisDrivers = [];
if ($httpCode == 200 && $response) {
$json = json_decode($response, true);
if (isset($json['status']) && $json['status'] === true) {
$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)) {
+2 -2
View File
@@ -100,7 +100,7 @@ try {
try {
// Fetch ride data from remote/local DB for server-side calculation
$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
FROM ride WHERE id = ? AND driver_id = ?
LIMIT 1
@@ -115,7 +115,7 @@ try {
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
$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) قد تُطبَّق نسبة
// خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts)
@@ -90,6 +90,6 @@ try {
} catch (Exception $e) {
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
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]);
$rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC);
if ($rideInfo) {
$agreedPrice = floatval($rideInfo['price']);
$carTypeStr = $rideInfo['car_type'] ?? 'Fixed Price';
$carTypeStr = $rideInfo['carType'] ?? 'Fixed Price';
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
if (in_array($carTypeStr, $fixedPriceTypes)) {
global $redis;
+15
View File
@@ -2188,3 +2188,18 @@ CREATE TABLE IF NOT EXISTS `competitor_surge_insights` (
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`)
) 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;
/*!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'
--
+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 name="exit_button">إغلاق التطبيق</string>
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
<string name="label">سيرو درايفر</string>
<string name="label">انطلق كابتن</string>
</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;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -522,7 +523,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -537,6 +538,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -555,6 +557,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -571,6 +574,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 63CVT8G5P8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
@@ -698,6 +702,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -708,7 +713,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -724,6 +729,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -734,7 +740,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
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>
</array>
<key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images
for uploading and access to connect to a call.</string>
<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>
<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>
<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>
<string></string>
<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>
<string>This app needs access to your location to provide you with the best ride experience.
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>
<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>
<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>
<string>This app needs access to your location to provide you with the best ride experience.
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>
<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>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add
voice recordings to your photos and videos and access to connect to a call.</string>
<string>Intaleq Driver uses your microphone to enable in-app voice calls with passengers for pickup coordination and safety communication.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to allow you to upload and manage
images.</string>
<string>Intaleq Driver accesses your photo library to allow uploading vehicle registration documents, driver identification cards, and profile pictures from your photo gallery.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@@ -129,4 +129,5 @@ class BoxName {
static const String isBusMode = 'isBusMode';
static const String busModeTripId = 'busModeTripId';
static const String busModeRouteId = 'busModeRouteId';
static const String isGmsAvailable = 'isGmsAvailable';
}
@@ -141,7 +141,9 @@ class LoginDriverController extends GetxController {
}
var dev = '';
Future<String>? _walletJwtFuture;
// static: لأن CRUD ينشئ LoginDriverController() جديد بكل طلب،
// فلو كانت instance field ما بيشتغل الـ single-flight أبداً.
static Future<String>? _walletJwtFuture;
getJwtWallet() async {
if (_walletJwtFuture != null) {
@@ -228,7 +230,41 @@ class LoginDriverController extends GetxController {
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();
// 1. Check secure storage first to avoid redundant API calls
@@ -259,14 +295,31 @@ class LoginDriverController extends GetxController {
if (isTokenValid) {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
return;
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
}
// التوكن منتهي فعلاً — بس لو آخر محاولة فشلت لازم ننتظر قبل ما نعيد
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
}
dev = Platform.isAndroid ? 'android' : 'ios';
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
// ⚠️ 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 = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
@@ -276,12 +329,21 @@ class LoginDriverController extends GetxController {
};
// Log.print('payload: ${payload}');
var response0 = await http.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
);
var response0 = await http
.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
)
.timeout(const Duration(seconds: 30));
Log.print('response0: ${response0.body}');
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) {
final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}');
@@ -297,31 +359,46 @@ class LoginDriverController extends GetxController {
if (jwt != null) {
// box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
// await AppInitializer().getKey();
} else {}
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
} else {
await EncryptionHelper.initialize();
// بدون driverID التجديد مضمون الفشل — نوقف بدون ما نضرب السيرفر
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد driverID');
}
var payload = {
'id': box.read(BoxName.driverID),
'password': box.read(BoxName.emailDriver),
'id': driverId,
'aud': '${AK.allowed}$dev',
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
);
var response1 = await http
.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
)
.timeout(const Duration(seconds: 30));
Log.print('response1.request: ${response1.request}');
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) {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
@@ -337,13 +414,32 @@ class LoginDriverController extends GetxController {
if (jwt != null) {
// await box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
}
// await AppInitializer().getKey();
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
}
}
// نجاح التجديد → تصفير العدّاد والـ 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 {
var status = await Permission.locationAlways.status;
if (!status.isGranted) {
@@ -653,10 +749,8 @@ class LoginDriverController extends GetxController {
var d = jsonDecoeded['data'][0];
var jwt = jsonDecoeded['jwt'];
// حفظ التوكن أولاً
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt.toString());
}
box.write(BoxName.emailDriver, (d['email']));
@@ -371,8 +371,7 @@ class RegistrationController extends GetxController {
final uri = Uri.parse(currentUrl);
final request = http.MultipartRequest('POST', uri);
final _jwt = box.read(BoxName.jwt);
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
final String _token = await storage.read(key: BoxName.jwt) ?? '';
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
String nonce = timestamp;
@@ -581,8 +580,8 @@ class RegistrationController extends GetxController {
try {
// ترويسات مشتركة
final _jwt = box.read(BoxName.jwt);
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}';
final String _jwtToken = await storage.read(key: BoxName.jwt) ?? '';
final bearer = 'Bearer $_jwtToken';
final hmac = '${box.read(BoxName.hmac)}';
String fingerPrint =
@@ -233,7 +233,7 @@ class FirebaseMessagesController extends GetxController {
case 'token change':
case 'TOKEN_CHANGE':
box.remove(BoxName.jwt);
await storage.delete(key: BoxName.jwt);
break;
case 'face detect':
@@ -22,7 +22,6 @@ import 'ssl_pinning.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -107,6 +106,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 — دالة مركزية لكل الطلبات
// ───────────────────────────────────────────────────────────────
@@ -119,6 +133,7 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
const totalTimeout = Duration(seconds: 60);
@@ -186,19 +201,33 @@ class CRUD {
}
}
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
final newToken = await _getJwt();
if (newToken.isEmpty) return 'token_expired';
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
}
// 5xx
@@ -218,18 +247,9 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final String token = await _ensureJwt();
if (token.isEmpty) return 'token_expired';
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -250,16 +270,8 @@ class CRUD {
}) async {
try {
// فحص صلاحية التوكن قبل الإرسال
String token = await _getJwt();
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final String token = await _ensureJwt();
if (token.isEmpty) return 'token_expired';
var url = Uri.parse(link);
var response = await _client.post(
@@ -279,15 +291,12 @@ class CRUD {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else if (response.statusCode == 429) {
Log.print('🛑 get: 429 rate limited — $link');
return 'rate_limited';
} else if (response.statusCode == 401) {
if (!_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
// تجديد واحد فقط؛ getJWT فيها single-flight + cooldown
await Get.put(LoginDriverController()).getJWT();
return 'token_expired';
} else if (response.statusCode >= 500) {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
@@ -572,12 +581,8 @@ class CRUD {
// ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ─────────
Future<void> sendEmail(String link, Map<String, String>? payload) async {
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
String token = await _getJwt();
if (!_isJwtValid(token)) {
await LoginDriverController().getJWT();
token = await _getJwt();
}
final String token = await _ensureJwt();
if (token.isEmpty) return;
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -479,8 +479,7 @@ class AI extends GetxController {
bool isLoadingVehicleFrontSy = false;
bool isLoadingVehicleBackSy = false;
Future<void> sendToAI(String type, {required File imageFile}) async {
final _jwt = box.read(BoxName.jwt);
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
final String _token = await storage.read(key: BoxName.jwt) ?? '';
final headers = {
'Authorization': 'Bearer $_token',
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:intaleq_maps/intaleq_maps.dart';
@@ -25,6 +26,37 @@ import 'background_service.dart';
import 'crud.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 {
// ===================================================================
// ====== Tunables ======
@@ -38,6 +70,29 @@ class LocationController extends GetxController with WidgetsBindingObserver {
static const int powerSaveTriggerLevel = 20;
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 ======
// ===================================================================
@@ -84,6 +139,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isReady = 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>> _behaviorBuffer = [];
@@ -121,7 +187,13 @@ class LocationController extends GetxController with WidgetsBindingObserver {
});
socket!.disconnect();
}
return;
}
// ‏أي تغيّر آخر (متاح ⇄ مشغول) يجب أن يصل السيرفر فوراً: هو الحدث الذي
// ‏ينقل الكابتن بين geo:drivers:available و geo:drivers:busy. وبلا هذا
// ‏ننتظر أول حدث GPS — وفلتر المسافة قد يؤجّله دقائق إن كان واقفاً.
_emitCurrentLocationNow();
});
bool deps = await _awaitDependencies();
@@ -130,6 +202,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_isReady = true;
initSocket();
await _detectGmsAvailability();
await _initLocationSettings();
_listenToBatteryChanges();
@@ -163,10 +236,15 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService();
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
Log.print("🔄 Initializing Socket on resume...");
initSocket();
// 🔥 Fix: iOS silently kills WebSockets in background.
// The socket_io_client might still report 'connected' as true for ~20s.
// 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 ||
state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND");
@@ -277,11 +355,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (sid != null || eid != null) {
isSocketConnected = true;
if (myLocation.latitude != 0) {
print("🚀 Sending initial location to socket after connect...");
emitLocationToSocket(myLocation, heading, speed);
}
// ‏هذا الـ emit هو ما يُسجّل الكابتن في geo:drivers:available بعد كل
// ‏اتصال جديد، فلا نتخطاه لمجرد أن أول تثبيت GPS لم يصل بعد.
print("🚀 Sending initial location to socket after connect...");
_emitCurrentLocationNow();
_startHeartbeat();
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الاتصال
@@ -474,6 +552,30 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_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
void emitLocationToSocket(LatLng pos, double head, double spd) {
@@ -555,13 +657,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _subscribeLocationStream() async {
_locSub?.cancel();
int interval = _isPowerSavingMode ? 10000 : 5000;
await location.enableBackgroundMode(enable: true);
location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
// مصدر الحقيقة الوحيد لإعدادات الموقع: _applyProfile.
await _applyProfile(force: true);
_locSub = location.onLocationChanged.listen((LocationData loc) async {
if (loc.latitude == null || loc.longitude == null) return;
@@ -577,6 +675,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
speed = loc.speed ?? 0.0;
heading = loc.heading ?? 0.0;
// إعادة تقييم ملف التتبّع مع كل قراءة — لا يستدعي changeSettings
// إلا إذا تغيّرت الحالة فعلاً (حالة السائق/الرحلة/السكون/البطارية).
_trackStationary();
await _applyProfile();
box.write('last_lat', pos.latitude);
box.write('last_lng', pos.longitude);
box.write('last_heading', heading);
@@ -635,6 +738,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_locSub?.cancel();
_locSub = null;
_appliedProfileSignature = null;
_stationarySince = null;
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
@@ -746,24 +851,123 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) {
_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 {
await location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
accuracy: p.accuracy,
interval: p.interval,
distanceFilter: p.distanceFilter,
);
Log.print(
"🔋 Location settings updated. Power Save: $_isPowerSavingMode");
_appliedProfileSignature = p.signature;
Log.print("📍 Location profile → $p");
} 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()) {
try {
await location.enableBackgroundMode(enable: true);
location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: 1000,
distanceFilter: 10);
// (ب) لا نضبط الإعدادات هنا: كانت interval: 1000 تتعارض مع إعدادات
// _subscribeLocationStream وتُبقي الـ GPS مشتعلاً كل ثانية.
// مصدر الحقيقة الوحيد الآن هو _applyProfile.
} catch (e) {
Log.print("Warning: $e");
}
@@ -488,7 +488,7 @@ class ScanDocumentsByApi extends GetxController {
update();
final String token =
box.read(BoxName.jwt)?.toString().split(AppInformation.addd)[0] ?? '';
await storage.read(key: BoxName.jwt) ?? '';
final String fingerPrint =
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
final String driverID = box.read(BoxName.driverID).toString();
@@ -312,8 +312,7 @@ class ImageController extends GetxController {
required String filename,
required String methodLabel,
}) async {
final jwt = box.read(BoxName.jwt);
final String token = jwt != null ? r(jwt).split(AppInformation.addd)[0] : '';
final String token = await storage.read(key: BoxName.jwt) ?? '';
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
final int fileSizeBytes = await file.length();
@@ -32,8 +32,7 @@ class HomeCaptainController extends GetxController {
Timer? activeTimer;
Map data = {};
bool isHomeMapActive = true;
InlqBitmap carIcon =
InlqBitmap.fromStyleImage('car_icon', size: 2.3);
InlqBitmap carIcon = InlqBitmap.fromStyleImage('car_icon', size: 2.3);
bool isMapReadyForCommands = false;
bool isLoading = true;
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>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().totalPoints;
// Toggle Active State
isActive = !isActive;
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();
if (!active) {
_goOffline();
return;
}
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());
addToken();
// ⚠️ الترتيب مقصود: الحالة تُكتب قبل تشغيل التتبّع.
// ‏أول update_location يُرسَل بعد اتصال السوكيت مباشرة، وهو الذي يُدخل
// ‏الكابتن إلى geo:drivers:available على السيرفر. وكتابة الحالة بعد
// ‏تشغيل التتبّع (كما كانت في آخر السطور) تعني أن ذلك الـ emit قد يحمل
// ‏حالة قديمة ('on' من رحلة سابقة لم تُغلق مثلاً).
_applyInitialDriverStatus();
await getlocation();
onButtonSelected();
setActive(true);
getDriverRate();
addCustomCarIcon();
getKazanPercent();
@@ -830,8 +884,8 @@ class HomeCaptainController extends GetxController {
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
// getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق
// ‏(يكتب 'blocked' عند وجود حظر ساري — ولم تعد أي كتابة بعده تدهسه)
checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً
// Camera follow timer — only moves when the driver has
// actually moved > 15 meters, saving GPU/battery on idle.
@@ -87,8 +87,7 @@ class ComplaintController extends GetxController {
var uri = Uri.parse(AppLink.uploadAudio);
var request = http.MultipartRequest('POST', uri);
final jwt = box.read(BoxName.jwt);
String token = jwt != null ? r(jwt).toString().split(Env.addd)[0] : '';
String token = await storage.read(key: BoxName.jwt) ?? '';
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
var mimeType = lookupMimeType(audioFile.path);
+13 -11
View File
@@ -50,18 +50,20 @@ class DefaultFirebaseOptions {
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8',
appId: '1:825988584191:android:492d9bc1df6b40c51632ca',
messagingSenderId: '825988584191',
projectId: 'siro-a6957',
storageBucket: 'siro-a6957.firebasestorage.app',
apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
appId: '1:1086900987150:android:e3daebe53bf691de77a35f',
messagingSenderId: '1086900987150',
projectId: 'intaleq-d48a7',
storageBucket: 'intaleq-d48a7.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
appId: '1:825988584191:ios:a86f1a54f0b1aaa21632ca',
messagingSenderId: '825988584191',
projectId: 'siro-a6957',
storageBucket: 'siro-a6957.firebasestorage.app',
iosBundleId: 'com.siro.siro-driver',
apiKey: 'AIzaSyAwG09AeehwBfktpKKJwCKQOtEUpHtr-p0',
appId: '1:1086900987150:ios:6d2c7f479c82ba7077a35f',
messagingSenderId: '1086900987150',
projectId: 'intaleq-d48a7',
storageBucket: 'intaleq-d48a7.firebasestorage.app',
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."
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:
sdk: ">=3.0.5 <4.0.0"
@@ -18,7 +18,7 @@ dependencies:
path: ./packages/get
get_storage:
path: ./packages/get_storage
intaleq_maps: ^2.2.1
intaleq_maps: ^2.3.0
secure_string_operations:
path: ./secure_string_operations
trip_overlay_plugin:
@@ -104,6 +104,7 @@ dev_dependencies:
flutter_launcher_icons:
android: "launcher_icon"
ios: true
remove_alpha_ios: true
image_path: "assets/images/logo.png"
min_sdk_android: 21
web:
@@ -4,5 +4,5 @@
تشغيل التطبيق على هذا الجهاز.</string>
<string name="exit_button">إغلاق التطبيق</string>
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
<string name="label">سيرو</string>
<string name="label">انطلق</string>
</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;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -851,6 +852,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
@@ -877,6 +879,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
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>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images
for uploading and access to connect to a call.</string>
<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>
<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>
<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>
<string>This app needs access to your location to provide you with the best ride experience.
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>
<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>
<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>
<string>This app needs access to your location to provide you with the best ride experience.
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>
<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>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add
voice recordings to your photos and videos and access to connect to a call.</string>
<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>
<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>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
@@ -82,26 +82,55 @@ class LoginController extends GetxController {
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
// ─────────────────────────────────────────────────────────────
Future<void> getJWT({bool force = false}) async {
// إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد
static Future<bool>? _jwtFuture;
static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
static Duration get jwtCooldownRemaining {
final d = _jwtCooldownUntil.difference(DateTime.now());
return d.isNegative ? Duration.zero : d;
}
Future<bool> getJWT({bool force = false}) async {
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal(force: force).catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal({bool force = false}) async {
if (!force && isTokenValid()) {
Log.print("JWT is still valid. Skipping request.");
return;
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
}
try {
dev = Platform.isAndroid ? 'android' : 'ios';
// تأكد إن البصمة محدّثة قبل أي طلب
await DeviceHelper.getDeviceFingerprint();
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
final passengerId = box.read(BoxName.passengerID);
final isRegistering = passengerId == null || passengerId.toString().isEmpty;
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
// ── أول تسجيل ─────────────────────────────────────────
// نرسل البصمة المشفرة مع باقي البيانات
// السيرفر سيعمل hash لها ويخزنها في JWT payload
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.passengerID) ?? AK.newId,
'id': passengerId ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
'fingerPrint': fp,
@@ -110,12 +139,14 @@ class LoginController extends GetxController {
var response = await http.post(
Uri.parse(AppLink.loginFirstTime),
body: payload,
);
Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}');
Log.print('payload: $payload');
Log.print('response code: ${response.statusCode}');
Log.print('response body: ${response.body}');
).timeout(const Duration(seconds: 30));
if (response.statusCode == 429) {
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
@@ -126,18 +157,20 @@ class LoginController extends GetxController {
: decoded['jwt']);
if (jwt != null) {
// نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage
box.write(BoxName.jwt, c(jwt));
storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
await EncryptionHelper.initialize();
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response.statusCode}');
} else {
// ── مستخدم موجود: تجديد التوكن
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد passengerID');
}
var payload = {
'id': box.read(BoxName.passengerID),
'id': passengerId,
'fingerPrint': fp,
'aud': '${AK.allowed}$dev',
};
@@ -145,11 +178,15 @@ class LoginController extends GetxController {
var response = await http.post(
Uri.parse(AppLink.loginJwtRider),
body: payload,
);
Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}');
).timeout(const Duration(seconds: 30));
if (response.statusCode == 429) {
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
Log.print('payload: $payload');
Log.print('response: ${response.body}');
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final String? jwt = decoded['data'] != null
@@ -159,16 +196,33 @@ class LoginController extends GetxController {
: decoded['jwt']);
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
}
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
return _onJwtFailure('renew: HTTP ${response.statusCode}');
}
} catch (e) {
Log.print('Error in getJWT: $e');
return _onJwtFailure('Error: $e');
}
}
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
return true;
}
bool _onJwtFailure(String reason) {
_jwtFailures++;
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
return false;
}
// ─────────────────────────────────────────────────────────────
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
// ─────────────────────────────────────────────────────────────
@@ -24,7 +24,6 @@ class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -98,31 +97,50 @@ class CRUD {
Future<String> _getJwt() async {
try {
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
if (encryptedJwt == null || encryptedJwt.isEmpty) {
final String? fallback = box.read(BoxName.jwt);
final jwt = await storage.read(key: BoxName.jwt);
if (jwt == null || jwt.toString().isEmpty) {
// إذا كان التخزين الآمن فارغاً، نحاول استخراج التوكن القديم من GetStorage للركاب القدامى
final fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
try {
return r(fallback).toString().split(Env.addd)[0]; // فك تشفير القديم
} catch (_) {
return fallback.toString(); // ربما تم تخزينه بدون تشفير
}
}
return '';
}
return r(encryptedJwt).toString().split(Env.addd)[0];
} catch (e) {
Log.print('Error reading JWT from SecureStorage: $e');
final String? fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
// التحقق السريع إذا كان التوكن لا يزال مشفراً (يبدأ برموز غريبة وليس ey)
if (!jwt.startsWith('ey')) {
try {
return r(jwt).toString().split(Env.addd)[0];
} catch (_) {}
}
return jwt;
} catch (_) {
return '';
}
}
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
/// Centralized request handler with retry for weak networks.
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
const totalTimeout = Duration(seconds: 60);
@@ -180,17 +198,33 @@ class CRUD {
}
}
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginController()).getJWT();
if (!refreshed) return 'token_expired';
final newToken = await _getJwt();
if (newToken.isEmpty) return 'token_expired';
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
}
if (sc >= 500) {
@@ -206,7 +240,14 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
String token = await _ensureJwt();
if (token.isEmpty) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
// باستثناء تسجيل الدخول لأنه لا يحتاج توكن
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -221,7 +262,12 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
String token = await _ensureJwt();
if (token.isEmpty) {
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -86,7 +86,7 @@ class MapSocketController extends GetxController {
io_client.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'id': passengerId, 'jwt': jwt})
.setQuery({'id': passengerId, 'jwt': jwt, 'EIO': '3'})
// محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart).
// كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق
// من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال
@@ -173,18 +173,6 @@ class MapSocketController extends GetxController {
socket.on('connect_error', (error) {
Log.print("❌ Socket Connect Error: $error");
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) {
+13 -11
View File
@@ -47,19 +47,21 @@ class DefaultFirebaseOptions {
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8',
appId: '1:825988584191:android:7e9088b719dcb7061632ca',
messagingSenderId: '825988584191',
projectId: 'siro-a6957',
storageBucket: 'siro-a6957.firebasestorage.app',
apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
appId: '1:1086900987150:android:b7231956aa6d3b3377a35f',
messagingSenderId: '1086900987150',
projectId: 'intaleq-d48a7',
storageBucket: 'intaleq-d48a7.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
appId: '1:825988584191:ios:4f60966dd0a69b3f1632ca',
messagingSenderId: '825988584191',
projectId: 'siro-a6957',
storageBucket: 'siro-a6957.firebasestorage.app',
iosBundleId: 'com.siroapp.rider',
apiKey: 'AIzaSyDzGO9a-1IDMLD2FxhmOO9ONL1gMssFa9g',
appId: '1:1086900987150:ios:a60973accd7f3dc777a35f',
messagingSenderId: '1086900987150',
projectId: 'intaleq-d48a7',
storageBucket: 'intaleq-d48a7.firebasestorage.app',
androidClientId: '1086900987150-060srlmdjocdcav377rbur4ka14m90b7.apps.googleusercontent.com',
iosClientId: '1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d.apps.googleusercontent.com',
iosBundleId: 'com.Intaleq.intaleq',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
+3 -2
View File
@@ -2,7 +2,7 @@ name: intaleq_rider
description: "A new Flutter project."
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:
sdk: ">=3.0.5 <4.0.0"
@@ -79,7 +79,7 @@ dependencies:
internet_connection_checker: ^3.0.1
connectivity_plus: ^6.1.5
app_links: ^7.0.0
intaleq_maps: ^2.2.1
intaleq_maps: ^2.3.0
socket_io_client: 1.0.2
# home_widget: ^0.7.0+1
@@ -94,6 +94,7 @@ dev_dependencies:
flutter_launcher_icons:
android: "launcher_icon"
ios: true
remove_alpha_ios: true
image_path: "assets/images/logo.png"
min_sdk_android: 21
web:
+12 -1
View File
@@ -17,6 +17,8 @@ function getInternalKey(): 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');
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'));
@@ -31,6 +33,7 @@ $headers = getallheaders();
$receivedKey = $headers['x-internal-key'] ?? $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
if (!empty($INTERNAL_KEY) && $receivedKey !== $INTERNAL_KEY) {
error_log("[api_get_nearby] Unauthorized: receivedKey ($receivedKey) != expectedKey ($INTERNAL_KEY)");
http_response_code(403);
echo json_encode(['status' => false, 'msg' => 'Unauthorized']);
exit;
@@ -79,7 +82,15 @@ try {
$profile = $redis->hgetall("driver:profile:$d_id");
$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:busy', $d_id);
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)
$redisPass = null;
$redisPassPaths = [
getenv('REDIS_PASS_KEY_PATH') ?: '',
__DIR__ . '/../loction-keys/.reds_pass_key',
'/keys/.reds_pass_key',
'/home/location/.reds_pass_key',
'/home/location/.reds_pass_key'
];
foreach ($redisPassPaths as $rpp) {
if (!empty($rpp) && file_exists($rpp)) {
if (file_exists($rpp)) {
$redisPass = trim((string) @file_get_contents($rpp));
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($redisPass)) logMsg('ℹ️ Redis password not set (OK for Docker — no auth required)');
else logMsg('✅ Redis password loaded successfully');
// ============================================================
// 🗄️ Redis Singleton
@@ -368,8 +373,10 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
if ($st === 'off') {
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
logMsg("[REDIS BATCH] Added Driver $driverId to geo:drivers:available at $lat, $lng");
} elseif ($st === 'on') {
$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 {
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
// 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");
$socket->disconnect();
return;
@@ -761,13 +768,27 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$driverState[$driverId] = [
'lat' => 0.0,
'lng' => 0.0,
'speed' => -999.0,
'speed' => -999.0,
'heading' => -999.0,
'status' => '',
'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)");
// 🆕 Deliver any pending orders missed during disconnection
@@ -862,6 +883,8 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
$state = &$driverState[$driverId];
$now = time();
logMsg("[SOCKET] update_location received for Driver $driverId | status=$status, lat=$lat, lng=$lng");
// 1. Forward للراكب (ASYNC + throttle)
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'] = [
'status' => $status,
'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!");
} else {
$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->disconnect();
return;
+1 -8
View File
@@ -53,14 +53,7 @@ try {
];
$con = new PDO($dsn, $user, $pass, $options);
// Ride DB Connection (for cross-db queries)
$rideDbname = getenv('DB_RIDE_NAME') ?: 'rideDB';
$rideDbHost = getenv('DB_RIDE_HOST') ?: (getenv('DB_HOST') ?: 'mysql');
$rideUser = getenv('DB_RIDE_USER') ?: 'root';
$ridePass = getenv('DB_RIDE_PASS') ?: getenv('MYSQL_ROOT_PASSWORD') ?: '';
$dsnRide = "mysql:host=$rideDbHost;dbname=$rideDbname;charset=utf8mb4";
$conRide = new PDO($dsnRide, $rideUser, $ridePass, $options);
// 5. JWT Authentication
include __DIR__ . "/functions.php";
@@ -3,17 +3,35 @@ include "../../connect.php";
$driverID = filterRequest("driverID");
try {
// 1. Get ride stats using $conRide
$sqlRide = "SELECT
(SELECT COUNT(*) FROM `ride` WHERE LOWER(`status`) IN ('finished','completed') AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 7 HOUR AND CURRENT_DATE() + INTERVAL 10 HOUR AND `driver_id` = :driverID) AS morning_count,
(SELECT COUNT(*) FROM `ride` WHERE LOWER(`status`) IN ('finished','completed') AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 15 HOUR AND CURRENT_DATE() + INTERVAL 18 HOUR AND `driver_id` = :driverID) AS afternoon_count,
(SELECT COALESCE(SUM(price), 0) FROM `ride` WHERE `driver_id` = :driverID AND LOWER(`status`) IN ('finished','completed') AND `created_at` > CURRENT_DATE() - INTERVAL 1 WEEK) AS total_amount_last_week
FROM dual";
$stmtRide = $conRide->prepare($sqlRide);
$stmtRide->bindValue(':driverID', $driverID);
$stmtRide->execute();
$rideStats = $stmtRide->fetch(PDO::FETCH_ASSOC) ?: [];
// 1. Get ride stats using local $conRide
$rideStats = [];
try {
$rideDbname = getenv('DB_RIDE_NAME') ?: 'rideDB';
$rideDbHost = getenv('DB_RIDE_HOST') ?: (getenv('DB_HOST') ?: 'mysql');
$rideUser = getenv('DB_RIDE_USER') ?: 'root';
$ridePass = getenv('DB_RIDE_PASS') ?: getenv('MYSQL_ROOT_PASSWORD') ?: '';
$dsnRide = "mysql:host=$rideDbHost;dbname=$rideDbname;charset=utf8mb4";
$options = [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$conRide = new PDO($dsnRide, $rideUser, $ridePass, $options);
$sqlRide = "SELECT
(SELECT COUNT(*) FROM `ride` WHERE LOWER(`status`) IN ('finished','completed') AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 7 HOUR AND CURRENT_DATE() + INTERVAL 10 HOUR AND `driver_id` = :driverID) AS morning_count,
(SELECT COUNT(*) FROM `ride` WHERE LOWER(`status`) IN ('finished','completed') AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 15 HOUR AND CURRENT_DATE() + INTERVAL 18 HOUR AND `driver_id` = :driverID) AS afternoon_count,
(SELECT COALESCE(SUM(price), 0) FROM `ride` WHERE `driver_id` = :driverID AND LOWER(`status`) IN ('finished','completed') AND `created_at` > CURRENT_DATE() - INTERVAL 1 WEEK) AS total_amount_last_week
FROM dual";
$stmtRide = $conRide->prepare($sqlRide);
$stmtRide->bindValue(':driverID', $driverID);
$stmtRide->execute();
$rideStats = $stmtRide->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Exception $e) {
error_log('[getAllPayment] DB_RIDE Connection Error: ' . $e->getMessage());
$rideStats = ['morning_count' => 0, 'afternoon_count' => 0, 'total_amount_last_week' => 0];
}
// 2. Get payment stats using $con
$sqlPayment = "SELECT COALESCE(SUM(amount), 0) AS total_amount FROM payments WHERE isGiven = 'waiting' AND `driverID` = :driverID";
@@ -13,6 +13,18 @@ WHERE
";
// كان المعرّف يُدمج في نص الاستعلام مباشرةً — حقن SQL.
try {
$rideDbname = getenv('DB_RIDE_NAME') ?: 'rideDB';
$rideDbHost = getenv('DB_RIDE_HOST') ?: (getenv('DB_HOST') ?: 'mysql');
$rideUser = getenv('DB_RIDE_USER') ?: 'root';
$ridePass = getenv('DB_RIDE_PASS') ?: getenv('MYSQL_ROOT_PASSWORD') ?: '';
$dsnRide = "mysql:host=$rideDbHost;dbname=$rideDbname;charset=utf8mb4";
$options = [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$conRide = new PDO($dsnRide, $rideUser, $ridePass, $options);
$stmt = $conRide->prepare($sql);
$stmt->bindValue(':driver_id', $driver_id);
$stmt->execute();
+7 -4
View File
@@ -75,9 +75,12 @@ function getLocationRedis(): ?\Redis {
catch (\Exception $_) { $_locationRedis = null; }
}
try {
$redisPass = '';
foreach ([getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'] as $p) {
if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; }
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
$keys = [getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'];
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');
$r = new \Redis();
@@ -221,7 +224,7 @@ $io->on('connection', function ($socket) {
socket_log("[WARNING] JWT Secret is not configured on the server!");
} else {
$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->disconnect();
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;