9 Commits
79 changed files with 831 additions and 260 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.");
+71 -37
View File
@@ -53,48 +53,82 @@ try {
$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);
}
$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'])) {
// ── مسار السائقين المسجلين عبر 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';
}
+6
View File
@@ -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)
@@ -14,7 +14,7 @@ try {
status,
start_location,
end_location,
car_type AS carType,
carType,
driver_id,distance,
price,
created_at
+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'
--
+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>
@@ -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)}',
@@ -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();
@@ -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',
);
}
+1
View File
@@ -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',
+2 -1
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: 2.0.0+66
version: 34.0.0+67
environment:
sdk: ">=3.0.5 <4.0.0"
@@ -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;
+12 -3
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");
}
}
}
@@ -876,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)) {
+6 -3
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();