From e9be1b6d4a52a871a9999d91da6c47435e6587b2 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 29 May 2026 22:41:24 +0300 Subject: [PATCH] add new features like realtime 2026-05-29-22 --- auth/syria/driver/sendWhatsAppDriver.php | 50 +++++- auth/syria/driver/verifyOtp.php | 41 ++++- auth/syria/sendWhatsOpt.php | 168 +++++------------- auth/syria/verifyOtp.php | 63 ++++--- .../driver/send_otp_driver.php | 56 ++++-- auth/token_passenger/send_otp.php | 55 ++++-- 6 files changed, 240 insertions(+), 193 deletions(-) diff --git a/auth/syria/driver/sendWhatsAppDriver.php b/auth/syria/driver/sendWhatsAppDriver.php index e26145b..74545fb 100755 --- a/auth/syria/driver/sendWhatsAppDriver.php +++ b/auth/syria/driver/sendWhatsAppDriver.php @@ -33,12 +33,50 @@ if (is_blacklisted_driver($con, $encryptionHelper, $receiver)) { exit(); } -/* 1) توليد الـ OTP */ -$otp = rand(10000, 99999); -$messageBody = "Your verification code for Intaleq is: " . $otp; +/* 1) توليد الـ OTP (3 خانات) */ +$otp = (string)rand(100, 999); -/* 🟢 2) تخطي الإرسال الفعلي */ -error_log("[send_otp_driver.php] Skipping actual WhatsApp send. OTP for $receiver: $otp"); +/* 2) إرسال الرمز عبر بوابة الفلاش كول / واتساب */ +$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php'; +$appKey = getenv('NABEH_OTP_APP_KEY'); + +$payload = [ + 'phone' => $receiver, + 'device_type' => 'android', + 'method' => 'whatsapp', + 'code' => $otp +]; + +$ch = curl_init($nabehUrl); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + "X-App-Key: $appKey" + ], + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5 +]); + +$res = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ($error) { + error_log("⚠️ [Flash Call OTP Driver] Curl Error: $error"); + jsonError('Failed to connect to OTP service'); + exit; +} + +$decoded = json_decode((string)$res, true); +if ($httpCode !== 200 || !($decoded['success'] ?? false)) { + error_log("❌ [Flash Call OTP Driver] Failed response: Code $httpCode | Body: " . (string)$res); + jsonError($decoded['message'] ?? 'Failed to request verification code'); + exit; +} /* 3) حفظ الـ OTP في قاعدة البيانات */ $receiver_enc = $encryptionHelper->encryptData($receiver); @@ -59,7 +97,7 @@ try { "); $stmt->execute([$receiver_enc, $otp_enc, $exp, $now]); - jsonSuccess(null, 'OTP generated and saved successfully (no message sent)'); + jsonSuccess(null, 'OTP sent and saved successfully'); error_log("[send_otp_driver.php] OTP saved for driver $receiver"); } catch (PDOException $e) { diff --git a/auth/syria/driver/verifyOtp.php b/auth/syria/driver/verifyOtp.php index acb2834..035b5f3 100755 --- a/auth/syria/driver/verifyOtp.php +++ b/auth/syria/driver/verifyOtp.php @@ -2,15 +2,45 @@ require_once __DIR__ . '/../../../connect.php'; $phoneNumber = filterRequest("phone_number"); +$otp = filterRequest("otp"); $email = $phoneNumber . '@intaleqapp.com'; -error_log("📥 [verifyOtp.php] Received phone number: $phoneNumber"); +error_log("📥 [verifyOtp.php] Received phone number: $phoneNumber | OTP: $otp"); + +if (empty($phoneNumber) || empty($otp)) { + jsonError("Phone number and OTP are required."); + exit(); +} // 🔐 تشفير البيانات $phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber); $email_encrypted = $encryptionHelper->encryptData($email); try { + // 🔍 1. التحقق من السجل المخزن في قاعدة البيانات + $stmtSelect = $con->prepare("SELECT * FROM phone_verification WHERE phone_number = ? ORDER BY created_at DESC LIMIT 1"); + $stmtSelect->execute([$phoneNumber_encrypted]); + $record = $stmtSelect->fetch(PDO::FETCH_ASSOC); + + if (!$record) { + jsonError("Verification session not found. Please request a new code."); + exit(); + } + + // 🔍 2. فك تشفير ومقارنة الرمز + $decryptedOtp = $encryptionHelper->decryptData($record['token_code']); + if ($decryptedOtp !== $otp) { + jsonError("Invalid verification code."); + exit(); + } + + // 🔍 3. التحقق من الصلاحية + $now = date('Y-m-d H:i:s'); + if ($record['expiration_time'] && $record['expiration_time'] < $now) { + jsonError("Verification code has expired. Please request a new one."); + exit(); + } + // 🧹 حذف أي رموز قديمة لنفس الرقم $con->prepare("DELETE FROM phone_verification WHERE phone_number = ?") ->execute([$phoneNumber_encrypted]); @@ -22,9 +52,6 @@ try { // 🔐 توليد رمز تجريبي (بدون OTP حقيقي لتجنب Null) $dummyToken = $encryptionHelper->encryptData('AUTO'); - // 🕒 الوقت الحالي - $now = date('Y-m-d H:i:s'); - // ✅ إدخال سجل تحقق مباشر $stmt = $con->prepare(" INSERT INTO phone_verification @@ -33,7 +60,7 @@ try { "); $stmt->execute([$phoneNumber_encrypted, $dummyToken, $email_encrypted, $driverID, $now]); - error_log("✅ [verifyOtp.php] Auto verification record inserted successfully for $phoneNumber"); + error_log("✅ [verifyOtp.php] Verification record inserted successfully for $phoneNumber"); // 🔍 التحقق إذا السائق موجود مسبقاً $checkDriverStmt = $con->prepare("SELECT * FROM driver WHERE phone = ?"); @@ -54,9 +81,9 @@ try { ] ]); } else { - error_log("🆕 [verifyOtp.php] Phone verified automatically. Driver not found."); + error_log("🆕 [verifyOtp.php] Phone verified. Driver not found."); printSuccess([ - "message" => "Phone number verified automatically (no OTP required).", + "message" => "Phone number verified successfully.", "isRegistered" => false, "driverID" => $driverID ]); diff --git a/auth/syria/sendWhatsOpt.php b/auth/syria/sendWhatsOpt.php index 1e2ff28..0c7360d 100755 --- a/auth/syria/sendWhatsOpt.php +++ b/auth/syria/sendWhatsOpt.php @@ -45,12 +45,50 @@ if (is_blacklisted($con, $encryptionHelper, $receiver)) { exit(); } -/* 1) Generate OTP */ -$otp = rand(10000, 99999); -$messageBody = "Your verification code for Intaleq is: " . $otp; +/* 1) Generate OTP (3 digits) */ +$otp = (string)rand(100, 999); -/* 🟢 2) Skip sending and log instead */ -error_log("[send_otp] Skipping actual send. OTP generated for $receiver: $otp"); +/* 2) Send via Flash Call / WhatsApp Gateway */ +$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php'; +$appKey = getenv('NABEH_OTP_APP_KEY'); + +$payload = [ + 'phone' => $receiver, + 'device_type' => 'android', + 'method' => 'whatsapp', + 'code' => $otp +]; + +$ch = curl_init($nabehUrl); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + "X-App-Key: $appKey" + ], + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5 +]); + +$res = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ($error) { + error_log("⚠️ [Flash Call OTP Passenger] Curl Error: $error"); + jsonError('Failed to connect to OTP service'); + exit; +} + +$decoded = json_decode((string)$res, true); +if ($httpCode !== 200 || !($decoded['success'] ?? false)) { + error_log("❌ [Flash Call OTP Passenger] Failed response: Code $httpCode | Body: " . (string)$res); + jsonError($decoded['message'] ?? 'Failed to request verification code'); + exit; +} /* 3) Save OTP (encrypted) */ $receiver_enc = $encryptionHelper->encryptData($receiver); @@ -70,128 +108,10 @@ try { "); $stmt->execute([$receiver_enc, $otp_enc, $exp, $now]); - jsonSuccess(null, 'OTP generated and saved successfully (no message sent)'); + jsonSuccess(null, 'OTP sent and saved successfully'); error_log("[send_otp] OTP saved successfully for $receiver"); } catch (PDOException $e) { error_log("[send_otp] DB error: ".$e->getMessage()); jsonError('OTP generated but failed to save to database'); } - -/* -require_once __DIR__ . '/../../connect.php'; - -error_log("--- [send_otp.php] Started ---"); - - -function normalize_phone($s) { return preg_replace('/\D+/', '', (string)$s); } - - -function is_blacklisted(PDO $con, $encryptionHelper, string $phone): bool { - $raw = trim($phone); - $norm = normalize_phone($raw); - - // شَفِّر قبل السؤال - $enc_raw = $encryptionHelper->encryptData($raw); - $enc_norm = $encryptionHelper->encryptData($norm); - - $sql = "SELECT 1 - FROM passenger_blacklist - WHERE phone IN (:enc_raw, :enc_norm) - AND (expires_at IS NULL OR expires_at > NOW()) - LIMIT 1"; - - $q = $con->prepare($sql); - $q->execute([ - 'enc_raw' => $enc_raw, - 'enc_norm' => $enc_norm, - ]); - - return (bool)$q->fetchColumn(); -} - -$receiver = filterRequest("receiver"); -if (!$receiver) { jsonError('Phone number is required.'); exit(); } - -if (is_blacklisted($con, $encryptionHelper, $receiver)) { - jsonError('This phone is blacklisted and cannot receive OTP.'); - error_log("[send_otp] BLOCKED (blacklisted): $receiver"); - exit(); -} - -$otp = rand(10000, 99999); -$messageBody = "Your verification code for Intaleq is: " . $otp; - -function normalize($raw) { - if (is_string($raw)) return json_decode($raw, true) ?: []; - if ($raw instanceof stdClass) return (array)$raw; - return is_array($raw) ? $raw : []; -} - -$response = normalize(sendWhatsAppFromServer($receiver, $messageBody)); -$sentOK = $response['success'] ?? false; - -if (!$sentOK) { - error_log("[send_otp] WA-Server failed ⇒ ".(($response['message'] ?? null) ?: json_encode($response))); - - $payload = [ - "number" => $receiver, - "type" => "text", - "message" => $messageBody, - "instance_id" => getenv("RASEEL_DRIVER_INSTANCE_ID"), - "access_token" => getenv("RASEEL_DRIVER_ACCESS_TOKEN") - ]; - $response = callAPI("POST", "https://raseelplus.com/api/send", json_encode($payload)); - $response = normalize($response); - - $sentOK = ($response['status'] ?? '') === 'success'; - if (!$sentOK) { - error_log("[send_otp] RaseelPlus failed ⇒ ".json_encode($response)); - jsonError('Failed to send OTP: '.($response['message'] ?? 'Unknown error')); - exit(); - } -} - -$receiver_enc = $encryptionHelper->encryptData($receiver); // الهاتف المُرسل (خام) مُشفّر -$otp_enc = $encryptionHelper->encryptData($otp); - -$exp = date('Y-m-d H:i:s', strtotime('+5 minutes')); -$now = date('Y-m-d H:i:s'); - -try { - $con->prepare("DELETE FROM phone_verification_passenger WHERE phone_number = ?") - ->execute([$receiver_enc]); - - $stmt = $con->prepare(" - INSERT INTO phone_verification_passenger - (phone_number, token, expiration_time, verified, created_at) - VALUES (?, ?, ?, 0, ?) - "); - $stmt->execute([$receiver_enc, $otp_enc, $exp, $now]); - - jsonSuccess(null, 'OTP sent and saved successfully'); - error_log("[send_otp] OTP saved for $receiver"); - -} catch (PDOException $e) { - error_log("[send_otp] DB error: ".$e->getMessage()); - jsonError('OTP sent but failed to save to database'); -} - -function callAPI($method, $url, $data) { - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_POSTFIELDS => $data, - CURLOPT_HTTPHEADER => [ - "Content-Type: application/json", - "Accept: application/json" - ], - ]); - $body = curl_exec($ch); - $err = curl_error($ch); - curl_close($ch); - return $err ? [] : json_decode($body, true); -} -*/ diff --git a/auth/syria/verifyOtp.php b/auth/syria/verifyOtp.php index 0e71039..ce2974a 100755 --- a/auth/syria/verifyOtp.php +++ b/auth/syria/verifyOtp.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../../connect.php'; error_log("[Auth_Debug] Start processing phone verification request."); $phoneNumber = filterRequest("phone_number"); +$otp = filterRequest("otp"); if (!$phoneNumber) { error_log("[Auth_Error] Phone number is missing in the request."); @@ -13,35 +14,60 @@ if (!$phoneNumber) { exit(); } -// تسجيل الرقم (مشفر أو عادي حسب الحاجة، يفضل عدم تسجيله عادي لأسباب الخصوصية لكن هنا للتوضيح) -error_log("[Auth_Debug] Received phone number (Masked): " . substr($phoneNumber, 0, 7) . "*****"); +if (!$otp) { + error_log("[Auth_Error] OTP is missing in the request."); + jsonError("OTP is required"); + exit(); +} + +// تسجيل الرقم +error_log("[Auth_Debug] Received phone number (Masked): " . substr($phoneNumber, 0, 7) . "***** | OTP: " . $otp); // تشفير رقم الهاتف $phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber); error_log("[Auth_Debug] Phone number encrypted successfully."); try { - // ✅ 1. حذف أي رموز قديمة لنفس الرقم + // ✅ 1. التحقق من السجل المخزن في قاعدة البيانات + $stmtSelect = $con->prepare("SELECT * FROM phone_verification_passenger WHERE phone_number = ? ORDER BY created_at DESC LIMIT 1"); + $stmtSelect->execute([$phoneNumber_encrypted]); + $record = $stmtSelect->fetch(PDO::FETCH_ASSOC); + + if (!$record) { + error_log("[Auth_Error] No verification record found for this number."); + jsonError("Verification session not found. Please request a new code."); + exit(); + } + + // ✅ 2. فك تشفير ومقارنة الرمز + $decryptedOtp = $encryptionHelper->decryptData($record['token']); + if ($decryptedOtp !== $otp) { + error_log("[Auth_Error] OTP mismatch. Expected: $decryptedOtp, Got: $otp"); + jsonError("Invalid verification code."); + exit(); + } + + // ✅ 3. التحقق من الصلاحية (خلال 5 دقائق) + $now = date('Y-m-d H:i:s'); + if ($record['expiration_time'] && $record['expiration_time'] < $now) { + error_log("[Auth_Error] OTP expired."); + jsonError("Verification code has expired. Please request a new one."); + exit(); + } + + // ✅ 4. حذف السجلات القديمة وإدخال سجل مؤكد (verified = 1) error_log("[Auth_Step_1] Deleting old verification records for this phone..."); - $stmtDelete = $con->prepare("DELETE FROM phone_verification_passenger WHERE phone_number = ?"); $stmtDelete->execute([$phoneNumber_encrypted]); - - error_log("[Auth_Step_1] Old records deleted (if any)."); - // ✅ 2. إدخال سجل جديد مع تحقق مباشر (بدون OTP) - $now = date('Y-m-d H:i:s'); - error_log("[Auth_Step_2] Inserting new verified record at: " . $now); - - $stmt = $con->prepare(" + $stmtInsert = $con->prepare(" INSERT INTO phone_verification_passenger (phone_number, token, expiration_time, verified, created_at) VALUES (?, NULL, NULL, 1, ?) "); - $stmt->execute([$phoneNumber_encrypted, $now]); - - error_log("[Auth_Step_2] New record inserted successfully."); + $stmtInsert->execute([$phoneNumber_encrypted, $now]); + error_log("[Auth_Step_1] Inserted verified record."); - // ✅ 3. فحص هل الراكب موجود مسبقاً + // ✅ 5. فحص هل الراكب موجود مسبقاً error_log("[Auth_Step_3] Checking if passenger exists in passengers table..."); $checkPassengerStmt = $con->prepare(" @@ -70,24 +96,19 @@ try { error_log("[Auth_Result] Passenger Not Found. Treating as new user."); printSuccess([ - "message" => "Phone number verified automatically (no OTP required).", + "message" => "Phone number verified successfully.", "isRegistered" => false ]); } } catch (PDOException $e) { - // تسجيل الخطأ بالتفصيل في ملف اللوج error_log("[Auth_DB_Exception] Error: " . $e->getMessage() . " | File: " . $e->getFile() . " | Line: " . $e->getLine()); - - // طباعة رسالة الخطأ للمستخدم (يفضل عدم إظهار تفاصيل الـ SQL للمستخدم النهائي لأسباب أمنية) jsonError("Database error occurred. Please contact support."); } catch (Exception $e) { - // التقاط أي أخطاء عامة أخرى error_log("[Auth_General_Exception] Error: " . $e->getMessage()); jsonError("An unexpected error occurred."); } // تسجيل نهاية الطلب error_log("[Auth_Debug] Request processing finished."); - ?> \ No newline at end of file diff --git a/auth/token_passenger/driver/send_otp_driver.php b/auth/token_passenger/driver/send_otp_driver.php index 6eb0c1f..2e0d5ce 100755 --- a/auth/token_passenger/driver/send_otp_driver.php +++ b/auth/token_passenger/driver/send_otp_driver.php @@ -2,8 +2,8 @@ // File: send_otp_driver.php (إصدار بدون RaseelPlus) require_once __DIR__ . '/../../../connect.php'; -/* 1) توليد رمز التحقق --------------------------------------------------- */ -$otp = rand(10000, 99999); +/* 1) توليد رمز التحقق (3 خانات) --------------------------------------------------- */ +$otp = (string)rand(100, 999); $receiver = filterRequest("receiver"); if (empty($receiver)) { @@ -11,25 +11,45 @@ if (empty($receiver)) { exit(); } -/* 2) نص الرسالة وإرسالها عبر دالتك الجديدة ------------------------------ */ -$messageBody = "رمز التحقق الخاص بك لتطبيق انطلق درايفر هو: " . $otp; +/* 2) إرسال عبر بوابة الفلاش كول / واتساب ------------------------------ */ +$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php'; +$appKey = getenv('NABEH_OTP_APP_KEY'); -/** - * نفترض أن sendWhatsAppFromServer() تُرجع: - * [ - * 'success' => true/false, - * 'message' => 'Message sent successfully!', - * 'details' => ['status' => 'PENDING' | 'SENT' | …] - * ] - */ -$raw = sendWhatsAppFromServer($receiver, $messageBody); -$response = is_string($raw) ? json_decode($raw, true) : (array) $raw; +$payload = [ + 'phone' => $receiver, + 'device_type' => 'android', + 'method' => 'whatsapp', + 'code' => $otp +]; -$sentOK = $response['success'] ?? false; -$waStatus = $response['details']['status'] ?? ''; +$ch = curl_init($nabehUrl); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + "X-App-Key: $appKey" + ], + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5 +]); -if ($sentOK ) { +$res = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); +if ($error) { + error_log("⚠️ [Flash Call OTP Token Driver] Curl Error: $error"); + jsonError('Failed to connect to OTP service'); + exit; +} + +$decoded = json_decode((string)$res, true); +$sentOK = ($httpCode === 200 && ($decoded['success'] ?? false)); + +if ($sentOK) { /* 3) تشفير البيانات وحفظها في DB ----------------------------------- */ $receiver_enc = $encryptionHelper->encryptData($receiver); $otp_enc = $encryptionHelper->encryptData($otp); @@ -56,7 +76,7 @@ if ($sentOK ) { } } else { - $errMsg = $response['message'] ?? 'Unknown error'; + $errMsg = $decoded['message'] ?? 'Unknown error'; jsonError('Failed to send OTP: ' . $errMsg); } diff --git a/auth/token_passenger/send_otp.php b/auth/token_passenger/send_otp.php index 3d596e9..4e0a518 100755 --- a/auth/token_passenger/send_otp.php +++ b/auth/token_passenger/send_otp.php @@ -2,8 +2,8 @@ // File: send_otp.php (بديل عن النسخة المعتمدة على RaseelPlus) require_once __DIR__ . '/../../connect.php'; -/* 1) توليد رمز التحقق */ -$otp = rand(10000, 99999); +/* 1) توليد رمز التحقق (3 خانات) */ +$otp = (string)rand(100, 999); $receiver = filterRequest("receiver"); if (empty($receiver)) { @@ -11,24 +11,45 @@ if (empty($receiver)) { exit(); } -/* 2) نصّ الرسالة وإرسالها عبر دالتك الجديدة */ -$messageBody = "Your verification code for Intaleq is: " . $otp; +/* 2) إرسال عبر بوابة الفلاش كول / واتساب */ +$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php'; +$appKey = getenv('NABEH_OTP_APP_KEY'); -$raw = sendWhatsAppFromServer($receiver, $messageBody); -$response = is_string($raw) ? json_decode($raw, true) : (array) $raw; +$payload = [ + 'phone' => $receiver, + 'device_type' => 'android', + 'method' => 'whatsapp', + 'code' => $otp +]; -/* - * نتوقع بنية مثل: - * [ - * 'success' => true, - * 'details' => ['status' => 'PENDING' | 'SENT' | …] - * ] - */ -$sentOK = $response['success'] ?? false; -$statusOK = in_array($response['details']['status'] ?? '', ['PENDING', 'SENT', 'DELIVERED'], true); +$ch = curl_init($nabehUrl); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + "X-App-Key: $appKey" + ], + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5 +]); -if ($sentOK ) { +$res = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); +if ($error) { + error_log("⚠️ [Flash Call OTP Token Passenger] Curl Error: $error"); + jsonError('Failed to connect to OTP service'); + exit; +} + +$decoded = json_decode((string)$res, true); +$sentOK = ($httpCode === 200 && ($decoded['success'] ?? false)); + +if ($sentOK) { /* 3) تشفير البيانات وحفظ الرمز في قاعدة البيانات */ $receiver_enc = $encryptionHelper->encryptData($receiver); $otp_enc = $encryptionHelper->encryptData($otp); @@ -54,7 +75,7 @@ if ($sentOK ) { } } else { - $errMsg = $response['message'] ?? 'Unknown error'; + $errMsg = $decoded['message'] ?? 'Unknown error'; jsonError('Failed to send OTP: ' . $errMsg); }