| |
| |
| |
| |
| |
| (function () { |
| 'use strict'; |
|
|
| const P = window.PORTAL; |
| if (!P) return; |
|
|
| |
| 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' }; |
| } |
| } |
|
|
| |
| let selectedPlanId = null; |
| let selectedPlanPrice = null; |
| let selectedPlanIsPromo = false; |
| let pollTimer = null; |
| let currentReference = null; |
| let purchaseInFlight = false; |
|
|
| |
| 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', () => { |
| |
| 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(); |
| }); |
| } |
|
|
| |
| 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.'); |
| } |
| }); |
| } |
|
|
| |
| function startPolling(reference) { |
| let attempts = 0; |
| const MAX = 100; |
|
|
| 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 { } |
| }, 3000); |
| } |
|
|
| |
| const cancelBtn = $('cancel-btn'); |
| if (cancelBtn) { |
| cancelBtn.addEventListener('click', () => { |
| clearInterval(pollTimer); |
| purchaseInFlight = false; |
| showScreen('select'); |
| if (payBtn) payBtn.disabled = false; |
| }); |
| } |
|
|
| |
| 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'); |
| } |
|
|
| |
| |
| |
| |
| 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); |
| |
| |
| |
| const target = data.landingUrl || (P.originUrl && /^https?:\/\//i.test(P.originUrl) ? P.originUrl : null); |
| if (target) { |
| window.location.href = target; |
| } |
| return; |
| } |
|
|
| |
| |
| const msg = data.error || data.msg || 'Could not validate the voucher.'; |
| showError(errorElId, msg + ' Retrying via Omada...'); |
| setTimeout(() => redirectToOmadaAuth(code, errorElId), 2000); |
| } catch (err) { |
| |
| 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 = ` |
| <div class="meta-row"><span class="meta-key">Time left</span><strong class="meta-val">${validFor}</strong></div> |
| <div class="meta-row"><span class="meta-key">Download</span><strong class="meta-val">${data.speedDown || 'Active'}</strong></div> |
| <div class="meta-row"><span class="meta-key">Upload</span><strong class="meta-val">${data.speedUp || 'Active'}</strong></div> |
| <div class="meta-row"><span class="meta-key">Reconnect</span><strong class="meta-val">Use this code</strong></div> |
| `; |
| } |
| 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.'); |
| } |
| } |
|
|
| |
| 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 (!P.clientMac) { |
| console.warn('[WiFi Portal] No clientMac β running in preview mode'); |
| } |
|
|
| })(); |
|
|