From 9841962b0b81b92930c4a6e1a4471dc2c0c134bf Mon Sep 17 00:00:00 2001 From: Alan Thai Date: Fri, 12 Apr 2024 11:11:35 -0400 Subject: [PATCH] Update analytics (#211) * Fix incorrect selector (#210) * refactor account checkbox * PR fixes * Analytics rename --- .../cartridge/client/default/js/account.js | 5 +- .../cartridge/client/default/js/analytics.js | 16 -- .../client/default/js/analytics/actions.js | 212 ++++++++++++++++++ .../client/default/js/analytics/constants.js | 25 +++ .../default/js/analytics/event-emitter.js | 36 +++ .../client/default/js/analytics/index.js | 126 +++++++++++ .../client/default/js/analytics/run.js | 115 ++++++++++ .../client/default/js/checkout/checkout.js | 23 +- .../cartridge/client/default/js/constant.js | 1 - .../default/js/eventListenerRegistration.js | 9 +- .../client/default/js/tokenization.js | 4 +- .../templates/default/boltEmbed.isml | 33 ++- 12 files changed, 559 insertions(+), 46 deletions(-) delete mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics.js create mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/actions.js create mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/constants.js create mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/event-emitter.js create mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/index.js create mode 100644 cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/run.js diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/account.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/account.js index fdf7377d..05a23c09 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/account.js +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/account.js @@ -1,8 +1,6 @@ 'use strict'; var util = require('./util.js'); -var constants = require('./constant.js'); -var analytics = require('./analytics.js'); /** * Auto log the user into their bolt account @@ -211,7 +209,7 @@ exports.setupListeners = async function () { }); Bolt.on('auto_account_check_complete', response => { - const $accountCheckbox = $('#acct-checkbox'); + const $accountCheckbox = $(window.BoltSelectors.boltAccountCheckbox); if (response.result instanceof Error) { if (response.result.message === 'Invalid email') { $('.submit-customer').attr('disabled', 'true'); @@ -228,7 +226,6 @@ exports.setupListeners = async function () { $accountCheckbox.show(); } } - analytics.checkoutStepComplete(constants.EventAccountRecognitionCheckPerformed, { hasBoltAccount: response.result, detectionMethod: 'email' }); }); }; diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics.js deleted file mode 100644 index 5535177a..00000000 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -/** - * Checkout step complete event handler. - * @param {string} eventName - The name of the event. - * @param {Object} props - The event properties. - */ -function checkoutStepComplete(eventName, props) { - if (window.BoltAnalytics == null) { - return; - } - - window.BoltAnalytics.checkoutStepComplete(eventName, props); -} - -exports.checkoutStepComplete = checkoutStepComplete; diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/actions.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/actions.js new file mode 100644 index 00000000..b77307a9 --- /dev/null +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/actions.js @@ -0,0 +1,212 @@ +/* eslint-disable no-restricted-syntax */ + +'use strict'; + +const click = event('click'); + +module.exports = { + waitFor, + urlMatchParts, + observeSelector, + render, + event, + click, + input: event('input'), + change: event('change'), + clickOnce: event('click', true), + inputOnce: event('input', true), + areAllFieldsFilled, + accountRegistrationCheckbox, + stepChange +}; + +const noop = () => {}; + +// Don't use this function for selector matching. Use observeSelector instead. +/** + * Waits for a condition to be true and emits an event. + * @param {Function} fn - The condition function. + * @param {Function} [eventData=noop] - The event data function. + * @returns {boolean} - True if the condition is met, false otherwise. + */ +function waitFor(fn, eventData = noop) { + return (emit, context) => { + if (fn(context)) { + emit(eventData(context)); + return true; + } + return false; + }; +} + +/** + * Matches URL parts and waits for the condition to be true. + * @param {string[]} urlSubstrings - The URL substrings to match. + * @param {Function} [eventData=noop] - The event data function. + * @param {Location} [location=window.location] - The location object. + * @returns {boolean} - True if the condition is met, false otherwise. + */ +function urlMatchParts(urlSubstrings, eventData = noop, location = window.location) { + const lowercase = urlSubstrings.map(url => url.toLowerCase()); + const partsMatch = () => lowercase.every(part => location.href.toLowerCase().includes(part)); + + return waitFor(partsMatch, eventData); +} + +/** + * Observes elements matching a selector and invokes a callback for each element. + * @param {string} selector - The CSS selector. + * @param {Function} callback - The callback function. + * @returns {boolean} - Always returns true. + */ +function observeSelector(selector, callback) { + return (emit, context) => { + const elements = document.querySelectorAll(selector); + + for (const element of elements) { + callback(emit, context, element, { disconnect: () => undefined }); + } + + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (!(node instanceof HTMLElement)) { + // eslint-disable-next-line no-continue + continue; + } + + const target = node.matches(selector) ? node : node.querySelector(selector); + if (target != null) { + callback(emit, context, target, observer); + } + } + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); + + return true; + }; +} + +/** + * Renders the specified selector and emits an event. + * @param {string} selector - The CSS selector. + * @param {Function} [eventData=noop] - The event data function. + * @returns {boolean} - Always returns true. + */ +function render(selector, eventData = noop) { + return observeSelector(selector, (emit, context, target, observer) => { + emit(eventData({ context, target })); + observer.disconnect(); + }); +} + +/** + * Creates an event listener function. + * @param {string} name - The event name. + * @param {boolean} once - Whether the event should be listened to only once. + * @returns {Function} - The event listener function. + */ +function event(name, once = false) { + return function (selector, eventData = noop) { + return observeSelector(selector, (emit, context, target, observer) => { + target.addEventListener(name, (eventObj) => { + emit(eventData({ event: eventObj, context })); + + if (once) { + observer.disconnect(); + } + }, { once }); + }); + }; +} + +/** + * Checks if all fields specified by the selectors are filled. + * @param {string[]} selectors - The CSS selectors for the fields. + * @returns {boolean} - True if all fields are filled, false otherwise. + */ +function areAllFieldsFilled(selectors) { + const unsubscribes = []; + const elementsMap = new Map(); + + return (emit, context) => { + for (const selector of selectors) { + observeSelector(selector, (_, _1, target, observer) => { + elementsMap.set(selector, target); + + const onBlur = () => { + const allFilled = Array.from(elementsMap.values()).every(input => input.value !== ''); + if (allFilled) { + emit(); + unsubscribes.forEach(unsubscribe => unsubscribe()); + } + }; + target.addEventListener('blur', onBlur); + + unsubscribes.push(() => { + target.removeEventListener('blur', onBlur); + observer.disconnect(); + }); + })(emit, context); + } + + return true; + }; +} + +/** + * Returns an array of functions for handling account registration checkbox. + * @param {string} selector - The CSS selector for the checkbox. + * @returns {Function[]} - Array of functions. + */ +function accountRegistrationCheckbox(selector) { + return [ + render(selector, ({ context, target }) => accountRegistrationChecked(context, target)), + click(selector, ({ context, event: clickEvent }) => accountRegistrationChecked(context, clickEvent.target)) + ]; +} + +/** + * Updates the account registration checked status in the context and returns the updated status. + * @param {Object} context - The context object. + * @param {HTMLElement} target - The target element. + * @returns {Object} - The updated status object. + */ +function accountRegistrationChecked(context, target) { + context.summary.createBoltAccountChecked = target.checked; + return { checked: context.summary.createBoltAccountChecked }; +} + +/** + * Performs a step change. + * @param {string} selector - The CSS selector. + * @param {string} stage - The stage of the step change. + * @param {Function} [eventData=noop] - The event data function. + * @returns {boolean} - Always returns true. + */ +function stepChange(selector, stage, eventData = noop) { + return (emit, context) => { + const target = document.querySelector(selector); + const initStage = target.getAttribute('data-checkout-stage'); + if (initStage === stage) { + emit(eventData(context)); + return true; + } + + const stageObserver = new MutationObserver(function (mutations) { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-checkout-stage') { + const currentStage = mutation.target.getAttribute('data-checkout-stage'); + if (currentStage === stage) { + emit(eventData(context)); + } + } + } + }); + stageObserver.observe(target, { attributes: true, attributeFilter: ['data-checkout-stage'] }); + + return true; + }; +} diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/constants.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/constants.js new file mode 100644 index 00000000..6735246c --- /dev/null +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/constants.js @@ -0,0 +1,25 @@ +'use strict'; + +exports.EventName = { + CHECKOUT_LOADED: 'checkout . loaded', + + RECOGNITION_EMAIL_FIELD_CHANGED: 'recognition . email field changed', + + SHIPPING_STEP_ENTERED: 'shipping . step entered', + SHIPPING_DETAILS_FULLY_ENTERED: 'shipping . details fully entered', + SHIPPING_OPTION_SELECTED: 'shipping . option selected', + SHIPPING_SUBMITTED: 'shipping . submitted', + SHIPPING_EDIT_BUTTON_CLICKED: 'shipping . edit button clicked', + + BILLING_STEP_ENTERED: 'billing . step entered', + BILLING_ADDRESS_DETAILS_ENTERED: 'billing . address details entered', + BILLING_PAYMENT_METHOD_SELECTED: 'billing . payment method selected', + BILLING_SUBMITTED: 'billing . submitted', + BILLING_EDIT_BUTTON_CLICKED: 'billing . edit button clicked', + + PAYMENT_STEP_ENTERED: 'payment . step entered', + PAYMENT_PAY_BUTTON_CLICKED: 'payment . pay button clicked', + PAYMENT_SUCCEEDED: 'payment . succeeded', + PAYMENT_FAILED: 'payment . failed', + PAYMENT_TOGGLE_REGISTRATION_CHECKBOX: 'payment . toggle registration checkbox' +}; diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/event-emitter.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/event-emitter.js new file mode 100644 index 00000000..9259af88 --- /dev/null +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/event-emitter.js @@ -0,0 +1,36 @@ +'use strict'; + +class EventEmitter { + constructor() { + this.events = {}; + } + + on(eventName, listener) { + if (!this.events[eventName]) { + this.events[eventName] = []; + } + this.events[eventName].push(listener); + + return () => { + this.events[eventName] = this.events[eventName].filter(l => l !== listener); + }; + } + + once(eventName, listener) { + const off = this.on(eventName, data => { + off(); + listener(data); + }); + } + + emit(eventName, data) { + const listeners = this.events[eventName]; + if (listeners) { + listeners.forEach(listener => { + listener(data); + }); + } + } +} + +module.exports = new EventEmitter(); diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/index.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/index.js new file mode 100644 index 00000000..397d8bb2 --- /dev/null +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/index.js @@ -0,0 +1,126 @@ +'use strict'; + +const account = require('../account'); +const action = require('./actions'); +const { run } = require('./run'); +const { EventName } = require('./constants'); +const eventEmitter = require('./event-emitter'); + +const initializeAnalytics = async () => { + await account.waitForBoltReady; + + run({ + [EventName.CHECKOUT_LOADED]: [ + action.urlMatchParts(['checkout'], context => { + const cartValue = document.querySelector('.grand-total-sum').textContent || ''; + const cartSubtotal = document.querySelector('.sub-total').textContent || ''; + const cart = buildCart(cartValue, cartSubtotal); + context.summary = Object.assign(context.summary, cart); + + return cart; + }) + ], + [EventName.RECOGNITION_EMAIL_FIELD_CHANGED]: [action.input(window.BoltSelectors.checkoutEmailField)], + + [EventName.SHIPPING_STEP_ENTERED]: [ + action.stepChange(window.BoltSelectors.checkoutStepTracker, 'shipping') + ], + [EventName.SHIPPING_DETAILS_FULLY_ENTERED]: [ + action.areAllFieldsFilled([ + window.BoltSelectors.shippingFirstName, + window.BoltSelectors.shippingLastName, + window.BoltSelectors.shippingAddress1, + window.BoltSelectors.shippingZipCode, + window.BoltSelectors.shippingPhoneNumber + ]) + ], + // Shipping option is custom to each merchant. Skip + // [EventName.SHIPPING_OPTION_SELECTED]: [], + [EventName.SHIPPING_SUBMITTED]: [action.clickOnce(window.BoltSelectors.shippingSubmitButton)], + [EventName.SHIPPING_EDIT_BUTTON_CLICKED]: [action.clickOnce(window.BoltSelectors.shippingEditButton)], + + [EventName.BILLING_STEP_ENTERED]: [ + action.stepChange(window.BoltSelectors.checkoutStepTracker, 'payment') + ], + [EventName.BILLING_ADDRESS_DETAILS_ENTERED]: [ + action.areAllFieldsFilled([ + window.BoltSelectors.billingFirstName, + window.BoltSelectors.billingLastName, + window.BoltSelectors.billingAddress1, + window.BoltSelectors.billingCity, + window.BoltSelectors.billingZipCode + ]) + ], + // Billing option is custom to each merchant. Skip + // [EventName.BILLING_PAYMENT_METHOD_SELECTED]: [], + [EventName.BILLING_SUBMITTED]: [action.click(window.BoltSelectors.billingSubmitButton)], + [EventName.BILLING_EDIT_BUTTON_CLICKED]: [ + action.clickOnce(window.BoltSelectors.billingEditButton) + ], + + [EventName.PAYMENT_STEP_ENTERED]: [ + action.stepChange(window.BoltSelectors.checkoutStepTracker, 'placeOrder') + ], + [EventName.PAYMENT_PAY_BUTTON_CLICKED]: [action.clickOnce(window.BoltSelectors.payButton)], + [EventName.PAYMENT_TOGGLE_REGISTRATION_CHECKBOX]: action.accountRegistrationCheckbox(window.BoltSelectors.boltAccountCheckbox), + [EventName.PAYMENT_SUCCEEDED]: [ + (emit) => { + eventEmitter.once(EventName.PAYMENT_SUCCEEDED, () => emit()); + }, + action.stepChange(window.BoltSelectors.checkoutStepTracker, 'submitted') + ], + [EventName.PAYMENT_FAILED]: [ + ((emit) => { + const errorAlert = document.querySelector('.alert-danger.error-message'); + + if (errorAlert == null) { + return false; + } + + const observer = new MutationObserver(function (mutationsList) { + // eslint-disable-next-line no-restricted-syntax + for (const mutation of mutationsList) { + if (mutation.type === 'attributes' && mutation.attributeName === 'style') { + const displayStyle = errorAlert != null && errorAlert.style.display; + if (displayStyle !== 'none') { + emit({ error: errorAlert.textContent.trim() }); + } + } + } + }); + + observer.observe(errorAlert, { attributes: true, attributeFilter: ['style'] }); + + return true; + }), + (emit) => { + eventEmitter.once(EventName.PAYMENT_FAILED, () => emit()); + } + ] + }); +}; + +exports.initializeAnalytics = initializeAnalytics; + +/** + * @param {string} total Total sum of the cart + * @param {string} subtotal Subtotal of the cart + * @returns {Object} representing the cart totals + */ +function buildCart(total, subtotal) { + return { + cartValue: currencyToNumber(total), + cartSubtotal: currencyToNumber(subtotal), + cartTotalRaw: total || '', + cartSubtotalRaw: subtotal || '' + }; +} + +/** + * @param {string} raw string representing a currency + * @returns {string} representing a number or '-' if the input is not a valid currency + */ +function currencyToNumber(raw = '') { + const currency = Number(raw.replace(/[^0-9.]/g, '')); + return Number.isNaN(currency) ? '-' : currency; +} diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/run.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/run.js new file mode 100644 index 00000000..d3f4a175 --- /dev/null +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/analytics/run.js @@ -0,0 +1,115 @@ +'use strict'; + +const { EventName } = require('./constants'); +const eventEmitter = require('./event-emitter'); + +module.exports = { + run, + paymentSucceeded() { + eventEmitter.emit(EventName.PAYMENT_COMPLETE); + }, + paymentFailed() { + eventEmitter.emit(EventName.PAYMENT_REJECTED); + } +}; + +/** + * Runs the checkout event listeners and attaches them to the DOM. + * @param {Object} checkoutEventListeners - The checkout event listeners. + * @returns {Function} - A function to disconnect the observer and clear the attached listeners. + */ +function run(checkoutEventListeners) { + const attachedListeners = new Set(); + + addGlobalProperties({ source: 'sfcc' }); + + const sharedContext = { emittedEvents, summary: {} }; + + // Attempt to attach listeners to the DOM immediately + attachListeners(checkoutEventListeners, sharedContext, attachedListeners); + + // If the listener was not attached above, it will be attempted to be attached again on every DOM change. + // This is necessary because the element is not always rendered immediately and may depend on an action to occur. + const observer = new MutationObserver(function () { + attachListeners(checkoutEventListeners, sharedContext, attachedListeners); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true + }); + + return () => { + observer.disconnect(); + attachedListeners.clear(); + }; +} + +/** + * Attaches listeners to the checkout event listeners and emits events with properties. + * @param {Object} checkoutEventListeners - The checkout event listeners. + * @param {Object} context - The context object. + * @param {Set} attachedListeners - The attached listeners. + */ +function attachListeners(checkoutEventListeners, context, attachedListeners) { + Object.entries(checkoutEventListeners).forEach(([eventName, listeners]) => { + listeners.forEach((listener) => { + if (attachedListeners.has(listener)) { + return; + } + + const emit = (data, isCustomEvent = false) => { + emitEventWithProperties(eventName, { eventData: () => data, isCustomEvent }, context); + }; + const attached = listener(emit, context); + if (attached) { + attachedListeners.add(listener); + } + }); + }); +} + +const emittedEvents = new Set(); +// By default, duplicated events are not emitted. This is to prevent +// network latency and perceived client-side performance issues. Some +// events should be emitted multiple times such as user element interaction +const allowedDuplicateEvents = new Set([ + EventName.SHIPPING_OPTION_SELECTED, + EventName.BILLING_PAYMENT_METHOD_SELECTED, + EventName.PAYMENT_PAY_BUTTON_CLICKED, + EventName.PAYMENT_TOGGLE_REGISTRATION_CHECKBOX, + EventName.PAYMENT_FAILED +]); + +/** + * Emits a checkout funnel event. By default, events are only emitted once. To emit duplicate events, pass { includeDuplicates: true } in the options argument. + * @param {string} eventName - The name of the event to emit. + * @param {Object} listener - The listener object. + * @param {Object} context - The context object. + */ +function emitEventWithProperties(eventName, listener, context) { + const eventData = listener.eventData ? listener.eventData(context) : undefined; + const allowDuplicateEvent = allowedDuplicateEvents.has(eventName); + + if (!allowDuplicateEvent && emittedEvents.has(eventName)) { + return; + } + + emittedEvents.add(eventName); + + if (window.BoltAnalytics == null) { + return; + } + window.BoltAnalytics.checkoutStepComplete(eventName, eventData); +} + +/** + * Adds global properties to Bolt Analytics. + * @param {Object} properties - The global properties to add. + */ +function addGlobalProperties(properties) { + if (window.BoltAnalytics == null) { + return; + } + window.BoltAnalytics.addGlobalProperties(properties); +} diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/checkout/checkout.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/checkout/checkout.js index 994e1908..35ba2332 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/checkout/checkout.js +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/checkout/checkout.js @@ -9,8 +9,7 @@ var scrollAnimate = require('base/components/scrollAnimate'); var billingHelpers = require('./billing'); var addressHelpers = require('./address'); var account = require('../account'); -var analytics = require('../analytics'); -var constants = require('../constant'); +var analytics = require('../analytics/run'); const { boltIgniteEnabled, isShopperLoggedIn: isBoltShopperLoggedIn } = window.BoltConfig || {}; @@ -253,17 +252,6 @@ const { boltIgniteEnabled, isShopperLoggedIn: isBoltShopperLoggedIn } = window.B } }); } - const eventPayload = { loginStatus: isBoltShopperLoggedIn ? 'logged-in' : 'guest' }; - - // sending both shipping event here as we don't know - // when the action is complete unless shopper clicks continue button - analytics.checkoutStepComplete( - constants.EventShippingDetailsFullyEntered, - eventPayload - ); - analytics.checkoutStepComplete( - constants.EventShippingMethodStepComplete - ); return defer; } if (stage === 'payment') { return wrapQ(async () => { // eslint-disable-line consistent-return @@ -437,10 +425,6 @@ const { boltIgniteEnabled, isShopperLoggedIn: isBoltShopperLoggedIn } = window.B } }); }); - // sending both shipping event here as we don't know when the action is complete unless - // shopper clicks continue button - analytics.checkoutStepComplete(constants.EventPaymentMethodSelected); - analytics.checkoutStepComplete(constants.EventPaymentDetailsFullyEntered); }); // return defer; } if (stage === 'placeOrder') { @@ -482,18 +466,17 @@ const { boltIgniteEnabled, isShopperLoggedIn: isBoltShopperLoggedIn } = window.B value: data.orderToken }); - analytics.checkoutStepComplete(constants.EventPaymentComplete); + analytics.paymentSucceeded(); redirect.submit(); defer.resolve(data); } }, error: function () { // enable the placeOrder button here - analytics.checkoutStepComplete(constants.EventPaymentRejected); + analytics.paymentFailed(); $('body').trigger('checkout:enableButton', $('.next-step-button button')); } }); - analytics.checkoutStepComplete(constants.EventClickPayButton); return defer; } diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/constant.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/constant.js index f05a29d6..aca0b4d6 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/constant.js +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/constant.js @@ -1,6 +1,5 @@ 'use strict'; -exports.EventAccountRecognitionCheckPerformed = 'Account recognition check performed'; exports.EventDetailEntryBegan = 'Detail entry began'; exports.EventShippingDetailsFullyEntered = 'Shipping details fully entered'; exports.EventShippingMethodStepComplete = 'Shipping method step complete'; diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/eventListenerRegistration.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/eventListenerRegistration.js index d5bf986c..23152b85 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/eventListenerRegistration.js +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/eventListenerRegistration.js @@ -2,10 +2,11 @@ const account = require('./account'); const boltStoredPayment = require('./boltStoredPayments'); +const { initializeAnalytics } = require('./analytics'); const { isShopperLoggedIn: isBoltShopperLoggedIn } = (window.BoltConfig || {}); -$(document).ready(async function () { +$(async function () { $('.submit-customer').attr('disabled', 'true'); // mount on the div container otherwise the iframe won't render @@ -74,12 +75,16 @@ $(document).ready(function () { }); // mount login status component -$(document).ready(async function () { +$(async function () { await account.waitForBoltReady(); account.mountLoginStatusComponent(); }); +$(async function () { + initializeAnalytics(); +}); + /** * Due to a limitation of login status component * Some browser like safari/chrome incognito is not diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/tokenization.js b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/tokenization.js index 82d827c8..5afa9555 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/tokenization.js +++ b/cartridges/int_bolt_embedded_sfra/cartridge/client/default/js/tokenization.js @@ -28,9 +28,9 @@ var getOrCreatePaymentComponent = function () { }; var renderBoltCreateAccountCheckField = function () { - if (window.Bolt && $('#acct-checkbox').length > 0) { + if (window.Bolt && $(window.BoltSelectors.boltAccountCheckbox).length > 0) { accountCheck = Bolt.create('account_checkbox', accountCheckOptions); - accountCheck.mount('#acct-checkbox'); + accountCheck.mount(window.BoltSelectors.boltAccountCheckbox); } }; diff --git a/cartridges/int_bolt_embedded_sfra/cartridge/templates/default/boltEmbed.isml b/cartridges/int_bolt_embedded_sfra/cartridge/templates/default/boltEmbed.isml index d2868666..2c65057b 100644 --- a/cartridges/int_bolt_embedded_sfra/cartridge/templates/default/boltEmbed.isml +++ b/cartridges/int_bolt_embedded_sfra/cartridge/templates/default/boltEmbed.isml @@ -35,7 +35,38 @@ editShippingHeader: ".shipping-section h2", shippingSummary: ".address-summary", addPayment: ".payment-information", - paymentSummary: ".payment-details" + paymentSummary: ".payment-details", + + // ----- ANALYTICS ----- // + + checkoutStepTracker: ".data-checkout-stage", + boltAccountCheckbox: "#acct-checkbox", + + shippingFirstName: ".shippingFirstName", + shippingLastName: ".shippingLastName", + shippingAddress1: ".shippingAddressOne", + shippingZipCode: ".shippingZipCode", + shippingPhopeNumber: ".shippingPhoneNumber", + shippingSubmitButton: ".submit-shipping", + shippingEditButton: ".shipping-summary .edit-button", + + billingFirstName: ".billingFirstName", + billingLastName: ".billingLastName", + billingAddress1: ".billingAddressOne", + billingCity: ".billingCity", + billingZipCode: ".billingZipCode", + billingSubmitButton: ".submit-payment", + billingEditButton: ".billing-address-block .btn-show-details", + + payButton: ".place-order", + } + + window.boltCheckSelectorsExist = function() { + const selectorExists = {}; + for (const [key, selector] of Object.entries(window.BoltSelectors)) { + selectorExists[key] = document.querySelector(selector) != null; + } + return selectorExists; }