UNPKG

@liquidcommerceteam/elements-sdk

Version:

LiquidCommerce Elements SDK

1,292 lines (1,255 loc) 685 kB
var CART_EVENT_ENUM; (function (CART_EVENT_ENUM) { CART_EVENT_ENUM["OOS"] = "OutOfStock"; CART_EVENT_ENUM["ITEMS_NOT_ADDED"] = "ItemsNotAdded"; CART_EVENT_ENUM["ITEMS_REQUESTED_NOT_ADDED"] = "ItemsRequestedNotAdded"; CART_EVENT_ENUM["ITEM_NOT_ENGRAVED"] = "ItemEngravingError"; CART_EVENT_ENUM["ADDRESS_CHANGE"] = "AddressChange"; CART_EVENT_ENUM["LOCATION_AVAILABILITY"] = "LocationAvailability"; CART_EVENT_ENUM["PARTNER_PRODUCT_CONFIGS"] = "PartnerProductConfigs"; CART_EVENT_ENUM["REMOVED_EXISTING_ITEMS"] = "RemovedExistingCartItems"; CART_EVENT_ENUM["RETAILER_MIN"] = "RetailerMinNotMet"; CART_EVENT_ENUM["NO_ITEMS_IN_CART"] = "NoItemsInCart"; CART_EVENT_ENUM["INVALID_ID"] = "InvalidId"; CART_EVENT_ENUM["NO_ID"] = "NoId"; CART_EVENT_ENUM["CART_CHECKOUT_PROCESSED"] = "CartCheckoutProcessed"; CART_EVENT_ENUM["NEW_CART"] = "NewCart"; CART_EVENT_ENUM["DEFAULT"] = "CartError"; CART_EVENT_ENUM["ITEM_QTY_CHANGE"] = "ItemQuantityChange"; CART_EVENT_ENUM["ITEM_ID_NOT_FOUND"] = "ItemIdNotFound"; CART_EVENT_ENUM["ITEMS_REMOVED"] = "ItemsRemoved"; // Coupon validation events CART_EVENT_ENUM["COUPON_PROCESSING_ERROR"] = "CouponProcessingError"; CART_EVENT_ENUM["COUPON_NOT_FOUND"] = "CouponNotFound"; CART_EVENT_ENUM["COUPON_EXPIRED"] = "CouponExpired"; CART_EVENT_ENUM["NO_APPLICABLE_DISCOUNT"] = "NoApplicableDiscount"; CART_EVENT_ENUM["COUPON_NOT_STARTED"] = "CouponNotStarted"; CART_EVENT_ENUM["MINIMUM_ORDER_VALUE_NOT_MET"] = "MinimumOrderValueNotMet"; CART_EVENT_ENUM["MINIMUM_ORDER_UNITS_NOT_MET"] = "MinimumOrderUnitsNotMet"; CART_EVENT_ENUM["MINIMUM_DISTINCT_ITEMS_NOT_MET"] = "MinimumDistinctItemsNotMet"; CART_EVENT_ENUM["QUOTA_EXCEEDED"] = "QuotaExceeded"; CART_EVENT_ENUM["USER_LIMIT_EXCEEDED"] = "UserLimitExceeded"; CART_EVENT_ENUM["NOT_FIRST_PURCHASE"] = "NotFirstPurchase"; CART_EVENT_ENUM["INVALID_COUPON"] = "InvalidCoupon"; CART_EVENT_ENUM["INVALID_MEMBERSHIP"] = "InvalidMembership"; CART_EVENT_ENUM["INVALID_DOMAIN"] = "InvalidDomain"; CART_EVENT_ENUM["INVALID_REQUIREMENTS"] = "InvalidRequirements"; CART_EVENT_ENUM["INVALID_ORGANIZATION"] = "InvalidOrganization"; CART_EVENT_ENUM["PRODUCT_NOT_ELIGIBLE"] = "ProductNotEligible"; CART_EVENT_ENUM["NOT_ENOUGH_PREVIOUS_ORDERS"] = "NotEnoughPreviousOrders"; //Presale validation events CART_EVENT_ENUM["PRESALE_ITEMS_NOT_ALLOWED"] = "PresaleItemsNotAllowed"; CART_EVENT_ENUM["PRESALE_LIMIT_EXCEEDED"] = "PresaleLimitExceeded"; CART_EVENT_ENUM["PRESALE_NOT_STARTED"] = "PresaleNotStarted"; CART_EVENT_ENUM["PRESALE_EXPIRED"] = "PresaleExpired"; CART_EVENT_ENUM["PRESALE_MIXED_CART"] = "PresaleMixedCart"; })(CART_EVENT_ENUM || (CART_EVENT_ENUM = {})); var CHECKOUT_EVENT_ENUM; (function (CHECKOUT_EVENT_ENUM) { CHECKOUT_EVENT_ENUM["ERROR_PROCESSING_GIFT_CARDS"] = "ErrorProcessingGiftCards"; CHECKOUT_EVENT_ENUM["INVALID_GIFT_CARD_CODE"] = "InvalidGiftCardCodes"; CHECKOUT_EVENT_ENUM["INVALID_GIFT_CARD_PARTNER"] = "InvalidGiftCardPartner"; CHECKOUT_EVENT_ENUM["INACTIVE_GIFT_CARD"] = "InactiveGiftCard"; CHECKOUT_EVENT_ENUM["GIFT_CARD_ALREADY_IN_USE"] = "GiftCardAlreadyInUse"; CHECKOUT_EVENT_ENUM["GIFT_CARD_EXPIRED"] = "GiftCardExpired"; CHECKOUT_EVENT_ENUM["GIFT_CARD_BALANCE_DEPLETED"] = "GiftCardBalanceDepleted"; })(CHECKOUT_EVENT_ENUM || (CHECKOUT_EVENT_ENUM = {})); var ENUM_ADDRESS_TYPE; (function (ENUM_ADDRESS_TYPE) { ENUM_ADDRESS_TYPE["SHIPPING"] = "shipping"; ENUM_ADDRESS_TYPE["BILLING"] = "billing"; })(ENUM_ADDRESS_TYPE || (ENUM_ADDRESS_TYPE = {})); var ELEMENTS_ENV; (function (ELEMENTS_ENV) { ELEMENTS_ENV["LOCAL"] = "local"; ELEMENTS_ENV["DEVELOPMENT"] = "development"; ELEMENTS_ENV["STAGING"] = "staging"; ELEMENTS_ENV["PRODUCTION"] = "production"; })(ELEMENTS_ENV || (ELEMENTS_ENV = {})); var ELEMENTS_ACTIONS_EVENT; (function (ELEMENTS_ACTIONS_EVENT) { ELEMENTS_ACTIONS_EVENT["CLIENT_INITIALIZED"] = "clientInitialized"; // Product-related events ELEMENTS_ACTIONS_EVENT["PRODUCT_LOADED"] = "productLoaded"; ELEMENTS_ACTIONS_EVENT["PRODUCT_QUANTITY_INCREASE"] = "productQuantityIncrease"; ELEMENTS_ACTIONS_EVENT["PRODUCT_QUANTITY_DECREASE"] = "productQuantityDecrease"; ELEMENTS_ACTIONS_EVENT["PRODUCT_ADD_TO_CART"] = "productAddToCart"; // Cart-related events ELEMENTS_ACTIONS_EVENT["CART_INITIALIZED"] = "cartInitialized"; ELEMENTS_ACTIONS_EVENT["CART_CLOSED"] = "cartClosed"; ELEMENTS_ACTIONS_EVENT["CART_OPENED"] = "cartOpened"; ELEMENTS_ACTIONS_EVENT["CART_UPDATED"] = "cartUpdated"; // Address-related events ELEMENTS_ACTIONS_EVENT["ADDRESS_UPDATED"] = "addressUpdated"; // Checkout-related events ELEMENTS_ACTIONS_EVENT["CHECKOUT_INITIALIZED"] = "checkoutInitialized"; ELEMENTS_ACTIONS_EVENT["CHECKOUT_OPENED"] = "checkoutOpened"; ELEMENTS_ACTIONS_EVENT["CHECKOUT_CLOSED"] = "checkoutClosed"; ELEMENTS_ACTIONS_EVENT["CHECKOUT_UPDATED"] = "checkoutUpdated"; })(ELEMENTS_ACTIONS_EVENT || (ELEMENTS_ACTIONS_EVENT = {})); var ELEMENTS_FORMS_EVENT; (function (ELEMENTS_FORMS_EVENT) { // Customer Information (ci) Form Events ELEMENTS_FORMS_EVENT["CI_FIRST_NAME_INPUT"] = "ciFirstNameInput"; ELEMENTS_FORMS_EVENT["CI_LAST_NAME_INPUT"] = "ciLastNameInput"; ELEMENTS_FORMS_EVENT["CI_EMAIL_INPUT"] = "ciEmailInput"; ELEMENTS_FORMS_EVENT["CI_PHONE_INPUT"] = "ciPhoneInput"; ELEMENTS_FORMS_EVENT["CI_BIRTHDATE_INPUT"] = "ciBirthDateInput"; ELEMENTS_FORMS_EVENT["CI_ADDRESS_TWO_INPUT"] = "ciAddressTwoInput"; ELEMENTS_FORMS_EVENT["CI_COMPANY_INPUT"] = "ciCompanyInput"; // Gift Information (gi) Form Events ELEMENTS_FORMS_EVENT["GI_FIRST_NAME_INPUT"] = "giFirstNameInput"; ELEMENTS_FORMS_EVENT["GI_LAST_NAME_INPUT"] = "giLastNameInput"; ELEMENTS_FORMS_EVENT["GI_EMAIL_INPUT"] = "giEmailInput"; ELEMENTS_FORMS_EVENT["GI_PHONE_INPUT"] = "giPhoneInput"; ELEMENTS_FORMS_EVENT["GI_BIRTHDATE_INPUT"] = "giBirthDateInput"; ELEMENTS_FORMS_EVENT["GI_ADDRESS_TWO_INPUT"] = "giAddressTwoInput"; ELEMENTS_FORMS_EVENT["GI_COMPANY_INPUT"] = "giCompanyInput"; ELEMENTS_FORMS_EVENT["GI_MESSAGE_INPUT"] = "giMessageInput"; // Billing Information (bi) Form Events ELEMENTS_FORMS_EVENT["BI_FIRST_NAME_INPUT"] = "biFirstNameInput"; ELEMENTS_FORMS_EVENT["BI_LAST_NAME_INPUT"] = "biLastNameInput"; ELEMENTS_FORMS_EVENT["BI_EMAIL_INPUT"] = "biEmailInput"; ELEMENTS_FORMS_EVENT["BI_PHONE_INPUT"] = "biPhoneInput"; ELEMENTS_FORMS_EVENT["BI_COMPANY_INPUT"] = "biCompanyInput"; ELEMENTS_FORMS_EVENT["BI_ADDRESS_ONE_INPUT"] = "biAddressOneInput"; ELEMENTS_FORMS_EVENT["BI_ADDRESS_TWO_INPUT"] = "biAddressTwoInput"; ELEMENTS_FORMS_EVENT["BI_CITY_INPUT"] = "biCityInput"; ELEMENTS_FORMS_EVENT["BI_STATE_INPUT"] = "biStateInput"; ELEMENTS_FORMS_EVENT["BI_ZIP_INPUT"] = "biZipInput"; })(ELEMENTS_FORMS_EVENT || (ELEMENTS_FORMS_EVENT = {})); var COMPONENT_TYPE; (function (COMPONENT_TYPE) { COMPONENT_TYPE["DRAWER"] = "drawer"; COMPONENT_TYPE["INPUT"] = "input"; COMPONENT_TYPE["BIRTHDATE_INPUT"] = "birthdate-input"; COMPONENT_TYPE["ENGRAVING_FORM"] = "engraving-form"; COMPONENT_TYPE["BUTTONS_CART_OPEN"] = "buttons-cart-open"; COMPONENT_TYPE["ADDRESS"] = "address"; COMPONENT_TYPE["PRODUCT"] = "product"; COMPONENT_TYPE["PRODUCT_IMAGE_CAROUSEL"] = "product-image-carousel"; COMPONENT_TYPE["PRODUCT_OPTIONS"] = "product-options"; COMPONENT_TYPE["PRODUCT_INTERACTIONS"] = "product-interactions"; COMPONENT_TYPE["PRODUCT_DESCRIPTION"] = "product-description"; COMPONENT_TYPE["PRODUCT_RETAILERS"] = "product-retailers"; COMPONENT_TYPE["PRODUCT_RETAILERS_CAROUSEL"] = "product-retailers-carousel"; COMPONENT_TYPE["PRODUCT_RETAILERS_POPUP"] = "product-retailers-popup"; COMPONENT_TYPE["PRODUCT_RETAILERS_POPUP_LIST"] = "product-retailers-popup-list"; COMPONENT_TYPE["PRODUCT_PRICE"] = "product-price"; COMPONENT_TYPE["PRODUCT_ADD_TO_CART_SECTION"] = "product-add-to-cart-section"; COMPONENT_TYPE["PRODUCT_ENGRAVING"] = "product-engraving"; COMPONENT_TYPE["PRODUCT_DRAWER"] = "product-drawer"; COMPONENT_TYPE["PRODUCT_LOADING"] = "product-loading"; COMPONENT_TYPE["CART"] = "cart"; COMPONENT_TYPE["CART_RETAILER"] = "cart-retailer"; COMPONENT_TYPE["CART_ITEM"] = "cart-item"; COMPONENT_TYPE["CART_ITEM_ENGRAVING"] = "cart-item-engraving"; COMPONENT_TYPE["CART_FOOTER"] = "cart-footer"; COMPONENT_TYPE["CART_ITEM_QUANTITY_PRICE"] = "cart-item-quantity-price"; COMPONENT_TYPE["CART_RETAILER_SUBTOTAL"] = "cart-retailer-subtotal"; COMPONENT_TYPE["CART_PROMO_CODE"] = "cart-promo-code"; COMPONENT_TYPE["CART_HEADER"] = "cart-header"; COMPONENT_TYPE["CART_BODY"] = "cart-body"; COMPONENT_TYPE["CART_FULFILLMENT"] = "cart-fulfillment"; COMPONENT_TYPE["CART_RETAILER_ALERT"] = "cart-retailer-alert"; COMPONENT_TYPE["CHECKOUT"] = "checkout"; COMPONENT_TYPE["CHECKOUT_INFORMATION_SECTION"] = "checkout-information-section"; COMPONENT_TYPE["CHECKOUT_STRIPE_FORM"] = "checkout-stripe-form"; COMPONENT_TYPE["CHECKOUT_PAYMENT_FORM"] = "checkout-payment-form"; COMPONENT_TYPE["CHECKOUT_BILLING_FORM"] = "checkout-billing-form"; COMPONENT_TYPE["CHECKOUT_SUMMARY_SECTION"] = "checkout-summary-section"; COMPONENT_TYPE["CHECKOUT_PROMO_CODE"] = "checkout-promo-code"; COMPONENT_TYPE["CHECKOUT_GIFT_CARDS"] = "checkout-gift-cards"; COMPONENT_TYPE["CHECKOUT_AMOUNTS"] = "checkout-amounts"; COMPONENT_TYPE["CHECKOUT_ITEMS"] = "checkout-items"; COMPONENT_TYPE["CHECKOUT_COMPLETED"] = "checkout-completed"; COMPONENT_TYPE["CHECKOUT_DELIVERY_INFORMATION_FORM"] = "checkout-delivery-information-form"; COMPONENT_TYPE["CHECKOUT_BUYER_INFORMATION_FORM"] = "checkout-buyer-information-form"; COMPONENT_TYPE["CHECKOUT_TIPS"] = "checkout-tips"; COMPONENT_TYPE["CHECKOUT_PC_GC"] = "checkout-pc-gc"; COMPONENT_TYPE["CHECKOUT_ITEM"] = "checkout-item"; COMPONENT_TYPE["CHECKOUT_ITEM_QUANTITY"] = "checkout-item-quantity"; COMPONENT_TYPE["CHECKOUT_PLACE_ORDER_BUTTON"] = "checkout-place-order-button"; })(COMPONENT_TYPE || (COMPONENT_TYPE = {})); var FULFILLMENT_TYPE; (function (FULFILLMENT_TYPE) { FULFILLMENT_TYPE["ON_DEMAND"] = "onDemand"; FULFILLMENT_TYPE["SHIPPING"] = "shipping"; })(FULFILLMENT_TYPE || (FULFILLMENT_TYPE = {})); const API_CLIENT_URL = { [ELEMENTS_ENV.LOCAL]: 'http://0.0.0.0:8080', [ELEMENTS_ENV.DEVELOPMENT]: 'https://dev.elements.liquidcommerce.cloud', [ELEMENTS_ENV.STAGING]: 'https://staging.elements.liquidcommerce.cloud', [ELEMENTS_ENV.PRODUCTION]: 'https://elements.liquidcommerce.cloud', }; class SingletonManager { constructor() { } /** * Sets the client constructor (call this once during app initialization) */ static setClientConstructor(lceConstructor) { SingletonManager.clientConstructor = lceConstructor; } /** * Gets the current client constructor */ static getClientConstructor() { return SingletonManager.clientConstructor; } /** * Retrieves an instance of the specified class using the provided instance creator function. * Used by services for their getInstance() methods. * * @template T The type of the instance. * @param {string} className The name of the class. * @param {() => T} instanceCreator The function that creates the instance. * @returns {T} The instance of the specified class. */ static getClassInstance(className, instanceCreator) { if (!SingletonManager.instances.has(className)) { SingletonManager.instances.set(className, instanceCreator()); } const instance = SingletonManager.instances.get(className); if (!instance) { throw new Error(`ElementsSdk: Instance for class ${className} could not be created.`); } return instance; } /** * Gets or creates a client instance for the given client configurations. * Uses the constructor set via setClientConstructor(). * * @template T The type of the client. * @param {IClientConfigs} clientConfigs The client configuration. * @returns {Promise<T>} The client instance. */ static async getClient(clientConfigs) { const clientInstanceKeyValues = ['apiKey', 'env', 'isBuilder', 'enableDebugging'] .map((key) => clientConfigs[key]) .join('_'); const clientInstanceKey = `LiquidCommerceElementsClient_${clientInstanceKeyValues}`; // Return existing instance if available if (SingletonManager.instances.has(clientInstanceKey)) { return SingletonManager.instances.get(clientInstanceKey); } // Ensure constructor is set if (!SingletonManager.clientConstructor) { throw new Error('LiquidCommerce Elements: Client constructor is not set.'); } // Create a new client instance const client = new SingletonManager.clientConstructor(clientConfigs); // Prepare the client await client.prepare(); // Store the instance SingletonManager.instances.set(clientInstanceKey, client); return client; } } SingletonManager.instances = new Map(); SingletonManager.clientConstructor = null; class ClientConfigService { constructor() { this.config = null; } static getInstance() { return SingletonManager.getClassInstance('ClientConfigService', () => new ClientConfigService()); } // ================================================ // INITIALIZATION // ================================================ /** * Initialize or update the config service with configuration * Can be called multiple times - will update the stored config */ initialize(apiKey, configInput = {}) { this.validateInputs(apiKey, configInput); this.config = Object.freeze(this.buildConfiguration(apiKey, configInput)); } // ================================================ // CONFIGURATION ACCESS // ================================================ /** * Get the full configuration object */ getConfigs() { this.ensureInitialized(); return { ...this.config }; } /** * Get a specific config value by key */ get(key) { this.ensureInitialized(); return this.config[key]; } /** * Check if service is initialized */ isInitialized() { return this.config !== null; } // ================================================ // ENVIRONMENT CHECKS // ================================================ isDevelopment() { return this.get('env') === ELEMENTS_ENV.DEVELOPMENT; } isStaging() { return this.get('env') === ELEMENTS_ENV.STAGING; } isProduction() { return this.get('env') === ELEMENTS_ENV.PRODUCTION; } isBuilder() { return this.get('isBuilder'); } isDebuggingEnabled() { return this.get('enableDebugging'); } debuggingDisabled() { return !this.get('enableDebugging'); } hasCustomTheme() { return this.get('customTheme') !== null; } // ================================================ // PRIVATE HELPERS // ================================================ validateInputs(apiKey, configInput) { if (!(apiKey === null || apiKey === void 0 ? void 0 : apiKey.trim())) { throw new Error('LiquidCommerce Elements: API key is required'); } if (configInput.env && !Object.values(ELEMENTS_ENV).includes(configInput.env)) { throw new Error(`LiquidCommerce Elements: Invalid environment "${configInput.env}"`); } } buildConfiguration(apiKey, configInput) { var _a, _b; const env = configInput.env || ELEMENTS_ENV.STAGING; const baseUrl = API_CLIENT_URL[env]; if (!baseUrl) { throw new Error(`LiquidCommerce Elements: No base URL configured for environment "${env}"`); } return { apiKey: apiKey.trim(), env, isBuilder: (_a = configInput.isBuilder) !== null && _a !== void 0 ? _a : false, enableDebugging: (_b = configInput.enableDebugging) !== null && _b !== void 0 ? _b : false, baseUrl, customTheme: configInput.customTheme || null, }; } ensureInitialized() { if (this.config === null) { throw new Error('LiquidCommerce Elements: Not initialized. Call initialize() first.'); } } } class LoggerService { constructor(context) { this.prefix = 'LiquidCommerce Elements'; this.colors = { debug: '#9CA3AF', // Light Gray log: '#60A5FA', // Bright Blue info: '#22D3EE', // Cyan warn: '#FB923C', // Orange error: '#F87171', // Bright Red prefix: '#C084FC', // Light Purple }; this.timestamp = true; this.useColors = true; this.enableLogging = false; this.context = context; } static getInstance(context) { return SingletonManager.getClassInstance('LoggerService', () => new LoggerService(context)); } /** * Enable or disable logging */ setEnableLogging(enable) { this.enableLogging = enable; } /** * Get formatted timestamp */ getTimestamp() { if (!this.timestamp) return ''; return new Date().toISOString().slice(11, 23); // HH:mm:ss.sss } /** * Get styled prefix with optional timestamp and context */ getPrefix(level) { if (!this.enableLogging) return []; const timestamp = this.getTimestamp(); const timeStr = timestamp ? `${timestamp} ` : ''; const contextStr = this.context ? ` ${this.context}` : ''; if (!this.useColors) { return [`[${timeStr}${this.prefix}${contextStr}]`]; } return [ `%c[${timeStr}%c${this.prefix}%c${contextStr}%c]`, `color: ${this.colors[level]}`, `color: ${this.colors.prefix}; font-weight: bold`, `color: ${this.colors[level]}`, `color: ${this.colors[level]}`, ]; } /** * Debug level logging */ debug(...args) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('debug'); console.debug(format, ...styles, ...args); } /** * Standard logging */ log(...args) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('log'); console.log(format, ...styles, ...args); } /** * Info level logging */ info(...args) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('info'); console.info(format, ...styles, ...args); } /** * Warning level logging */ warn(...args) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('warn'); console.warn(format, ...styles, ...args); } /** * Error level logging */ error(...args) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('error'); console.error(format, ...styles, ...args); } /** * Group logging */ group(label, collapsed = false) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('log'); if (collapsed) { console.groupCollapsed(format, ...styles, label); } else { console.group(format, ...styles, label); } } /** * End group logging */ groupEnd() { if (!this.enableLogging) return; console.groupEnd(); } /** * Table logging */ table(data) { if (!this.enableLogging) return; const [format, ...styles] = this.getPrefix('log'); console.log(format, ...styles, 'Table data:'); console.table(data); } } var description="LiquidCommerce Elements SDK";var version="1.0.0";var pkg = {description:description,version:version}; /* * Google Tag Manager Service * * Supported Events: * - view_item: Track when a user views an item * - view_item_list: Track when a user views a list of items * - select_item: Track when a user selects an item from a list * - add_to_cart: Track when a user adds an item to cart * - view_cart: Track when a user views their cart * - remove_from_cart: Track when a user removes an item from cart * - begin_checkout: Track when a user begins checkout * - add_shipping_info: Track when user adds shipping info during checkout * - add_payment_info: Track when user adds payment info during checkout * - purchase: Track completed purchases * - increaseQuantity: Helper method for quantity increases (uses add_to_cart) * - decreaseQuantity: Helper method for quantity decreases (uses remove_from_cart) * * All events automatically include ElementsSDK source tracking. * * Features: * - Event queueing: Events fired before initialization are queued and sent after GTM loads * - Multiple container support: Partner and LiquidCommerce GTM containers * - Error handling and recovery * - Safe execution with proper validation */ class GoogleTagManagerService { constructor() { this.isInitialized = false; this.isInitializing = false; this.containerLoadStatus = []; this.currency = 'USD'; this.SCRIPT_LOAD_TIMEOUT = 5000; // 5 seconds this.logger = LoggerService.getInstance(); // Event queue for events fired before GTM initialization this.eventQueue = []; this.MAX_QUEUE_SIZE = 100; // Prevent memory issues this.QUEUE_TIMEOUT = 30000; // 30 seconds max queue retention this.clientConfigService = ClientConfigService.getInstance(); } static getInstance() { return SingletonManager.getClassInstance('GoogleTagManagerService', () => new GoogleTagManagerService()); } /** * Wait for DOM to be ready */ waitForDOMReady() { return new Promise((resolve) => { if (typeof window === 'undefined') { resolve(); return; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => resolve()); } else { resolve(); } }); } /** * Initialize gtag function properly */ initializeGtag() { if (typeof window === 'undefined') { this.logger.error('GTM initialization failed: window object not available'); return false; } try { // Initialize dataLayer first window.dataLayer = window.dataLayer || []; // Initialize gtag function window.gtag = (...args) => { window.dataLayer.push(args); }; // Push initial GTM configuration window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js', }); // Verify gtag function is working if (typeof window.gtag !== 'function') { this.logger.error('GTM initialization failed: gtag function not properly initialized'); return false; } return true; } catch (error) { this.logger.error('GTM initialization failed:', error); return false; } } /** * Load a single GTM container script */ loadGTMScript(containerId) { return new Promise((resolve, reject) => { try { const script = document.createElement('script'); script.src = `https://www.googletagmanager.com/gtm.js?id=${containerId}`; script.async = true; const timeoutId = setTimeout(() => { script.onerror = null; script.onload = null; reject(new Error(`GTM script load timeout for container ${containerId}`)); }, this.SCRIPT_LOAD_TIMEOUT); script.onload = () => { clearTimeout(timeoutId); this.logger.info(`GTM container ${containerId} loaded successfully`); resolve(); }; script.onerror = () => { clearTimeout(timeoutId); reject(new Error(`Failed to load GTM container ${containerId}`)); }; document.head.appendChild(script); } catch (error) { reject(new Error(`Error creating script for container ${containerId}: ${error}`)); } }); } /** * Load all GTM containers */ async loadAllContainers(containerIds) { const loadPromises = containerIds.map(async (containerId) => { const status = { containerId, loaded: false, }; try { await this.loadGTMScript(containerId); status.loaded = true; } catch (error) { status.error = error instanceof Error ? error.message : 'Unknown error'; this.logger.warn(`GTM container ${containerId} failed to load:`, status.error); } this.containerLoadStatus.push(status); }); await Promise.all(loadPromises); // Check if all containers loaded successfully const failedContainers = this.containerLoadStatus.filter((status) => !status.loaded); if (failedContainers.length > 0) { this.logger.warn(`GTM partial initialization: ${failedContainers.length} of ${containerIds.length} containers failed to load`); for (const status of failedContainers) { this.logger.warn(`Failed container: ${status.containerId} - ${status.error}`); } } // Check if at least one container loaded const successfulContainers = this.containerLoadStatus.filter((status) => status.loaded); if (successfulContainers.length === 0) { throw new Error('All GTM containers failed to load'); } this.logger.info(`GTM initialization complete: ${successfulContainers.length} of ${containerIds.length} containers loaded successfully`); } /** * Process all queued events after GTM initialization */ processEventQueue() { if (this.eventQueue.length === 0) { return; } // Process events in order const processedEvents = []; for (const queuedEvent of this.eventQueue) { try { // Check if event is not too old (prevent processing stale events) const eventAge = Date.now() - queuedEvent.timestamp; if (eventAge > this.QUEUE_TIMEOUT) { this.logger.warn(`Skipping stale queued event: ${queuedEvent.methodName} (${eventAge}ms old)`); continue; } // Execute the queued event window.gtag('event', queuedEvent.eventName, queuedEvent.eventData); processedEvents.push(queuedEvent); } catch (error) { this.logger.error(`Error processing queued event ${queuedEvent.methodName}:`, error); } } // Clear processed events from queue this.eventQueue = []; } /** * Add event to queue when GTM is not ready */ queueEvent(methodName, eventName, eventData) { // Prevent queue from growing too large if (this.eventQueue.length >= this.MAX_QUEUE_SIZE) { this.logger.warn(`GTM event queue full. Dropping oldest event to make room for: ${methodName}`); this.eventQueue.shift(); // Remove oldest event } const queuedEvent = { methodName, eventName, eventData, timestamp: Date.now(), }; this.eventQueue.push(queuedEvent); } async initialize(config) { // Prevent multiple initialization attempts if (this.isInitialized || this.isInitializing) { return this.initializationPromise; } // Skip initialization in certain environments if (typeof window === 'undefined' || this.clientConfigService.isBuilder() || !config) { return Promise.resolve(); } this.isInitializing = true; this.initializationPromise = this._doInitialization(config); try { await this.initializationPromise; } catch (error) { this.isInitializing = false; this.initializationPromise = undefined; throw error; } return this.initializationPromise; } async _doInitialization(config) { try { // Validate required fields if (!config.partnerName) { throw new Error('GTM initialization failed: partnerName is required'); } // Build container IDs array based on enabled tracking flags const containerIdsToInitialize = []; if (config.partnerEnableGaTracking && config.partnerGtmId) { if (!config.partnerGtmId.startsWith('GTM-')) { throw new Error(`Invalid Partner GTM Container ID format: ${config.partnerGtmId}. Must start with "GTM-"`); } containerIdsToInitialize.push(config.partnerGtmId); } if (config.liquidCommerceEnableGaTracking && config.liquidCommerceGtmId) { if (!config.liquidCommerceGtmId.startsWith('GTM-')) { throw new Error(`Invalid LiquidCommerce GTM Container ID format: ${config.liquidCommerceGtmId}. Must start with "GTM-"`); } containerIdsToInitialize.push(config.liquidCommerceGtmId); } // If no containers are enabled, log warning and return if (containerIdsToInitialize.length === 0) { this.logger.warn('GTM initialization skipped: No tracking containers enabled'); this.isInitialized = true; this.isInitializing = false; return; } // Wait for DOM to be ready await this.waitForDOMReady(); // Initialize gtag function - if this fails, don't proceed if (!this.initializeGtag()) { throw new Error('GTM initialization failed: Could not initialize gtag function'); } // Load all GTM containers await this.loadAllContainers(containerIdsToInitialize); // Set configuration this.partnerName = config.partnerName; this.isInitialized = true; this.isInitializing = false; // Process any queued events after successful initialization this.processEventQueue(); } catch (error) { this.isInitializing = false; this.logger.error('GTM initialization failed:', error); throw error; } } /** * Safe method execution wrapper with event queueing */ safeExecute(methodName, eventName, eventDataFn) { try { if (this.clientConfigService.isBuilder()) { return; } // If not initialized, queue the event if (!this.isInitialized) { const eventData = eventDataFn(); this.queueEvent(methodName, eventName, eventData); return; } // Execute immediately if initialized const eventData = eventDataFn(); window.gtag('event', eventName, eventData); } catch (error) { this.logger.error(`GTM ${methodName} error:`, error); // Don't re-throw to avoid breaking SDK flow } } validateItem(item) { if (!item.item_id && !item.item_name) { throw new Error('Either item_id or item_name is required'); } } formatItemForEvent(item) { this.validateItem(item); return { ...item, quantity: item.quantity || 1, price: item.price || 0, }; } calculateValue(items) { return items.reduce((sum, item) => sum + (item.price || 0) * (item.quantity || 1), 0); } /** * Add ElementsSDK source tracking to all events */ addSourceTracking(eventData) { return { ...eventData, tenant_source: `${pkg.description} v${pkg.version}`, tenant_name: this.partnerName, }; } /** * Track when a user views an item * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#view_item */ viewItem(item) { this.safeExecute('viewItem', 'view_item', () => { const eventData = { currency: this.currency, value: this.calculateValue([item]), items: [this.formatItemForEvent(item)], }; return this.addSourceTracking(eventData); }); } /** * Track when a user views a list of items * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#view_item_list */ viewItemList(items, listId, listName) { this.safeExecute('viewItemList', 'view_item_list', () => { const eventData = { currency: this.currency, item_list_id: listId, item_list_name: listName, items: items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track when a user selects an item from a list * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#select_item */ selectItem(item, listId, listName) { this.safeExecute('selectItem', 'select_item', () => { const eventData = { item_list_id: listId, item_list_name: listName, items: [this.formatItemForEvent(item)], }; return this.addSourceTracking(eventData); }); } /** * Track when a user adds an item to cart * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_to_cart */ addToCart(item) { this.safeExecute('addToCart', 'add_to_cart', () => { const eventData = { currency: this.currency, value: this.calculateValue([item]), items: [this.formatItemForEvent(item)], }; return this.addSourceTracking(eventData); }); } /** * Track when a user views their cart * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#view_cart */ viewCart(items) { this.safeExecute('viewCart', 'view_cart', () => { const eventData = { currency: this.currency, value: this.calculateValue(items), items: items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track when a user removes an item from cart * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#remove_from_cart */ removeFromCart(item) { this.safeExecute('removeFromCart', 'remove_from_cart', () => { const eventData = { currency: this.currency, value: this.calculateValue([item]), items: [this.formatItemForEvent(item)], }; return this.addSourceTracking(eventData); }); } /** * Track when a user begins checkout * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#begin_checkout */ beginCheckout(items, coupon) { this.safeExecute('beginCheckout', 'begin_checkout', () => { const eventData = { currency: this.currency, value: this.calculateValue(items), coupon, items: items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track when user adds shipping info during checkout * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_shipping_info */ addShippingInfo(items, shippingTier, coupon) { this.safeExecute('addShippingInfo', 'add_shipping_info', () => { const eventData = { currency: this.currency, value: this.calculateValue(items), shipping_tier: shippingTier, coupon, items: items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track when user adds payment info during checkout * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_payment_info */ addPaymentInfo(items, paymentType, coupon) { this.safeExecute('addPaymentInfo', 'add_payment_info', () => { const eventData = { currency: this.currency, value: this.calculateValue(items), payment_type: paymentType, coupon, items: items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track a completed purchase * https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase */ purchase(purchaseData) { this.safeExecute('purchase', 'purchase', () => { const eventData = { transaction_id: purchaseData.transaction_id, value: purchaseData.value, currency: this.currency, tax: purchaseData.tax, shipping: purchaseData.shipping, coupon: purchaseData.coupon, items: purchaseData.items.map((item) => this.formatItemForEvent(item)), }; return this.addSourceTracking(eventData); }); } /** * Track when a user increases product quantity in cart */ increaseQuantity(item) { this.safeExecute('increaseQuantity', 'add_to_cart', () => { // Use add_to_cart event for quantity increases const eventData = { currency: this.currency, value: item.price || 0, items: [this.formatItemForEvent({ ...item, quantity: 1 })], }; return this.addSourceTracking(eventData); }); } /** * Track when a user decreases product quantity in cart */ decreaseQuantity(item) { this.safeExecute('decreaseQuantity', 'remove_from_cart', () => { // Use remove_from_cart event for quantity decreases const eventData = { currency: this.currency, value: item.price || 0, items: [this.formatItemForEvent({ ...item, quantity: 1 })], }; return this.addSourceTracking(eventData); }); } } const mainComponents = [COMPONENT_TYPE.ADDRESS, COMPONENT_TYPE.PRODUCT, COMPONENT_TYPE.CART, COMPONENT_TYPE.CHECKOUT]; const productGroupComponents = [ COMPONENT_TYPE.PRODUCT, COMPONENT_TYPE.PRODUCT_IMAGE_CAROUSEL, COMPONENT_TYPE.PRODUCT_OPTIONS, COMPONENT_TYPE.PRODUCT_INTERACTIONS, COMPONENT_TYPE.PRODUCT_DESCRIPTION, COMPONENT_TYPE.PRODUCT_RETAILERS_CAROUSEL, COMPONENT_TYPE.PRODUCT_RETAILERS_POPUP, COMPONENT_TYPE.PRODUCT_RETAILERS_POPUP_LIST, COMPONENT_TYPE.PRODUCT_PRICE, COMPONENT_TYPE.PRODUCT_ADD_TO_CART_SECTION, COMPONENT_TYPE.PRODUCT_ENGRAVING, COMPONENT_TYPE.PRODUCT_DRAWER, COMPONENT_TYPE.PRODUCT_LOADING, ]; const addressGroupComponents = [COMPONENT_TYPE.ADDRESS]; const cartGroupComponents = [ COMPONENT_TYPE.CART, COMPONENT_TYPE.CART_RETAILER, COMPONENT_TYPE.CART_ITEM, COMPONENT_TYPE.CART_ITEM_ENGRAVING, COMPONENT_TYPE.CART_FOOTER, COMPONENT_TYPE.CART_ITEM_QUANTITY_PRICE, COMPONENT_TYPE.CART_RETAILER_SUBTOTAL, COMPONENT_TYPE.CART_PROMO_CODE, COMPONENT_TYPE.CART_HEADER, COMPONENT_TYPE.CART_BODY, COMPONENT_TYPE.CART_FULFILLMENT, COMPONENT_TYPE.CART_RETAILER_ALERT, ]; const checkoutGroupComponents = [ COMPONENT_TYPE.CHECKOUT, COMPONENT_TYPE.CHECKOUT_INFORMATION_SECTION, COMPONENT_TYPE.CHECKOUT_PAYMENT_FORM, COMPONENT_TYPE.CHECKOUT_STRIPE_FORM, COMPONENT_TYPE.CHECKOUT_BILLING_FORM, COMPONENT_TYPE.CHECKOUT_SUMMARY_SECTION, COMPONENT_TYPE.CHECKOUT_PROMO_CODE, COMPONENT_TYPE.CHECKOUT_GIFT_CARDS, COMPONENT_TYPE.CHECKOUT_COMPLETED, COMPONENT_TYPE.CHECKOUT_DELIVERY_INFORMATION_FORM, COMPONENT_TYPE.CHECKOUT_BUYER_INFORMATION_FORM, COMPONENT_TYPE.CHECKOUT_TIPS, COMPONENT_TYPE.CHECKOUT_PC_GC, COMPONENT_TYPE.CHECKOUT_ITEM, COMPONENT_TYPE.CHECKOUT_ITEM_QUANTITY, ]; [COMPONENT_TYPE.DRAWER, COMPONENT_TYPE.INPUT, COMPONENT_TYPE.ENGRAVING_FORM]; const isMainComponent = (componentType) => { return mainComponents.includes(componentType); }; class FontManagerService { constructor() { this.googleFontsUrl = ''; this.defaultFontFamilies = [{ name: 'Poppins', weights: [400, 500, 600, 700] }]; } loadGoogleFonts(fonts, globalDisplay = 'swap') { if (!fonts || fonts.length === 0) return; const finalFonts = [...this.defaultFontFamilies, ...fonts]; const familyParams = finalFonts .map((font) => { const encodedFamily = encodeURIComponent(font.name); return `family=${encodedFamily}:wght@${font.weights.join(';')}`; }) .join('&'); const displayParam = `display=${globalDisplay}`; this.googleFontsUrl = `https://fonts.googleapis.com/css2?${familyParams}&${displayParam}`; this.injectGoogleFontsResourceHints(); this.injectGoogleFontsLink(); } updateGoogleFonts(fonts, globalDisplay = 'swap') { // Store reference to an existing link before adding a new one const existingLink = document.querySelector('link[data-liquid-fonts]'); // Add new fonts first to ensure a smooth transition this.loadGoogleFonts(fonts, globalDisplay); // Remove the previous link after new fonts are loaded // This prevents flash of an unstyled text (FOUT) if (existingLink) { existingLink.remove(); } } injectGoogleFontsResourceHints() { // Check if resource hints already exist to avoid duplicates const existingPreconnectApi = document.querySelector('link[href="https://fonts.googleapis.com"][rel="preconnect"]'); const existingPreconnectCdn = document.querySelector('link[href="https://fonts.gstatic.com"][rel="preconnect"]'); if (!existingPreconnectApi) { const preconnectApi = document.createElement('link'); preconnectApi.rel = 'preconnect'; preconnectApi.href = 'https://fonts.googleapis.com'; preconnectApi.setAttribute('data-liquid-fonts-hint', ''); document.head.appendChild(preconnectApi); } if (!existingPreconnectCdn) { const preconnectCdn = document.createElement('link'); preconnectCdn.rel = 'preconnect'; preconnectCdn.href = 'https://fonts.gstatic.com'; preconnectCdn.crossOrigin = ''; preconnectCdn.setAttribute('data-liquid-fonts-hint', ''); document.head.appendChild(preconnectCdn); } } injectGoogleFontsLink() { if (!this.googleFontsUrl) return; const linkElement = document.createElement('link'); linkElement.rel = 'stylesheet'; linkElement.href = this.googleFontsUrl; linkElement.setAttribute('data-liquid-fonts', ''); document.head.appendChild(linkElement); } } const getAddressStyles = () => ` .address-container { width: 100%; max-width: 100%; min-width: 350px; height: 100%; font-family: var(--heading-font-family, Poppins); position: relative; display: flex; flex-direction: column; gap: 16px; } .address-container .address-title { overflow: hidden; color: var(--default-text-color, #18181B); text-overflow: ellipsis; font-family: var(--heading-font-family, Poppins); font-size: 14px; font-style: normal; font-weight: 500; line-height: 100%; padding-bottom: 8px; } .address-container .address-input-container { position: relative; } .address-container .address-input-wrapper { display: flex; flex-direction: row; align-items: center; gap: 4px; align-self: stretch; padding: 4px 12px; border-radius: var(--card-border-radius, 6px); border: 1px solid var(--accent-color, #E4E4E7); box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); } .address-container .search-icon-wrapper svg { stroke: var(--default-text-color, #18181B); } /* Remove bottom border radius when suggestions are shown */ .address-container .suggestions-container.show ~ .address-input-wrapper, .address-container .address-input-container:has(.suggestions-container.show) .address-input-wrapper { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } /* Alternative approach using a modifier class */ .address-container .address-input-wrapper.suggestions-open { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .address-container .address-input { overflow: hidden; color: var(--default-text-color, #18181B); text-overflow: ellipsis; font-family: var(--heading-font-family, Poppins); font-size: 14px; font-style: normal; font-weight: 400; line-height: 20px; width: 100%; border: none } .address-container .address-input:focus { outline: none; border: none; box-shadow: none; } .address-container .suggestions-container { position: absolute; top: 100%; left: 0; right: 0; z-index: 1000; list-style: none; margin: 0; padding: 0; overflow-y: auto; background: white; border: 1px solid var(--accent-color, #E4E4E7); border-radius: 0 0 8px 8px; border-top: none; } .address-container .suggestions-container.show { border-top-left-radius: 0; border-top-right-radius: 0; } .address-container .suggestions-container.hide { display: none; } .address-container .suggestions-container.show { display: block; } .address-container .suggestion-item { padding: 12px; cursor: pointer; border-bottom: 1px solid var(--accent-color, #E4E4E7); transition: background-color 0.2s ease; } .address-container .suggestion-item:last-child { border-bottom: none; } .address-container .suggestion-item:hover { background-color: var(--default-text-color-30, #F3F4F6); } .address-container .suggestion-item.no-suggestions { color: var(--warning-color, #6B7280); text-align: center; cursor: default; } .address-container .suggestion-item.no-suggestions:hover { background-color: transparent; } .address-container .error-message { display: flex; align-items: center; gap: 8px; padding: 12px; border: 1px solid var(--error-color, #B91C1C); border-radius: 6px; color: var(--error-color, #B91C1C); font-size: 14px; position: relative; z-index: 999; } .address-container .error-icon { font-size: 16px; } .address-container .error-text { flex: 1; } .address-container .address-actions { display: flex; padding-top: 16px; justify-content: space-between; gap: 8px; position: relative; z-index: 998; } .address-container .address-actions > button { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 8px 16px; border-radius: var(--button-border-radius, 6px); transition: all 0.2s ease; width: 50%; font-family: var(--typography-font-family-font-sans, Poppins); font-size: 14px; font-style: normal; font-weight: 500; line-height: 20px; } .address-container .address-actions .cancel-button { color: var(--default-text-color, #18181B); border: 1px solid var(--accent-color, #E4E4E7); } .address-container .address-actions .check-button { color: var(--selected-text-color, #18181B); background-color: var(--primary-color, #E4E4E7); border: 1px solid transparent; } .address-container .address-actions .check-button:first-child { width: 100%;