1/** 2 * pricing_v2.js â interactions for the /pricing/ page. 3 * 4 * Responsibilities: 5 * 1. Currency toggle â flip .is-azn / .is-usd / .is-eur on the plan grid 6 * (CSS in pricing_v2.css does the price-swap show/hide) and rewrite 7 * the CTA hrefs so /cart-preview/ opens in the same currency the 8 * reader was looking at. 9 * 2. Business lead form â AJAX submit to pricing.php?action=business_lead, 10 * including a reCAPTCHA v3 token with action='business_lead'. Shows 11 * inline success / error in .pricing-v2-form-status. 12 * 13 * No jQuery â vanilla keeps the bundle small and the footprint focused. 14 * 15 * Server-side dependencies (injected by pricing.php into html_footer 16 * before this script loads): 17 * window.RECAPTCHA_SITE_KEY â site key for grecaptcha.execute() 18 * window.PRICING_I18N â { errNetwork, errGeneric, successTitle, 19 * successBody } â only client-rendered 20 * strings; server-returned errors flow 21 * back in the AJAX JSON already in the 22 * current language. 23 */ 24(function () { 25 'use strict'; 26 27 /* ================================================================ 28 * i18n bundle â populated server-side in pricing.php via 29 * window.PRICING_I18N = { errNetwork, errGeneric, 30 * successTitle, successBody } 31 * Only client-only strings live here (things the JS renders without 32 * a server round-trip). Server-returned errors already arrive in the 33 * right language inside the AJAX JSON, so they don't need a fallback. 34 * The `||` chain keeps English as a safety net in case the server 35 * shipped the page without the injection (e.g. during a partial 36 * deploy). 37 * ============================================================== */ 38 var I18N = (window.PRICING_I18N && typeof window.PRICING_I18N === 'object') ? window.PRICING_I18N : {}; 39 function t(key, fallback) { 40 return (I18N[key] && typeof I18N[key] === 'string') ? I18N[key] : fallback; 41 } 42 43 document.addEventListener('DOMContentLoaded', function () { 44 initCurrencyToggle(); 45 initBusinessReveal(); 46 initBusinessForm(); 47 }); 48 49 /* ================================================================ 50 * Business card reveal / collapse 51 * 52 * Trend for Business ships as a CTA-only card â the full contact 53 * form is hidden (via the native `hidden` attribute) until the user 54 * opts in. This keeps the pricing page visually calm: two clean 55 * Premium options on top, a single dark "Contact sales" card below. 56 * 57 * Reveal flow: 58 * - Click #pricing-v2-business-reveal â hide intro, unhide form, 59 * scroll form into view (block:'nearest' so we don't yank the 60 * viewport when it's already visible), focus first input. 61 * - Click #pricing-v2-business-cancel â inverse. Preserve whatever 62 * the user typed so an accidental Back doesn't nuke their draft; 63 * only clear the ephemeral status line. 64 * 65 * Handlers are no-ops if the card isn't on the page (defensive â 66 * this script might eventually be loaded on other pages). 67 * ============================================================== */ 68 function initBusinessReveal() { 69 var revealBtn = document.getElementById('pricing-v2-business-reveal'); 70 var cancelBtn = document.getElementById('pricing-v2-business-cancel'); 71 var intro = document.getElementById('pricing-v2-business-intro'); 72 var form = document.getElementById('pricing-v2-business-form'); 73 var status = document.getElementById('pricing-v2-business-status'); 74 75 if (!revealBtn || !cancelBtn || !intro || !form) return; 76 77 revealBtn.addEventListener('click', function () { 78 intro.hidden = true; 79 form.hidden = false; 80 81 // Focus the first field for keyboard users. Defer until after 82 // the browser has painted the un-hidden form, otherwise some 83 // engines skip the focus on an element that was display:none 84 // a tick earlier. 85 var firstField = document.getElementById('pv2-first-name'); 86 window.requestAnimationFrame(function () { 87 if (firstField) firstField.focus({ preventScroll: true }); 88 form.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 89 }); 90 }); 91 92 cancelBtn.addEventListener('click', function () { 93 form.hidden = true; 94 intro.hidden = false;
95 96 // Clear the status line â a stale "Please enter your email" 97 // hanging around after the user dismisses and reopens the 98 // form would look like a live error. 99 if (status) { 100 status.className = 'pricing-v2-form-status'; 101 status.textContent = ''; 102 } 103 104 // Put focus back on the reveal button so keyboard users 105 // stay oriented (otherwise focus falls to <body>). 106 revealBtn.focus({ preventScroll: true }); 107 }); 108 } 109 110 /* ================================================================ 111 * Currency toggle 112 * 113 * The grid (#pricing-v2-grid) carries one of: 114 * .is-azn .is-usd .is-eur 115 * CSS uses those to show/hide the per-card .pricing-v2-price spans 116 * and the per-month hint inside the annual card. This handler just 117 * flips the class + keeps the segmented-pill active state + rewrites 118 * the CTA querystring so the downstream /cart-preview/ inherits the 119 * same choice. 120 * ============================================================== */ 121 function initCurrencyToggle() { 122 var grid = document.getElementById('pricing-v2-grid'); 123 var buttons = document.querySelectorAll('.pricing-v2-currency-item'); 124 if (!grid || !buttons.length) return; 125 126 var CTA_SELECTOR = '.pricing-v2-cta[data-product-code]'; 127 var VALID = { azn: true, usd: true, eur: true }; 128 129 function apply(cur) { 130 if (!VALID[cur]) cur = 'azn'; 131 132 // Grid class â drives CSS visibility of the three price spans. 133 grid.classList.remove('is-azn', 'is-usd', 'is-eur'); 134 grid.classList.add('is-' + cur); 135 136 // Segmented-pill active state + a11y pressed flag. 137 buttons.forEach(function (b) { 138 var active = b.getAttribute('data-currency') === cur; 139 b.classList.toggle('is-active', active); 140 b.setAttribute('aria-pressed', active ? 'true' : 'false'); 141 }); 142 143 // Rewrite CTA hrefs â preserves the path (/cart-preview/P1/) 144 // but swaps the ?currency= param. Robust against future query 145 // params on the same URL (we only touch the `currency` key). 146 document.querySelectorAll(CTA_SELECTOR).forEach(function (a) { 147 var href = a.getAttribute('href') || ''; 148 var split = href.split('?'); 149 var path = split[0]; 150 var qs = split[1] || ''; 151 152 var params = new URLSearchParams(qs); 153 params.set('currency', cur); 154 a.setAttribute('href', path + '?' + params.toString()); 155 }); 156 } 157 158 buttons.forEach(function (btn) { 159 btn.addEventListener('click', function () { 160 apply(btn.getAttribute('data-currency')); 161 }); 162 }); 163 164 // Idempotent initial sync â if the markup ships with .is-azn already 165 // (it does â see the template) the call below is effectively a no-op, 166 // but it also ensures the CTA hrefs get rewritten if someone ever 167 // ships the page in a different default currency. 168 var initial = grid.className.match(/is-(azn|usd|eur)/); 169 apply(initial ? initial[1] : 'azn'); 170 } 171 172 /* ================================================================ 173 * Business lead form 174 * 175 * Submit flow: 176 * 1. Block native submit, mark button loading. 177 * 2. grecaptcha.execute() with action='business_lead' â token. 178 * 3. POST FormData (+ g-recaptcha-response) to pricing.php. 179 * 4. Render JSON response into #pricing-v2-business-status. 180 * On success: replace the whole form with a success message. 181 * 182 * The server is also the source of truth â front-end validation here 183 * is only for rendering speed (the browser's native tooltip on required 184 * fields). Never trust it for business logic. 185 * ============================================================== */ 186 function initBusinessForm() { 187 var form = document.getElementById('pricing-v2-business-form'); 188 if (!form) return; 189 190 var submitBtn = document.getElementById('pricing-v2-business-submit'); 191 var status = document.getElementById('pricing-v2-business-status'); 192 193 form.addEventListener('submit', function (e) { 194 e.preventDefault(); 195 196 // Reset any prior state (user retrying after an error). 197 status.className = 'pricing-v2-form-status'; 198 status.textContent = ''; 199 submitBtn.classList.add('is-loading'); 200 submitBtn.disabled = true; 201 202 // Wait for reCAPTCHA v3 to be ready before executing. Loading 203 // the api.js script is async, so on slow connections the user 204 // might hit submit before grecaptcha is defined â grecaptcha.ready 205 // handles that race condition for us. 206 if (typeof window.grecaptcha === 'undefined' || !window.RECAPTCHA_SITE_KEY) { 207 // No reCAPTCHA available (ad blocker?). Submit without a 208 // token â the server will reject and show a clear error. 209 sendForm(''); 210 return; 211 } 212 213 window.grecaptcha.ready(function () { 214 window.grecaptcha 215 .execute(window.RECAPTCHA_SITE_KEY, { action: 'business_lead' }) 216 .then(sendForm) 217 .catch(function () { 218 sendForm(''); 219 }); 220 }); 221 }); 222 223 function sendForm(token) { 224 var formData = new FormData(form); 225 formData.append('action', 'business_lead'); 226 if (token) formData.append('g-recaptcha-response', token); 227 228 fetch('/pricing?action=business_lead', { 229 method: 'POST', 230 body: formData, 231 credentials: 'same-origin', 232 // Prevents CSRF-surface leak via Referer for any cross-origin 233 // images/scripts that might later be embedded on the page. 234 referrerPolicy: 'same-origin' 235 }) 236 .then(function (r) { return r.json(); }) 237 .then(function (data) { 238 if (data && data.success) { 239 renderSuccess(); 240 } else { 241 renderError((data && data.text) || t('errGeneric', 'Something went wrong. Please try again later.')); 242 } 243 }) 244 .catch(function () { 245 renderError(t('errNetwork', 'Network error. Please check your connection and try again.')); 246 }) 247 .then(function () { 248 submitBtn.classList.remove('is-loading'); 249 submitBtn.disabled = false;
250 }); 251 } 252 253 function renderError(text) { 254 status.className = 'pricing-v2-form-status is-error'; 255 status.textContent = text; 256 } 257 258 function renderSuccess() { 259 // Meta Pixel Lead â the Trend-for-Business B2B inquiry submitted 260 // successfully. A qualified sales lead, distinct from a free 261 // consumer registration (CompleteRegistration). No value/currency: 262 // no synthetic lead value is assigned anywhere in this flow. 263 if (window.fbq) { 264 fbq('track', 'Lead', { content_name: 'Trend for Business', content_category: 'B2B' }); 265 } 266 // Replace the form's interior with a success block. Preserves the 267 // card layout (heading + perks are siblings) so the "Contact sales" 268 // context stays visible â just the form is swapped for a thank-you. 269 // Title + body text come from window.PRICING_I18N (server-injected 270 // per current language). Using textContent on child nodes instead 271 // of innerHTML so any future translator strings can't introduce 272 // HTML by accident. 273 var wrap = form.parentNode; 274 var success = document.createElement('div'); 275 success.className = 'pricing-v2-form-status is-success'; 276 success.style.display = 'block'; 277 278 var strong = document.createElement('strong'); 279 strong.textContent = t('successTitle', 'Thanks â your message is on its way.'); 280 success.appendChild(strong); 281 success.appendChild(document.createElement('br')); 282 success.appendChild(document.createTextNode( 283 t('successBody', 'Our sales team will get back to you within one working day.') 284 )); 285 286 wrap.replaceChild(success, form); 287 } 288 } 289})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.