(()=>{"use strict"; /* ---------- DOM helpers ---------- */ const $ = (s, c=document) => c.querySelector(s); const $$ = (s, c=document) => Array.from(c.querySelectorAll(s)); /* ---------- Elements ---------- */ const billingToggle = $('#billingToggle'); const annualNote = $('#annual-note'); const planCards = $$('#plans .plan-card'); const tooltip = $('#tooltip') || (()=>{const d=document.createElement('div');d.id='tooltip';d.className='tooltip';document.body.appendChild(d);return d;})(); const errBox = $('#err') || (()=>{const d=document.createElement('div');d.id='err';document.body.appendChild(d);return d;})(); /* ---------- Price IDs (YOUR LIVE VALUES) ---------- */ const PRICE_TOPUPS = { voice: { 'starter': {5:'price_1RvBt6Jxl0qmUUN7fHSCYi8c',10:'price_1RvBt5Jxl0qmUUN7CUlJb7cb',25:'price_1RvBt3Jxl0qmUUN7mOHNlvvH',50:'price_1RvBszJxl0qmUUN7oULgfLop',100:'price_1RvBseJxl0qmUUN7OCWp23I4'}, 'business': {5:'price_1RvBsOJxl0qmUUN7Oqw9auKc',10:'price_1RvBsMJxl0qmUUN7ahuFcvw0',25:'price_1RvBsLJxl0qmUUN7GGhXVwVB',50:'price_1RvBsJJxl0qmUUN7OhS7BfY1',100:'price_1RwJy3Jxl0qmUUN7j8OwppxN'}, 'business-plus':{5:'price_1RvBrcJxl0qmUUN7ytaadCL6',10:'price_1RvBrbJxl0qmUUN7ifGEa92r',25:'price_1RvBriJxl0qmUUN7LtNC3LCx',50:'price_1RvBrkJxl0qmUUN7QEUn38Sb',100:'price_1RvBrlJxl0qmUUN7F1jx7WCw'}, }, messages: { 'starter': {5:'price_1RwJrtJxl0qmUUN7biAR5wko',10:'price_1RvBscJxl0qmUUN7SKBctBRe',25:'price_1RvBsZJxl0qmUUN7VVxARC7O',50:'price_1RvBsYJxl0qmUUN7ItssINl4',100:'price_1RvBsVJxl0qmUUN7Cj79v5fb'}, 'business': {5:'price_1RvBr9Jxl0qmUUN76IaJQK6W',10:'price_1RvBrBJxl0qmUUN7KovIhgnO',25:'price_1RvBrFJxl0qmUUN75R8d1KRY',50:'price_1RvBrJJxl0qmUUN7MmY4knh3',100:'price_1RvBrTJxl0qmUUN7wtLBHFnE'}, 'business-plus':{5:'price_1RvBrpJxl0qmUUN7i0HJ159n',10:'price_1RvBrrJxl0qmUUN724MRVVEh',25:'price_1RvBrwJxl0qmUUN7icPeLiUP',50:'price_1RvBrxJxl0qmUUN7ZM1UusFP',100:'price_1RvBrzJxl0qmUUN7PyYt6LGu'}, } }; /* ---------- Allowances for meters (base + packs) ---------- */ const BASE = { 'starter': { voice:200, msg:2000 }, 'business': { voice:600, msg:6000 }, 'business-plus': { voice:2500, msg:20000 }, }; const PACK_QTY = { 'starter': { voice:{5:98,10:196,25:490,50:980,100:1960}, msg:{5:550,10:1100,25:2750,50:5500,100:11000} }, 'business': { voice:{5:110,10:220,25:550,50:1100,100:2200}, msg:{5:730,10:1460,25:3650,50:7300,100:14600} }, 'business-plus': { voice:{5:106,10:212,25:531,50:1062,100:2125}, msg:{5:700,10:1400,25:3500,50:7000,100:14000} }, }; /* ---------- Render prices ---------- */ function renderPrices(){ const isAnnual = !!billingToggle?.checked; planCards.forEach(card=>{ const monthly = Number(card.dataset.monthly||0); const moneyEl = card.querySelector('[data-money]'); const perEl = card.querySelector('[data-period]'); const noteEl = card.querySelector('[data-yrnote]'); if(!moneyEl||!perEl||!noteEl) return; if(isAnnual){ const billed = monthly*12*0.9; moneyEl.textContent = '£'+ billed.toFixed(2).replace(/\.00$/,''); perEl.textContent = '/year'; noteEl.textContent = `Billed £${billed.toFixed(2)} / yr (10% off)`; noteEl.style.display='block'; } else { moneyEl.textContent = '£'+ monthly; perEl.textContent = '/month'; noteEl.style.display='none'; } }); if(annualNote) annualNote.style.display = (billingToggle?.checked ? 'inline-block' : 'none'); } /* ---------- Tooltips ---------- */ $$('.info').forEach(el=>{ el.addEventListener('mouseenter', ()=>{ const text = el.dataset.tip || ''; if(!text) return; tooltip.textContent = text; tooltip.style.display = 'block'; tooltip.style.visibility = 'hidden'; const r = el.getBoundingClientRect(); tooltip.style.top='-9999px'; tooltip.style.left='-9999px'; const tr = tooltip.getBoundingClientRect(); const top = (r.bottom + window.scrollY + 8) + 'px'; const left = Math.min(window.scrollX + document.documentElement.clientWidth - tr.width - 12, r.left + window.scrollX) + 'px'; tooltip.style.top=top; tooltip.style.left=left; tooltip.style.visibility='visible'; }); el.addEventListener('mouseleave', ()=>{ tooltip.style.display='none'; }); }); /* ---------- Plan selection ---------- */ function selectPlan(card){ $$('.subscribe-btn').forEach(b=>b.classList.remove('selected')); const btn = $('.subscribe-btn', card); if(btn) btn.classList.add('selected'); updateSummaryAndMeters(); } planCards.forEach(card=>{ const btn = $('.subscribe-btn', card); if(btn) btn.addEventListener('click', ()=> selectPlan(card)); }); selectPlan(planCards[0]); /* ---------- Top-up selectors ---------- */ $$('.options').forEach(group=>{ group.addEventListener('click', e=>{ const opt = e.target.closest('.option'); if(!opt) return; $$('.option', group).forEach(o=>o.classList.remove('selected')); opt.classList.add('selected'); updateSummaryAndMeters(); }); }); /* ---------- Meters & summary ---------- */ const summaryPlan = $('#summary-plan'); const summaryVoice= $('#summary-voice'); const summaryMsg = $('#summary-msg'); const voiceBar = $('#voiceMeter .bar'); const voiceVal = $('#voiceMeter .value'); const msgBar = $('#messageMeter .bar'); const msgVal = $('#messageMeter .value'); function currentPlanKey(){ const sel = $('.subscribe-btn.selected'); return sel ? sel.closest('.plan-card').dataset.plan : 'starter'; } function selectedTopup(kind){ const sel = $(`.topup[data-type="${kind}"] .option.selected`); return Number(sel?.dataset.amount || 0); } function updateSummaryAndMeters(){ const key = currentPlanKey(); const title = $(`.plan-card[data-plan="${key}"] .title`)?.textContent || 'Starter'; summaryPlan.textContent = `You’re on the ${title} plan`; const v£ = selectedTopup('voice'), m£ = selectedTopup('messages'); const vExtra = v£ ? (PACK_QTY[key].voice[v£]||0) : 0; const mExtra = m£ ? (PACK_QTY[key].msg[m£]||0) : 0; const vBase = BASE[key].voice, mBase = BASE[key].msg; const vTotal = vBase + vExtra, mTotal = mBase + mExtra; summaryVoice.textContent = v£ ? `£${v£} → ${vExtra.toLocaleString('en-GB')} minutes (Base ${vBase.toLocaleString('en-GB')} → Total ${vTotal.toLocaleString('en-GB')})` : `No Voice Top-Up (Base ${vBase.toLocaleString('en-GB')})`; summaryMsg.textContent = m£ ? `£${m£} → ${mExtra.toLocaleString('en-GB')} messages (Base ${mBase.toLocaleString('en-GB')} → Total ${mTotal.toLocaleString('en-GB')})` : `No Message Top-Up (Base ${mBase.toLocaleString('en-GB')})`; voiceBar.style.width = vTotal ? '100%' : '0%'; voiceVal.textContent = vTotal ? `Allowance: ${vTotal.toLocaleString('en-GB')}` : ''; msgBar.style.width = mTotal ? '100%' : '0%'; msgVal.textContent = mTotal ? `Allowance: ${mTotal.toLocaleString('en-GB')}` : ''; } /* ---------- Billing toggle ---------- */ billingToggle?.addEventListener('change', renderPrices); renderPrices(); /* ---------- Checkout (lazy-init Stripe; never breaks UI) ---------- */ function showErr(msg){ errBox.textContent = 'Checkout error: ' + msg; errBox.style.display='block'; console.error('[VC]', msg); } async function getStripe(){ if (window.__vc_stripe) return window.__vc_stripe; if (typeof window.Stripe !== 'function') throw new Error('Stripe.js did not load (blocked by CSP/ad-block).'); const pk = (window.VC_PUBLIC && window.VC_PUBLIC.pk) || ''; if (!pk || !/^pk_live_/.test(pk)) throw new Error('Publishable key missing or invalid.'); window.__vc_stripe = window.Stripe(pk); return window.__vc_stripe; } $('#purchase')?.addEventListener('click', async ()=>{ try{ const stripe = await getStripe(); const card = $('.subscribe-btn.selected')?.closest('.plan-card') || planCards[0]; const priceId = (billingToggle?.checked) ? card.getAttribute('data-annual-id') : card.getAttribute('data-monthly-id'); if(!priceId) return showErr('Missing Stripe Price ID for selected plan.'); // Store top-ups to chain after subscription success (optional) const planKey = card.dataset.plan; const v£ = selectedTopup('voice'), m£ = selectedTopup('messages'); const pending = []; if(v£>0){ const id = PRICE_TOPUPS.voice[planKey]?.[v£]; if(!id) return showErr(`Missing voice top-up Price ID £${v£} (${planKey}).`); pending.push({price:id, quantity:1}); } if(m£>0){ const id = PRICE_TOPUPS.messages[planKey]?.[m£]; if(!id) return showErr(`Missing message top-up Price ID £${m£} (${planKey}).`); pending.push({price:id, quantity:1}); } sessionStorage.setItem('vc_pending_topups', JSON.stringify(pending)); const { error } = await stripe.redirectToCheckout({ mode:'subscription', lineItems:[{ price:priceId, quantity:1 }], successUrl: (window.VC_PUBLIC && VC_PUBLIC.successUrl) || (location.origin + '/success'), cancelUrl: (window.VC_PUBLIC && VC_PUBLIC.cancelUrl) || (location.origin + '/cancel'), }); if(error) showErr(error.message || 'Stripe redirect failed.'); }catch(e){ showErr(e.message || String(e)); } }); /* ---------- Auto-run top-ups after success (optional) ---------- */ if (location.pathname.endsWith('/success')){ (async ()=>{ try{ const pending = JSON.parse(sessionStorage.getItem('vc_pending_topups')||'[]'); sessionStorage.removeItem('vc_pending_topups'); if(!pending.length) return; const stripe = await getStripe(); const { error } = await stripe.redirectToCheckout({ mode:'payment', lineItems: pending, successUrl: location.origin + '/success?topups=done', cancelUrl: location.origin + '/success?topups=cancel', }); if(error) showErr(error.message || 'Top-up checkout failed.'); }catch(e){ /* ignore */ } })(); } })(); https://voicecenta.ai/post-sitemap.xml 2025-06-25T04:57:45+00:00 https://voicecenta.ai/page-sitemap.xml 2025-08-17T09:08:32+00:00