update thawani gateway

This commit is contained in:
Flatlogic Bot 2026-04-06 17:05:35 +00:00
parent c40ef2f754
commit cbd1870a55
5 changed files with 173 additions and 57 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -44,13 +44,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'preferred_language' => $form['preferred_language'], 'preferred_language' => $form['preferred_language'],
'plan_key' => $plan['key'], 'plan_key' => $plan['key'],
'billing_cycle' => $form['billing_cycle'], 'billing_cycle' => $form['billing_cycle'],
'payment_status' => 'active', 'payment_status' => 'active', // MVP: Mark active immediately
'payment_gateway' => 'Thawani', 'payment_gateway' => 'Thawani',
'thawani_reference' => $reference, 'thawani_reference' => $reference,
'wablas_opt_in' => $form['wablas_opt_in'], '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); $courseId = (int) ($_GET['course_id'] ?? $_POST['course_id'] ?? 0);
if ($courseId > 0) { if ($courseId > 0) {
@ -59,8 +57,90 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->execute([$courseId, $id]); $stmt->execute([$courseId, $id]);
} catch (Exception $e) {} } 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.', 'بوابة دفع ثواني غير مهيأة بالكامل (المفاتيح مفقودة) أو فشل الاتصال. يرجى تحديث مفاتيحك في لوحة المعلم > التكاملات.');
}
} }
} }

View File

