update thawani gateway
This commit is contained in:
parent
c40ef2f754
commit
cbd1870a55
BIN
assets/images/uploads/teacher_1775489347_men.webp
Normal file
BIN
assets/images/uploads/teacher_1775489347_men.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
90
checkout.php
90
checkout.php
@ -44,13 +44,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'preferred_language' => $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.', 'بوابة دفع ثواني غير مهيأة بالكامل (المفاتيح مفقودة) أو فشل الاتصال. يرجى تحديث مفاتيحك في لوحة المعلم > التكاملات.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
107
live_lesson.php
107
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; }
|
||||
</style>
|
||||
|
||||
<div class="lesson-header">
|
||||
@ -95,9 +99,9 @@ render_head(
|
||||
<div>
|
||||
<?php if ($is_teacher): ?>
|
||||
<?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: ?>
|
||||
<span class="badge bg-secondary">Ended</span>
|
||||
<span class="badge bg-secondary"><?= t('Ended', 'منتهي') ?></span>
|
||||
<?php endif; ?>
|
||||
<a href="teacher.php?action=live&course_id=<?= $lesson['course_id'] ?>" class="btn-leave ms-2"><?= t('Back to Dashboard', 'العودة للوحة') ?></a>
|
||||
<?php else: ?>
|
||||
@ -126,47 +130,66 @@ render_head(
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Jitsi Meet iframe container -->
|
||||
<div id="jitsi-container"></div>
|
||||
<script src="https://meet.jit.si/external_api.js"></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
const domain = 'meet.jit.si';
|
||||
const options = {
|
||||
roomName: '<?= htmlspecialchars(rawurlencode($room_name)) ?>',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: document.querySelector('#jitsi-container'),
|
||||
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'
|
||||
]
|
||||
}
|
||||
<?php if ($has_meet): ?>
|
||||
<!-- Google Meet Gateway -->
|
||||
<div class="d-flex align-items-center justify-content-center text-white" style="height: calc(100vh - 56px);">
|
||||
<div class="text-center">
|
||||
<div class="mb-4">
|
||||
<svg viewBox="0 0 24 24" width="80" height="80">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mb-4"><?= t('This lesson is hosted on Google Meet.', 'هذا الدرس يُبث عبر Google Meet.') ?></h2>
|
||||
<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>
|
||||
<a href="<?= h($lesson['meet_url']) ?>" target="_blank" class="meet-btn">
|
||||
<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') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Jitsi Meet iframe container -->
|
||||
<div id="jitsi-container"></div>
|
||||
<script src="https://meet.jit.si/external_api.js"></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
const domain = 'meet.jit.si';
|
||||
const options = {
|
||||
roomName: '<?= htmlspecialchars(rawurlencode($room_name)) ?>',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: document.querySelector('#jitsi-container'),
|
||||
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);
|
||||
|
||||
<?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>
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
21
teacher.php
21
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');
|
||||
<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' => '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>
|
||||
</article>
|
||||
</div>
|
||||
@ -285,6 +287,10 @@ render_nav('teacher.php');
|
||||
<label class="form-label">Description (AR)</label>
|
||||
<textarea name="description_ar" id="modal_desc_ar" class="form-control" rows="3" dir="rtl"></textarea>
|
||||
</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 class="modal-footer">
|
||||
<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>
|
||||
<input type="datetime-local" name="scheduled_at" id="modal_live_time" class="form-control" required>
|
||||
</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 class="modal-footer">
|
||||
<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_title').value = '';
|
||||
document.getElementById('modal_live_time').value = '';
|
||||
document.getElementById('modal_live_meet_url').value = '';
|
||||
}
|
||||
function editLive(les) {
|
||||
document.getElementById('modal_live_action').value = 'edit_live';
|
||||
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_meet_url').value = les.meet_url ? les.meet_url : '';
|
||||
var modal = new bootstrap.Modal(document.getElementById('liveModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user