@hifilabs/pixel 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -8,6 +8,7 @@ var BalancePixel = (() => {
8
8
 
9
9
  // src/browser.ts
10
10
  (function() {
11
+ const PIXEL_VERSION = "0.7.0";
11
12
  function parseUserAgent(ua) {
12
13
  let device_type = "desktop";
13
14
  if (/ipad|tablet|android(?!.*mobile)/i.test(ua))
@@ -85,7 +86,7 @@ var BalancePixel = (() => {
85
86
  }
86
87
  let trackingSource = "pixel";
87
88
  if (!artistId) {
88
- console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
89
+ logError(" Error: data-artist-id attribute is required");
89
90
  return;
90
91
  }
91
92
  const SESSION_KEY = "session_id";
@@ -94,9 +95,11 @@ var BalancePixel = (() => {
94
95
  const FAN_ID_KEY = "fan_id_hash";
95
96
  const VISITOR_ID_KEY = "visitor_id";
96
97
  const CONSENT_STORAGE_KEY = "balance_consent";
98
+ const CONSENT_TTL = 365 * 24 * 60 * 60 * 1e3;
99
+ const CONSENT_REFRESH_THRESHOLD = 30 * 24 * 60 * 60 * 1e3;
97
100
  const SESSION_DURATION = 60 * 60 * 1e3;
98
101
  const STORAGE_PREFIX = "balance_";
99
- const API_ENDPOINT = customEndpoint ? customEndpoint : useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
102
+ const API_ENDPOINT = customEndpoint ? customEndpoint : useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/ingestEvents` : `https://us-central1-artist-os-distro.cloudfunctions.net/ingestEvents`;
100
103
  let sessionId = null;
101
104
  let visitorId = null;
102
105
  let fanIdHash = null;
@@ -113,9 +116,14 @@ var BalancePixel = (() => {
113
116
  const IDLE_TIMEOUT = 2 * 60 * 1e3;
114
117
  let lastActivityTime = Date.now();
115
118
  let isIdle = false;
119
+ const SILENT_MODE = !debug;
116
120
  const log = (...args) => {
117
121
  if (debug)
118
- console.log("[BALANCE Pixel]", ...args);
122
+ console.log("[BLN]", ...args);
123
+ };
124
+ const logError = (...args) => {
125
+ if (!SILENT_MODE)
126
+ console.error("[BLN]", ...args);
119
127
  };
120
128
  const CONSENT_STYLES = {
121
129
  base: `
@@ -227,6 +235,9 @@ var BalancePixel = (() => {
227
235
  }
228
236
  hasStoredConsent() {
229
237
  try {
238
+ if (window._balanceConsentNeedsRefresh) {
239
+ return false;
240
+ }
230
241
  return localStorage.getItem(CONSENT_STORAGE_KEY) !== null;
231
242
  } catch {
232
243
  return false;
@@ -269,6 +280,14 @@ var BalancePixel = (() => {
269
280
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
270
281
  });
271
282
  }
283
+ if (accepted && !window._balanceInitialPageviewFired) {
284
+ window._balanceInitialPageviewFired = true;
285
+ if (window.balance?.page) {
286
+ window.balance.page();
287
+ }
288
+ startHeartbeat();
289
+ log("Initial pageview fired after consent granted");
290
+ }
272
291
  this.remove();
273
292
  }
274
293
  remove() {
@@ -338,7 +357,7 @@ var BalancePixel = (() => {
338
357
  currentStorageTier = "local";
339
358
  log(`Storage tier upgraded, migrated ${keysToMigrate.length} items`);
340
359
  } catch (error) {
341
- console.error("[BALANCE Pixel] Storage migration failed:", error);
360
+ logError(" Storage migration failed:", error);
342
361
  }
343
362
  }
344
363
  function generateUUID() {
@@ -438,6 +457,19 @@ var BalancePixel = (() => {
438
457
  const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
439
458
  if (stored) {
440
459
  const parsed = JSON.parse(stored);
460
+ if (parsed.expiresAt && Date.now() > parsed.expiresAt) {
461
+ log("Consent expired - clearing stored consent");
462
+ localStorage.removeItem(CONSENT_STORAGE_KEY);
463
+ consent = null;
464
+ return;
465
+ }
466
+ if (parsed.expiresAt) {
467
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
468
+ if (timeUntilExpiry > 0 && timeUntilExpiry < CONSENT_REFRESH_THRESHOLD) {
469
+ log("Consent nearing expiration - will prompt for refresh");
470
+ window._balanceConsentNeedsRefresh = true;
471
+ }
472
+ }
441
473
  consent = parsed.preferences || null;
442
474
  log("Loaded consent:", consent);
443
475
  }
@@ -451,12 +483,15 @@ var BalancePixel = (() => {
451
483
  const storage = {
452
484
  preferences,
453
485
  method: "explicit",
454
- version: 1
486
+ version: 1,
487
+ expiresAt: Date.now() + CONSENT_TTL
488
+ // 12-month expiration
455
489
  };
456
490
  localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(storage));
457
- log("Consent saved:", preferences);
491
+ log("Consent saved with TTL:", preferences);
492
+ window._balanceConsentNeedsRefresh = false;
458
493
  } catch (e) {
459
- console.error("[BALANCE Pixel] Could not save consent:", e);
494
+ logError(" Could not save consent:", e);
460
495
  }
461
496
  if (preferences.analytics === true) {
462
497
  upgradeStorageTier();
@@ -547,7 +582,7 @@ var BalancePixel = (() => {
547
582
  }
548
583
  log("Events sent successfully");
549
584
  } catch (error) {
550
- console.error("[BALANCE Pixel] Failed to send events:", error);
585
+ logError(" Failed to send events:", error);
551
586
  if (eventQueue.length < 50) {
552
587
  eventQueue.push(...events);
553
588
  }
@@ -683,9 +718,10 @@ var BalancePixel = (() => {
683
718
  event_name: "identify",
684
719
  fan_id_hash: fanIdHash,
685
720
  metadata: {
686
- email,
687
- // Pass email for display_name fallback (extracted prefix only stored)
721
+ // GDPR FIX: Never send plaintext email - only hashed identifier
688
722
  email_sha256: fanIdHash,
723
+ email_display: maskedEmail,
724
+ // Safe for UI display (e.g., "j***@example.com")
689
725
  traits,
690
726
  consent_preferences: consent || void 0,
691
727
  storage_tier: currentStorageTier
@@ -693,7 +729,7 @@ var BalancePixel = (() => {
693
729
  });
694
730
  enqueueEvent(event);
695
731
  } catch (error) {
696
- console.error("[BALANCE Pixel] Failed to identify:", error);
732
+ logError(" Failed to identify:", error);
697
733
  }
698
734
  }
699
735
  function purchase(revenue, currency = "USD", properties = {}) {
@@ -725,14 +761,7 @@ var BalancePixel = (() => {
725
761
  if (consentUIEnabled) {
726
762
  log("Consent UI enabled, waiting for explicit user consent (Tier 0 mode)");
727
763
  } else {
728
- consent = {
729
- analytics: true,
730
- marketing: true,
731
- personalization: true,
732
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
733
- };
734
- log("Default consent enabled (all tracking):", consent);
735
- upgradeStorageTier();
764
+ log("No consent - operating in privacy-first mode (session storage only, limited tracking)");
736
765
  }
737
766
  }
738
767
  if (consent?.analytics && !visitorId) {
@@ -753,9 +782,19 @@ var BalancePixel = (() => {
753
782
  useEmulator,
754
783
  endpoint: API_ENDPOINT
755
784
  });
756
- window._balanceInitialPageviewFired = true;
757
- trackPageView();
758
- startHeartbeat();
785
+ if (consent?.analytics === true) {
786
+ window._balanceInitialPageviewFired = true;
787
+ trackPageView();
788
+ startHeartbeat();
789
+ log("Full tracking enabled (user consented)");
790
+ } else if (consentUIEnabled) {
791
+ log("Tracking dormant - waiting for explicit consent via ConsentManager");
792
+ } else {
793
+ window._balanceInitialPageviewFired = true;
794
+ trackPageView();
795
+ startHeartbeat();
796
+ log("Privacy-first tracking enabled (session-scoped, no persistent IDs)");
797
+ }
759
798
  ["mousemove", "keydown", "scroll", "touchstart"].forEach((eventType) => {
760
799
  document.addEventListener(eventType, resetIdleTimer, { passive: true });
761
800
  });
@@ -772,6 +811,12 @@ var BalancePixel = (() => {
772
811
  }
773
812
  });
774
813
  }
814
+ if (window.balance?._version && window.balance._version !== PIXEL_VERSION) {
815
+ console.warn(
816
+ `[BLN] Version conflict: ${window.balance._version} already loaded, skipping ${PIXEL_VERSION}`
817
+ );
818
+ return;
819
+ }
775
820
  const existingQueue = window.balance?.q || [];
776
821
  function processQueue() {
777
822
  if (existingQueue.length > 0) {
@@ -811,7 +856,9 @@ var BalancePixel = (() => {
811
856
  getAttribution: () => attribution,
812
857
  setConsent,
813
858
  getConsent,
814
- hasConsent
859
+ hasConsent,
860
+ _version: PIXEL_VERSION
861
+ // Expose version for collision detection
815
862
  };
816
863
  function initConsentManager() {
817
864
  if (consentUIEnabled && !consent) {
@@ -822,16 +869,17 @@ var BalancePixel = (() => {
822
869
  });
823
870
  }
824
871
  }
872
+ const scheduleIdleWork = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
825
873
  if (document.readyState === "loading") {
826
874
  document.addEventListener("DOMContentLoaded", () => {
827
875
  init();
828
876
  processQueue();
829
- initConsentManager();
877
+ scheduleIdleWork(() => initConsentManager());
830
878
  });
831
879
  } else {
832
880
  init();
833
881
  processQueue();
834
- initConsentManager();
882
+ scheduleIdleWork(() => initConsentManager());
835
883
  }
836
884
  log("Pixel script loaded");
837
885
  })();
@@ -1,4 +1,4 @@
1
- var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,a,{enumerable:!0,configurable:!0,writable:!0,value:g}):f[a]=g;var T=(f,a,g)=>(Be(f,typeof a!="symbol"?a+"":a,g),g);(function(){function f(e){let t="desktop";/ipad|tablet|android(?!.*mobile)/i.test(e)?t="tablet":/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(e)&&(t="mobile");let n="Unknown";/edg/i.test(e)?n="Edge":/opr|opera/i.test(e)?n="Opera":/firefox/i.test(e)?n="Firefox":/chrome/i.test(e)?n="Chrome":/safari/i.test(e)&&(n="Safari");let o="Unknown";return/iphone|ipad/i.test(e)?o="iOS":/android/i.test(e)?o="Android":/windows/i.test(e)?o="Windows":/mac os/i.test(e)?o="macOS":/linux/i.test(e)?o="Linux":/cros/i.test(e)&&(o="ChromeOS"),{device_type:t,browser:n,os:o}}let a=null;function g(){if(!a)try{a=f(navigator.userAgent)}catch{a={device_type:"desktop",browser:"Unknown",os:"Unknown"}}return a}let s=document.currentScript,A=s?.dataset.artistId,P=s?.dataset.projectId,D=P?fe(P):void 0;function fe(e){return!e||e.startsWith("release_")||e.startsWith("merch_")||e.startsWith("link_")||e.startsWith("custom_")?e:`custom_${e}`}let W=s?.dataset.emulator==="true",ge=s?.dataset.debug==="true",O=parseInt(s?.dataset.heartbeatInterval||"30000",10),q=s?.dataset.heartbeat!=="false",V=s?.dataset.source,Y=s?.dataset.endpoint,J=s?.dataset.consentUi==="true",pe=s?.dataset.consentStyle||"brutalist",me=s?.dataset.primaryColor,he=s?.dataset.bannerPosition||"bottom";function be(){if(V)return V;let e=typeof window.dataLayer<"u"&&Array.isArray(window.dataLayer),t=typeof window.gtag=="function";return e&&t?"gtm":"pixel"}let _="pixel";if(!A){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let G="session_id",L="session_timestamp",Q="attribution",X="fan_id_hash",N="visitor_id",U="balance_consent",ye=60*60*1e3,p="balance_",R=Y||(W?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint"),k=null,c=null,l=null,i=null,m={},d=[],M=null,u="session",h=null,ve=0,B=0,b=0,w=!0,we=2*60*1e3,Z=Date.now(),x=!1,r=(...e)=>{ge&&console.log("[BALANCE Pixel]",...e)},H={base:`
1
+ var BalancePixel=(()=>{var ze=Object.defineProperty;var Ve=(l,d,c)=>d in l?ze(l,d,{enumerable:!0,configurable:!0,writable:!0,value:c}):l[d]=c;var A=(l,d,c)=>(Ve(l,typeof d!="symbol"?d+"":d,c),c);(function(){let l="0.7.0";function d(e){let t="desktop";/ipad|tablet|android(?!.*mobile)/i.test(e)?t="tablet":/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(e)&&(t="mobile");let n="Unknown";/edg/i.test(e)?n="Edge":/opr|opera/i.test(e)?n="Opera":/firefox/i.test(e)?n="Firefox":/chrome/i.test(e)?n="Chrome":/safari/i.test(e)&&(n="Safari");let o="Unknown";return/iphone|ipad/i.test(e)?o="iOS":/android/i.test(e)?o="Android":/windows/i.test(e)?o="Windows":/mac os/i.test(e)?o="macOS":/linux/i.test(e)?o="Linux":/cros/i.test(e)&&(o="ChromeOS"),{device_type:t,browser:n,os:o}}let c=null;function be(){if(!c)try{c=d(navigator.userAgent)}catch{c={device_type:"desktop",browser:"Unknown",os:"Unknown"}}return c}let a=document.currentScript,O=a?.dataset.artistId,N=a?.dataset.projectId,L=N?he(N):void 0;function he(e){return!e||e.startsWith("release_")||e.startsWith("merch_")||e.startsWith("link_")||e.startsWith("custom_")?e:`custom_${e}`}let K=a?.dataset.emulator==="true",Y=a?.dataset.debug==="true",R=parseInt(a?.dataset.heartbeatInterval||"30000",10),J=a?.dataset.heartbeat!=="false",G=a?.dataset.source,Q=a?.dataset.endpoint,U=a?.dataset.consentUi==="true",ye=a?.dataset.consentStyle||"brutalist",ve=a?.dataset.primaryColor,we=a?.dataset.bannerPosition||"bottom";function Ie(){if(G)return G;let e=typeof window.dataLayer<"u"&&Array.isArray(window.dataLayer),t=typeof window.gtag=="function";return e&&t?"gtm":"pixel"}let k="pixel";if(!O){x(" Error: data-artist-id attribute is required");return}let X="session_id",M="session_timestamp",Z="attribution",ee="fan_id_hash",F="visitor_id",C="balance_consent",xe=365*24*60*60*1e3,Se=30*24*60*60*1e3,_e=60*60*1e3,p="balance_",H=Q||(K?"http://localhost:5001/artist-os-distro/us-central1/ingestEvents":"https://us-central1-artist-os-distro.cloudfunctions.net/ingestEvents"),E=null,s=null,u=null,i=null,m={},f=[],j=null,g="session",b=null,ke=0,B=0,h=0,w=!0,Ce=2*60*1e3,te=Date.now(),I=!1,Ee=!Y,r=(...e)=>{Y&&console.log("[BLN]",...e)},x=(...e)=>{Ee||console.error("[BLN]",...e)},$={base:`
2
2
  :host {
3
3
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4
4
  position: fixed;
@@ -83,12 +83,12 @@ var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,
83
83
  }
84
84
  .btn.accept { background: #fff; color: #000; border-color: #fff; }
85
85
  .btn:hover { opacity: 0.8; }
86
- `};class xe{constructor(t){T(this,"container",null);T(this,"shadow",null);T(this,"config");if(this.config=t,this.hasStoredConsent()){r("ConsentManager: Consent already exists, not showing banner");return}this.container=document.createElement("div"),this.container.id="balance-consent-manager",this.shadow=this.container.attachShadow({mode:"closed"}),this.render(),document.body.appendChild(this.container),r("ConsentManager: Banner rendered")}hasStoredConsent(){try{return localStorage.getItem(U)!==null}catch{return!1}}render(){if(!this.shadow)return;let t=this.config.style||"brutalist",n=H.base,o=H[t]||H.brutalist,S=this.config.position==="top"?"top: 0;":"bottom: 0;",ue=this.config.primaryColor?`.btn.accept { background: ${this.config.primaryColor} !important; border-color: ${this.config.primaryColor} !important; color: #fff !important; }`:"";this.shadow.innerHTML=`
86
+ `};class Te{constructor(t){A(this,"container",null);A(this,"shadow",null);A(this,"config");if(this.config=t,this.hasStoredConsent()){r("ConsentManager: Consent already exists, not showing banner");return}this.container=document.createElement("div"),this.container.id="balance-consent-manager",this.shadow=this.container.attachShadow({mode:"closed"}),this.render(),document.body.appendChild(this.container),r("ConsentManager: Banner rendered")}hasStoredConsent(){try{return window._balanceConsentNeedsRefresh?!1:localStorage.getItem(C)!==null}catch{return!1}}render(){if(!this.shadow)return;let t=this.config.style||"brutalist",n=$.base,o=$[t]||$.brutalist,_=this.config.position==="top"?"top: 0;":"bottom: 0;",me=this.config.primaryColor?`.btn.accept { background: ${this.config.primaryColor} !important; border-color: ${this.config.primaryColor} !important; color: #fff !important; }`:"";this.shadow.innerHTML=`
87
87
  <style>
88
88
  ${n}
89
- :host { ${S} }
89
+ :host { ${_} }
90
90
  ${o}
91
- ${ue}
91
+ ${me}
92
92
  </style>
93
93
  <div class="banner" role="dialog" aria-label="Cookie consent" aria-modal="false">
94
94
  <p class="text">
@@ -99,4 +99,4 @@ var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,
99
99
  <button id="accept" class="btn accept">Accept</button>
100
100
  </div>
101
101
  </div>
102
- `,this.shadow.getElementById("accept")?.addEventListener("click",()=>this.handleConsent(!0)),this.shadow.getElementById("decline")?.addEventListener("click",()=>this.handleConsent(!1))}handleConsent(t){window.balance?.setConsent&&window.balance.setConsent({analytics:t,marketing:t,personalization:t,timestamp:new Date().toISOString()}),this.remove()}remove(){this.container&&(this.container.remove(),this.container=null,this.shadow=null,r("ConsentManager: Banner removed"))}}function ee(){try{return u==="local"?localStorage:sessionStorage}catch{return null}}function C(e){let t=ee();if(!t)return null;try{let n=p+e,o=t.getItem(n);if(!o&&u==="session")try{o=localStorage.getItem(n)}catch{}return o}catch{return null}}function I(e,t){let n=ee();if(n)try{n.setItem(p+e,t)}catch{}}function j(){if(u!=="local"){r("Upgrading storage tier: session -> local");try{let e=[];for(let t=0;t<sessionStorage.length;t++){let n=sessionStorage.key(t);n?.startsWith(p)&&e.push(n)}for(let t of e){let n=sessionStorage.getItem(t);n&&localStorage.setItem(t,n)}for(let t of e)sessionStorage.removeItem(t);u="local",r(`Storage tier upgraded, migrated ${e.length} items`)}catch(e){console.error("[BALANCE Pixel] Storage migration failed:",e)}}}function F(){return crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function Ie(){try{let e=C(G),t=C(L);if(e&&t&&Date.now()-parseInt(t,10)<ye)return I(L,Date.now().toString()),e;let n=F();return I(G,n),I(L,Date.now().toString()),n}catch{return F()}}function Se(){let e=new URLSearchParams(window.location.search),t={};return["source","medium","campaign","content","term"].forEach(n=>{let o=e.get(`utm_${n}`);o&&(t[`utm_${n}`]=o)}),t}function _e(){try{let e=C(Q);if(e){m=JSON.parse(e),r("Loaded attribution:",m);return}let t=Se();Object.keys(t).length>0&&(m=t,I(Q,JSON.stringify(t)),r("Captured attribution:",m))}catch{}}function ke(){try{l=C(X)}catch{}}function te(){if(!i?.analytics)return null;try{let e=localStorage.getItem(p+N);if(e)return e;let t=F();return localStorage.setItem(p+N,t),r("Created persistent visitor ID:",t.substring(0,8)+"..."),t}catch{return null}}function Ce(){if(!i?.analytics){c=null;return}try{c=localStorage.getItem(p+N),c&&r("Loaded visitor ID:",c.substring(0,8)+"...")}catch{}}function Ee(){try{let e=localStorage.getItem(U);e&&(i=JSON.parse(e).preferences||null,r("Loaded consent:",i))}catch{}}function ne(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1};localStorage.setItem(U,JSON.stringify(o)),r("Consent saved:",e)}catch(o){console.error("[BALANCE Pixel] Could not save consent:",o)}e.analytics===!0&&(j(),c||(c=te()));let n=y({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});v(n);try{window.dispatchEvent(new CustomEvent("balance:consent:updated",{detail:e})),r("DOM event balance:consent:updated dispatched")}catch{}}function Te(){return i}function Ae(e){return i?.[e]===!0}async function Pe(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),S=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(S)).map(Re=>Re.toString(16).padStart(2,"0")).join("")}function y(e){let t=g(),n={artist_id:A,fan_session_id:k,visitor_id:c||void 0,fan_id_hash:l||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,device_type:t.device_type,browser:t.browser,os:t.os,tracking_source:_,...e,...m};return D&&!e.projectId&&(n.projectId=D),n}function v(e){d.push(e),r("Event queued:",e.event_name,"(queue:",d.length,")"),d.length>=10&&E()}async function E(){if(d.length===0)return;let e=[...d];d=[],r("Flushing",e.length,"events to",R);try{let t=await fetch(R,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);r("Events sent successfully")}catch(t){console.error("[BALANCE Pixel] Failed to send events:",t),d.length<50&&d.push(...e)}}function De(){M&&clearInterval(M),M=window.setInterval(()=>{d.length>0&&E()},5e3)}function z(){b||(ve=Date.now()),b=Date.now(),w=!0,r("Active time tracking started/resumed")}function re(){b&&w&&(B+=Date.now()-b),w=!1,r("Active time tracking paused, accumulated:",B,"ms")}function Oe(){let e=B;return w&&b&&(e+=Date.now()-b),e}function Le(){Z=Date.now(),x&&(x=!1,z(),r("User returned from idle"))}function oe(){if(document.visibilityState==="hidden"){r("Skipping heartbeat - tab hidden");return}if(Date.now()-Z>we){x||(x=!0,re(),r("User idle - pausing heartbeat"));return}let e=Oe(),t=Math.round(e/1e3),n=y({event_name:"engagement_heartbeat",metadata:{time_on_page_seconds:t,time_on_page_ms:e,heartbeat_interval_ms:O,is_active:w&&!x}});v(n),r("Heartbeat sent:",t,"seconds active")}function Ne(){if(!q){r('Heartbeat disabled via data-heartbeat="false"');return}h&&clearInterval(h),z(),h=window.setInterval(()=>{oe()},O),r("Heartbeat started with interval:",O,"ms")}function Ue(){h&&(clearInterval(h),h=null),q&&(oe(),r("Heartbeat stopped, final time sent"))}function $(e={}){let t=y({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});v(t)}function ie(e,t={}){let n=y({event_name:"custom",metadata:{event_type:e,...t}});v(n)}async function ae(e,t={}){try{if(i&&i.analytics===!1){r("Identify skipped - user declined analytics consent");return}l=await Pe(e),i?.analytics===!0&&j(),I(X,l);let n=e.split("@"),o=n[0].charAt(0)+"***@"+(n[1]||"");r("Fan identified:",{name:t.name||"(no name)",email:o,hash:l.substring(0,16)+"...",traits:t,storageTier:u});let S=y({event_name:"identify",fan_id_hash:l,metadata:{email:e,email_sha256:l,traits:t,consent_preferences:i||void 0,storage_tier:u}});v(S)}catch(n){console.error("[BALANCE Pixel] Failed to identify:",n)}}function se(e,t="USD",n={}){let o=y({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});v(o)}function ce(){_=be(),r("Tracking source detected:",_),Ee(),i?.analytics===!0?(u="local",r("Storage tier: local (analytics consent granted)")):(u="session",r("Storage tier: session (privacy by default)")),k=Ie(),ke(),Ce(),i||(J?r("Consent UI enabled, waiting for explicit user consent (Tier 0 mode)"):(i={analytics:!0,marketing:!0,personalization:!0,timestamp:new Date().toISOString()},r("Default consent enabled (all tracking):",i),j())),i?.analytics&&!c&&(c=te()),_e(),De(),r("Initialized",{artistId:A,projectId:D||"(none - will track to all projects)",rawProjectId:P||"(none)",sessionId:k,visitorId:c?c.substring(0,8)+"...":null,fanIdHash:l,consent:i,storageTier:u,trackingSource:_,useEmulator:W,endpoint:R}),window._balanceInitialPageviewFired=!0,$(),Ne(),["mousemove","keydown","scroll","touchstart"].forEach(e=>{document.addEventListener(e,Le,{passive:!0})}),window.addEventListener("beforeunload",()=>{Ue(),E()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(re(),E()):z()})}let K=window.balance?.q||[];function le(){if(K.length>0){r("Processing",K.length,"queued commands");for(let e of K){let[t,...n]=e;switch(t){case"track":ie(n[0],n[1]);break;case"identify":ae(n[0],n[1]);break;case"page":$(n[0]);break;case"purchase":se(n[0],n[1],n[2]);break;case"setConsent":ne(n[0]);break;default:r("Unknown queued command:",t)}}}}window.balance={track:ie,identify:ae,page:$,purchase:se,getSessionId:()=>k,getVisitorId:()=>c,getFanIdHash:()=>l,getAttribution:()=>m,setConsent:ne,getConsent:Te,hasConsent:Ae};function de(){J&&!i&&new xe({style:pe,primaryColor:me,position:he})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{ce(),le(),de()}):(ce(),le(),de()),r("Pixel script loaded")})();})();
102
+ `,this.shadow.getElementById("accept")?.addEventListener("click",()=>this.handleConsent(!0)),this.shadow.getElementById("decline")?.addEventListener("click",()=>this.handleConsent(!1))}handleConsent(t){window.balance?.setConsent&&window.balance.setConsent({analytics:t,marketing:t,personalization:t,timestamp:new Date().toISOString()}),t&&!window._balanceInitialPageviewFired&&(window._balanceInitialPageviewFired=!0,window.balance?.page&&window.balance.page(),W(),r("Initial pageview fired after consent granted")),this.remove()}remove(){this.container&&(this.container.remove(),this.container=null,this.shadow=null,r("ConsentManager: Banner removed"))}}function ne(){try{return g==="local"?localStorage:sessionStorage}catch{return null}}function T(e){let t=ne();if(!t)return null;try{let n=p+e,o=t.getItem(n);if(!o&&g==="session")try{o=localStorage.getItem(n)}catch{}return o}catch{return null}}function S(e,t){let n=ne();if(n)try{n.setItem(p+e,t)}catch{}}function re(){if(g!=="local"){r("Upgrading storage tier: session -> local");try{let e=[];for(let t=0;t<sessionStorage.length;t++){let n=sessionStorage.key(t);n?.startsWith(p)&&e.push(n)}for(let t of e){let n=sessionStorage.getItem(t);n&&localStorage.setItem(t,n)}for(let t of e)sessionStorage.removeItem(t);g="local",r(`Storage tier upgraded, migrated ${e.length} items`)}catch(e){x(" Storage migration failed:",e)}}}function z(){return crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function De(){try{let e=T(X),t=T(M);if(e&&t&&Date.now()-parseInt(t,10)<_e)return S(M,Date.now().toString()),e;let n=z();return S(X,n),S(M,Date.now().toString()),n}catch{return z()}}function Pe(){let e=new URLSearchParams(window.location.search),t={};return["source","medium","campaign","content","term"].forEach(n=>{let o=e.get(`utm_${n}`);o&&(t[`utm_${n}`]=o)}),t}function Ae(){try{let e=T(Z);if(e){m=JSON.parse(e),r("Loaded attribution:",m);return}let t=Pe();Object.keys(t).length>0&&(m=t,S(Z,JSON.stringify(t)),r("Captured attribution:",m))}catch{}}function Oe(){try{u=T(ee)}catch{}}function oe(){if(!i?.analytics)return null;try{let e=localStorage.getItem(p+F);if(e)return e;let t=z();return localStorage.setItem(p+F,t),r("Created persistent visitor ID:",t.substring(0,8)+"..."),t}catch{return null}}function Ne(){if(!i?.analytics){s=null;return}try{s=localStorage.getItem(p+F),s&&r("Loaded visitor ID:",s.substring(0,8)+"...")}catch{}}function Le(){try{let e=localStorage.getItem(C);if(e){let t=JSON.parse(e);if(t.expiresAt&&Date.now()>t.expiresAt){r("Consent expired - clearing stored consent"),localStorage.removeItem(C),i=null;return}if(t.expiresAt){let n=t.expiresAt-Date.now();n>0&&n<Se&&(r("Consent nearing expiration - will prompt for refresh"),window._balanceConsentNeedsRefresh=!0)}i=t.preferences||null,r("Loaded consent:",i)}}catch{}}function ie(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1,expiresAt:Date.now()+xe};localStorage.setItem(C,JSON.stringify(o)),r("Consent saved with TTL:",e),window._balanceConsentNeedsRefresh=!1}catch(o){x(" Could not save consent:",o)}e.analytics===!0&&(re(),s||(s=oe()));let n=y({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});v(n);try{window.dispatchEvent(new CustomEvent("balance:consent:updated",{detail:e})),r("DOM event balance:consent:updated dispatched")}catch{}}function Re(){return i}function Ue(e){return i?.[e]===!0}async function Me(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),_=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(_)).map($e=>$e.toString(16).padStart(2,"0")).join("")}function y(e){let t=be(),n={artist_id:O,fan_session_id:E,visitor_id:s||void 0,fan_id_hash:u||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,device_type:t.device_type,browser:t.browser,os:t.os,tracking_source:k,...e,...m};return L&&!e.projectId&&(n.projectId=L),n}function v(e){f.push(e),r("Event queued:",e.event_name,"(queue:",f.length,")"),f.length>=10&&D()}async function D(){if(f.length===0)return;let e=[...f];f=[],r("Flushing",e.length,"events to",H);try{let t=await fetch(H,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);r("Events sent successfully")}catch(t){x(" Failed to send events:",t),f.length<50&&f.push(...e)}}function Fe(){j&&clearInterval(j),j=window.setInterval(()=>{f.length>0&&D()},5e3)}function V(){h||(ke=Date.now()),h=Date.now(),w=!0,r("Active time tracking started/resumed")}function ae(){h&&w&&(B+=Date.now()-h),w=!1,r("Active time tracking paused, accumulated:",B,"ms")}function He(){let e=B;return w&&h&&(e+=Date.now()-h),e}function je(){te=Date.now(),I&&(I=!1,V(),r("User returned from idle"))}function se(){if(document.visibilityState==="hidden"){r("Skipping heartbeat - tab hidden");return}if(Date.now()-te>Ce){I||(I=!0,ae(),r("User idle - pausing heartbeat"));return}let e=He(),t=Math.round(e/1e3),n=y({event_name:"engagement_heartbeat",metadata:{time_on_page_seconds:t,time_on_page_ms:e,heartbeat_interval_ms:R,is_active:w&&!I}});v(n),r("Heartbeat sent:",t,"seconds active")}function W(){if(!J){r('Heartbeat disabled via data-heartbeat="false"');return}b&&clearInterval(b),V(),b=window.setInterval(()=>{se()},R),r("Heartbeat started with interval:",R,"ms")}function Be(){b&&(clearInterval(b),b=null),J&&(se(),r("Heartbeat stopped, final time sent"))}function P(e={}){let t=y({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});v(t)}function ce(e,t={}){let n=y({event_name:"custom",metadata:{event_type:e,...t}});v(n)}async function le(e,t={}){try{if(i&&i.analytics===!1){r("Identify skipped - user declined analytics consent");return}u=await Me(e),i?.analytics===!0&&re(),S(ee,u);let n=e.split("@"),o=n[0].charAt(0)+"***@"+(n[1]||"");r("Fan identified:",{name:t.name||"(no name)",email:o,hash:u.substring(0,16)+"...",traits:t,storageTier:g});let _=y({event_name:"identify",fan_id_hash:u,metadata:{email_sha256:u,email_display:o,traits:t,consent_preferences:i||void 0,storage_tier:g}});v(_)}catch(n){x(" Failed to identify:",n)}}function de(e,t="USD",n={}){let o=y({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});v(o)}function ue(){k=Ie(),r("Tracking source detected:",k),Le(),i?.analytics===!0?(g="local",r("Storage tier: local (analytics consent granted)")):(g="session",r("Storage tier: session (privacy by default)")),E=De(),Oe(),Ne(),i||r(U?"Consent UI enabled, waiting for explicit user consent (Tier 0 mode)":"No consent - operating in privacy-first mode (session storage only, limited tracking)"),i?.analytics&&!s&&(s=oe()),Ae(),Fe(),r("Initialized",{artistId:O,projectId:L||"(none - will track to all projects)",rawProjectId:N||"(none)",sessionId:E,visitorId:s?s.substring(0,8)+"...":null,fanIdHash:u,consent:i,storageTier:g,trackingSource:k,useEmulator:K,endpoint:H}),i?.analytics===!0?(window._balanceInitialPageviewFired=!0,P(),W(),r("Full tracking enabled (user consented)")):U?r("Tracking dormant - waiting for explicit consent via ConsentManager"):(window._balanceInitialPageviewFired=!0,P(),W(),r("Privacy-first tracking enabled (session-scoped, no persistent IDs)")),["mousemove","keydown","scroll","touchstart"].forEach(e=>{document.addEventListener(e,je,{passive:!0})}),window.addEventListener("beforeunload",()=>{Be(),D()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(ae(),D()):V()})}if(window.balance?._version&&window.balance._version!==l){console.warn(`[BLN] Version conflict: ${window.balance._version} already loaded, skipping ${l}`);return}let q=window.balance?.q||[];function fe(){if(q.length>0){r("Processing",q.length,"queued commands");for(let e of q){let[t,...n]=e;switch(t){case"track":ce(n[0],n[1]);break;case"identify":le(n[0],n[1]);break;case"page":P(n[0]);break;case"purchase":de(n[0],n[1],n[2]);break;case"setConsent":ie(n[0]);break;default:r("Unknown queued command:",t)}}}}window.balance={track:ce,identify:le,page:P,purchase:de,getSessionId:()=>E,getVisitorId:()=>s,getFanIdHash:()=>u,getAttribution:()=>m,setConsent:ie,getConsent:Re,hasConsent:Ue,_version:l};function ge(){U&&!i&&new Te({style:ye,primaryColor:ve,position:we})}let pe=window.requestIdleCallback||(e=>setTimeout(e,1));document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{ue(),fe(),pe(()=>ge())}):(ue(),fe(),pe(()=>ge())),r("Pixel script loaded")})();})();
@@ -53,7 +53,12 @@ export interface BalanceContextValue {
53
53
  setConsent: (preferences: ConsentPreferences) => void;
54
54
  getConsent: () => ConsentPreferences | null;
55
55
  hasConsent: (type: 'analytics' | 'marketing' | 'personalization') => boolean;
56
+ /** True when the pixel script has loaded successfully */
56
57
  isReady: boolean;
58
+ /** True if the pixel script failed to load */
59
+ isError: boolean;
60
+ /** Error object if script loading failed */
61
+ error: Error | null;
57
62
  artistId: string;
58
63
  projectId?: string;
59
64
  endpoint?: string;
package/dist/index.js CHANGED
@@ -100,6 +100,8 @@ var defaultContextValue = {
100
100
  hasConsent: () => false,
101
101
  // State - SSR-safe defaults
102
102
  isReady: false,
103
+ isError: false,
104
+ error: null,
103
105
  artistId: "",
104
106
  projectId: void 0,
105
107
  endpoint: void 0,
@@ -284,6 +286,8 @@ function BalanceProvider({
284
286
  children
285
287
  }) {
286
288
  const [isReady, setIsReady] = (0, import_react4.useState)(false);
289
+ const [isError, setIsError] = (0, import_react4.useState)(false);
290
+ const [error, setError] = (0, import_react4.useState)(null);
287
291
  const [shouldLoadScript, setShouldLoadScript] = (0, import_react4.useState)(false);
288
292
  (0, import_react4.useEffect)(() => {
289
293
  ensureGlobalStub();
@@ -390,6 +394,8 @@ function BalanceProvider({
390
394
  hasConsent: hasConsent2,
391
395
  // State
392
396
  isReady,
397
+ isError,
398
+ error,
393
399
  artistId,
394
400
  projectId,
395
401
  endpoint,
@@ -406,6 +412,8 @@ function BalanceProvider({
406
412
  getConsent2,
407
413
  hasConsent2,
408
414
  isReady,
415
+ isError,
416
+ error,
409
417
  artistId,
410
418
  projectId,
411
419
  endpoint,
@@ -419,8 +427,17 @@ function BalanceProvider({
419
427
  }, [debug]);
420
428
  const handleScriptError = (0, import_react4.useCallback)((e) => {
421
429
  console.error("[BalanceProvider] Failed to load pixel script:", e);
430
+ setIsError(true);
431
+ setError(e);
422
432
  }, []);
423
433
  const resolvedScriptUrl = scriptUrl ?? DEFAULT_SCRIPT_URL;
434
+ (0, import_react4.useEffect)(() => {
435
+ if (debug && !scriptUrl) {
436
+ console.warn(
437
+ '[BalanceProvider] Using default CDN URL. For production, consider hosting the pixel script locally:\n1. Copy index.js from artist-os-distro/apps/web/public/scripts/\n2. Place it in your public/scripts/index.js\n3. Set scriptUrl="/scripts/index.js"\n\nThis avoids external dependencies and CSP issues.'
438
+ );
439
+ }
440
+ }, [debug, scriptUrl]);
424
441
  const content = /* @__PURE__ */ import_react4.default.createElement(BalanceContext.Provider, { value: contextValue }, shouldLoadScript && /* @__PURE__ */ import_react4.default.createElement(
425
442
  import_script2.default,
426
443
  {
@@ -2880,6 +2897,17 @@ var import_react23 = require("react");
2880
2897
 
2881
2898
  // src/storage/StorageManager.ts
2882
2899
  var DEFAULT_PREFIX = "balance_";
2900
+ var TTL_DURATIONS = {
2901
+ /** Consent should be refreshed annually per GDPR best practices */
2902
+ CONSENT: 365 * 24 * 60 * 60 * 1e3,
2903
+ // 12 months
2904
+ /** Session IDs expire with the session anyway */
2905
+ SESSION: 30 * 60 * 1e3,
2906
+ // 30 minutes
2907
+ /** Visitor IDs can persist longer for returning user detection */
2908
+ VISITOR: 90 * 24 * 60 * 60 * 1e3
2909
+ // 90 days
2910
+ };
2883
2911
  var StorageManager = class {
2884
2912
  constructor(config) {
2885
2913
  __publicField(this, "prefix");
@@ -3061,6 +3089,80 @@ var StorageManager = class {
3061
3089
  } catch {
3062
3090
  }
3063
3091
  }
3092
+ /**
3093
+ * Set item with TTL (Time To Live)
3094
+ * The item will automatically expire after the specified duration
3095
+ */
3096
+ setItemWithTTL(key, value, ttlMs) {
3097
+ const ttlWrapper = {
3098
+ value,
3099
+ expiresAt: Date.now() + ttlMs
3100
+ };
3101
+ this.setItem(key, JSON.stringify(ttlWrapper));
3102
+ }
3103
+ /**
3104
+ * Get item with TTL check
3105
+ * Returns null if the item has expired (and removes it)
3106
+ */
3107
+ getItemWithTTL(key) {
3108
+ const raw = this.getItem(key);
3109
+ if (!raw)
3110
+ return null;
3111
+ try {
3112
+ const parsed = JSON.parse(raw);
3113
+ if (typeof parsed === "object" && "expiresAt" in parsed && "value" in parsed) {
3114
+ if (Date.now() > parsed.expiresAt) {
3115
+ this.removeItem(key);
3116
+ return null;
3117
+ }
3118
+ return parsed.value;
3119
+ }
3120
+ return raw;
3121
+ } catch {
3122
+ return raw;
3123
+ }
3124
+ }
3125
+ /**
3126
+ * Set JSON item with TTL
3127
+ */
3128
+ setJSONWithTTL(key, value, ttlMs) {
3129
+ try {
3130
+ this.setItemWithTTL(key, JSON.stringify(value), ttlMs);
3131
+ } catch {
3132
+ }
3133
+ }
3134
+ /**
3135
+ * Get JSON item with TTL check
3136
+ */
3137
+ getJSONWithTTL(key) {
3138
+ const value = this.getItemWithTTL(key);
3139
+ if (!value)
3140
+ return null;
3141
+ try {
3142
+ return JSON.parse(value);
3143
+ } catch {
3144
+ return null;
3145
+ }
3146
+ }
3147
+ /**
3148
+ * Check if an item's TTL needs refresh (e.g., consent nearing expiration)
3149
+ * Returns true if the item will expire within the threshold
3150
+ */
3151
+ needsTTLRefresh(key, refreshThresholdMs = 30 * 24 * 60 * 60 * 1e3) {
3152
+ const raw = this.getItem(key);
3153
+ if (!raw)
3154
+ return false;
3155
+ try {
3156
+ const parsed = JSON.parse(raw);
3157
+ if (typeof parsed === "object" && "expiresAt" in parsed) {
3158
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
3159
+ return timeUntilExpiry > 0 && timeUntilExpiry < refreshThresholdMs;
3160
+ }
3161
+ return false;
3162
+ } catch {
3163
+ return false;
3164
+ }
3165
+ }
3064
3166
  };
3065
3167
  var storageManagerInstance = null;
3066
3168
  function getStorageManager() {
package/dist/index.mjs CHANGED
@@ -31,6 +31,8 @@ var defaultContextValue = {
31
31
  hasConsent: () => false,
32
32
  // State - SSR-safe defaults
33
33
  isReady: false,
34
+ isError: false,
35
+ error: null,
34
36
  artistId: "",
35
37
  projectId: void 0,
36
38
  endpoint: void 0,
@@ -215,6 +217,8 @@ function BalanceProvider({
215
217
  children
216
218
  }) {
217
219
  const [isReady, setIsReady] = useState(false);
220
+ const [isError, setIsError] = useState(false);
221
+ const [error, setError] = useState(null);
218
222
  const [shouldLoadScript, setShouldLoadScript] = useState(false);
219
223
  useEffect2(() => {
220
224
  ensureGlobalStub();
@@ -321,6 +325,8 @@ function BalanceProvider({
321
325
  hasConsent: hasConsent2,
322
326
  // State
323
327
  isReady,
328
+ isError,
329
+ error,
324
330
  artistId,
325
331
  projectId,
326
332
  endpoint,
@@ -337,6 +343,8 @@ function BalanceProvider({
337
343
  getConsent2,
338
344
  hasConsent2,
339
345
  isReady,
346
+ isError,
347
+ error,
340
348
  artistId,
341
349
  projectId,
342
350
  endpoint,
@@ -350,8 +358,17 @@ function BalanceProvider({
350
358
  }, [debug]);
351
359
  const handleScriptError = useCallback2((e) => {
352
360
  console.error("[BalanceProvider] Failed to load pixel script:", e);
361
+ setIsError(true);
362
+ setError(e);
353
363
  }, []);
354
364
  const resolvedScriptUrl = scriptUrl ?? DEFAULT_SCRIPT_URL;
365
+ useEffect2(() => {
366
+ if (debug && !scriptUrl) {
367
+ console.warn(
368
+ '[BalanceProvider] Using default CDN URL. For production, consider hosting the pixel script locally:\n1. Copy index.js from artist-os-distro/apps/web/public/scripts/\n2. Place it in your public/scripts/index.js\n3. Set scriptUrl="/scripts/index.js"\n\nThis avoids external dependencies and CSP issues.'
369
+ );
370
+ }
371
+ }, [debug, scriptUrl]);
355
372
  const content = /* @__PURE__ */ React3.createElement(BalanceContext.Provider, { value: contextValue }, shouldLoadScript && /* @__PURE__ */ React3.createElement(
356
373
  Script2,
357
374
  {
@@ -2811,6 +2828,17 @@ import { useCallback as useCallback16, useEffect as useEffect9, useRef as useRef
2811
2828
 
2812
2829
  // src/storage/StorageManager.ts
2813
2830
  var DEFAULT_PREFIX = "balance_";
2831
+ var TTL_DURATIONS = {
2832
+ /** Consent should be refreshed annually per GDPR best practices */
2833
+ CONSENT: 365 * 24 * 60 * 60 * 1e3,
2834
+ // 12 months
2835
+ /** Session IDs expire with the session anyway */
2836
+ SESSION: 30 * 60 * 1e3,
2837
+ // 30 minutes
2838
+ /** Visitor IDs can persist longer for returning user detection */
2839
+ VISITOR: 90 * 24 * 60 * 60 * 1e3
2840
+ // 90 days
2841
+ };
2814
2842
  var StorageManager = class {
2815
2843
  constructor(config) {
2816
2844
  __publicField(this, "prefix");
@@ -2992,6 +3020,80 @@ var StorageManager = class {
2992
3020
  } catch {
2993
3021
  }
2994
3022
  }
3023
+ /**
3024
+ * Set item with TTL (Time To Live)
3025
+ * The item will automatically expire after the specified duration
3026
+ */
3027
+ setItemWithTTL(key, value, ttlMs) {
3028
+ const ttlWrapper = {
3029
+ value,
3030
+ expiresAt: Date.now() + ttlMs
3031
+ };
3032
+ this.setItem(key, JSON.stringify(ttlWrapper));
3033
+ }
3034
+ /**
3035
+ * Get item with TTL check
3036
+ * Returns null if the item has expired (and removes it)
3037
+ */
3038
+ getItemWithTTL(key) {
3039
+ const raw = this.getItem(key);
3040
+ if (!raw)
3041
+ return null;
3042
+ try {
3043
+ const parsed = JSON.parse(raw);
3044
+ if (typeof parsed === "object" && "expiresAt" in parsed && "value" in parsed) {
3045
+ if (Date.now() > parsed.expiresAt) {
3046
+ this.removeItem(key);
3047
+ return null;
3048
+ }
3049
+ return parsed.value;
3050
+ }
3051
+ return raw;
3052
+ } catch {
3053
+ return raw;
3054
+ }
3055
+ }
3056
+ /**
3057
+ * Set JSON item with TTL
3058
+ */
3059
+ setJSONWithTTL(key, value, ttlMs) {
3060
+ try {
3061
+ this.setItemWithTTL(key, JSON.stringify(value), ttlMs);
3062
+ } catch {
3063
+ }
3064
+ }
3065
+ /**
3066
+ * Get JSON item with TTL check
3067
+ */
3068
+ getJSONWithTTL(key) {
3069
+ const value = this.getItemWithTTL(key);
3070
+ if (!value)
3071
+ return null;
3072
+ try {
3073
+ return JSON.parse(value);
3074
+ } catch {
3075
+ return null;
3076
+ }
3077
+ }
3078
+ /**
3079
+ * Check if an item's TTL needs refresh (e.g., consent nearing expiration)
3080
+ * Returns true if the item will expire within the threshold
3081
+ */
3082
+ needsTTLRefresh(key, refreshThresholdMs = 30 * 24 * 60 * 60 * 1e3) {
3083
+ const raw = this.getItem(key);
3084
+ if (!raw)
3085
+ return false;
3086
+ try {
3087
+ const parsed = JSON.parse(raw);
3088
+ if (typeof parsed === "object" && "expiresAt" in parsed) {
3089
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
3090
+ return timeUntilExpiry > 0 && timeUntilExpiry < refreshThresholdMs;
3091
+ }
3092
+ return false;
3093
+ } catch {
3094
+ return false;
3095
+ }
3096
+ }
2995
3097
  };
2996
3098
  var storageManagerInstance = null;
2997
3099
  function getStorageManager() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifilabs/pixel",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "BALANCE Pixel - Lightweight browser tracking script for artist fan analytics",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",