1/* GA4 / funnel analytics. Same consent model as the main site 2 (localStorage 'ft_cookie_consent'); nothing loads until consent = accepted. 3 window.ftFunnelEvent mirrors the app's onboarding_step events so we can see 4 per-screen drop-off. Loaded before engine.js so ftFunnelEvent exists at step 1. */ 5(function () { 6 var ID = 'G-BW2N7EB3GM'; // FastTrack GA4 / Firebase measurement ID 7 var TT_ID = 'DA7GND3C77U5KJC7O8VG'; // TikTok Pixel ID (Ads Manager > Tools > Events). Empty = TikTok off. 8 9 window.ftLoadAnalytics = function () { 10 if (window.__ftGA) return; 11 window.__ftGA = true; 12 var s = document.createElement('script'); 13 s.async = true; 14 s.src = 'https://www.googletagmanager.com/gtag/js?id=' + ID; 15 document.head.appendChild(s); 16 window.dataLayer = window.dataLayer || []; 17 window.gtag = function () { dataLayer.push(arguments); }; 18 gtag('js', new Date()); 19 gtag('config', ID, { anonymize_ip: true }); 20 ftLoadTikTok(); 21 }; 22 23 // TikTok Pixel, behind the same consent as GA. Official loader, trimmed. 24 function ftLoadTikTok() { 25 if (!TT_ID || window.__ftTT) return; 26 window.__ftTT = true; 27 !function (w, d, t) { 28 w.TiktokAnalyticsObject = t; var ttq = w[t] = w[t] || []; 29 ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie', 'holdConsent', 'revokeConsent', 'grantConsent']; 30 ttq.setAndDefer = function (t, e) { t[e] = function () { t.push([e].concat(Array.prototype.slice.call(arguments, 0))); }; }; 31 for (var i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]); 32 ttq.instance = function (t) { var e = ttq._i[t] || []; for (var n = 0; n < ttq.methods.length; n++) ttq.setAndDefer(e, ttq.methods[n]); return e; }; 33 ttq.load = function (e, n) { 34 var r = 'https://analytics.tiktok.com/i18n/pixel/events.js'; 35 ttq._i = ttq._i || {}; ttq._i[e] = []; ttq._i[e]._u = r; ttq._t = ttq._t || {}; ttq._t[e] = +new Date(); ttq._o = ttq._o || {}; ttq._o[e] = n || {}; 36 var s = d.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = r + '?sdkid=' + e + '&lib=' + t; 37 var a = d.getElementsByTagName('script')[0]; a.parentNode.insertBefore(s, a); 38 }; 39 ttq.load(TT_ID); 40 ttq.page(); 41 }(window, document, 'ttq'); 42 } 43 44 // Map our funnel events onto TikTok's standard names, so engine.js does not 45 // need to know TikTok exists. First funnel step counts as ViewContent. 46 function ttBridge(name) { 47 try { 48 if (!TT_ID || typeof window.ttq === 'undefined') return; 49 if (name === 'funnel_step' && !window.__ftTTView) { window.__ftTTView = true; ttq.track('ViewContent'); } 50 else if (name === 'account_created') ttq.track('CompleteRegistration'); 51 else if (name === 'begin_checkout') ttq.track('InitiateCheckout'); 52 else if (name === 'purchase') ttq.track('CompletePayment'); 53 } catch (e) {} 54 } 55 56 // Generic GA4 event. No-op until GA is loaded (consent given). 57 window.ftEvent = function (name, params) { 58 try { 59 if (typeof window.gtag === 'function') window.gtag('event', name, params || {}); 60 } catch (e) {} 61 ttBridge(name); 62 }; 63 64 // Funnel step event. No-op until GA is loaded (consent given). 65 window.ftFunnelEvent = function (stepId, index) { 66 window.ftEvent('funnel_step', { step_id: stepId, step_index: index }); 67 }; 68 69 function consent() { try { return localStorage.getItem('ft_cookie_consent'); } catch (e) { return null; } } 70 function maybeLoad() { if (consent() === 'accepted') window.ftLoadAnalytics(); } 71 72 // A choice already made on the site applies here too: honor it, skip the banner. 73 if (consent()) { maybeLoad(); return; } 74 75 // No prior choice (e.g. ad traffic landing directly on /start/): show the banner. 76 document.addEventListener('DOMContentLoaded', function () { 77 var C = (window.FUNNEL_STRINGS && window.FUNNEL_STRINGS.consent) || {}; 78 var MSG = C.msg || 'We use cookies to measure traffic, with your consent.', LINK = C.link || 'Privacy Policy'; 79 var bar = document.createElement('div'); 80 bar.setAttribute('role', 'dialog'); bar.setAttribute('aria-label', 'Cookies'); 81 bar.style.cssText = 'position:fixed;left:0;right:0;bottom:0;z-index:3000;background:#2C2C2C;color:#fff;padding:14px 18px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center;font-size:.88rem;box-shadow:0 -4px 20px rgba(0,0,0,.15);'; 82 var msg = document.createElement('span'); 83 msg.style.cssText = 'flex:1;min-width:220px;line-height:1.5;'; 84 msg.innerHTML = MSG + ' <a href="/privacy.html" style="color:#E8B4B8;text-decoration:underline;">' + LINK + '</a>'; 85 var wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;gap:10px;flex-shrink:0;'; 86 function mk(label, primary) { 87 var b = document.createElement('button'); b.textContent = label; 88 b.style.cssText = 'border:none;cursor:pointer;padding:9px 20px;border-radius:20px;font-weight:600;font-size:.88rem;white-space:nowrap;' + 89 (primary ? 'background:#E8B4B8;color:#2C2C2C;' : 'background:transparent;color:#fff;border:1px solid rgba(255,255,255,.45);'); 90 return b; 91 } 92 var acc = mk(C.accept || 'Accept', true), dec = mk(C.decline || 'Decline', false);
93 function choose(v) { try { localStorage.setItem('ft_cookie_consent', v); } catch (e) {} bar.remove(); maybeLoad(); } 94 acc.addEventListener('click', function () { choose('accepted'); }); 95 dec.addEventListener('click', function () { choose('declined'); }); 96 wrap.appendChild(dec); wrap.appendChild(acc); 97 bar.appendChild(msg); bar.appendChild(wrap); 98 document.body.appendChild(bar); 99 }); 100})();
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.