From cbd1870a555336a9c82c9aee49f5c996faffa33b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 6 Apr 2026 17:05:35 +0000 Subject: [PATCH] update thawani gateway --- .../uploads/teacher_1775489347_men.webp | Bin 0 -> 5780 bytes checkout.php | 90 ++++++++++++++- includes/app.php | 12 +- live_lesson.php | 107 +++++++++++------- teacher.php | 21 +++- 5 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 assets/images/uploads/teacher_1775489347_men.webp diff --git a/assets/images/uploads/teacher_1775489347_men.webp b/assets/images/uploads/teacher_1775489347_men.webp new file mode 100644 index 0000000000000000000000000000000000000000..c1016a7fccdb42a76ba44570f89f8a943279b788 GIT binary patch literal 5780 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 $form['preferred_language'], 'plan_key' => $plan['key'], 'billing_cycle' => $form['billing_cycle'], - 'payment_status' => 'active', + 'payment_status' => 'active', // MVP: Mark active immediately 'payment_gateway' => 'Thawani', 'thawani_reference' => $reference, 'wablas_opt_in' => $form['wablas_opt_in'], ]); - $_SESSION['subscription_id'] = $id; - $_SESSION['student_email'] = $form['email']; $courseId = (int) ($_GET['course_id'] ?? $_POST['course_id'] ?? 0); if ($courseId > 0) { @@ -59,8 +57,90 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->execute([$courseId, $id]); } catch (Exception $e) {} } - header('Location: ' . app_url('subscription.php', ['id' => $id, 'created' => 1])); - exit; + + // --- Thawani Integration --- + $stmtThawani = db()->query("SELECT thawani_secret_key, thawani_publishable_key, thawani_mode FROM platform_profile WHERE id = 1"); + $thawaniConfig = $stmtThawani->fetch(PDO::FETCH_ASSOC); + + $thawaniRedirect = null; + if ($thawaniConfig && !empty($thawaniConfig['thawani_secret_key']) && !empty($thawaniConfig['thawani_publishable_key'])) { + $mode = $thawaniConfig['thawani_mode'] ?? 'test'; + $baseUrl = $mode === 'live' ? 'https://checkout.thawani.om' : 'https://uatcheckout.thawani.om'; + + $priceOMR = $course ? (float) $course['price'] : (float) ($cycle === 'yearly' ? $plan['price_yearly'] : $plan['price_monthly']); + $priceBaisa = (int) ($priceOMR * 1000); + + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + $protocol = $_SERVER['HTTP_X_FORWARDED_PROTO'] . "://"; + } + $host = $_SERVER['HTTP_HOST']; + $scriptDir = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); + $appBaseUrl = $protocol . $host . $scriptDir; + + $successUrl = $appBaseUrl . '/subscription.php?id=' . $id . '&created=1'; + $cancelUrl = $appBaseUrl . '/checkout.php?plan=' . $plan['key'] . '&cycle=' . $cycle; + if ($courseId > 0) { + $cancelUrl .= '&course_id=' . $courseId; + } + + $productName = $course ? (current_lang() === 'ar' ? $course['name_ar'] : $course['name_en']) : plan_name($plan); + $productName = mb_substr($productName, 0, 40); // Thawani character limit + if (empty($productName)) $productName = 'Course Enrollment'; + + $payload = [ + 'client_reference_id' => (string) $id, + 'mode' => 'payment', + 'products' => [ + [ + 'name' => $productName, + 'quantity' => 1, + 'unit_amount' => $priceBaisa + ] + ], + 'success_url' => $successUrl, + 'cancel_url' => $cancelUrl, + 'metadata' => [ + 'subscription_id' => $id, + 'customer_email' => $form['email'] + ] + ]; + + $ch = curl_init("$baseUrl/api/v1/checkout/session"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "thawani-api-key: {$thawaniConfig['thawani_secret_key']}", + "Content-Type: application/json" + ]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + $response = curl_exec($ch); + curl_close($ch); + + if ($response) { + $resData = json_decode($response, true); + if (isset($resData['success']) && $resData['success'] && isset($resData['data']['session_id'])) { + $sessionId = $resData['data']['session_id']; + db()->query("UPDATE subscriptions SET thawani_reference = " . db()->quote($sessionId) . " WHERE id = " . $id); + $thawaniRedirect = "$baseUrl/pay/$sessionId?key={$thawaniConfig['thawani_publishable_key']}"; + } else { + error_log('Thawani Session Error: ' . $response); + } + } + } + + if ($thawaniRedirect) { + $_SESSION['subscription_id'] = $id; + $_SESSION['student_email'] = $form['email']; + header('Location: ' . $thawaniRedirect); + exit; + } else { + db()->query("DELETE FROM student_subscriptions WHERE id = " . $id); + if ($courseId > 0) { + db()->query("DELETE FROM course_students WHERE course_id = " . $courseId . " AND student_id = " . $id); + } + $errors[] = t('Thawani payment gateway is not fully configured (missing keys) or the API call failed. Please update your API keys in the Teacher Panel > Integrations.', 'بوابة دفع ثواني غير مهيأة بالكامل (المفاتيح مفقودة) أو فشل الاتصال. يرجى تحديث مفاتيحك في لوحة المعلم > التكاملات.'); + } } } diff --git a/includes/app.php b/includes/app.php index f1270ec..f327d26 100644 --- a/includes/app.php +++ b/includes/app.php @@ -260,8 +260,8 @@ function subjects_catalog(): array $rows = $stmt->fetchAll(); $result = []; foreach ($rows as $row) { - $row['modules_en'] = json_decode($row['modules_en'] ?? '[]', true) ?: []; - $row['modules_ar'] = json_decode($row['modules_ar'] ?? '[]', true) ?: []; + $row['modules_en'] = json_decode($row['modules_en'] ? (string)$row['modules_en'] : '[]', true) ?: []; + $row['modules_ar'] = json_decode($row['modules_ar'] ? (string)$row['modules_ar'] : '[]', true) ?: []; $class_id = (int)$row['class_id']; $subject_id = (int)$row['id']; @@ -384,22 +384,22 @@ function subject_summary(array $subject): string function subject_teacher(array $subject): string { - return current_lang() === 'ar' ? $subject['teacher_ar'] : $subject['teacher_en']; + return (string)(current_lang() === 'ar' ? ($subject['teacher_ar'] ?? '') : ($subject['teacher_en'] ?? '')); } function subject_level(array $subject): string { - return current_lang() === 'ar' ? $subject['level_ar'] : $subject['level_en']; + return (string)(current_lang() === 'ar' ? ($subject['level_ar'] ?? '') : ($subject['level_en'] ?? '')); } function subject_duration(array $subject): string { - return current_lang() === 'ar' ? $subject['duration_ar'] : $subject['duration_en']; + return (string)(current_lang() === 'ar' ? ($subject['duration_ar'] ?? '') : ($subject['duration_en'] ?? '')); } function subject_next_live(array $subject): string { - return current_lang() === 'ar' ? $subject['next_live_ar'] : $subject['next_live_en']; + return (string)(current_lang() === 'ar' ? ($subject['next_live_ar'] ?? '') : ($subject['next_live_en'] ?? '')); } function subject_modules(array $subject): array diff --git a/live_lesson.php b/live_lesson.php index 5f35509..5466075 100644 --- a/live_lesson.php +++ b/live_lesson.php @@ -55,6 +55,7 @@ if ($is_teacher && isset($_GET['end'])) { $room_name = $lesson['room_name']; $user_display_name = $is_teacher ? 'Teacher' : 'Student'; +$has_meet = !empty($lesson['meet_url']); render_head( t('Live Lesson: ', 'درس مباشر: ') . t($lesson['title'], $lesson['title']), @@ -83,6 +84,9 @@ render_head( .btn-end { background: #dc3545; color: white; border: none; padding: 5px 15px; border-radius: 4px; text-decoration: none; font-size: 0.9rem; } .btn-end:hover { background: #c82333; color: white; } .btn-leave { background: #6c757d; color: white; border: none; padding: 5px 15px; border-radius: 4px; text-decoration: none; font-size: 0.9rem; } + .meet-btn { padding: 15px 30px; font-size: 1.2rem; border-radius: 50px; text-decoration: none; display: inline-flex; align-items: center; gap: 10px; font-weight: bold; background: #fff; color: #3c4043; transition: all 0.3s; } + .meet-btn:hover { background: #f8f9fa; transform: translateY(-2px); box-shadow: 0 4px 15px rgba(255,255,255,0.2); color: #1a73e8; } + .meet-btn svg { width: 24px; height: 24px; }
@@ -95,9 +99,9 @@ render_head(
- + - Ended + @@ -126,47 +130,66 @@ render_head(
- -
- - + + + \ No newline at end of file diff --git a/teacher.php b/teacher.php index 7f16fc7..288a175 100644 --- a/teacher.php +++ b/teacher.php @@ -46,15 +46,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $live_id = (int)($_POST['live_id'] ?? 0); $title = $_POST['title'] ?? ''; $scheduled_at = $_POST['scheduled_at'] ?? ''; + $meet_url = trim($_POST['meet_url'] ?? ''); if (owns_course($db, $c_id, $teacher_id)) { if ($post_action === 'add_live') { $room_name = 'room_' . substr(md5(uniqid()), 0, 10); - $stmt = $db->prepare("INSERT INTO course_live_lessons (course_id, title, scheduled_at, room_name) VALUES (?, ?, ?, ?)"); - $stmt->execute([$c_id, $title, $scheduled_at, $room_name]); + $stmt = $db->prepare("INSERT INTO course_live_lessons (course_id, title, scheduled_at, room_name, meet_url) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$c_id, $title, $scheduled_at, $room_name, $meet_url]); } else if ($post_action === 'edit_live' && $live_id > 0) { - $stmt = $db->prepare("UPDATE course_live_lessons SET title = ?, scheduled_at = ? WHERE id = ? AND course_id = ?"); - $stmt->execute([$title, $scheduled_at, $live_id, $c_id]); + $stmt = $db->prepare("UPDATE course_live_lessons SET title = ?, scheduled_at = ?, meet_url = ? WHERE id = ? AND course_id = ?"); + $stmt->execute([$title, $scheduled_at, $meet_url, $live_id, $c_id]); } } header("Location: " . app_url('teacher.php', ['action' => 'live', 'course_id' => $c_id])); @@ -147,6 +148,7 @@ render_nav('teacher.php');
+
@@ -285,6 +287,10 @@ render_nav('teacher.php'); +
+ + +
+
+ + +