@liquidcommerceteam/elements-sdk
Version:
LiquidCommerce Elements SDK
1,292 lines (1,255 loc) • 685 kB
JavaScript
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%;