1/** 2 * Zephyr Cart Upsell Popup 3 * 4 * Interstitial "You might also like" popup shown once per session when the 5 * customer proceeds to checkout from the cart drawer. Intercepts the checkout 6 * click, fetches eligible suggestions, and lets the shopper add simple products 7 * before continuing. Configured from foot.phtml via cartUpsellConfig. 8 */ 9 10// Configuration (set from PHP in foot.phtml) 11var cartUpsellConfig = { 12 getUpsellsUrl: '', 13 addUpsellsUrl: '', 14 checkoutUrl: '', 15 formKey: '', 16 enabled: false, 17 storeId: '0', 18 devMode: false 19}; 20 21document.addEventListener('alpine:init', function() { 22 Alpine.store('upsell', { 23 isOpen: false, 24 isLoading: false, 25 products: [], 26 header: '', 27 subheader: '', 28 selected: [], 29 _checkoutUrl: '', 30 31 cookieName: function() { 32 return cartUpsellConfig.storeId + '_zephyrUpsell'; 33 }, 34 35 // Whether the popup has already been shown this session 36 seenThisSession: function() { 37 return document.cookie.indexOf(this.cookieName() + '=') !== -1; 38 }, 39 40 markSeen: function() { 41 // ~3 hour expiry, matches the legacy AOV modal behavior 42 var expires = new Date(Date.now() + 3 * 60 * 60 * 1000).toUTCString(); 43 document.cookie = this.cookieName() + '=1; path=/; expires=' + expires + '; SameSite=Lax'; 44 }, 45 46 /** 47 * Decide whether to interject the popup or go straight to checkout. 48 * Called from the drawer's checkout button (@click.prevent). 49 */ 50 maybeProceed: function(checkoutUrl) { 51 var url = checkoutUrl || cartUpsellConfig.checkoutUrl; 52 this._checkoutUrl = url; 53 54 // Feature off -> straight to checkout. 55 // In dev mode the once-per-session cookie is ignored so the popup 56 // shows on every checkout click (no need to keep clearing the cookie). 57 if (!cartUpsellConfig.enabled) { 58 window.location.href = url; 59 return; 60 } 61 if (!cartUpsellConfig.devMode && this.seenThisSession()) { 62 window.location.href = url; 63 return; 64 } 65 66 var self = this; 67 this.isLoading = true; 68 fetch(cartUpsellConfig.getUpsellsUrl, { 69 credentials: 'same-origin', 70 headers: { 'X-Requested-With': 'XMLHttpRequest' } 71 }) 72 .then(function(r) { return r.json(); }) 73 .then(function(data) { 74 self.isLoading = false; 75 if (data && data.success && data.products && data.products.length) { 76 self.products = data.products; 77 self.header = data.header || 'You might also like'; 78 self.subheader = data.subheader || ''; 79 self.selected = []; 80 self.markSeen(); // once per session, whether or not they add 81 self.open(); 82 self.fireViewItemList(); 83 } else { 84 // No eligible suggestions -> proceed without marking seen, 85 // so a later cart with eligible upsells can still trigger it 86 window.location.href = url; 87 } 88 }) 89 .catch(function() { 90 // On any error, don't trap the user â continue to checkout 91 self.isLoading = false; 92 window.location.href = url; 93 }); 94 }, 95 96 open: function() { 97 this.isOpen = true; 98 if (window.innerWidth < 768) { 99 document.body.style.overflow = 'hidden'; 100 } 101 }, 102 103 close: function() { 104 this.isOpen = false; 105 document.body.style.overflow = ''; 106 }, 107 108 isSelected: function(id) { 109 return this.selected.indexOf(id) !== -1; 110 }, 111 112 toggle: function(id) { 113 var idx = this.selected.indexOf(id); 114 if (idx === -1) { 115 this.selected.push(id); 116 } else { 117 this.selected.splice(idx, 1); 118 } 119 }, 120 121 // Continue to checkout without adding anything 122 skip: function() { 123 this.close(); 124 window.location.href = this._checkoutUrl || cartUpsellConfig.checkoutUrl; 125 }, 126 127 // Add the selected simple products, then continue to checkout 128 addSelectedAndCheckout: function() { 129 var self = this; 130 var url = this._checkoutUrl || cartUpsellConfig.checkoutUrl; 131 132 if (!this.selected.length) { 133 this.skip(); 134 return; 135 } 136 137 this.isLoading = true; 138 139 // GA4 add_to_cart for the chosen upsells (before navigating away) 140 this.fireAddToCart(); 141 142 fetch(cartUpsellConfig.addUpsellsUrl, { 143 method: 'POST', 144 credentials: 'same-origin', 145 headers: { 146 'Content-Type': 'application/json', 147 'X-Requested-With': 'XMLHttpRequest' 148 }, 149 body: JSON.stringify({ 150 product_ids: self.selected, 151 form_key: cartUpsellConfig.formKey 152 }) 153 }) 154 .then(function(r) { return r.json(); }) 155 .then(function() { 156 window.location.href = url; 157 }) 158 .catch(function() { 159 // Even on failure, continue to checkout 160 window.location.href = url; 161 }); 162 }, 163 164 fireAddToCart: function() { 165 if (typeof zGA4 !== 'function') return; 166 var self = this; 167 var items = this.products 168 .filter(function(p) { return self.isSelected(p.id); }) 169 .map(function(p) { 170 return { 171 item_id: p.sku || String(p.id), 172 item_name: p.name, 173 price: p.price_numeric || 0, 174 quantity: 1 175 }; 176 }); 177 if (items.length) { 178 zGA4('add_to_cart', { ecommerce: { items: items } }); 179 } 180 }, 181 182 fireViewItemList: function() { 183 if (typeof zGA4 !== 'function') return; 184 var items = this.products.map(function(p) { 185 return { 186 item_id: p.sku || String(p.id), 187 item_name: p.name, 188 price: p.price_numeric || 0 189 }; 190 }); 191 if (items.length) { 192 zGA4('view_item_list', { 193 item_list_name: 'Cart Upsell', 194 ecommerce: { items: items } 195 }); 196 } 197 } 198 }); 199});
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.