PageSourceSearch

https://app.offerhype.co/widget.js

js offerhype.co collected 2026-10-08 03:33:05 UTC 19,739 bytes, 512 lines download raw bytes

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, '&quot;') + '" 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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.