PageSourceSearch

https://www.velvetstyle.it/wp-content/plugins/velvet-push/assets/prompt.js?ver=0.4.6

js velvetstyle.it collected 2026-10-02 13:29:21 UTC 11,106 bytes, 246 lines download raw bytes

1/* Velvet Push — soft-prompt UX (PUSH_NOTIFICATIONS.md §3).
2 * Never fires the native permission prompt on load: a denied native prompt is
3 * permanently lost in Chrome. A branded 2-step in-page prompt gates it, shown only on
4 * engagement, with hard frequency caps. Config comes from VELVET_PUSH_CFG (localized). */
5(function () {
6  'use strict';
7  var CFG = window.VELVET_PUSH_CFG;
8  if (!CFG || !CFG.vapidKey) { return; }
9
10  // iOS Safari (not an installed PWA) has no PushManager — web push is impossible in a tab.
11  // Nudge the visitor to add the site to the Home Screen instead, then stop.
12  var pushSupported = ('serviceWorker' in navigator) && ('PushManager' in window);
13  if (!pushSupported) {
14    var iOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
15    var standalone = (window.navigator.standalone === true) ||
16      (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
17    if (iOS && !standalone && CFG.ios) { velvetPushIosHint(); }
18    return;
19  }
20  if (typeof Notification === 'undefined' || Notification.permission === 'denied') {
21    return; // already decided — never nag
22  }
23
24  var LS = window.localStorage;
25  var K_DISMISS = 'velvet_push_dismiss_until';
26  var K_COUNT = 'velvet_push_prompt_count';
27  var K_SESSION = 'velvet_push_sessions';
28  var MAX_LIFETIME = 3; // §3.1 max soft-prompt exposures
29  var SUPPRESS_DAYS = 14;
30
31  function now() { return Date.now(); }
32  function getNum(k) { var v = parseInt(LS.getItem(k) || '0', 10); return isNaN(v) ? 0 : v; }
33
34  // Count a session (once per tab-open) to support the "2nd session" trigger.
35  if (!sessionStorage.getItem('velvet_push_session_counted')) {
36    sessionStorage.setItem('velvet_push_session_counted', '1');
37    LS.setItem(K_SESSION, String(getNum(K_SESSION) + 1));
38  }
39
40  function suppressed() {
41    var until = parseInt(LS.getItem(K_DISMISS) || '0', 10);
42    if (until && now() < until) { return true; }
43    if (getNum(K_COUNT) >= MAX_LIFETIME) { return true; }
44    return false;
45  }
46  if (suppressed()) { return; }
47
48  function detectBrowser() {
49    var ua = navigator.userAgent;
50    if (/Edg\//.test(ua)) { return 'edge'; }
51    if (/Firefox\//.test(ua)) { return 'firefox'; }
52    if (/Chrome\//.test(ua)) { return 'chrome'; }
53    if (/Safari\//.test(ua)) { return 'safari'; }
54    return 'other';
55  }
56  function detectOS() {
57    var ua = navigator.userAgent;
58    if (/Android/.test(ua)) { return 'android'; }
59    if (/iPhone|iPad|iPod/.test(ua)) { return 'ios'; }
60    if (/Windows/.test(ua)) { return 'windows'; }
61    if (/Mac OS X/.test(ua)) { return 'macos'; }
62    if (/Linux/.test(ua)) { return 'linux'; }
63    return 'other';
64  }
65  function deviceClass() {
66    return /Mobi|Android|iPhone|iPod/.test(navigator.userAgent) ? 'mobile'
67      : (/iPad|Tablet/.test(navigator.userAgent) ? 'tablet' : 'desktop');
68  }
69
70  function urlBase64ToUint8Array(base64) {
71    var padding = '='.repeat((4 - (base64.length % 4)) % 4);
72    var b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
73    var raw = window.atob(b64);
74    var out = new Uint8Array(raw.length);
75    for (var i = 0; i < raw.length; i++) { out[i] = raw.charCodeAt(i); }
76    return out;
77  }
78
79  var shown = false;
80
81  function render() {
82    if (shown) { return; }
83    shown = true;
84    var isMobile = deviceClass() !== 'desktop';
85    var box = document.createElement('div');
86    box.setAttribute('role', 'dialog');
87    box.style.cssText = [
88      'position:fixed', 'z-index:2147483000',
89      isMobile ? 'left:12px' : 'right:20px', isMobile ? 'right:12px' : 'right:20px',
90      'bottom:' + (isMobile ? '12px' : '20px'), isMobile ? '' : 'max-width:360px',
91      'background:#fff', 'color:#111', 'border-radius:14px',
92      'box-shadow:0 10px 40px rgba(0,0,0,.18)', 'padding:16px 18px',
93      'font:14px/1.45 -apple-system,Segoe UI,Roboto,sans-serif'
94    ].join(';');
95    var h = document.createElement('div');
96    h.textContent = CFG.copy.headline;
97    h.style.cssText = 'font-weight:600;margin-bottom:12px';
98    var row = document.createElement('div');
99    row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end';
100    var no = document.createElement('button');
101    no.textContent = CFG.copy.dismiss;
102    no.style.cssText = 'background:transparent;border:0;color:#666;padding:8px 12px;cursor:pointer;font:inherit';
103    var yes = document.createElement('button');
104    yes.textContent = CFG.copy.cta;
105    yes.style.cssText = 'background:#111;border:0;color:#fff;padding:8px 16px;border-radius:8px;
105cursor:pointer;font:inherit;font-weight:600';
106    row.appendChild(no); row.appendChild(yes);
107    box.appendChild(h); box.appendChild(row);
108    document.body.appendChild(box);
109    LS.setItem(K_COUNT, String(getNum(K_COUNT) + 1));
110
111    function close() { if (box.parentNode) { box.parentNode.removeChild(box); } }
112    no.addEventListener('click', function () {
113      LS.setItem(K_DISMISS, String(now() + SUPPRESS_DAYS * 864e5));
114      close();
115    });
116    yes.addEventListener('click', function () {
117      close();
118      requestAndSubscribe();
119    });
120  }
121
122  // Small visible message — user feedback + a diagnostic channel on iOS (no console there).
123  function toast(msg) {
124    var t = document.createElement('div');
125    t.textContent = msg;
126    t.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483001;' +
127      'background:#111;color:#fff;border-radius:10px;padding:12px 14px;text-align:center;' +
128      'font:13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;box-shadow:0 8px 30px rgba(0,0,0,.25)';
129    document.body.appendChild(t);
130    setTimeout(function () { if (t.parentNode) { t.parentNode.removeChild(t); } }, 8000);
131  }
132
133  function requestAndSubscribe() {
134    Notification.requestPermission().then(function (perm) {
135      if (perm !== 'granted') {
136        LS.setItem(K_DISMISS, String(now() + SUPPRESS_DAYS * 864e5));
137        return;
138      }
139      // Register, then WAIT for the SW to be active (serviceWorker.ready) before
140      // subscribing — iOS rejects pushManager.subscribe() on a not-yet-active worker.
141      // Fallback: hosts with broken/blocked rewrites (the pretty /velvet-push-sw.js
142      // 404s) retry via the query-var route — same scope '/', served by index.php.
143      navigator.serviceWorker.register(CFG.swUrl, { scope: '/' })
144        .catch(function () {
145          var qv = (CFG.swUrl.indexOf('://') > -1 ? CFG.swUrl.split('/').slice(0, 3).join('/') : '') + '/index.php?velvet_push=sw';
146          return navigator.serviceWorker.register(qv, { scope: '/' });
147        })
148        .then(function () { return navigator.serviceWorker.ready; })
149        .then(function (reg) {
150          return reg.pushManager.subscribe({
151            userVisibleOnly: true,
152            applicationServerKey: urlBase64ToUint8Array(CFG.vapidKey)
153          });
154        }).then(function (sub) {
155          var json = sub.toJSON();
156          return fetch(CFG.subscribeUrl, {
157            method: 'POST',
158            headers: { 'Content-Type': 'application/json' },
159            body: JSON.stringify({
160              endpoint: sub.endpoint,
161              keys: json.keys,
162              ua_browser: detectBrowser(),
163              ua_os: detectOS(),
164              device_class: deviceClass(),
165              lang: (navigator.language || 'it').slice(0, 2),
166              page_url: location.href,
167              prompt_variant: 'soft_v1'
168            })
169          });
170        }).then(function (res) {
171          if (res && res.ok) { toast('Notifiche attivate ✓'); }
172          else if (res) { toast('Errore invio iscrizione (HTTP ' + res.status + ')'); }
173        }).catch(function (e) {
174          toast('Attivazione non riuscita: ' + (e ? (e.name || '') + ' — ' + (e.message || '') : 'errore'));
175        });
176    }).catch(function (e) {
177      toast('Permesso non richiesto: ' + (e && e.message ? e.message : 'errore'));
178    });
179  }
180
181  /* Engagement triggers (§3.2) — whichever comes first. */
182  var t = CFG.triggers || {};
183  var fired = false;
184  function fire() { if (!fired && !suppressed()) { fired = true; render(); } }
185
186  // 2nd session (returning visitor — highest grant rate).
187  if (getNum(K_SESSION) >= (t.session_n || 2)) {
188    setTimeout(fire, 3000);
189  }
190  // Dwell time.
191  if (t.dwell_s) { setTimeout(fire, t.dwell_s * 1000); }
192  // Scroll depth.
193  var pct = t.scroll_pct || 60;
194  window.addEventListener('scroll', function onScroll() {
195    var st = window.scrollY || document.documentElement.scrollTop;
196    var h = document.documentElement.scrollHeight - window.innerHeight;
197    if (h > 0 && (st / h) * 100 >= pct) {
198      window.removeEventListener('scroll', onScroll);
199      fire();
200    }
201  }, { passive: true });
202
203  /* iOS Safari tab: an "Add to Home Screen" hint (separate 2-max cap, 30-day snooze),
204   * shown on the same engagement triggers. Web push starts only once installed. */
205  function velvetPushIosHint() {
206    var LS2 = window.localStorage, ios = CFG.ios, tg = CFG.triggers || {};
207    var K_D = 'velvet_push_ios_dismiss_until', K_C = 'velvet_push_ios_count';
208    function n(k) { var v = parseInt(LS2.getItem(k) || '0', 10); return isNaN(v) ? 0 : v; }
209    if (n(K_C) >= 2) { return; }
210    var until = parseInt(LS2.getItem(K_D) || '0', 10);
211    if (until && Date.now() < until) { return; }
212    var shown = false;
213    function fire() {
214      if (shown) { return; }
215      shown = true;
216      var box = document.createElement('div');
217      box.setAttribute('role', 'dialog');
218      box.style.cssText = 'position:fixed;z-index:2147483000;left:12px;right:12px;bottom:12px;' +
219        'background:#fff;color:#111;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.18);' +
220        'padding:16px 18px;font:14px/1.45 -apple-system,Segoe UI,Roboto,sans-serif';
221      var h = document.createElement('div'); h.textContent = ios.headline;
222      h.style.cssText = 'font-weight:600;margin-bottom:6px';
223      var b = document.createElement('div'); b.textContent = ios.body;
224      b.style.cssText = 'margin-bottom:12px;color:#333';
225      var row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:flex-end';
226      var ok = document.createElement('button'); ok.textContent = ios.dismiss;
227      ok.style.cssText = 'background:#111;border:0;color:#fff;padding:8px 16px;border-radius:8px;' +
228        'cursor:pointer;font:inherit;font-weight:600';
229      row.appendChild(ok);
230      box.appendChild(h); box.appendChild(b); box.appendChild(row);
231      document.body.appendChild(box);
232      LS2.setItem(K_C, String(n(K_C) + 1));
233      ok.addEventListener('click', function () {
234        LS2.setItem(K_D, String(Date.now() + 30 * 864e5));
235        if (box.parentNode) { box.parentNode.removeChild(box); }
236      });
237    }
238    if (tg.dwell_s) { setTimeout(fire, tg.dwell_s * 1000); }
239    var p = tg.scroll_pct || 60;
240    window.addEventListener('scroll', function onScroll() {
241      var st = window.scrollY || document.documentElement.scrollTop;
242      var hh = document.documentElement.scrollHeight - window.innerHeight;
243      if (hh > 0 && (st / hh) * 100 >= p) { window.removeEventListener('scroll', onScroll); fire(); }
244    }, { passive: true });
245  }
246})();

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.