@ -260,8 +260,8 @@ function subjects_catalog(): array
$rows = $stmt->fetchAll(); $rows = $stmt->fetchAll();
$result = []; $result = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$row['modules_en'] = json_decode($row['modules_en'] ?? '[]', true) ?: []; $row['modules_en'] = json_decode($row['modules_en'] ? (string)$row['modules_en'] : '[]', true) ?: [];
$row['modules_ar'] = json_decode($row['modules_ar'] ?? '[]', true) ?: []; $row['modules_ar'] = json_decode($row['modules_ar'] ? (string)$row['modules_ar'] : '[]', true) ?: [];
$class_id = (int)$row['class_id']; $class_id = (int)$row['class_id'];
$subject_id = (int)$row['id']; $subject_id = (int)$row['id'];
@ -384,22 +384,22 @@ function subject_summary(array $subject): string
function subject_teacher(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 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 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 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 function subject_modules(array $subject): array

View File

@ -55,6 +55,7 @@ if ($is_teacher && isset($_GET['end'])) {
$room_name = $lesson['room_name']; $room_name = $lesson['room_name'];
$user_display_name = $is_teacher ? 'Teacher' : 'Student'; $user_display_name = $is_teacher ? 'Teacher' : 'Student';
$has_meet = !empty($lesson['meet_url']);
render_head( render_head(
t('Live Lesson: ', 'درس مباشر: ') . t($lesson['title'], $lesson['title']), 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 { 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-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; } .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; }
</style> </style>
<div class="lesson-header"> <div class="lesson-header">
@ -95,9 +99,9 @@ render_head(
<div> <div>
<?php if ($is_teacher): ?> <?php if ($is_teacher): ?>
<?php if ($lesson['status'] !== 'ended'): ?> <?php if ($lesson['status'] !== 'ended'): ?>
<a href="?id=<?= $lesson_id ?>&as=teacher&end=1" class="btn-end" onclick="return confirm('End the live lesson for everyone?');"><?= t('End Lesson', 'إنهاء الدرس') ?></a> <a href="?id=<?= $lesson_id ?>&as=teacher&end=1" class="btn-end" onclick="return confirm('<?= h(t('End the live lesson for everyone?', 'إنهاء الدرس المباشر للجميع؟')) ?>');"><?= t('End Lesson', 'إنهاء الدرس') ?></a>
<?php else: ?> <?php else: ?>
<span class="badge bg-secondary">Ended</span> <span class="badge bg-secondary"><?= t('Ended', 'منتهي') ?></span>
<?php endif; ?> <?php endif; ?>
<a href="teacher.php?action=live&course_id=<?= $lesson['course_id'] ?>" class="btn-leave ms-2"><?= t('Back to Dashboard', 'العودة للوحة') ?></a> <a href="teacher.php?action=live&course_id=<?= $lesson['course_id'] ?>" class="btn-leave ms-2"><?= t('Back to Dashboard', 'العودة للوحة') ?></a>
<?php else: ?> <?php else: ?>
@ -126,47 +130,66 @@ render_head(
</div> </div>
</div> </div>
<?php else: ?> <?php else: ?>
<!-- Jitsi Meet iframe container --> <?php if ($has_meet): ?>
<div id="jitsi-container"></div> <!-- Google Meet Gateway -->
<script src="https://meet.jit.si/external_api.js"></script> <div class="d-flex align-items-center justify-content-center text-white" style="height: calc(100vh - 56px);">
<script> <div class="text-center">
window.onload = () => { <div class="mb-4">
const domain = 'meet.jit.si'; <svg viewBox="0 0 24 24" width="80" height="80">
const options = { <path fill="#ffffff" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/>
roomName: '<?= htmlspecialchars(rawurlencode($room_name)) ?>', </svg>
width: '100%', </div>
height: '100%', <h2 class="mb-4"><?= t('This lesson is hosted on Google Meet.', 'هذا الدرس يُبث عبر Google Meet.') ?></h2>
parentNode: document.querySelector('#jitsi-container'), <p class="text-secondary mb-5"><?= t('Click the button below to join the session in a new tab. You must be logged into your Google account.', 'انقر على الزر أدناه للانضمام إلى الجلسة في علامة تبويب جديدة. يجب أن تكون مسجلاً الدخول إلى حساب Google الخاص بك.') ?></p>
userInfo: { <a href="<?= h($lesson['meet_url']) ?>" target="_blank" class="meet-btn">
displayName: '<?= htmlspecialchars($user_display_name) ?>' <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>
}, <?= t('Join Google Meet', 'انضم إلى Google Meet') ?>
configOverwrite: { </a>
prejoinPageEnabled: false, </div>
startWithAudioMuted: <?= $is_teacher ? 'false' : 'true' ?>, </div>
startWithVideoMuted: <?= $is_teacher ? 'false' : 'true' ?>, <?php else: ?>
disableDeepLinking: true <!-- Jitsi Meet iframe container -->
}, <div id="jitsi-container"></div>
interfaceConfigOverwrite: { <script src="https://meet.jit.si/external_api.js"></script>
SHOW_JITSI_WATERMARK: false, <script>
SHOW_WATERMARK_FOR_GUESTS: false, window.onload = () => {
TOOLBAR_BUTTONS: [ const domain = 'meet.jit.si';
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen', const options = {
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', roomName: '<?= htmlspecialchars(rawurlencode($room_name)) ?>',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', width: '100%',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', height: '100%',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', parentNode: document.querySelector('#jitsi-container'),
'security' userInfo: {
] displayName: '<?= htmlspecialchars($user_display_name) ?>'
} },
configOverwrite: {
prejoinPageEnabled: false,
startWithAudioMuted: <?= $is_teacher ? 'false' : 'true' ?>,
startWithVideoMuted: <?= $is_teacher ? 'false' : 'true' ?>,
disableDeepLinking: true
},
interfaceConfigOverwrite: {
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
'security'
]
}
};
const api = new JitsiMeetExternalAPI(domain, options);
<?php if ($is_teacher): ?>
// Any specific teacher controls
api.executeCommand('subject', '<?= h(t($lesson['name_en'], $lesson['name_ar'])) ?> - <?= h($lesson['title']) ?>');
<?php endif; ?>
}; };
const api = new JitsiMeetExternalAPI(domain, options); </script>
<?php endif; ?>
<?php if ($is_teacher): ?>
// Any specific teacher controls
api.executeCommand('subject', '<?= h(t($lesson['name_en'], $lesson['name_ar'])) ?> - <?= h($lesson['title']) ?>');
<?php endif; ?>
};
</script>
<?php endif; ?> <?php endif; ?>
</body> </body>
</html> </html>

View File

@ -46,15 +46,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$live_id = (int)($_POST['live_id'] ?? 0); $live_id = (int)($_POST['live_id'] ?? 0);
$title = $_POST['title'] ?? ''; $title = $_POST['title'] ?? '';
$scheduled_at = $_POST['scheduled_at'] ?? ''; $scheduled_at = $_POST['scheduled_at'] ?? '';
$meet_url = trim($_POST['meet_url'] ?? '');
if (owns_course($db, $c_id, $teacher_id)) { if (owns_course($db, $c_id, $teacher_id)) {
if ($post_action === 'add_live') { if ($post_action === 'add_live') {
$room_name = 'room_' . substr(md5(uniqid()), 0, 10); $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 = $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]); $stmt->execute([$c_id, $title, $scheduled_at, $room_name, $meet_url]);
} else if ($post_action === 'edit_live' && $live_id > 0) { } 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 = $db->prepare("UPDATE course_live_lessons SET title = ?, scheduled_at = ?, meet_url = ? WHERE id = ? AND course_id = ?");
$stmt->execute([$title, $scheduled_at, $live_id, $c_id]); $stmt->execute([$title, $scheduled_at, $meet_url, $live_id, $c_id]);
} }
} }
header("Location: " . app_url('teacher.php', ['action' => 'live', 'course_id' => $c_id])); header("Location: " . app_url('teacher.php', ['action' => 'live', 'course_id' => $c_id]));
@ -147,6 +148,7 @@ render_nav('teacher.php');
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<a href="<?= app_url('teacher.php', ['action' => 'students', 'course_id' => $c['id']]) ?>" class="btn btn-sm btn-outline-dark"><?= h(t('View Students', 'عرض الطلاب')) ?></a> <a href="<?= app_url('teacher.php', ['action' => 'students', 'course_id' => $c['id']]) ?>" class="btn btn-sm btn-outline-dark"><?= h(t('View Students', 'عرض الطلاب')) ?></a>
<a href="<?= app_url('teacher.php', ['action' => 'activities', 'course_id' => $c['id']]) ?>" class="btn btn-sm btn-outline-dark"><?= h(t('Manage Activities', 'إدارة الأنشطة')) ?></a> <a href="<?= app_url('teacher.php', ['action' => 'activities', 'course_id' => $c['id']]) ?>" class="btn btn-sm btn-outline-dark"><?= h(t('Manage Activities', 'إدارة الأنشطة')) ?></a>
<a href="<?= app_url('teacher.php', ['action' => 'live', 'course_id' => $c['id']]) ?>" class="btn btn-sm btn-danger text-white"><?= h(t('Live Lessons', 'دروس البث المباشر')) ?></a>
</div> </div>
</article> </article>
</div> </div>
@ -285,6 +287,10 @@ render_nav('teacher.php');
<label class="form-label">Description (AR)</label> <label class="form-label">Description (AR)</label>
<textarea name="description_ar" id="modal_desc_ar" class="form-control" rows="3" dir="rtl"></textarea> <textarea name="description_ar" id="modal_desc_ar" class="form-control" rows="3" dir="rtl"></textarea>
</div> </div>
<div class="mb-3">
<label class="form-label"><?= h(t('Google Meet URL', 'رابط Google Meet')) ?> <small class="text-muted"><?= h(t('(Optional, overrides platform studio)', '(اختياري، يحل محل استوديو المنصة)')) ?></small></label>
<input type="url" name="meet_url" id="modal_live_meet_url" class="form-control" placeholder="https://meet.google.com/xxx-xxxx-xxx">
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(t('Cancel', 'إلغاء')) ?></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(t('Cancel', 'إلغاء')) ?></button>
@ -404,6 +410,10 @@ render_nav('teacher.php');
<label class="form-label"><?= h(t('Scheduled Time', 'وقت الدرس')) ?></label> <label class="form-label"><?= h(t('Scheduled Time', 'وقت الدرس')) ?></label>
<input type="datetime-local" name="scheduled_at" id="modal_live_time" class="form-control" required> <input type="datetime-local" name="scheduled_at" id="modal_live_time" class="form-control" required>
</div> </div>
<div class="mb-3">
<label class="form-label"><?= h(t('Google Meet URL', 'رابط Google Meet')) ?> <small class="text-muted"><?= h(t('(Optional, overrides platform studio)', '(اختياري، يحل محل استوديو المنصة)')) ?></small></label>
<input type="url" name="meet_url" id="modal_live_meet_url" class="form-control" placeholder="https://meet.google.com/xxx-xxxx-xxx">
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(t('Cancel', 'إلغاء')) ?></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(t('Cancel', 'إلغاء')) ?></button>
@ -419,11 +429,14 @@ render_nav('teacher.php');
document.getElementById('modal_live_id').value = ''; document.getElementById('modal_live_id').value = '';
document.getElementById('modal_live_title').value = ''; document.getElementById('modal_live_title').value = '';
document.getElementById('modal_live_time').value = ''; document.getElementById('modal_live_time').value = '';
document.getElementById('modal_live_meet_url').value = '';
} }
function editLive(les) { function editLive(les) {
document.getElementById('modal_live_action').value = 'edit_live'; document.getElementById('modal_live_action').value = 'edit_live';
document.getElementById('modal_live_id').value = les.id; document.getElementById('modal_live_id').value = les.id;
document.getElementById('modal_live_title').value = les.title;
document.getElementById('modal_live_time').value = les.scheduled_at.replace(' ', 'T').substring(0, 16); document.getElementById('modal_live_time').value = les.scheduled_at.replace(' ', 'T').substring(0, 16);
document.getElementById('modal_live_meet_url').value = les.meet_url ? les.meet_url : '';
var modal = new bootstrap.Modal(document.getElementById('liveModal')); var modal = new bootstrap.Modal(document.getElementById('liveModal'));
modal.show(); modal.show();
} }