const backStyle = {'border': 'none', 'borderRadius': '2rem', 'display': 'flex', 'height': '200px', 'inset': 'auto 1.05rem 5rem auto', 'maxHeight': '100%', 'maxWidth': '100vm', 'mobile': {'inset': 'auto 1rem 3rem auto'}, 'position': 'fixed', 'smInset': 'auto 1rem 3rem auto', 'transition': 'all 0.3s ease-out', 'width': '480px', 'zIndex': '99999'}; const config = { token: "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3MiOiAicGlyZWxseTM2MEBhcHBzcG90LmdzZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogInBpcmVsbHkzNjBAYXBwc3BvdC5nc2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9nb29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiZTRhMTgwY2UtYjFmZC00ZmRhLTk1NWEtY2UwOTNiNzY1ZDQ2IiwgImlhdCI6IDE3NzgwMTkzNzUsICJleHAiOiAxNzc4MDIyOTc1LCAiY2xhaW1zIjogeyJjb252SUQiOiAiMjhhZWFkZDk3NGZlMzY5NjRmZTciLCAiY3VzSUQiOiAiYmY3NDg2MjM3N2Y2NDQ3Y2EyMDlkIiwgImRlYWxlclVSTCI6ICJodHRwczovL2J1ZGdldG5vcndhbGsuY29tLyIsICJkZWFsZXJOYW1lIjogIkJ1ZGdldCBDYXIgU2FsZXMgb2YgTm9yd2FsayIsICJkZWFsZXJDb3VudHJ5IjogIlVTIiwgIkFJTmFtZSI6ICJEYXZpZCBHYWxsYXJkbyIsICJ3aWRnZXRUZW1wbGF0ZSI6ICJIZXkgW0N1c3RvbWVyIE5hbWVdISBcdWQ4M2RcdWRjNGIgV2VsY29tZSB0byBbZGVhbGVyc2hpcCBuYW1lXS4gSSdtIGhlcmUgdG8gaGVscCB5b3UgZmluZCB0aGUgcmlnaHQgY2FyLCBnZXQgcHJpY2luZywgb3IgYm9vayBhIHRlc3QgZHJpdmUgXHUyMDE0IHdoYXRldmVyIHlvdSBuZWVkLiIsICJpc09wZW4iOiBmYWxzZSwgInVpZCI6ICJIakxUejdpZUtDZm9lc25uWWhDWU5sQlJONWgyIiwgInBpY051bSI6IDcsICJwaG90b1VSTCI6ICJodHRwczovL3N0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vcHVibGljLWlmcmFtZS9zYWxlc0FnZW50L3dpZGdldFBob3RvUGF0aC9IakxUejdpZUtDZm9lc25uWWhDWU5sQlJONWgyLy1PYUNObzBKRFVlNnFqSF9QY0dvLmpwZyIsICJwaG90b1VSTHMiOiBbImh0dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9wdWJsaWMtaWZyYW1lL3NhbGVzQWdlbnQvd2lkZ2V0UGhvdG9QYXRoL0hqTFR6N2llS0Nmb2Vzbm5ZaENZTmxCUk41aDIvLU9hQ05vMEpEVWU2cWpIX1BjR28uanBnIiwgImh0dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9wdWJsaWMtaWZyYW1lL0FJQXZhdGFyL0VsbGlwc2UxNTE5LTcuanBnIiwgImh0dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9wdWJsaWMtaWZyYW1lL0FJQXZhdGFyL0VsbGlwc2UxNTE5LTMuanBnIl0sICJwaG90b0NvdW50IjogMX19.MBr3JnM99aHxaSGAsFksrlE3dy_P_RVVcW0Ped3cerH2nEx-K6INPAKPpC_cgJEfkW2Zk1H5dvjYu722toa7g_pi_rMB_FlI1_O58iSTO-8no5cBqWMJ8Jp-tUTOY_1SbkXTytId6Cn2qjQSAfnzK0idfM2BFhOVTZP96mWuOXHx53OxL2CTvtPLaf_iv3l-BLs2zM__MVw-69a1LLYbIVML4LHysIB5aXer_3pJ9sBkoJ4isPmCoWSfc6At27ULsXTn-Mfl0k7UFrDUDDU4j9dOh-jhGjdyVCmMDuY17-jt0gbFqkdC7bytChe4HFvJ1bNL7dDbAYAy7RhMWtHfbw", url: "https://salesagent-widget.web.app/", iframeId: 'salesAgent-widget-iframe', cookieFreeMode: true, allowedOrigins: [ "https://salesagent-widget.web.app/", "https://budgetnorwalk.com/", ], styles: backStyle, attributes: { 'data-termly-embed': 'essential' }, log_link: "https://us-central1-pirelly360.cloudfunctions.net/salesagent-widget-init-api/lead-js", }; (function () { try { // Extract URL parameters safely const params = new URLSearchParams(window.location.search); const leadId = params.get("lead_id"); const source = params.get("source"); const linkType = params.get("link_type"); const vin = params.get("vin"); // Only proceed if we have the required lead_id parameter if (!leadId) { return; // Silently exit if no tracking needed } // Validate required config if (!config || !config.token) { console.warn("Widget tracking: Missing dealer token"); return; } // Prepare tracking payload const trackingPayload = { lead_id: leadId, source: source || "unknown", link_type: linkType || "website", vin: vin || null, dealer_token: config.token, window_url: window.location.href }; // Send tracking request with timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout fetch(config.log_link, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(trackingPayload), signal: controller.signal }) .then(response => { clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { // Successfully tracked - optional debug logging if (typeof console !== 'undefined' && console.debug) { console.debug("Link click tracked successfully:", data); } }) .catch(err => { clearTimeout(timeoutId); // Handle different error types if (err.name === 'AbortError') { console.warn("Link tracking timeout - request took too long"); } else if (err.message && err.message.includes('NetworkError')) { console.warn("Link tracking failed - network error"); } else { console.error("Link tracking error:", err.message || err); } // Don't block page load on tracking failure }); } catch (err) { // Catch any unexpected errors in the tracking code console.error("Widget tracking initialization error:", err); } })(); const createIframe = () => { const iframe = document.createElement('iframe'); // Apply attributes with security enhancements Object.entries({ ...config.attributes, 'data-termly-origin': window.location.origin, 'data-termly-domain': window.location.hostname }).forEach(([key, value]) => { iframe.setAttribute(key, value); }); // Secure URL construction with encoded parameters const params = new URLSearchParams({ token: encodeURIComponent(config.token), width: window.innerWidth, height: window.innerHeight, origin: encodeURIComponent(window.location.origin), parentDomain: encodeURIComponent(window.location.hostname), href: window.location.href, termlyEmbed: 'essential', _: Date.now() // Cache buster }); iframe.src = `${config.url}?${params}`; iframe.id = config.iframeId; iframe.name = 'salesAgentWidget'; iframe.loading = 'eager'; // Apply styles with responsive defaults Object.assign(iframe.style, { position: 'fixed', border: 'none', zIndex: '999999', ...config.styles, // Always anchor to the bottom-right corner; the React app inside handles spacing inset: 'auto 0 0 auto', // Responsive fallbacks maxWidth: '100vw', maxHeight: '100vh', // Force the iframe into its own GPU compositing layer. // This prevents position:fixed from breaking when a dealer's page has CSS // transforms on ancestor elements (a very common cause of widgets disappearing // on scroll). transform: 'translateZ(0)', WebkitTransform: 'translateZ(0)', backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden', }); return iframe; }; // Enhanced message handler with origin validation function createMessageHandler(iframeId) { const script = document.createElement('script'); script.innerHTML = ` (function() { const salesAgentWidgetIframe = document.getElementById("${iframeId}"); const allowedOrigins = ${JSON.stringify(config.allowedOrigins.map(o => o.replace(/\/$/, '')))}; // Termly override if needed if (window.TERMLY_CUSTOM_BLOCKING_MAP) { window.TERMLY_CUSTOM_BLOCKING_MAP["${config.url.split('?')[0]}"] = "essential"; } // ── Mobile full-screen: lock the host page so it can't scroll behind // the widget. We snapshot the current scroll position, pin the body // in place, then restore everything when the widget leaves full-screen // mode. Using position:fixed is the only iOS-Safari-safe way to // truly stop background scrolling. let savedScrollY = 0; let bodyLocked = false; const saved = { htmlOverflow: '', bodyOverflow: '', bodyPosition: '', bodyTop: '', bodyWidth: '', bodyHeight: '', bodyTouchAction: '', }; function lockBody() { if (bodyLocked) return; savedScrollY = window.scrollY || window.pageYOffset || 0; saved.htmlOverflow = document.documentElement.style.overflow; saved.bodyOverflow = document.body.style.overflow; saved.bodyPosition = document.body.style.position; saved.bodyTop = document.body.style.top; saved.bodyWidth = document.body.style.width; saved.bodyHeight = document.body.style.height; saved.bodyTouchAction = document.body.style.touchAction; document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; document.body.style.position = 'fixed'; document.body.style.top = '-' + savedScrollY + 'px'; document.body.style.width = '100%'; document.body.style.height = '100%'; document.body.style.touchAction = 'none'; bodyLocked = true; } function unlockBody() { if (!bodyLocked) return; document.documentElement.style.overflow = saved.htmlOverflow; document.body.style.overflow = saved.bodyOverflow; document.body.style.position = saved.bodyPosition; document.body.style.top = saved.bodyTop; document.body.style.width = saved.bodyWidth; document.body.style.height = saved.bodyHeight; document.body.style.touchAction = saved.bodyTouchAction; window.scrollTo(0, savedScrollY); bodyLocked = false; } window.addEventListener("message", function(event) { try { // Validate origin // const originMatches = allowedOrigins.some(allowed => // event.origin === allowed || // event.origin === window.location.origin // ); // if (!originMatches) return; const { width, height, isChatOpen, mobileFullscreen, type } = event.data; // Handle resize messages if (type === 'WIDGET_RESIZE') { if (mobileFullscreen) { // Full-screen takeover on mobile: iframe fills the viewport, // no rounded corners, no bottom-right pinning. The chat header's // X button is the only way to close it. salesAgentWidgetIframe.style.inset = "0"; salesAgentWidgetIframe.style.borderRadius = "0"; lockBody(); } else { salesAgentWidgetIframe.style.inset = "auto 0 0 auto"; salesAgentWidgetIframe.style.borderRadius = isChatOpen ? '2rem 2rem 0 2rem' : '0'; unlockBody(); } salesAgentWidgetIframe.style.width = width; salesAgentWidgetIframe.style.height = height; // Re-assert GPU layer after every resize so scroll-disappear can't regress salesAgentWidgetIframe.style.transform = 'translateZ(0)'; salesAgentWidgetIframe.style.WebkitTransform = 'translateZ(0)'; } // Handle Termly-specific messages if (type === 'TERMLY_STATUS') { console.log('Termly status:', event.data.status); } } catch(e) { console.warn('Widget message error:', e); } }); })(); `; return script; } // Enhanced route change detection function listenForRouteChanges(iframe) { let currentUrl = window.location.href; const observer = new MutationObserver(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; safePostMessage(iframe, { type: 'ROUTE_CHANGE', url: currentUrl }); } }); observer.observe(document, { subtree: true, childList: true, attributes: true }); } // Safe message posting with fallback function safePostMessage(iframe, message) { try { if (iframe.contentWindow) { config.allowedOrigins.forEach(origin => { iframe.contentWindow.postMessage(message, origin); }); } } catch(e) { console.warn('PostMessage failed:', e); } } // Initialization with error handling const init = () => { try { const iframe = createIframe(); document.body.appendChild(iframe); const messageHandler = createMessageHandler(config.iframeId); document.head.appendChild(messageHandler); // Better than body listenForRouteChanges(iframe); // Fallback for Termly blocking setTimeout(() => { if (!iframe.contentWindow) { console.warn('Iframe may be blocked by Termly'); safePostMessage(iframe, { type: 'TERMLY_CHECK' }); } }, 2000); } catch(e) { console.error('Widget initialization failed:', e); } }; // Modern initialization if (document.readyState !== 'loading') { init(); } else { document.addEventListener('DOMContentLoaded', init, { once: true }); } // Handle potential Termly blocking window.addEventListener('message', (event) => { if (event.data?.type === 'TERMLY_BLOCKED') { console.warn('Termly blocked iframe loading'); // Implement fallback UI here if needed } });