import {attributionFrom, validPlan, liveReady, stripeCheckoutUrl, verifiedOrder, returnUrl} from './missed-calls-core.mjs'; const config = JSON.parse(document.querySelector('#missed-calls-config').textContent); const live = liveReady(config); const hosted = config.mode === 'hosted'; const $ = selector => document.querySelector(selector); const all = selector => [...document.querySelectorAll(selector)]; const storage = { get(key) { try { return JSON.parse(sessionStorage.getItem('tava:mc:' + key)); } catch { return null; } }, set(key, value) { try { sessionStorage.setItem('tava:mc:' + key, JSON.stringify(value)); } catch { /* Private mode still works. */ } } }; const attribution = attributionFrom(location.search, storage.get('attribution') || {}); storage.set('attribution', attribution); let selectedPlan = validPlan(config, storage.get('plan')) ? storage.get('plan') : 'lead_response'; let paidOrder = null; let busy = false; const status = $('[data-signup-status]'); const checkoutDialog = $('#mc-checkout-dialog'); let checkoutTrigger = null; function openCheckout() { if (!checkoutDialog.open) { checkoutTrigger = document.activeElement; checkoutDialog.showModal(); document.body.style.overflow = 'hidden'; } } checkoutDialog.addEventListener('close', () => { document.body.style.overflow = ''; checkoutTrigger?.focus({preventScroll: true}); }); $('[data-checkout-close]').addEventListener('click', () => checkoutDialog.close()); checkoutDialog.addEventListener('click', event => { if (event.target === checkoutDialog) { const box = checkoutDialog.getBoundingClientRect(); if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom) checkoutDialog.close(); }}); // Local hooks only. An analytics adapter can subscribe to this event. // No business form values, session IDs, consent records, or EIN enter this payload. function track(name) { const aliases = {landing_page_view:'page_view', see_how_it_works_click:'see_how_it_works', lead_response_select:'missed_call_protection_selected', ai_receptionist_select:'call_coverage_selected', business_setup_completed:'business_details_completed'}; for (const event of [name, aliases[name]].filter(Boolean)) document.dispatchEvent(new CustomEvent('tava:landing_analytics', {detail: { event, page: '/missed-calls/', plan: selectedPlan, mode: hosted ? 'hosted' : live ? 'live' : 'preview', attribution: {...attribution} }})); } function oncePerOrder(name) { const key = name + ':' + paidOrder.orderId; if (!storage.get(key)) { track(name); storage.set(key, true); } } function showStep(step, focus = true) { if (['details', 'activation'].includes(step) && !paidOrder) return; openCheckout(); all('[data-step]').forEach(panel => { panel.hidden = panel.dataset.step !== step; }); const order = ['plan', 'payment', 'details', 'activation']; all('[data-progress]').forEach(item => { const active = item.dataset.progress === step; if (active) item.setAttribute('aria-current', 'step'); else item.removeAttribute('aria-current'); item.classList.toggle('mc-done', live && paidOrder !== null && order.indexOf(item.dataset.progress) < order.indexOf(step)); }); status.textContent = ''; if (focus) { checkoutDialog.scrollTop = 0; $(`[data-step="${step}"] h3`).focus({preventScroll: true}); } $('[data-selected-plan]').textContent = config.plans[selectedPlan].name + ' · ' + config.plans[selectedPlan].priceLabel + ' · No setup fee'; } function selectPlan(id, emit = true) { if (!validPlan(config, id) || paidOrder) return; selectedPlan = id; storage.set('plan', id); all('input[name="plan"]').forEach(input => { input.checked = input.value === id; }); if (emit) track(id === 'lead_response' ? 'lead_response_select' : 'ai_receptionist_select'); } async function request(endpoint, payload) { const response = await fetch(endpoint, { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json', ...(paidOrder ? {'X-CSRF-Token': paidOrder.csrfToken} : {})}, body: JSON.stringify(payload), signal: AbortSignal.timeout(20000) }); if (!response.ok) throw new Error('Request failed'); return response.json(); } selectPlan(selectedPlan, false); $('[data-next-payment]').hidden = false; all('[data-plan]').forEach(link => { if (hosted) { const url = new URL(config.plans[link.dataset.plan].paymentUrl); for (const [key, value] of Object.entries(attribution)) url.searchParams.set(key, value); link.href = url.href; link.addEventListener('click', () => { selectPlan(link.dataset.plan); track('checkout_started'); }); return; } link.addEventListener('click', event => { event.preventDefault(); if (paidOrder) { showStep(paidOrder.setupComplete ? 'activation' : 'details'); return; } selectPlan(link.dataset.plan); showStep('plan'); }); }); all('input[name="plan"]').forEach(input => input.addEventListener('change', () => selectPlan(input.value))); $('[data-next-payment]').addEventListener('click', () => showStep('payment')); $('[data-back-plan]').addEventListener('click', () => { if (!paidOrder) showStep('plan'); }); $('[data-back-payment]').addEventListener('click', () => { if (!paidOrder) showStep('payment'); }); $('[data-back-details]').addEventListener('click', () => showStep('details')); $('[data-activation-call]').addEventListener('click', () => track('activation_call_clicked')); $('[data-hero-start]').addEventListener('click', () => track('hero_consultation_clicked')); all('[data-how-click]').forEach(link => link.addEventListener('click', () => track('see_how_it_works_click'))); track('landing_page_view'); if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(entries => { if (entries.some(entry => entry.isIntersecting)) { track('pricing_view'); observer.disconnect(); } }, {threshold: .15}); observer.observe($('#pricing')); } else { track('pricing_view'); } if (config.video.enabled !== false && config.video.src) { const video = $('[data-product-video]'); video.src = config.video.src; if (config.video.captions) { const captions = document.createElement('track'); Object.assign(captions, {kind: 'captions', src: config.video.captions, srclang: 'en', label: 'English', default: true}); video.appendChild(captions); } $('[data-video-placeholder]').hidden = true; video.hidden = false; video.addEventListener('play', () => track('video_play'), {once: true}); } if (live) { $('[data-preview-note]').hidden = true; $('[data-checkout-contact]').hidden = true; $('[data-details-preview]').hidden = true; $('[data-checkout]').hidden = false; $('[data-payment-copy]').textContent = 'Continue to Stripe to review your price and enter payment details securely. You’ll return here for your business setup.'; $('#pricing .mc-caption').textContent = 'Choose your package, complete secure checkout, and tell us about your business.'; $('[data-checkout]').addEventListener('click', async () => { if (busy || paidOrder) return; busy = true; $('[data-checkout]').disabled = true; status.textContent = 'Opening secure checkout…'; try { const result = await request(config.checkout.createEndpoint, {plan: selectedPlan, attribution, returnUrl: returnUrl(location.origin, attribution)}); const url = stripeCheckoutUrl(result.url); track('checkout_started'); location.assign(url); } catch { status.textContent = 'Checkout could not open. No payment was taken here. Please try again or contact support@tavafortrades.com.'; busy = false; $('[data-checkout]').disabled = false; } }); } function enableBusinessSetup() { all('input[name="plan"]').forEach(input => { input.disabled = true; }); $('[data-business-fields]').disabled = false; $('[data-save-business]').hidden = false; $('[data-back-payment]').hidden = true; $('[data-plan-done]').textContent = 'Choose plan ✓'; if (paidOrder.setupComplete) completeSetup(); else { showStep('details'); oncePerOrder('business_setup_started'); } } function completeSetup() { paidOrder.setupComplete = true; $('[data-details-done]').textContent = 'Business details ✓'; $('[data-activation-eyebrow]').textContent = 'Details saved'; $('[data-activation-note]').textContent = 'Your details are saved. Choose a time to finish setup with us. Your service goes live after number and messaging approval and a successful test.'; $('[data-activation-call]').textContent = 'Book activation call →'; showStep('activation'); } $('#mc-business-form').addEventListener('submit', async event => { event.preventDefault(); if (!live || !paidOrder || busy) return; // Expand the optional disclosure before validation so invalid fields can receive focus. $('.mc-business-more').open = true; if (!event.currentTarget.reportValidity()) return; const business = Object.fromEntries(new FormData(event.currentTarget)); busy = true; $('[data-save-business]').disabled = true; status.textContent = 'Saving your business details…'; try { const result = await request(config.checkout.setupEndpoint, {orderId: paidOrder.orderId, business}); if (result.saved !== true) throw new Error('Not saved'); oncePerOrder('business_setup_completed'); completeSetup(); $('#mc-business-form').reset(); } catch { status.textContent = 'Your details could not be saved. Please try again. Your payment is unchanged.'; } finally { busy = false; $('[data-save-business]').disabled = false; } }); // Browser validation happens before submit; reveal hidden invalid inputs first. $('#mc-business-form').addEventListener('invalid', () => { $('.mc-business-more').open = true; }, true); const params = new URLSearchParams(location.search); if (params.get('checkout') === 'cancelled') { showStep('payment', false); status.textContent = 'Checkout was cancelled. You can choose a different plan or try again.'; } const sessionId = params.get('session_id'); if (sessionId) { // A URL flag can never unlock the form or create a completed-payment event. showStep('payment', false); if (!live) status.textContent = 'This is a setup preview. No payment can be verified here.'; else { busy = true; $('[data-checkout]').disabled = true; status.textContent = 'Confirming your payment…'; try { const result = await request(config.checkout.statusEndpoint, {sessionId}); if (!verifiedOrder(result, config)) throw new Error('Unverified payment'); selectPlan(result.plan, false); paidOrder = result; oncePerOrder('checkout_completed'); // Booking must be confirmed by the authenticated backend, never inferred from a click. if (result.activationBooked === true) oncePerOrder('activation_call_booked'); enableBusinessSetup(); } catch { status.textContent = 'We couldn’t confirm payment yet. If you completed checkout, don’t pay again. Refresh this page or contact support@tavafortrades.com.'; } finally { busy = false; /* Keep checkout disabled after a return to avoid duplicate payment. */ } } }