PageSourceSearch

https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-upsell.js?v=1126

js simplystamps.com collected 2026-10-02 06:57:46 UTC 6,873 bytes, 199 lines download raw bytes

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.