/** * Internal dependencies */ import { COOKIE_NAME } from './constants'; import { getCookie } from './utils'; import type { SessionCookieData } from './types/shared'; /** * Session Manager for WooCommerce Analytics * */ export default class SessionManager { public sessionId: string | null = null; public landingPage: string | null = null; public isEngaged: boolean = false; public isNewSession: boolean = false; private isInitialized: boolean = false; /** * Initialize the session manager */ init = () => { if ( this.isInitialized ) { return; } this.loadOrCreateSession(); this.isInitialized = true; }; /** * Load existing session or create new one */ loadOrCreateSession() { const cookie = this.getSessionCookie(); if ( cookie && cookie.session_id ) { // Load existing session this.sessionId = cookie.session_id; this.landingPage = cookie.landing_page || null; this.isEngaged = cookie.is_engaged || false; this.isNewSession = false; } else { this.createNewSession(); } } /** * Create a new session */ createNewSession() { this.isNewSession = true; const sessionData = { session_id: this.generateRandomUuid(), landing_page: JSON.stringify( window.wcAnalytics?.breadcrumbs || [] ), expires: this.getSessionExpirationTime(), }; if ( this.setSessionCookie( sessionData ) ) { // Only set session data if cookie was set successfully this.sessionId = sessionData.session_id; this.landingPage = sessionData.landing_page; this.isEngaged = false; } } /** * Get session cookie data * * @return SessionCookieData | null */ getSessionCookie(): SessionCookieData | null { const rawCookie = getCookie( COOKIE_NAME ); if ( ! rawCookie ) { return null; } try { return JSON.parse( decodeURIComponent( rawCookie ) ) as SessionCookieData; } catch ( _error ) { // eslint-disable-next-line no-console console.error( 'Error parsing session cookie', _error ); return null; } } /** * Set session cookie * * @param sessionData - Session data * @return boolean */ setSessionCookie( sessionData: SessionCookieData ): boolean { const encoded = encodeURIComponent( JSON.stringify( sessionData ) ); const expires = sessionData.expires || this.getSessionExpirationTime(); document.cookie = `${ COOKIE_NAME }=${ encoded }; expires=${ expires }; path=/; secure; samesite=strict`; const isCookieSet = getCookie( COOKIE_NAME ) === encoded; return isCookieSet; } /** * Generate session expiration time * 30 minutes from now or at midnight UTC, whichever comes first * * @return string */ getSessionExpirationTime(): string { const thirtyMinutesFromNow = Date.now() + 30 * 60 * 1000; const midnightUTC = new Date(); midnightUTC.setUTCDate( midnightUTC.getUTCDate() + 1 ); midnightUTC.setUTCHours( 0, 0, 0, 0 ); const expirationTime = Math.min( thirtyMinutesFromNow, midnightUTC.getTime() ); return new Date( expirationTime ).toUTCString(); } /** * Generate a random UUID v4 token * * @return string */ generateRandomUuid(): string { // Use modern crypto.randomUUID() if available (most efficient) if ( typeof crypto !== 'undefined' && crypto.randomUUID ) { return crypto.randomUUID(); } // Use crypto.getRandomValues for better security if ( typeof crypto !== 'undefined' && crypto.getRandomValues ) { const bytes = new Uint8Array( 16 ); crypto.getRandomValues( bytes ); // Set version (4) and variant bits according to RFC 4122 // Set version (4) and variant bits according to RFC 4122 without using bitwise operators // eslint-disable-next-line no-bitwise bytes[ 6 ] = ( bytes[ 6 ] & 0x0f ) | 0x40; // Version 4 // eslint-disable-next-line no-bitwise bytes[ 8 ] = ( bytes[ 8 ] & 0x3f ) | 0x80; // Variant 10 // Convert to hex string with proper formatting const hex = Array.from( bytes, b => b.toString( 16 ).padStart( 2, '0' ) ).join( '' ); return [ hex.slice( 0, 8 ), hex.slice( 8, 12 ), hex.slice( 12, 16 ), hex.slice( 16, 20 ), hex.slice( 20, 32 ), ].join( '-' ); } // Fallback for older browsers (Math.random - less secure) return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, c => { const r = Math.floor( Math.random() * 16 ); const v = c === 'x' ? r : ( r % 4 ) + 8; return v.toString( 16 ); } ); } /** * Set engaged and update session cookie * */ setEngaged() { if ( this.isEngaged ) { // Engagement already recorded return; } const sessionData = this.getSessionCookie(); if ( sessionData && sessionData.session_id ) { sessionData.is_engaged = true; this.setSessionCookie( sessionData ); } this.isEngaged = true; } /** * Clear session data (for consent withdrawal) */ clearSession() { // Clear cookie document.cookie = `${ COOKIE_NAME }=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure; samesite=strict`; // Reset session variables this.sessionId = null; this.landingPage = null; this.isEngaged = false; this.isNewSession = false; this.isInitialized = false; } }