1/** 2 * WCK Started Checkout 3 * 4 * Incoming event object 5 * @typedef {object} kl_checkout 6 * @property {string} email - Email of current logged in user 7 * 8 * @property {object} event_data - Data for started checkout event 9 * @property {object} $extra - Event data 10 * @property {string} $service - Value will always be "woocommerce" 11 * @property {int} value - Total value of checkout event 12 * @property {array} Categories - Product categories (array of strings) 13 * @property {string} Currency - Currency type 14 * @property {string} CurrencySymbol - Currency type symbol 15 * @property {array} ItemNames - List of items in the cart 16 * 17 */ 18 19 20/** 21 * Attach event listeners to save billing fields. 22 */ 23 24// Constants for the klaviyo api url prefix and revision 25const KLAVIYO_API_URL_PREFIX = 'https://a.klaviyo.com/'; 26const KLAVIYO_API_REVISION = '2026-04-15'; 27 28var identify_object = { 29 'company_id': public_key.token, 30 'properties': {} 31}; 32 33var klaviyo_cookie_id = '__kla_id'; 34 35function buildProfileRequestPayload(event_attributes) { 36 const topLevelAttributes = ['email', 'first_name', 'last_name']; 37 38 // Destructure event_attributes: 39 // - properties: gets the properties object from event_attributes, defaulting to empty object if not present 40 // - restAttributes: gets all other fields from event_attributes using the rest operator (...) 41 const { properties = {}, ...restAttributes } = event_attributes || {}; 42 43 // Create a new object for the filtered properties to avoid mutating the original input 44 const filteredProperties = { ...properties }; 45 const dataAttributes = { ...restAttributes }; 46 47 // Move top level attributes from properties to data level 48 topLevelAttributes.forEach(field => { 49 if (filteredProperties[field] !== undefined) { 50 dataAttributes[field] = filteredProperties[field]; 51 delete filteredProperties[field]; 52 } 53 }); 54 55 // Add the filtered properties back to dataAttributes 56 dataAttributes.properties = filteredProperties; 57 58 return JSON.stringify({ 59 data: { 60 type: "profile", 61 attributes: dataAttributes 62 } 63 }) 64} 65 66function buildEventRequestPayload(customer_properties, event_properties, metric_attributes) { 67 return JSON.stringify({ 68 data: { 69 type: 'event', 70 attributes: { 71 properties: { 72 ...event_properties, 73 }, 74 metric: { 75 data: { 76 type: 'metric', 77 attributes: { 78 ...metric_attributes, 79 } 80 } 81 }, 82 profile: { 83 data: { 84 type: 'profile', 85 attributes: { 86 ...customer_properties, 87 } 88 } 89 } 90 } 91 } 92 }) 93} 94 95function makePublicAPIcall(endpoint, event_data) { 96 var company_id = public_key.token; 97 jQuery.ajax(KLAVIYO_API_URL_PREFIX + endpoint + '?company_id=' + company_id, { 98 type: "POST", 99 contentType: "application/json", 100 data: event_data, 101 headers: { 102 'revision': KLAVIYO_API_REVISION, 103 'X-Klaviyo-User-Agent': plugin_meta_data.data, 104 } 105 }); 106} 107 108function decodeKlaviyoCookieData(cookie) { 109 var name = klaviyo_cookie_id + "="; 110 var decodedCookie = decodeURIComponent(cookie); 111 var ca = decodedCookie.split(';'); 112 for (var i = 0; i < ca.length; i++) { 113 var c = ca[i]; 114 while (c.charAt(0) == ' ') { 115 c = c.substring(1); 116 } 117 if (c.indexOf(name) == 0) { 118 return decodeURIComponent(atob(c.substring(name.length, c.length))); 119 } 120 } 121 return ""; 122} 123 124function getKlaviyoCookie() { 125 return decodeKlaviyoCookieData(document.cookie); 126} 127 128function encodeKlaviyoCookieData(cookie_data) { 129 return btoa(encodeURIComponent(JSON.stringify(cookie_data))); 130} 131 132function setKlaviyoCookie(cookie_data) { 133 // Get existing cookie data 134 const existingCookieData = getKlaviyoCookie(); 135 let mergedData = {}; 136 137 // If we have existing data, parse it and merge with new data 138 if (existingCookieData) { 139 try { 140 const parsedExistingData = typeof existingCookieData === 'string' ? 141 JSON.parse(existingCookieData) : existingCookieData; 142 mergedData = { ...parsedExistingData, ...cookie_data }; 143 } catch (e) { 144 // If parsing fails, just use the new data 145 mergedData = cookie_data; 146 } 147 } else { 148 mergedData = cookie_data; 149 } 150 151 const cvalue = encodeKlaviyoCookieData(mergedData); 152 const date = new Date(); 153 date.setTime(date.getTime() + (63072e6)); // adding 2 years in milliseconds to current time 154 const expires = "expires=" + date.toUTCString(); 155 document.cookie = klaviyo_cookie_id + "=" + cvalue + ";" + expires + "; path=/"; 156} 157 158 /** 159 * Queries the dom for first_name, last_name, and email inputs being displayed on the checkout page. 160 * If both shipping and billing forms are present, both input nodes will be returned for the type (ie. first_name) 161 * @return {object} an object of dom nodes (firstNameNode, lastNameNode, emailNode) 162 */ 163function getTrackingNodes() {
164 var emailNodes = jQuery('input[id*="email"]:visible, input[name*="email"]:visible'); 165 var firstNameNodes = jQuery('input[id*="first_name"]:visible, input[name*="first_name"]:visible'); 166 var lastNameNodes = jQuery('input[id*="last_name"]:visible, input[name*="last_name"]:visible'); 167 168 return { firstNameNodes, lastNameNodes, emailNodes}; 169} 170 171/** 172 * The event listener to be added to visible email, first_name, and last_name nodes. 173 * It makes a call to client/profile with the values from the email field and either 174 * the first_name or last_name value depending on the caller 175 * @return {undefined} 176 */ 177function identifyUser(nameType, self) { 178 var { emailNodes } = getTrackingNodes(); 179 var email = emailNodes.val(); 180 var identify_properties = { 181 [nameType]: jQuery.trim(jQuery(self).val()) 182 } 183 if (email) { 184 identify_properties["email"] = email; 185 setKlaviyoCookie(identify_properties); 186 identify_object.properties = identify_properties; 187 makePublicAPIcall('client/profiles/', buildProfileRequestPayload(identify_object)); 188 } 189} 190 191/** 192 * Adds the event listeners for tracking on the first name and last name inputs. 193 * If both the shipping and billing forms are visible, listeners will be added to all first name and last name nodes 194 * @return {undefined} 195 */ 196function klIdentifyBillingField() { 197 var { firstNameNodes, lastNameNodes } = getTrackingNodes(); 198 firstNameNodes.each(function(){ 199 var node = jQuery(this); 200 node.change(() => identifyUser("first_name", node)); 201 }); 202 lastNameNodes.each(function(){ 203 var node = jQuery(this); 204 node.change(() => identifyUser("last_name", node)); 205 }); 206} 207 208window.addEventListener("load", function () { 209 // Custom checkouts/payment platforms may still load this file but won't 210 // fire woocommerce_after_checkout_form hook to load checkout data. 211 if (typeof kl_checkout === 'undefined') { 212 return; 213 } 214 215 var WCK = WCK || {}; 216 WCK.trackStartedCheckout = function () { 217 var metric_attributes = { 218 'name': 'Started Checkout', 219 'service': 'woocommerce' 220 } 221 var customer_properties = {} 222 if (kl_checkout.email) { 223 customer_properties['email'] = kl_checkout.email; 224 // Identify user once we have an email. This will trigger AVAB if enabled. 225 klaviyo.identify(customer_properties); 226 } else if (kl_checkout.exchange_id) { 227 // Klaviyo use is already identified if we have an exchange ID. 228 customer_properties['_kx'] = kl_checkout.exchange_id; 229 } else { 230 return; 231 } 232 233 makePublicAPIcall('client/events/', buildEventRequestPayload(customer_properties, kl_checkout.event_data, metric_attributes)); 234 }; 235 236 var klCookie = getKlaviyoCookie(); 237 238 // Priority of emails for syncing Started Checkout event: Logged-in user, 239 // cookied exchange ID, cookied email, billing email address 240 if (kl_checkout.email !== "") { 241 identify_object.properties = { 242 'email': kl_checkout.email 243 }; 244 makePublicAPIcall('client/profiles/', buildProfileRequestPayload(identify_object)); 245 setKlaviyoCookie(identify_object.properties); 246 WCK.trackStartedCheckout(); 247 } else if (klCookie && JSON.parse(klCookie).$exchange_id !== undefined) { 248 kl_checkout.exchange_id = JSON.parse(klCookie).$exchange_id; 249 WCK.trackStartedCheckout(); 250 } else if (klCookie && JSON.parse(klCookie).email !== undefined) { 251 kl_checkout.email = JSON.parse(klCookie).email; 252 WCK.trackStartedCheckout(); 253 } else { 254 if (jQuery) { 255 var { firstNameNodes, lastNameNodes, emailNodes } = getTrackingNodes(); 256 emailNodes.change(function () { 257 var elem = jQuery(this), 258 email = jQuery.trim(elem.val()); 259 260 if (email && /@/.test(email)) { 261 var params = { 262 "email": email 263 }; 264 265 if (firstNameNodes.length > 0) { 266 // Values come from first visible input node in the DOM 267 params["first_name"] = firstNameNodes.val(); 268 } 269 if (lastNameNodes.length > 0) { 270 params["last_name"] = lastNameNodes.val(); 271 } 272 273 setKlaviyoCookie(params); 274 kl_checkout.email = params.email; 275 identify_object.properties = params; 276 makePublicAPIcall('client/profiles/', buildProfileRequestPayload((identify_object))); 277 WCK.trackStartedCheckout(); 278 } 279 }); 280 281 // Save billing fields 282 klIdentifyBillingField(); 283 } 284 } 285});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.