1(function () { 2 'use strict'; 3 4 var orgId; 5 window.SaaraAI = window.SaaraAI || {}; 6 window.SaaraAI.baseUrl = "https://work.flyos.ai"; 7 8 9 // A simple, safe way to find our own script tag. This is more reliable 10 // than document.currentScript in all browsers. 11 var currentScript = document.currentScript || (function () { 12 var scripts = document.getElementsByTagName('script'); 13 return scripts[scripts.length - 1]; 14 })(); 15 16 function triggerOnBrowserClose(callback) { 17 let triggered = false; 18 19 function fire() { 20 if (triggered) return; 21 triggered = true; 22 document.removeEventListener("mouseout", handleMouseOut); 23 document.removeEventListener("mousemove", handleMouseMove); 24 callback(); 25 } 26 27 function handleMouseOut(e) { 28 if (e.clientY <= 0) fire(); 29 } 30 31 function handleMouseMove(e) { 32 if (e.clientY < 10) fire(); 33 } 34 35 document.addEventListener("mouseout", handleMouseOut); 36 document.addEventListener("mousemove", handleMouseMove); 37 } 38 39 40 function initialize() { 41 // --- 1. Extract the Organization ID from our own script's URL --- 42 var scriptSrc = currentScript.src; 43 try { 44 orgId = new URL(scriptSrc).searchParams.get('org_id'); 45 } catch (e) { 46 console.error('SaaraAI Script Error: Could not parse script URL.', e); 47 return; 48 } 49 50 if (!orgId) { 51 console.error('SaaraAI Script Error: Organization ID (org_id) is missing from the script tag.'); 52 return; 53 } 54 55 // --- 2. Create global namespace for shared variables --- 56 window.SaaraAI = window.SaaraAI || {}; 57 window.SaaraAI.organizationId = orgId; 58 59 // --- Run parallel tasks (fingerprint + session + triggers) --- 60 runParallelTasks(orgId); 61 62 // --- 4. Load Dynamic Widgets from DB (e.g. newsletter popup, sales popup) --- 63 loadWidgets(); 64 65 // --- 5. Load Global Merchant Apps (legacy script + CSS loaders) --- 66 var apiUrl = window.SaaraAI.baseUrl + '/global-script/config?org_id=' + orgId; 67 68 fetch(apiUrl) 69 .then(function (response) { 70 if (!response.ok) { 71 throw new Error('Network response was not ok: ' + response.statusText); 72 } 73 return response.json(); 74 }) 75 .then(function (data) { 76 if (data && data.scripts) { 77 data.scripts.forEach(function (app) { 78 console.log('SaaraAI: Loading app -', app.name); 79 80 if (app.css_url) { 81 loadCss(app.css_url); 82 } 83 84 if (app.js_url) { 85 loadScript(app); 86 } 87 }); 88 } 89 }) 90 .catch(function (error) { 91 console.error('SaaraAI Script Error: Failed to fetch or process configuration.', error); 92 }); 93 } 94 95 96 /** 97 * Runs several independent, parallel tasks in the background. 98 * These tasks do not block the main script execution. 99 */ 100 function runParallelTasks(orgId) { 101 startHeartbeat(orgId); 102 identifyUser(orgId); 103 getWidgetAction(orgId); 104 saveSessionData(orgId); 105 } 106 107 function startHeartbeat(orgId) { 108 var heartbeatUrl = window.SaaraAI.baseUrl + '/global-script/heartbeat'; 109 110 function buildPayload() { 111 return { 112 org_id: orgId, 113 current_url: window.location.href, 114 origin: window.location.origin, 115 page_title: document.title || '' 116 }; 117 } 118 119 function sendHeartbeat() { 120 var payload = JSON.stringify(buildPayload()); 121 122 if (navigator.sendBeacon) { 123 navigator.sendBeacon(heartbeatUrl, new Blob([payload], { type: 'application/json' })); 124 return; 125 } 126 127 fetch(heartbeatUrl, { 128 method: 'POST', 129 headers: { 'Content-Type': 'application/json' }, 130 body: payload, 131 keepalive: true 132 }).catch(function () {}); 133 } 134 135 sendHeartbeat(); 136 window.SaaraAI.heartbeatIntervalId = window.setInterval(sendHeartbeat, 60000); 137 138 document.addEventListener('visibilitychange', function () { 139 if (document.visibilityState === 'visible') { 140 sendHeartbeat(); 141 } 142 }); 143 144 window.addEventListener('pageshow', sendHeartbeat); 145 window.addEventListener('pagehide', sendHeartbeat); 146 } 147 148 /** 149 * Collects a device fingerprint for user identification. 150 */ 151 function collectFingerprint() { 152 const nav = navigator; 153 const screenInfo = { 154 width: screen.width, 155 height: screen.height,
156 availWidth: screen.availWidth, 157 availHeight: screen.availHeight, 158 colorDepth: screen.colorDepth, 159 pixelRatio: window.devicePixelRatio 160 }; 161 162 const data = { 163 ua: nav.userAgent, 164 platform: nav.platform, 165 vendor: nav.vendor, 166 languages: nav.languages, 167 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 168 language: nav.language, 169 hardwareConcurrency: nav.hardwareConcurrency || null, 170 deviceMemory: nav.deviceMemory || null, 171 maxTouchPoints: nav.maxTouchPoints || 0, 172 cookieEnabled: nav.cookieEnabled, 173 doNotTrack: nav.doNotTrack, 174 connection: nav.connection ? { 175 effectiveType: nav.connection.effectiveType, 176 downlink: nav.connection.downlink 177 } : null, 178 screen: screenInfo, 179 viewport: { w: window.innerWidth, h: window.innerHeight }, 180 pluginsLength: (nav.plugins && nav.plugins.length) || 0, 181 time: Date.now() 182 }; 183 184 // Canvas fingerprint (simple) 185 try { 186 const c = document.createElement('canvas'); 187 c.width = 200; c.height = 50; 188 const ctx = c.getContext('2d'); 189 ctx.textBaseline = 'top'; 190 ctx.font = "14px 'Arial'"; 191 ctx.fillStyle = '#f60'; 192 ctx.fillRect(125, 1, 62, 20); 193 ctx.fillStyle = '#069'; 194 ctx.fillText('fingerprint-test', 2, 15); 195 data.canvas = c.toDataURL(); 196 } catch (e) { data.canvas = null; } 197 198 return data; 199 } 200 201 /** 202 * 1. Identifies the current user by collecting a device fingerprint. 203 * This function will contain more complex logic in the future before the API call. 204 */ 205 function identifyUser(orgId) { 206 console.log('SaaraAI: Starting user identification process...'); 207 var apiUrl = window.SaaraAI.baseUrl + '/widget/identify-user/'; 208 var fingerprintData = collectFingerprint(); 209 210 fetch(apiUrl, { 211 method: 'POST', 212 headers: { 'Content-Type': 'application/json' }, 213 body: JSON.stringify({ 214 merchantId: orgId, 215 fingerprint: fingerprintData, 216 }), 217 }) 218 .then(function (response) { 219 if (!response.ok) { 220 console.warn('SaaraAI: User identification request failed.'); 221 return null; 222 } 223 return response.json(); 224 }) 225 .then(function (data) { 226 if (data && data.user) { 227 console.log('SaaraAI: User identified:', data.user); 228 // Store user data globally if needed, e.g., window.SaaraAI.user = data.user; 229 } 230 }) 231 .catch(function (error) { 232 console.error('SaaraAI Script Error: User identification failed.', error); 233 }); 234 } 235 236 /** 237 * 2. Determines which dynamic action/widget to trigger. 238 */ 239 function getWidgetAction(orgId) { 240 var apiUrl = window.SaaraAI.baseUrl + '/widget/trigger/'; 241 fetch(apiUrl, { 242 method: 'POST', 243 headers: { 'Content-Type': 'application/json' }, 244 body: JSON.stringify({ 245 merchantId: orgId, 246 currentUrl: window.location.href, 247 }), 248 }) 249 .then(function (response) { 250 if (!response.ok) { 251 return null; // Fail silently if no widget to trigger 252 } 253 return response.json(); 254 }) 255 .then(function (data) { 256 if (data && data.widget) { 257 var widget = data.widget; 258 console.log('SaaraAI: Dynamically triggering widget:', widget.name); 259 loadWidget(widget.config, widget.type, widget.name); 260 } 261 }) 262 .catch(function (error) { 263 console.error('SaaraAI Script Error: Dynamic widget trigger failed.', error); 264 }); 265 } 266 267 /** 268 * 3. Saves session data. 269 */ 270 function saveSessionData(orgId) { 271 var apiUrl = window.SaaraAI.baseUrl + '/save-session'; 272 // Dummy data for now 273 var sessionData = { 274 startTime: new Date().toISOString(), 275 pageUrl: window.location.href, 276 referrer: document.referrer, 277 }; 278 279 fetch(apiUrl, { 280 method: 'POST', 281 headers: { 'Content-Type': 'application/json' }, 282 body: JSON.stringify({ 283 merchantId: orgId, 284 session: sessionData, 285 }), 286 }) 287 .then(function (response) { 288 if (!response.ok) { 289 console.warn('SaaraAI: Save session data request failed.'); 290 } else { 291 console.log('SaaraAI: Session data saved successfully.'); 292 } 293 }) 294 .catch(function (error) { 295 console.error('SaaraAI Script Error: Saving session data failed.', error); 296 }); 297 } 298 299 /** 300 * Creates and injects a <script> tag into the document's <head>. 301 * @param {object} app - The script object from our API response. 302 */ 303 function loadScript(app) { 304 var script = document.createElement('script'); 305 script.src = app.js_url; 306 script.async = true; 307 // This is how we pass the specific config to the app script. 308 // The app script can then find its own tag and read this data. 309 script.dataset.saaraConfig = JSON.stringify(app.config); 310 script.dataset.saaraAppName = app.name; 311 document.head.appendChild(script); 312 } 313 314 /** 315 * Creates and injects a <link rel="stylesheet"> tag into the document's <head>. 316 * @param {string} href - The URL of the CSS file. 317 */ 318 function loadCss(href) { 319 var link = document.createElement('link'); 320 link.rel = 'stylesheet'; 321 link.type = 'text/css'; 322 link.href = href; 323 document.head.appendChild(link); 324 } 325 326 // --- Core Engine v2.0 Widget Loading Logic --- 327 328 /** 329 * Fetches and loads widget configurations based on targeting rules. 330 * This logic is merged from the original core-engine.js. 331 */ 332 function loadWidgets() { 333 var currentUrl = window.location.href;
334 var urlParams = new URLSearchParams(window.location.search); 335 var ugcToken = urlParams.get('ugc_review_token'); 336 var configApiUrl = window.SaaraAI.baseUrl + '/widget/config/merchant/'; 337 338 fetch(configApiUrl, { 339 method: 'POST', 340 headers: { 'Content-Type': 'application/json' }, 341 body: JSON.stringify({ 342 merchantId: orgId, 343 ugcToken: ugcToken, 344 currentUrl: currentUrl, 345 }), 346 }) 347 .then(function (response) { 348 if (!response.ok) { 349 throw new Error('Configuration request failed with status ' + response.status); 350 } 351 return response.json(); 352 }) 353 .then(function (config) { 354 // Check UGC Agent widget - this should only be triggered if the ugc_review_token is present. 355 if (ugcToken && config.features && config.features.ugcAgent && config.features.ugcAgent.enabled) { 356 var targetingRules = config.features.ugcAgent.targetingRules; 357 if (matchesTargeting(targetingRules, currentUrl)) { 358 console.log('SaaraAI: UGC Agent enabled and targeting rules matched. Loading widget...'); 359 // The widget name for a token-based trigger is always 'Collect UGC'. 360 loadWidget(config.features.ugcAgent, 'ugcAgent', 'Collect UGC'); 361 } else { 362 console.log('SaaraAI: UGC Agent enabled but targeting rules did not match current page.'); 363 } 364 } 365 366 // Check Sales Agent widget 367 if (config.features && config.features.salesAgent && config.features.salesAgent.enabled) { 368 var salesAgentTargeting = config.features.salesAgent.targetingRules; 369 if (matchesTargeting(salesAgentTargeting, currentUrl)) { 370 console.log('SaaraAI: Sales Agent enabled and targeting rules matched. Loading widget...'); 371 loadWidget(config.features.salesAgent, 'salesAgent', 'Sales Widget'); 372 } else { 373 console.log('SaaraAI: Sales Agent enabled but targeting rules did not match current page.'); 374 } 375 } 376 377 // --- Load AI-generated custom widgets dynamically --- 378 if (config.customWidgets && config.customWidgets.length > 0) { 379 config.customWidgets.forEach(function (cw) { 380 if (matchesTargeting(cw.targetingRules, currentUrl)) { 381 var trigger = cw.targetingRules && cw.targetingRules.trigger; 382 var delaySeconds = cw.targetingRules && cw.targetingRules.delaySeconds; 383 var scrollPercent = cw.targetingRules && cw.targetingRules.scrollPercent; 384 var inactivitySeconds = cw.targetingRules && cw.targetingRules.inactivitySeconds; 385 386 function injectWidget() { 387 console.log('SaaraAI: Loading custom widget: ' + cw.name); 388 try { 389 // Inject CSS 390 if (cw.code && cw.code.css) { 391 var style = document.createElement('style'); 392 style.setAttribute('data-saara-widget', cw.id); 393 style.textContent = cw.code.css; 394 document.head.appendChild(style); 395 } 396 // Inject HTML 397 if (cw.code && cw.code.html) { 398 var container = document.createElement('div'); 399 container.setAttribute('data-saara-widget', cw.id); 400 container.innerHTML = cw.code.html; 401 document.body.appendChild(container); 402 } 403 // Execute JS with widgetConfig context 404 if (cw.code && cw.code.js) { 405 var script = document.createElement('script'); 406 script.setAttribute('data-saara-widget', cw.id); 407 script.textContent = '(function(){var widgetConfig={merchantId:"' + orgId + '",baseUrl:"' + window.SaaraAI.baseUrl + '",widgetId:"' + cw.id + '"};' + cw.code.js + '})();'; 408 document.head.appendChild(script); 409 } 410 trackWidgetLoad(cw.name, 'custom', cw.id); 411 } catch (err) { 412 console.error('SaaraAI Error: Failed to load custom widget ' + cw.name, err); 413 } 414 } 415 416 if (trigger === 'add_to_cart') { 417 var atcInjected = false;
418 419 function onAddToCart() { 420 if (atcInjected) return; 421 atcInjected = true; 422 console.log('SaaraAI: Add to cart detected, triggering widget "' + cw.name + '"'); 423 injectWidget(); 424 } 425 426 // --- Strategy 1: Click on button/link whose text or attributes match --- 427 var atcPattern = /add[\s\-_]?to[\s\-_]?(cart|bag|basket|trolley)|buy[\s\-_]?now/i; 428 429 document.addEventListener('click', function (e) { 430 if (atcInjected) return; 431 var el = e.target; 432 // Walk up to 3 levels to handle clicks on child elements (e.g. <span> inside <button>) 433 for (var i = 0; i < 3 && el; i++) { 434 var tag = el.tagName && el.tagName.toLowerCase(); 435 if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'submit') { 436 var text = (el.textContent || el.value || '').trim(); 437 var label = el.getAttribute('aria-label') || ''; 438 var name = el.getAttribute('name') || ''; 439 var id = el.id || ''; 440 var cls = el.className || ''; 441 if ( 442 atcPattern.test(text) || 443 atcPattern.test(label) || 444 atcPattern.test(name) || 445 atcPattern.test(id) || 446 atcPattern.test(cls) 447 ) { 448 onAddToCart(); 449 return; 450 } 451 } 452 el = el.parentElement; 453 } 454 }, true); 455 456 // --- Strategy 2: Shopify & common platform cart events --- 457 document.addEventListener('cart:add', onAddToCart); 458 document.addEventListener('theme:cart:add', onAddToCart); 459 document.addEventListener('add-to-cart', onAddToCart); 460 461 // --- Strategy 3: Intercept fetch/XHR POST to common cart endpoints --- 462 var cartEndpointPattern = /\/cart\/(add|update)|\/api\/cart|CartAdd/i; 463 464 var origFetch = window.fetch; 465 window.fetch = function (input, init) { 466 var url = (typeof input === 'string' ? input : (input && input.url)) || ''; 467 if (!atcInjected && (init && init.method || '').toUpperCase() === 'POST' && cartEndpointPattern.test(url)) { 468 onAddToCart(); 469 } 470 return origFetch.apply(this, arguments); 471 }; 472 473 var origOpen = XMLHttpRequest.prototype.open; 474 var origSend = XMLHttpRequest.prototype.send; 475 XMLHttpRequest.prototype.open = function (method, url) { 476 this._saaraMethod = method; 477 this._saaraUrl = url; 478 return origOpen.apply(this, arguments); 479 }; 480 XMLHttpRequest.prototype.send = function () { 481 if (!atcInjected && 482 (this._saaraMethod || '').toUpperCase() === 'POST' && 483 cartEndpointPattern.test(this._saaraUrl || '')) { 484 onAddToCart(); 485 } 486 return origSend.apply(this, arguments); 487 }; 488 489 } else if (trigger === 'login_click') { 490 var loginInjected = false;
491 492 function onLoginClick() { 493 if (loginInjected) return; 494 loginInjected = true; 495 console.log('SaaraAI: Login/Signup click detected, triggering widget "' + cw.name + '"'); 496 injectWidget(); 497 } 498 499 // --- Pattern to match login/signup actions --- 500 var loginPattern = /login|log[\s\-_]?in|sign[\s\-_]?in|signup|sign[\s\-_]?up|register|create[\s\-_]?account|join/i; 501 502 // --- Strategy 1: Click detection --- 503 document.addEventListener('click', function (e) { 504 if (loginInjected) return; 505 var el = e.target; 506 507 // Traverse up to handle nested elements (span inside button etc.) 508 for (var i = 0; i < 3 && el; i++) { 509 var tag = el.tagName && el.tagName.toLowerCase(); 510 511 if (tag === 'button' || tag === 'a' || tag === 'input') { 512 var text = (el.textContent || el.value || '').trim(); 513 var label = el.getAttribute('aria-label') || ''; 514 var name = el.getAttribute('name') || ''; 515 var id = el.id || ''; 516 var cls = el.className || ''; 517 var href = el.getAttribute('href') || ''; 518 519 if ( 520 loginPattern.test(text) || 521 loginPattern.test(label) || 522 loginPattern.test(name) || 523 loginPattern.test(id) || 524 loginPattern.test(cls) || 525 loginPattern.test(href) 526 ) { 527 onLoginClick(); 528 return; 529 } 530 } 531 532 el = el.parentElement; 533 } 534 }, true); 535 536 // --- Strategy 2: Detect navigation to auth routes --- 537 var authRoutePattern = /login|signin|signup|register|account/i; 538 539 window.addEventListener('popstate', function () { 540 if (!loginInjected && authRoutePattern.test(location.href)) { 541 onLoginClick(); 542 } 543 }); 544 545 // --- Strategy 3: Intercept API calls (login/signup requests) --- 546 var authApiPattern = /login|signin|signup|register|auth|account/i; 547 548 var origFetch = window.fetch; 549 window.fetch = function (input, init) { 550 var url = (typeof input === 'string' ? input : (input && input.url)) || ''; 551 if ( 552 !loginInjected && 553 (init && init.method || '').toUpperCase() === 'POST' && 554 authApiPattern.test(url) 555 ) { 556 onLoginClick(); 557 } 558 return origFetch.apply(this, arguments); 559 }; 560 561 var origOpen = XMLHttpRequest.prototype.open; 562 var origSend = XMLHttpRequest.prototype.send; 563 564 XMLHttpRequest.prototype.open = function (method, url) { 565 this._saaraMethod = method; 566 this._saaraUrl = url; 567 return origOpen.apply(this, arguments); 568 }; 569 570 XMLHttpRequest.prototype.send = function () { 571 if ( 572 !loginInjected && 573 (this._saaraMethod || '').toUpperCase() === 'POST' && 574 authApiPattern.test(this._saaraUrl || '') 575 ) { 576 onLoginClick(); 577 } 578 return origSend.apply(this, arguments); 579 }; 580 } 581 else if (trigger === 'inactivity') { 582 var inactivityDelay = inactivitySeconds > 0 ? inactivitySeconds : 30;
583 console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + inactivityDelay + 's of inactivity'); 584 var inactivityTimer = null; 585 var inactivityInjected = false; 586 var inactivityLastMove = 0; 587 var inactivityEvents = ['mousemove', 'keydown', 'scroll', 'click', 'touchstart']; 588 589 function resetInactivityTimer(e) { 590 if (inactivityInjected) return; 591 if (e && e.type === 'mousemove') { 592 var now = Date.now(); 593 if (now - inactivityLastMove < 500) return; 594 inactivityLastMove = now; 595 } 596 clearTimeout(inactivityTimer); 597 inactivityTimer = setTimeout(function () { 598 if (inactivityInjected) return; 599 inactivityInjected = true; 600 inactivityEvents.forEach(function (ev) { 601 document.removeEventListener(ev, resetInactivityTimer); 602 }); 603 injectWidget(); 604 }, inactivityDelay * 1000); 605 } 606 607 inactivityEvents.forEach(function (ev) { 608 document.addEventListener(ev, resetInactivityTimer, { passive: true }); 609 }); 610 resetInactivityTimer(null); 611 } else if (trigger === 'timed_delay' && delaySeconds > 0) { 612 console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + delaySeconds + 's delay'); 613 setTimeout(injectWidget, delaySeconds * 1000); 614 } else if (trigger === 'exit_intent') { 615 console.log('SaaraAI: Widget "' + cw.name + '" will load on exit intent'); 616 triggerOnBrowserClose(injectWidget); 617 } else if (trigger === 'scroll_depth' && scrollPercent > 0) { 618 console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + scrollPercent + '% scroll depth'); 619 var injected = false; 620 function onScroll() { 621 if (injected) return; 622 var scrolled = (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100; 623 if (scrolled >= scrollPercent) { 624 injected = true; 625 window.removeEventListener('scroll', onScroll); 626 injectWidget(); 627 } 628 } 629 window.addEventListener('scroll', onScroll); 630 } else if (trigger === 'abandoned_cart') { 631 var hasTargetUrls = cw.targetingRules.targetUrls && cw.targetingRules.targetUrls.length > 0; 632 var isCartPage = hasTargetUrls 633 ? true // already filtered by matchesTargeting() above 634 : /\/(cart|checkout|basket|bag|trolley)(\/|$|\?|#)/i.test(window.location.pathname) 635 || /[?&](cart|checkout)=/i.test(window.location.search); 636 637 if (!isCartPage) { 638 console.log('SaaraAI: Widget "' + cw.name + '" skipped - not a cart page'); 639 return; 640 } 641
642 var delay = inactivitySeconds > 0 ? inactivitySeconds : 30; 643 console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + delay + 's of cart inactivity'); 644 var cartTimer = null; 645 var cartInjected = false; 646 var lastMoveTime = 0; 647 var activityEvents = ['mousemove', 'keydown', 'scroll', 'click', 'touchstart']; 648 649 function cancelCartTimer() { 650 if (cartTimer) clearTimeout(cartTimer); 651 } 652 653 function startCartTimer() { 654 cancelCartTimer(); 655 cartTimer = setTimeout(function () { 656 if (cartInjected) return; 657 cartInjected = true; 658 activityEvents.forEach(function (ev) { 659 document.removeEventListener(ev, resetCartTimer); 660 }); 661 injectWidget(); 662 }, delay * 1000); 663 } 664 665 function resetCartTimer(e) { 666 if (cartInjected) return; 667 // Throttle mousemove to at most once every 500ms 668 if (e && e.type === 'mousemove') { 669 var now = Date.now(); 670 if (now - lastMoveTime < 500) return; 671 lastMoveTime = now; 672 } 673 startCartTimer(); 674 } 675 676 activityEvents.forEach(function (ev) { 677 document.addEventListener(ev, resetCartTimer, { passive: true }); 678 }); 679 startCartTimer(); 680 } else { 681 injectWidget(); 682 } 683 } 684 }); 685 } 686 }) 687 .catch(function (error) { 688 console.error("SaaraAI Error: Could not load widget configuration.", error); 689 }); 690 } 691 692 /** 693 * Checks if the current URL matches any of the targeting rules. 694 */ 695 function matchesTargeting(targetingRules, currentUrl) { 696 if (!targetingRules || !targetingRules.targetUrls || targetingRules.targetUrls.length === 0) { 697 return true; // No rules means show on all pages 698 } 699 var currentUrlNormalized = currentUrl.split('?')[0].split('#')[0]; 700 return targetingRules.targetUrls.some(function (targetUrl) { 701 var targetUrlNormalized = targetUrl.split('?')[0].split('#')[0]; 702 return currentUrlNormalized === targetUrlNormalized; 703 }); 704 } 705 706 /** 707 * Loads a specific widget by either executing its rendered script or loading an external URL. 708 */ 709 710 function loadWidget(widgetConfig, widgetType, widgetName) { 711 if (!widgetName) { 712 console.error('SaaraAI Error: widgetName is required for tracking.'); 713 return; 714 } 715 716 const targetingRules = widgetConfig?.targetingRules || {}; 717 const targetUrls = targetingRules?.targetUrls || []; 718 const exitIntent = targetingRules?.exit_intent || "on_load"; 719 720 const currentUrl = window.location.href; 721 722 // â URL NORMALIZER (removes query + hash + trailing slash) 723 function normalizeUrl(url) { 724 if (!url) return ""; 725 return url 726 .split("?")[0] 727 .split("#")[0] 728 .replace(/\/$/, "") 729 .toLowerCase(); 730 } 731 732 function isTargetUrlMatch() { 733 if (!targetUrls.length) return true; // No targeting = show everywhere 734 735 const currentNormalized = normalizeUrl(currentUrl); 736 737 return targetUrls.some(target => { 738 return normalizeUrl(target) === currentNormalized; 739 }); 740 } 741 742 // â STOP if URL does not match 743 if (!isTargetUrlMatch()) {
744 console.log("SaaraAI: Widget skipped - URL not matched"); 745 return; 746 } 747 748 function executeWidget() { 749 try { 750 if (widgetConfig.renderedScript) { 751 console.log('SaaraAI: Loading ' + widgetType + ' using database-driven template'); 752 executeRenderedScript(widgetConfig.renderedScript); 753 trackWidgetLoad(widgetName); 754 } else if (widgetConfig.scriptUrl) { 755 console.log('SaaraAI: Loading ' + widgetType + ' using legacy script file'); 756 loadExternalScript(widgetConfig.scriptUrl).then(function () { 757 if ( 758 widgetType === 'ugcAgent' && 759 window.SaaraAIPlatformUgcAgent && 760 typeof window.SaaraAIPlatformUgcAgent.init === 'function' 761 ) { 762 var urlParams = new URLSearchParams(window.location.search); 763 var ugcToken = urlParams.get('ugc_review_token'); 764 var initOptions = widgetConfig.options || {}; 765 initOptions.merchantId = orgId; 766 initOptions.ugcToken = ugcToken; 767 window.SaaraAIPlatformUgcAgent.init(initOptions); 768 } 769 trackWidgetLoad(widgetName); 770 }); 771 } 772 } catch (error) { 773 console.error('SaaraAI Error: Failed to load ' + widgetType + ' widget', error); 774 } 775 } 776 777 // â EXIT INTENT HANDLING 778 if (exitIntent === "browser_close") { 779 console.log("SaaraAI: Widget will trigger on browser close"); 780 triggerOnBrowserClose(executeWidget); 781 } else { 782 console.log("SaaraAI: Widget triggered on load"); 783 executeWidget(); 784 } 785 } 786 787 788 /** 789 * Tracks the loading of a widget for analytics. 790 */ 791 function trackWidgetLoad(widgetName, widgetType, widgetId) { 792 var trackingUrl = window.SaaraAI.baseUrl + '/widget/track-load/'; 793 var payload = { 794 merchantId: orgId, 795 widgetName: widgetName, 796 }; 797 if (widgetType) payload.widgetType = widgetType; 798 if (widgetId) payload.widgetId = widgetId; 799 fetch(trackingUrl, { 800 method: 'POST', 801 headers: { 'Content-Type': 'application/json' }, 802 body: JSON.stringify(payload), 803 }).catch(function (error) { 804 console.debug('SaaraAI: Widget load tracking failed', error); 805 }); 806 } 807 808 /** 809 * Executes a script string by creating a new script tag. 810 */ 811 function executeRenderedScript(scriptContent) { 812 try { 813 var script = document.createElement('script'); 814 script.textContent = scriptContent; 815 document.head.appendChild(script); 816 } catch (error) { 817 console.error('SaaraAI Error: Failed to execute widget script', error); 818 } 819 } 820 821 /** 822 * Loads an external script file asynchronously. 823 */ 824 function loadExternalScript(src) { 825 return new Promise(function (resolve, reject) { 826 var script = document.createElement('script'); 827 script.src = src; 828 script.async = true; 829 script.onload = resolve; 830 script.onerror = function () { 831 reject(new Error('Failed to load script: ' + src)); 832 }; 833 document.head.appendChild(script); 834 }); 835 } 836 837 // --- 5. Run the initialization logic --- 838 // We wait for the DOM to be fully loaded to ensure we don't block page rendering. 839 if (document.readyState === 'loading') { 840 document.addEventListener('DOMContentLoaded', initialize); 841 } else { 842 initialize(); 843 } 844 845})();
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.