/** * WiFi Portal Engine * Handles the guest payment & authorization flow. * Requires window.PORTAL to be set before this script loads. */ (function () { 'use strict'; const P = window.PORTAL; if (!P) return; // ── DOM refs ────────────────────────────────────────────────────────────── const $ = id => document.getElementById(id); const screens = { select: $('screen-select'), waiting: $('screen-waiting'), code: $('screen-code'), success: $('screen-success'), }; function showScreen(name) { Object.values(screens).forEach(s => s && s.classList.remove('active')); if (screens[name]) screens[name].classList.add('active'); } function showError(elId, msg) { const el = $(elId); if (!el) return; el.textContent = msg; el.classList.add('show'); } function clearError(elId) { const el = $(elId); if (el) { el.textContent = ''; el.classList.remove('show'); } } async function readJsonSafe(res) { const text = await res.text(); try { return text ? JSON.parse(text) : {}; } catch { return { error: text || 'Unexpected server response' }; } } // ── State ───────────────────────────────────────────────────────────────── let selectedPlanId = null; let selectedPlanPrice = null; let selectedPlanIsPromo = false; let pollTimer = null; let currentReference = null; let purchaseInFlight = false; // ── Plan selection ──────────────────────────────────────────────────────── const planRadios = document.querySelectorAll('input[name="plan"]'); const payBtn = $('pay-btn'); const phoneInput = $('phone-input'); function isPhoneValid() { return phoneInput?.value?.replace(/\D/g, '').length >= 9; } function updatePayBtn() { if (!payBtn) return; if (selectedPlanId && isPhoneValid()) { payBtn.textContent = selectedPlanIsPromo ? 'Claim promo' : `Pay ${parseInt(selectedPlanPrice).toLocaleString()} TZS & Connect`; payBtn.disabled = false; } else if (selectedPlanId) { payBtn.textContent = 'Enter your phone number'; payBtn.disabled = true; } else { payBtn.textContent = 'Select a plan to continue'; payBtn.disabled = true; } } planRadios.forEach(radio => { radio.addEventListener('change', () => { selectedPlanId = radio.dataset.planId; selectedPlanPrice = radio.dataset.planPrice; selectedPlanIsPromo = radio.dataset.planPromo === '1' || Number(selectedPlanPrice) === 0; updatePayBtn(); }); }); if (phoneInput) { phoneInput.addEventListener('input', () => { // Auto-format TZ numbers: 0765 123 456 (groups 4-3-3) let digits = phoneInput.value.replace(/\D/g, '').slice(0, 10); if (digits.length > 7) digits = digits.slice(0, 4) + ' ' + digits.slice(4, 7) + ' ' + digits.slice(7); else if (digits.length > 4) digits = digits.slice(0, 4) + ' ' + digits.slice(4); phoneInput.value = digits; updatePayBtn(); }); } // ── Pay button ──────────────────────────────────────────────────────────── if (payBtn) { payBtn.addEventListener('click', async () => { if (purchaseInFlight) return; clearError('select-error'); const phone = phoneInput?.value?.trim(); if (!selectedPlanId) return showError('select-error', 'Please select a plan.'); if (!phone || phone.replace(/\D/g, '').length < 9) { return showError('select-error', 'Enter a valid phone number.'); } if (selectedPlanIsPromo && !P.clientMac) { return showError('select-error', 'Open this portal from the WiFi login page on the device you want to connect.'); } purchaseInFlight = true; payBtn.disabled = true; showScreen('waiting'); try { const endpoint = selectedPlanIsPromo ? 'promo' : 'purchase'; const payload = selectedPlanIsPromo ? { planId: selectedPlanId, phone, clientMac: P.clientMac } : { planId: selectedPlanId, phone }; const res = await fetch(`/portal/${P.siteId}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await readJsonSafe(res); if (selectedPlanIsPromo && res.ok && data.code) { purchaseInFlight = false; payBtn.disabled = false; showCodeEntry(data.code); return; } if (!res.ok || !data.reference) { showScreen('select'); purchaseInFlight = false; payBtn.disabled = false; return showError('select-error', data.error || (selectedPlanIsPromo ? 'Could not issue promo code. Please ask the attendant.' : 'Purchase failed. Try again.')); } currentReference = data.reference; startPolling(data.reference); } catch (err) { showScreen('select'); purchaseInFlight = false; payBtn.disabled = false; showError('select-error', 'Network error. Please try again.'); } }); } // ── Polling ─────────────────────────────────────────────────────────────── function startPolling(reference) { let attempts = 0; const MAX = 100; // ~5 min at 3s clearInterval(pollTimer); pollTimer = setInterval(async () => { attempts++; if (attempts > MAX) { clearInterval(pollTimer); purchaseInFlight = false; showScreen('select'); if (payBtn) payBtn.disabled = false; showError('select-error', 'Payment timed out. Please try again.'); return; } try { const res = await fetch(`/portal/${P.siteId}/check/${reference}`); const data = await res.json(); if (data.status === 'paid' && data.code) { clearInterval(pollTimer); await authorizeWithCode(data.code); } else if (data.status === 'failed') { clearInterval(pollTimer); purchaseInFlight = false; showScreen('select'); if (payBtn) payBtn.disabled = false; showError('select-error', 'Payment was declined. Please try again.'); } } catch { /* network blip — keep polling */ } }, 3000); } // Cancel button const cancelBtn = $('cancel-btn'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { clearInterval(pollTimer); purchaseInFlight = false; showScreen('select'); if (payBtn) payBtn.disabled = false; }); } // ── Auth with code ──────────────────────────────────────────────────────── async function authorizeWithCode(code) { if (!P.clientMac) { purchaseInFlight = false; showScreen('code'); showError('code-error', 'This page was opened outside the WiFi login flow. Reopen the hotspot portal on the device that is joining the WiFi.'); return; } await authorizeViaServer(code, 'select-error'); } // Call WiFiBiz's /portal/:siteId/omada-auth directly. WiFiBiz validates the // voucher with Omada server-side and records the session in one step — no // fragile browser→Omada redirect hop that can silently fail and leave // WiFiBiz unaware the guest connected. async function authorizeViaServer(code, errorElId) { showScreen('waiting'); const waitingText = $('waiting-text'); if (waitingText) waitingText.textContent = 'Connecting your device...'; try { const res = await fetch(`/portal/${P.siteId}/omada-auth`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(omadaAuthPayload(code)), }); const data = await readJsonSafe(res); if (res.ok && data.success) { purchaseInFlight = false; showSuccess(data); // Redirect to Omada's landing URL immediately — this is what makes the // captive portal detect connectivity and close the login screen. Don't // wait; the guest is already authorized. const target = data.landingUrl || (P.originUrl && /^https?:\/\//i.test(P.originUrl) ? P.originUrl : null); if (target) { window.location.href = target; } return; } // Server-side auth failed — fall back to the redirect approach as a // last resort so the guest isn't stuck. const msg = data.error || data.msg || 'Could not validate the voucher.'; showError(errorElId, msg + ' Retrying via Omada...'); setTimeout(() => redirectToOmadaAuth(code, errorElId), 2000); } catch (err) { // Network error talking to WiFiBiz — fall back to redirect. redirectToOmadaAuth(code, errorElId); } } function showSuccess(data) { const codeEl = $('success-code'); if (codeEl) codeEl.textContent = data.code || '—'; const metaEl = $('success-meta'); if (metaEl) { const hours = data.expiresIn ? Math.ceil(data.expiresIn / 3600) : '?'; const validFor = hours === '?' ? 'Available' : hours < 24 ? `${hours} ${hours === 1 ? 'hour' : 'hours'}` : `${Math.ceil(hours / 24)} ${Math.ceil(hours / 24) === 1 ? 'day' : 'days'}`; metaEl.innerHTML = `
Time left${validFor}
Download${data.speedDown || 'Active'}
Upload${data.speedUp || 'Active'}
ReconnectUse this code
`; } showScreen('success'); } const successCopyBtn = $('success-copy-btn'); if (successCopyBtn) { successCopyBtn.addEventListener('click', async () => { const code = $('success-code')?.textContent?.trim(); if (!code || code === '—') return; try { await navigator.clipboard.writeText(code); successCopyBtn.textContent = 'Copied'; setTimeout(() => { successCopyBtn.textContent = 'Copy code'; }, 1800); } catch { successCopyBtn.textContent = 'Select code'; setTimeout(() => { successCopyBtn.textContent = 'Copy code'; }, 1800); } }); } const successContinueBtn = $('success-continue-btn'); if (successContinueBtn) { successContinueBtn.addEventListener('click', () => { const target = P.originUrl && /^https?:\/\//i.test(P.originUrl) ? P.originUrl : 'http://example.com'; window.location.href = target; }); } function omadaAuthPayload(code) { return { code, clientMac: P.clientMac, apMac: P.apMac, gatewayMac: P.gatewayMac, ssidName: P.ssidName, radioId: P.radioId, vid: P.vid, originUrl: P.originUrl, }; } function redirectToOmadaAuth(code, errorElId) { const returnUrl = P.omadaReturnUrl; if (!returnUrl) { showError(errorElId, 'Open this portal from the WiFi login page, then enter the voucher again.'); return; } try { const url = new URL(returnUrl, window.location.href); url.searchParams.set('authCode', code); url.searchParams.set('authSource', 'wifibiz'); window.location.href = url.toString(); } catch { showError(errorElId, 'Could not return to Omada to validate the voucher. Reopen the WiFi login page.'); } } // ── Code entry (reconnect) ──────────────────────────────────────────────── const showCodeEntryLink = $('show-code-entry'); const showPlansLink = $('show-plans'); const codeInput = $('code-input'); const codeSubmitBtn = $('code-submit-btn'); if (showCodeEntryLink) { showCodeEntryLink.addEventListener('click', () => { clearError('code-error'); showScreen('code'); }); } if (showPlansLink) { showPlansLink.addEventListener('click', () => showScreen('select')); } function showCodeEntry(prefill) { if (codeInput && prefill) codeInput.value = prefill; clearError('code-error'); showScreen('code'); } if (codeInput) { codeInput.addEventListener('input', () => { codeInput.value = codeInput.value.replace(/[^a-zA-Z0-9]/g, '').slice(0, 32); }); } if (codeSubmitBtn) { codeSubmitBtn.addEventListener('click', async () => { clearError('code-error'); const code = codeInput?.value?.trim(); if (!code || code.length < 6) { return showError('code-error', 'Enter the voucher code exactly as shown.'); } if (!P.clientMac) { return showError('code-error', 'Open this portal from the WiFi login page on the device you want to connect.'); } codeSubmitBtn.disabled = true; codeSubmitBtn.textContent = 'Connecting...'; try { await authorizeViaServer(code, 'code-error'); } catch { showError('code-error', 'Could not connect. Please try again.'); } finally { codeSubmitBtn.disabled = false; codeSubmitBtn.textContent = 'Connect'; } }); } // If clientMac is missing (portal opened in browser), warn in console only if (!P.clientMac) { console.warn('[WiFi Portal] No clientMac — running in preview mode'); } })();