1(function () { 2 'use strict'; 3 4 var WIDGET_VERSION = '20260717a'; 5 6 // Find the script tag that loaded us 7 var script = 8 document.currentScript || 9 (function () { 10 var scripts = document.getElementsByTagName('script'); 11 for (var i = scripts.length - 1; i >= 0; i--) { 12 if (scripts[i].src && scripts[i].src.indexOf('widget.js') !== -1) return scripts[i]; 13 } 14 return null; 15 })(); 16 17 if (!script) return; 18 19 var getConfigFromScript = function (s) { 20 if (!s) return null; 21 var accountAttr = s.getAttribute('data-account') || s.getAttribute('data-campaign'); 22 var apiAttr = s.getAttribute('data-api'); 23 try { 24 var srcUrl = new URL(s.src || '', window.location.href); 25 accountAttr = accountAttr || srcUrl.searchParams.get('account') || srcUrl.searchParams.get('campaign'); 26 apiAttr = apiAttr || srcUrl.searchParams.get('api'); 27 } catch (e) { 28 // ignore URL parsing errors and continue 29 } 30 return accountAttr && apiAttr ? { account: accountAttr, apiBase: apiAttr } : null; 31 }; 32 33 var findConfig = function () { 34 var direct = getConfigFromScript(script); 35 if (direct) return direct; 36 var scripts = document.getElementsByTagName('script'); 37 for (var i = scripts.length - 1; i >= 0; i--) { 38 if (scripts[i].src && scripts[i].src.indexOf('widget.js') !== -1) { 39 var found = getConfigFromScript(scripts[i]); 40 if (found) return found; 41 } 42 } 43 return null; 44 }; 45 46 var config = findConfig(); 47 48 try { 49 var currentSrc = new URL(script.src, window.location.href); 50 var currentVersion = currentSrc.searchParams.get('v'); 51 if (currentVersion !== WIDGET_VERSION && !window.__OH_WIDGET_VERSION_BOOTSTRAPPED__) { 52 window.__OH_WIDGET_VERSION_BOOTSTRAPPED__ = true; 53 currentSrc.searchParams.set('v', WIDGET_VERSION); 54 var freshScript = document.createElement('script'); 55 freshScript.src = currentSrc.toString(); 56 freshScript.async = true; 57 var attrs = script.attributes; 58 for (var a = 0; a < attrs.length; a++) { 59 var attr = attrs[a]; 60 if (attr && attr.name !== 'src') freshScript.setAttribute(attr.name, attr.value); 61 } 62 if (config) { 63 freshScript.setAttribute('data-account', config.account); 64 freshScript.setAttribute('data-api', config.apiBase); 65 } 66 script.parentNode.insertBefore(freshScript, script.nextSibling); 67 return; 68 } 69 } catch (e) { 70 // ignore URL parsing errors and continue 71 } 72 73 var account = config && config.account; 74 var apiBase = config && config.apiBase; 75 if (!account || !apiBase) { 76 var attempts = window.__OH_WIDGET_CONFIG_RETRIES__ || 0; 77 if (attempts < 20) { 78 window.__OH_WIDGET_CONFIG_RETRIES__ = attempts + 1; 79 setTimeout(function () { 80 var cfg = findConfig(); 81 if (!cfg) return; 82 var retryScript = document.createElement('script'); 83 retryScript.src = script.src || 'https://app.offerhype.co/widget.js?v=' + WIDGET_VERSION; 84 try { 85 var retrySrc = new URL(retryScript.src, window.location.href); 86 retrySrc.searchParams.set('v', WIDGET_VERSION); 87 retryScript.src = retrySrc.toString(); 88 } catch (e) {} 89 retryScript.async = true; 90 retryScript.setAttribute('data-account', cfg.account); 91 retryScript.setAttribute('data-api', cfg.apiBase); 92 (document.head || document.documentElement).appendChild(retryScript); 93 }, 250); 94 return; 95 } 96 console.warn('[OfferHype] widget missing data-account or data-api attribute'); 97 return; 98 } 99 100 var STORAGE_KEY_FOR = function (cid) { return 'oh_seen_' + cid; }; 101 var ACTIVE_POPUP = null; 102 var QUEUE = []; 103 var RUNNING = false; 104 105 var fetchData = function () { 106 var u = apiBase + '/functions/v1/widget-data?account=' + encodeURIComponent(account) + 107 '&url=' + encodeURIComponent(window.location.href); 108 return fetch(u, { 109 method: 'GET', 110 mode: 'cors', 111 }) 112 .then(function (r) { 113 if (!r.ok) throw new Error('HTTP ' + r.status); 114 return r.json(); 115 }); 116 }; 117 118 var trackEvent = function (type, campaignId) { 119 try { 120 if (type === 'view') { 121 var key = 'oh_view_' + campaignId; 122 try { 123 if (sessionStorage.getItem(key)) return; 124 sessionStorage.setItem(key, '1'); 125 } catch (e) { /* storage blocked â fall through and count */ } 126 } 127 fetch(apiBase + '/functions/v1/track-event', { 128 method: 'POST', 129 mode: 'cors', 130 headers: { 'Content-Type': 'application/json' }, 131 body: JSON.stringify({ campaign_id: campaignId, type: type }), 132 keepalive: true, 133 }); 134 } catch (e) { 135 // silent 136 } 137 }; 138 139 var getSeen = function (cid) { 140 try { 141 return JSON.parse(localStorage.getItem(STORAGE_KEY_FOR(cid)) || '[]'); 142 } catch (e) { 143 return []; 144 } 145 }; 146 147 var markSeen = function (cid, id) { 148 try { 149 var seen = getSeen(cid); 150 if (seen.indexOf(id) === -1) { 151 seen.push(id); 152 if (seen.length > 100) seen = seen.slice(-100); 153 localStorage.setItem(STORAGE_KEY_FOR(cid), JSON.stringify(seen)); 154 } 155 } catch (e) {} 156 }; 157 158 var ATTRIBUTION_PARAMS = ['fbclid', 'gclid', 'gbraid', 'wbraid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'igshid', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id']; 159 var appendAttributionParams = function (url) { 160 try { 161 var src = new URL(window.location.href); 162 var dest = new URL(url, window.location.href); 163 ATTRIBUTION_PARAMS.forEach(function (k) { 164 var v = src.searchParams.get(k); 165 if (v && !dest.searchParams.has(k)) dest.searchParams.set(k, v); 166 }); 167 return dest.toString(); 168 } catch (e) { 169 return url; 170 } 171 }; 172 173 var injectStyles = function () { 174 if (document.getElementById('oh-widget-styles')) return; 175 var s = document.createElement('style'); 176 s.id = 'oh-widget-styles'; 177 s.textContent = 178 '.oh-popup{position:fixed;z-index:2147483647;width:340px;max-width:calc(100vw - 24px);' + 179 'box-shadow:0 10px 30px rgba(0,0,0,.15);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' + 180 'border:1px solid rgba(0,0,0,.06);overflow:hidden;' + 181 'opacity:0;transform:translateY(20px);transition:opacity .35s ease,transform .35s ease;cursor:default}' + 182 '.oh-popup.oh-show{opacity:1;transform:translateY(0)}' + 183 '.oh-popup.oh-clickable{cursor:pointer}' + 184 '.oh-popup .oh-row{display:flex;align-items:flex-start;gap:12px;padding:16px;position:relative}' + 185 '.oh-popup .oh-img{width:48px;height:48px;border-radius:50%;object-fit:contain;background:#fff;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:.95em;overflow:hidden}' + 186 '.oh-popup .oh-body{flex:1;min-width:0;line-height:1.35}' + 187 '.oh-popup .oh-name{font-weight:600}' + 188 '.oh-popup .oh-action{opacity:.8}' + 189 '.oh-popup .oh-meta{font-size:.78em;opacity:.6;margin-top:2px}' + 190 '.oh-popup .oh-stars{display:inline-flex;gap:1px;margin-top:4px}' + 191 '.oh-popup .oh-stars svg{width:14px;
191height:14px}' + 192 '.oh-popup .oh-review{font-size:.85em;font-style:italic;opacity:.75;margin-top:4px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}' + 193 '.oh-popup .oh-close{position:absolute;top:6px;right:8px;background:transparent;border:0;cursor:pointer;font-size:16px;opacity:.5;line-height:1;padding:2px 6px;color:inherit}' + 194 '.oh-popup .oh-close:hover{opacity:1}' + 195 '.oh-popup .oh-badge{display:flex;align-items:center;gap:6px;padding:8px 16px;font-size:12px;font-weight:500;border-top:1px solid rgba(0,0,0,.05);text-decoration:none}' + 196 '.oh-popup .oh-badge svg{width:14px;height:14px}' + 197 '@media (max-width:480px){.oh-popup{left:12px!important;right:12px!important;width:auto;max-width:none}}'; 198 document.head.appendChild(s); 199 }; 200 201 var timeAgo = function (iso) { 202 var diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); 203 if (diff < 60) return 'just now'; 204 if (diff < 3600) return Math.floor(diff / 60) + ' min ago'; 205 if (diff < 86400) return Math.floor(diff / 3600) + ' hr ago'; 206 return Math.floor(diff / 86400) + ' days ago'; 207 }; 208 209 var loadedFonts = {}; 210 var loadGoogleFont = function (family) { 211 if (!family) return; 212 var name = String(family).trim(); 213 if (!name || loadedFonts[name]) return; 214 loadedFonts[name] = true; 215 var link = document.createElement('link'); 216 link.rel = 'stylesheet'; 217 link.href = 218 'https://fonts.googleapis.com/css2?family=' + 219 encodeURIComponent(name).replace(/%20/g, '+') + 220 ':wght@400;500;600;700&display=swap'; 221 document.head.appendChild(link); 222 }; 223 224 var formatPrivacyName = function (fullName, mode) { 225 if (mode === 'anonymous') return 'Someone'; 226 var parts = String(fullName || '').trim().split(/\s+/).filter(Boolean); 227 if (parts.length === 0) return ''; 228 var first = parts[0]; 229 var last = parts.length > 1 ? parts[parts.length - 1] : ''; 230 var fi = first.charAt(0).toUpperCase(); 231 var li = last ? last.charAt(0).toUpperCase() : ''; 232 switch (mode) { 233 case 'first-name': return first; 234 case 'first-last-initial': return last ? first + ' ' + li + '.' : first; 235 case 'first-initial-last': return last ? fi + '. ' + last : fi + '.'; 236 case 'initials-only': return last ? fi + '.' + li + '.' : fi + '.'; 237 default: return fullName; 238 } 239 }; 240 241 var render = function (notif, settings, rules, campaignId) { 242 var pos = settings.position || 'bottom-left'; 243 var radius = (settings.borderRadius != null ? settings.borderRadius : 12) + 'px'; 244 var fontSize = (settings.fontSize != null ? settings.fontSize : 14) + 'px'; 245 var bg = settings.bgColor || '#ffffff'; 246 var fg = settings.textColor || '#1a1a2e'; 247 var accent = settings.accentColor || '#0551c6'; 248 var fontFamily = (settings.fontFamily || '').trim(); 249 if (fontFamily) loadGoogleFont(fontFamily); 250 251 var el = document.createElement('div'); 252 el.className = 'oh-popup'; 253 el.style.position = 'fixed'; 254 el.style.background = bg; 255 el.style.color = fg; 256 el.style.borderRadius = radius; 257 el.style.fontSize = fontSize; 258 if (fontFamily) { 259 el.style.fontFamily = 260 '"' + fontFamily + '", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'; 261 } 262 if (pos === 'bottom-right') { 263 el.style.right = '20px'; 264 el.style.bottom = '20px'; 265 } else { 266 el.style.left = '20px'; 267 el.style.bottom = '20px'; 268 } 269 270 var imgSrc = notif.image_url || settings.imageUrl || ''; 271 var rawName = notif.name || ''; 272 var privacyMode = settings.privacyMode || 'off'; 273 var name = formatPrivacyName(rawName, privacyMode); 274 var hideAvatar = privacyMode === 'anonymous' || privacyMode === 'initials-only' || privacyMode === 'first-initial-last'; 275 var initialsSource = privacyMode === 'anonymous' ? '?' : name; 276 var initials = initialsSource.split(/\s+/).filter(Boolean).map(function (p) { return p[0]; }).join('').toUpperCase().slice(0, 2); 277 var avatarStyle = 'background:' + accent + '1f;color:' + accent + ';'; 278 var imgHtml = imgSrc && !hideAvatar 279 ? '<img class="oh-img" src="' + imgSrc.replace(/"/g, '"') + '" alt="">' 280 : '<div class="oh-img" style="' + avatarStyle + '">
280' + escapeHtml(initials) + '</div>'; 281 282 var timeHtml = !rules.hideTimestamp 283 ? '<div class="oh-meta">' + timeAgo(notif.created_at) + '</div>' 284 : ''; 285 286 var actionText = settings.actionText || notif.action || ''; 287 288 var starsHtml = ''; 289 if (settings.showStarRating && notif.rating) { 290 var r = Math.max(0, Math.min(5, Number(notif.rating) || 0)); 291 var stars = ''; 292 for (var s = 1; s <= 5; s++) { 293 var fill = s <= r ? accent : 'none'; 294 stars += '<svg viewBox="0 0 24 24" fill="' + fill + '" stroke="' + accent + 295 '" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">' + 296 '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>'; 297 } 298 starsHtml = '<div class="oh-stars">' + stars + '</div>'; 299 } 300 301 var reviewHtml = notif.review_text 302 ? '<div class="oh-review">"' + escapeHtml(notif.review_text) + '"</div>' 303 : ''; 304 305 var badgeHtml = ''; 306 if (settings.showBadge) { 307 var checkSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="' + accent + 308 '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + 309 '<circle cx="12" cy="12" r="10"></circle>' + 310 '<polyline points="9 12 12 15 16 10"></polyline></svg>'; 311 var href = settings.badgeUrl || 'https://offerhype.com'; 312 badgeHtml = '<a class="oh-badge" href="' + escapeHtml(href) + 313 '" target="_blank" rel="noopener" style="color:' + accent + '">' + 314 checkSvg + '<span>Verified by OfferHype</span></a>'; 315 } 316 317 el.innerHTML = 318 '<div class="oh-row">' + 319 imgHtml + 320 '<div class="oh-body">' + 321 '<div><span class="oh-name">' + escapeHtml(name) + '</span> ' + 322 '<span class="oh-action">' + escapeHtml(actionText) + '</span></div>' + 323 starsHtml + 324 reviewHtml + 325 timeHtml + 326 '</div>' + 327 '<button class="oh-close" aria-label="Close">Ã</button>' + 328 '</div>' + 329 badgeHtml; 330 331 var clickUrl = rules.clickUrl; 332 if (clickUrl) { 333 el.classList.add('oh-clickable'); 334 el.addEventListener('click', function (e) { 335 if (e.target && e.target.classList.contains('oh-close')) return; 336 trackEvent('click', campaignId); 337 window.open(appendAttributionParams(clickUrl), '_blank', 'noopener'); 338 }); 339 } 340 341 el.querySelector('.oh-close').addEventListener('click', function (e) { 342 e.stopPropagation(); 343 hide(el); 344 }); 345 346 document.body.appendChild(el); 347 requestAnimationFrame(function () { el.classList.add('oh-show'); }); 348 trackEvent('view', campaignId); 349 if (notif.id) markSeen(campaignId, notif.id); 350 return el; 351 }; 352 353 var hide = function (el) { 354 if (!el) return; 355 el.classList.remove('oh-show'); 356 if (ACTIVE_POPUP === el) ACTIVE_POPUP = null; 357 setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 400); 358 }; 359 360 var escapeHtml = function (s) { 361 return String(s).replace(/[&<>"']/g, function (c) { 362 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 363 }); 364 }; 365 366 // Match the current page URL against a campaign's display_urls list. 367 // Patterns: empty list = do NOT show anywhere. Supports `*` as a wildcard. 368 // Examples: "https://shop.com/*", "*/checkout", "https://shop.com/pricing" 369 var matchesUrl = function (patterns, currentUrl) { 370 if (!patterns || !patterns.length) return false; 371 var stripped = currentUrl.split('#')[0]; 372 // Also build a `www.`-less variant of the current URL so saved patterns 373 // like "https://youvegotsales.com" match visits to "https://www.youvegotsales.com/". 374 var strippedNoWww = stripped.replace(/^(https?:\/\/)www\./i, '$1'); 375 var candidates = stripped === strippedNoWww ? [stripped] : [stripped, strippedNoWww]; 376 var hasAny = false; 377 for (var i = 0; i < patterns.length; i++) { 378 var p = (patterns[i] || '').trim(); 379 if (!p) continue; 380 hasAny = true; 381 // Also strip a leading `www.` from the pattern itself so saved 382 // "https://www.example.com" still matches a bare "https://example.com" visit. 383 var pNoWww = p.replace(/^(https?:\/\/)www\./i, '$1'); 384 var patternVariants = p === pNoWww ? [p] : [p, pNoWww]; 385 var hasWildcard = p.indexOf('*') !== -1; 386 for (var c = 0; c < candidates.length; c++) { 387 var cur = candidates[c]; 388 for (var v = 0; v < patternVariants.length; v++) { 389 var pat = patternVariants[v]; 390 if (pat === currentUrl || pat === cur) return true; 391 if (hasWildcard) { 392 var regex = new RegExp( 393 '^' + 394 pat.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + 395 '$' 396 ); 397 if (regex.test(cur)) return true; 398 } else { 399 // Plain entry = prefix match, but only at a URL boundary so 400 // "https://offerhype.co" does NOT match "https://offerhype.com". 401 // Allowed continuations after the prefix: end, "/", "?", "#". 402 if (cur === pat) return true; 403 if (cur.indexOf(pat) === 0) { 404 var nextChar = cur.charAt(pat.length); 405 if (nextChar === '' || nextChar === '/' || nextChar === '?' || nextChar === '#') { 406 return true; 407 } 408 } 409 } 410 } 411 } 412 } 413 return false;
414 }; 415 416 var buildQueueForCampaign = function (campaign) { 417 var settings = campaign.settings || {}; 418 var rules = campaign.rules || {}; 419 var notifs = (campaign.notifications || []).slice(); 420 if (!notifs.length) return []; 421 if (rules.disableOnMobile && window.matchMedia('(max-width: 768px)').matches) return []; 422 if (rules.doNotRepeat) { 423 var seen = getSeen(campaign.id); 424 notifs = notifs.filter(function (n) { return seen.indexOf(n.id) === -1; }); 425 if (!notifs.length) return []; 426 } 427 428 var displayMs = (settings.displayDuration || 5) * 1000; 429 var delayMs = (settings.delayBetween || 8) * 1000; 430 var initialMs = (settings.initialDelay || 3) * 1000; 431 var loop = settings.loop !== false; 432 433 return [{ 434 campaign: campaign, 435 settings: settings, 436 rules: rules, 437 notifications: notifs, 438 displayMs: displayMs, 439 delayMs: delayMs, 440 initialMs: initialMs, 441 loop: loop, 442 idx: 0, 443 started: false, 444 }]; 445 }; 446 447 var runQueue = function () { 448 if (RUNNING) return; 449 RUNNING = true; 450 451 var next = function () { 452 if (!QUEUE.length) { 453 RUNNING = false; 454 return; 455 } 456 457 var item = QUEUE.shift(); 458 var waitMs = item.started ? item.delayMs : item.initialMs; 459 item.started = true; 460 461 setTimeout(function () { 462 if (ACTIVE_POPUP && ACTIVE_POPUP.parentNode) { 463 hide(ACTIVE_POPUP); 464 } 465 466 if (item.idx >= item.notifications.length) { 467 if (!item.loop) { 468 next(); 469 return; 470 } 471 item.idx = 0; 472 } 473 474 var notif = item.notifications[item.idx]; 475 item.idx += 1; 476 ACTIVE_POPUP = render(notif, item.settings, item.rules, item.campaign.id); 477 478 setTimeout(function () { 479 if (ACTIVE_POPUP) hide(ACTIVE_POPUP); 480 if (item.loop || item.idx < item.notifications.length) { 481 QUEUE.push(item); 482 } 483 next(); 484 }, item.displayMs); 485 }, waitMs); 486 }; 487 488 next(); 489 }; 490 491 var start = function (data) { 492 if (!data || !data.campaigns || !data.campaigns.length) return; 493 var currentUrl = window.location.href; 494 var matched = data.campaigns.filter(function (c) { 495 return matchesUrl(c.display_urls, currentUrl); 496 }); 497 if (!matched.length) return; 498 injectStyles(); 499 QUEUE = []; 500 for (var i = 0; i < matched.length; i++) { 501 var items = buildQueueForCampaign(matched[i]); 502 for (var j = 0; j < items.length; j++) QUEUE.push(items[j]); 503 } 504 if (QUEUE.length) runQueue(); 505 }; 506 507 if (document.readyState === 'loading') { 508 document.addEventListener('DOMContentLoaded', function () { fetchData().then(start).catch(function (e) { console.warn('[OfferHype]', e); }); }); 509 } else { 510 fetchData().then(start).catch(function (e) { console.warn('[OfferHype]', e); }); 511 } 512})();
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.