@magic-spells/gift-with-purchase 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -26
- package/dist/gift-with-purchase.cjs.css +3 -0
- package/dist/gift-with-purchase.cjs.css.map +1 -1
- package/dist/gift-with-purchase.cjs.js +276 -186
- package/dist/gift-with-purchase.cjs.js.map +1 -1
- package/dist/gift-with-purchase.esm.css +3 -0
- package/dist/gift-with-purchase.esm.css.map +1 -1
- package/dist/gift-with-purchase.esm.js +276 -186
- package/dist/gift-with-purchase.esm.js.map +1 -1
- package/dist/gift-with-purchase.min.css +1 -1
- package/dist/gift-with-purchase.min.css.map +1 -1
- package/dist/gift-with-purchase.min.js +1 -1
- package/dist/gift-with-purchase.min.js.map +1 -1
- package/dist/gift-with-purchase.scss +5 -0
- package/package.json +4 -4
- package/src/gift-with-purchase.js +276 -186
- package/src/gift-with-purchase.scss +5 -0
|
@@ -3,47 +3,112 @@
|
|
|
3
3
|
* Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates
|
|
4
4
|
*/
|
|
5
5
|
class GiftWithPurchase extends HTMLElement {
|
|
6
|
-
// private fields
|
|
7
6
|
#threshold = 0;
|
|
8
7
|
#currentAmount = 0;
|
|
9
8
|
#variantId = null;
|
|
10
9
|
#isActive = false;
|
|
11
10
|
#isAdded = false;
|
|
12
11
|
#promoEnded = false;
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
12
|
+
#productAvailable = true;
|
|
13
|
+
#isDisabled = false;
|
|
14
|
+
#cartPanel = null;
|
|
15
|
+
#handlers = {};
|
|
16
|
+
#debounceTimer = null;
|
|
17
|
+
#attachRetryTimer = null;
|
|
18
|
+
#isMutating = false; // prevents overlapping cart mutations
|
|
19
|
+
#pendingCart = null; // stores cart snapshot during mutation for recheck
|
|
20
|
+
#messageAbove = null;
|
|
21
|
+
#messageBelow = null;
|
|
22
|
+
#moneyFormat = null;
|
|
18
23
|
|
|
19
24
|
static get observedAttributes() {
|
|
20
|
-
return [
|
|
25
|
+
return [
|
|
26
|
+
'threshold',
|
|
27
|
+
'current',
|
|
28
|
+
'variant-id',
|
|
29
|
+
'promo-ended',
|
|
30
|
+
'product-available',
|
|
31
|
+
'message-above',
|
|
32
|
+
'message-below',
|
|
33
|
+
'money-format',
|
|
34
|
+
];
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
constructor() {
|
|
24
38
|
super();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const _ = this;
|
|
40
|
+
_.#threshold = parseFloat(_.getAttribute('threshold')) || 0;
|
|
41
|
+
_.#currentAmount = parseFloat(_.getAttribute('current')) || 0;
|
|
42
|
+
_.#variantId = _.getAttribute('variant-id');
|
|
43
|
+
_.#promoEnded = _.hasAttribute('promo-ended');
|
|
44
|
+
_.#productAvailable = _.#parseBooleanValue(_.getAttribute('product-available'), true);
|
|
45
|
+
_.#messageAbove = _.getAttribute('message-above');
|
|
46
|
+
_.#messageBelow = _.getAttribute('message-below');
|
|
47
|
+
_.#moneyFormat = _.getAttribute('money-format');
|
|
48
|
+
_.#handlers = { cartDataChange: _.#handleCartDataChange.bind(_) };
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
connectedCallback() {
|
|
36
|
-
this
|
|
37
|
-
|
|
52
|
+
const _ = this;
|
|
53
|
+
|
|
54
|
+
_.#calculateInitialState();
|
|
55
|
+
_.#render();
|
|
56
|
+
_.#updateVisualState();
|
|
57
|
+
_.#attachListeners();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#calculateInitialState() {
|
|
61
|
+
const _ = this;
|
|
62
|
+
// Calculate initial active state based on attributes (before any cart events)
|
|
63
|
+
const convertedThreshold = _.#getConvertedThreshold();
|
|
64
|
+
_.#isDisabled = _.#promoEnded || !_.#productAvailable;
|
|
65
|
+
_.#isActive = _.#currentAmount >= convertedThreshold && !_.#isDisabled;
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
disconnectedCallback() {
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
const _ = this;
|
|
70
|
+
if (_.#debounceTimer) clearTimeout(_.#debounceTimer);
|
|
71
|
+
if (_.#attachRetryTimer) clearTimeout(_.#attachRetryTimer);
|
|
72
|
+
if (_.#cartPanel) _.#cartPanel.removeEventListener('cart-panel:data-changed', _.#handlers.cartDataChange);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
76
|
+
const _ = this;
|
|
77
|
+
if (oldValue === newValue) return;
|
|
78
|
+
|
|
79
|
+
switch (name) {
|
|
80
|
+
case 'threshold':
|
|
81
|
+
_.#threshold = parseFloat(newValue) || 0;
|
|
82
|
+
break;
|
|
83
|
+
case 'current':
|
|
84
|
+
_.#currentAmount = parseFloat(newValue) || 0;
|
|
85
|
+
break;
|
|
86
|
+
case 'variant-id':
|
|
87
|
+
_.#variantId = newValue;
|
|
88
|
+
break;
|
|
89
|
+
case 'promo-ended':
|
|
90
|
+
_.#promoEnded = newValue !== null;
|
|
91
|
+
break;
|
|
92
|
+
case 'product-available':
|
|
93
|
+
_.#productAvailable = _.#parseBooleanValue(newValue, true);
|
|
94
|
+
break;
|
|
95
|
+
case 'message-above':
|
|
96
|
+
_.#messageAbove = newValue;
|
|
97
|
+
break;
|
|
98
|
+
case 'message-below':
|
|
99
|
+
_.#messageBelow = newValue;
|
|
100
|
+
break;
|
|
101
|
+
case 'money-format':
|
|
102
|
+
_.#moneyFormat = newValue;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Recalculate state and update UI if component is connected
|
|
107
|
+
if (_.isConnected) {
|
|
108
|
+
_.#calculateInitialState();
|
|
109
|
+
_.#updateVisualState();
|
|
110
|
+
_.#updateMessages();
|
|
111
|
+
}
|
|
47
112
|
}
|
|
48
113
|
|
|
49
114
|
#render() {
|
|
@@ -56,24 +121,50 @@ class GiftWithPurchase extends HTMLElement {
|
|
|
56
121
|
this.#updateMessages();
|
|
57
122
|
}
|
|
58
123
|
|
|
124
|
+
#formatMoney(amount) {
|
|
125
|
+
if (!this.#moneyFormat) return amount.toFixed(2).replace(/\.00$/, '');
|
|
126
|
+
|
|
127
|
+
const amountFixed = amount.toFixed(2);
|
|
128
|
+
const amountNoDecimals = Math.round(amount).toString();
|
|
129
|
+
const amountWithComma = amountFixed.replace('.', ',');
|
|
130
|
+
const amountNoDecimalsWithComma = amountNoDecimals.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
131
|
+
|
|
132
|
+
return this.#moneyFormat
|
|
133
|
+
.replace(/\{\{\s*amount_no_decimals_with_comma_separator\s*\}\}/g, amountNoDecimalsWithComma)
|
|
134
|
+
.replace(/\{\{\s*amount_with_comma_separator\s*\}\}/g, amountWithComma)
|
|
135
|
+
.replace(/\{\{\s*amount_no_decimals\s*\}\}/g, amountNoDecimals)
|
|
136
|
+
.replace(/\{\{\s*amount\s*\}\}/g, amountFixed);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#getConvertedThreshold() {
|
|
140
|
+
// Convert threshold using Shopify currency rate if available (for multi-currency stores)
|
|
141
|
+
const rate = parseFloat(window.Shopify?.currency?.rate) || 1;
|
|
142
|
+
return this.#threshold * rate;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#parseBooleanValue(value, defaultValue = true) {
|
|
146
|
+
if (value === null || typeof value === 'undefined') return defaultValue;
|
|
147
|
+
if (value === '') return true;
|
|
148
|
+
const normalized = String(value).trim().toLowerCase();
|
|
149
|
+
if (normalized === 'false' || normalized === '0') return false;
|
|
150
|
+
if (normalized === 'true' || normalized === '1') return true;
|
|
151
|
+
return defaultValue;
|
|
152
|
+
}
|
|
153
|
+
|
|
59
154
|
#updateMessages() {
|
|
60
|
-
const
|
|
155
|
+
const _ = this;
|
|
156
|
+
const messageEl = _.querySelector('[data-content-gwp-message]');
|
|
61
157
|
if (!messageEl) return;
|
|
62
158
|
|
|
63
159
|
let message = '';
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
message =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const remaining = this.#threshold - this.#currentAmount;
|
|
73
|
-
const formattedAmount = remaining.toFixed(2).replace(/\.00$/, '');
|
|
74
|
-
message = this.#messageBelow
|
|
75
|
-
.replace(/\{\s*amount\s*\}/g, formattedAmount)
|
|
76
|
-
.replace(/\{amount\}/g, formattedAmount);
|
|
160
|
+
if (_.#isActive && _.#messageAbove) {
|
|
161
|
+
message = _.#messageAbove;
|
|
162
|
+
} else if (!_.#isActive && _.#messageBelow) {
|
|
163
|
+
const remaining = _.#getConvertedThreshold() - _.#currentAmount;
|
|
164
|
+
const formattedAmount = _.#formatMoney(remaining);
|
|
165
|
+
message = _.#messageBelow
|
|
166
|
+
.replace(/\[\s*amount\s*\]/g, formattedAmount)
|
|
167
|
+
.replace(/\[amount\]/g, formattedAmount);
|
|
77
168
|
}
|
|
78
169
|
|
|
79
170
|
messageEl.textContent = message;
|
|
@@ -81,214 +172,206 @@ class GiftWithPurchase extends HTMLElement {
|
|
|
81
172
|
}
|
|
82
173
|
|
|
83
174
|
#attachListeners() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
this.#cartDialog.addEventListener(
|
|
90
|
-
'cart-dialog:data-changed',
|
|
91
|
-
this.#boundHandleCartDataChange
|
|
92
|
-
);
|
|
175
|
+
const _ = this;
|
|
176
|
+
_.#cartPanel = _.closest('cart-panel');
|
|
177
|
+
|
|
178
|
+
if (_.#cartPanel) {
|
|
179
|
+
_.#cartPanel.addEventListener('cart-panel:data-changed', _.#handlers.cartDataChange);
|
|
93
180
|
} else {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.#cartDialog.addEventListener(
|
|
100
|
-
'cart-dialog:data-changed',
|
|
101
|
-
this.#boundHandleCartDataChange
|
|
102
|
-
);
|
|
103
|
-
} else {
|
|
104
|
-
console.error('GWP - cart-dialog still not found after delay');
|
|
105
|
-
}
|
|
181
|
+
_.#attachRetryTimer = setTimeout(() => {
|
|
182
|
+
_.#attachRetryTimer = null;
|
|
183
|
+
_.#cartPanel = _.closest('cart-panel');
|
|
184
|
+
if (_.#cartPanel) _.#cartPanel.addEventListener('cart-panel:data-changed', _.#handlers.cartDataChange);
|
|
185
|
+
else console.error('GWP - cart-panel still not found after delay');
|
|
106
186
|
}, 100);
|
|
107
187
|
}
|
|
108
188
|
}
|
|
109
189
|
|
|
110
190
|
#handleCartDataChange(event) {
|
|
191
|
+
const _ = this;
|
|
111
192
|
const cart = event.detail;
|
|
112
|
-
// console.log('GWP - handleCartDataChange cart: ', cart.calculated_subtotal, cart);
|
|
113
|
-
|
|
114
193
|
if (!cart || typeof cart.calculated_subtotal === 'undefined') return;
|
|
115
|
-
if (
|
|
194
|
+
if (_.#debounceTimer) clearTimeout(_.#debounceTimer);
|
|
116
195
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
196
|
+
_.#debounceTimer = setTimeout(() => {
|
|
197
|
+
_.#debounceTimer = null;
|
|
198
|
+
_.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;
|
|
199
|
+
_.#checkGiftInCart(cart);
|
|
200
|
+
_.#updateState(cart);
|
|
122
201
|
}, 300);
|
|
123
202
|
}
|
|
124
203
|
|
|
125
|
-
// checks to see if the gift is already in the cart
|
|
126
204
|
#checkGiftInCart(cart) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
205
|
+
const _ = this;
|
|
206
|
+
const giftLines = _.#getGiftLines(cart, true);
|
|
207
|
+
_.#isAdded = giftLines.length > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#getGiftLines(cart, matchVariantId = true) {
|
|
211
|
+
const _ = this;
|
|
212
|
+
if (!cart?.items) return [];
|
|
213
|
+
return cart.items.filter((item) => {
|
|
214
|
+
if (item.properties?._gwp_item !== 'true') return false;
|
|
215
|
+
if (!matchVariantId) return true;
|
|
216
|
+
if (!_.#variantId) return false;
|
|
217
|
+
return item.variant_id?.toString() === _.#variantId.toString();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#recheckIfPending() {
|
|
222
|
+
const _ = this;
|
|
223
|
+
if (_.#pendingCart) {
|
|
224
|
+
const cart = _.#pendingCart;
|
|
225
|
+
_.#pendingCart = null;
|
|
226
|
+
_.#checkGiftInCart(cart);
|
|
227
|
+
_.#updateState(cart);
|
|
130
228
|
}
|
|
131
|
-
const giftLines = cart.items.filter(
|
|
132
|
-
(lineItem) =>
|
|
133
|
-
lineItem.variant_id.toString() === this.#variantId.toString() &&
|
|
134
|
-
lineItem.properties?._gwp_item === 'true'
|
|
135
|
-
);
|
|
136
|
-
this.#isAdded = giftLines.length > 0;
|
|
137
|
-
if (this.#promoEnded && giftLines.length) this.#removeAllGiftItems(giftLines);
|
|
138
229
|
}
|
|
139
230
|
|
|
140
231
|
#updateState(cart) {
|
|
141
|
-
|
|
142
|
-
const wasActive = this.#isActive;
|
|
143
|
-
this.#isActive = this.#currentAmount >= this.#threshold && !this.#promoEnded;
|
|
232
|
+
const _ = this;
|
|
144
233
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.#removeGiftFromCart(cart);
|
|
234
|
+
// If mutation in progress, queue this cart for recheck later
|
|
235
|
+
if (_.#isMutating) {
|
|
236
|
+
_.#pendingCart = cart;
|
|
237
|
+
return;
|
|
150
238
|
}
|
|
151
239
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
240
|
+
const wasActive = _.#isActive;
|
|
241
|
+
const convertedThreshold = _.#getConvertedThreshold();
|
|
242
|
+
_.#isDisabled = _.#promoEnded || !_.#productAvailable;
|
|
243
|
+
_.#isActive = _.#currentAmount >= convertedThreshold && !_.#isDisabled;
|
|
157
244
|
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
if (_.#isDisabled) _.#removeAllGiftsFromCart(cart);
|
|
246
|
+
else if (_.#isActive && !wasActive && !_.#isAdded && _.#variantId) _.#addGiftToCart();
|
|
247
|
+
else if (!_.#isActive && _.#isAdded && _.#variantId) _.#removeGiftFromCart(cart);
|
|
248
|
+
|
|
249
|
+
_.#updateVisualState();
|
|
250
|
+
_.#updateMessages();
|
|
160
251
|
}
|
|
161
252
|
|
|
162
253
|
#updateVisualState() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
254
|
+
const _ = this;
|
|
255
|
+
if (_.#promoEnded) {
|
|
256
|
+
_.setAttribute('state', 'ended');
|
|
257
|
+
_.style.display = 'none';
|
|
166
258
|
return;
|
|
167
259
|
}
|
|
168
260
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.setAttribute('state', 'active');
|
|
261
|
+
if (!_.#productAvailable) {
|
|
262
|
+
_.setAttribute('state', 'disabled');
|
|
263
|
+
_.style.display = 'none';
|
|
264
|
+
return;
|
|
174
265
|
}
|
|
175
|
-
|
|
266
|
+
|
|
267
|
+
_.style.display = '';
|
|
268
|
+
if (_.#isAdded) _.setAttribute('state', 'added');
|
|
269
|
+
else if (_.#isActive) _.setAttribute('state', 'active');
|
|
270
|
+
else _.setAttribute('state', 'inactive');
|
|
176
271
|
}
|
|
177
272
|
|
|
178
273
|
async #addGiftToCart() {
|
|
274
|
+
const _ = this;
|
|
275
|
+
_.#isMutating = true;
|
|
179
276
|
try {
|
|
180
277
|
const res = await fetch('/cart/add.js', {
|
|
181
278
|
method: 'POST',
|
|
182
279
|
credentials: 'same-origin',
|
|
183
|
-
headers: {
|
|
184
|
-
'Content-Type': 'application/json',
|
|
185
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
186
|
-
},
|
|
280
|
+
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
|
187
281
|
body: JSON.stringify({
|
|
188
|
-
items: [
|
|
189
|
-
{
|
|
190
|
-
id: this.#variantId,
|
|
191
|
-
quantity: 1,
|
|
192
|
-
properties: {
|
|
193
|
-
_gwp_item: 'true',
|
|
194
|
-
_hide_in_cart: 'true',
|
|
195
|
-
_ignore_price_in_subtotal: 'true',
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
],
|
|
282
|
+
items: [{ id: _.#variantId, quantity: 1, properties: { _gwp_item: 'true', _hide_in_cart: 'true', _ignore_price_in_subtotal: 'true' } }],
|
|
199
283
|
}),
|
|
200
284
|
});
|
|
201
285
|
if (!res.ok) throw new Error(`http ${res.status}`);
|
|
202
286
|
await res.json();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
detail: { variantId: this.#variantId },
|
|
207
|
-
bubbles: true,
|
|
208
|
-
})
|
|
209
|
-
);
|
|
287
|
+
_.#isAdded = true;
|
|
288
|
+
_.dispatchEvent(new CustomEvent('gwp:added', { detail: { variantId: _.#variantId }, bubbles: true }));
|
|
289
|
+
_.#cartPanel?.getCartAndRefresh();
|
|
210
290
|
} catch (err) {
|
|
211
291
|
console.error('giftwithpurchase: add error', err);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
})
|
|
217
|
-
);
|
|
292
|
+
_.dispatchEvent(new CustomEvent('gwp:error', { detail: { action: 'add', error: err.message }, bubbles: true }));
|
|
293
|
+
} finally {
|
|
294
|
+
_.#isMutating = false;
|
|
295
|
+
_.#recheckIfPending();
|
|
218
296
|
}
|
|
219
297
|
}
|
|
220
298
|
|
|
221
299
|
async #removeGiftFromCart(cart) {
|
|
300
|
+
const _ = this;
|
|
301
|
+
if (!cart?.items) return;
|
|
302
|
+
|
|
303
|
+
const giftLines = _.#getGiftLines(cart, true);
|
|
304
|
+
if (!giftLines.length) {
|
|
305
|
+
_.#isAdded = false;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_.#isMutating = true;
|
|
222
310
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
311
|
+
await _.#removeAllGiftItems(giftLines);
|
|
312
|
+
} finally {
|
|
313
|
+
_.#isMutating = false;
|
|
314
|
+
_.#recheckIfPending();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
229
317
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
318
|
+
async #removeAllGiftsFromCart(cart) {
|
|
319
|
+
const _ = this;
|
|
320
|
+
if (!cart?.items) return;
|
|
235
321
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
322
|
+
const giftLines = _.#getGiftLines(cart, false);
|
|
323
|
+
if (!giftLines.length) {
|
|
324
|
+
_.#isAdded = false;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_.#isMutating = true;
|
|
329
|
+
try {
|
|
330
|
+
await _.#removeAllGiftItems(giftLines);
|
|
331
|
+
} finally {
|
|
332
|
+
_.#isMutating = false;
|
|
333
|
+
_.#recheckIfPending();
|
|
246
334
|
}
|
|
247
335
|
}
|
|
248
336
|
|
|
249
337
|
async #removeAllGiftItems(giftLines) {
|
|
338
|
+
const _ = this;
|
|
250
339
|
try {
|
|
251
340
|
await Promise.all(
|
|
252
|
-
giftLines.map((
|
|
253
|
-
fetch('/cart/change.js', {
|
|
341
|
+
giftLines.map(async (item) => {
|
|
342
|
+
const res = await fetch('/cart/change.js', {
|
|
254
343
|
method: 'POST',
|
|
255
344
|
credentials: 'same-origin',
|
|
256
|
-
headers: {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
body: JSON.stringify({ id: giftItem.key, quantity: 0 }),
|
|
261
|
-
})
|
|
262
|
-
)
|
|
263
|
-
);
|
|
264
|
-
this.#isAdded = false;
|
|
265
|
-
// note: you can broadcast cart again here if you want real-time re-render after remove
|
|
266
|
-
this.dispatchEvent(
|
|
267
|
-
new CustomEvent('gwp:removed', {
|
|
268
|
-
detail: { variantId: this.#variantId },
|
|
269
|
-
bubbles: true,
|
|
345
|
+
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
|
346
|
+
body: JSON.stringify({ id: item.key, quantity: 0 }),
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) throw new Error(`http ${res.status}`);
|
|
270
349
|
})
|
|
271
350
|
);
|
|
351
|
+
_.#isAdded = false;
|
|
352
|
+
_.dispatchEvent(new CustomEvent('gwp:removed', { detail: { variantId: _.#variantId }, bubbles: true }));
|
|
353
|
+
_.#cartPanel?.getCartAndRefresh();
|
|
272
354
|
} catch (err) {
|
|
273
355
|
console.error('giftwithpurchase: bulk remove error', err);
|
|
274
|
-
|
|
275
|
-
new CustomEvent('gwp:error', {
|
|
276
|
-
detail: { action: 'remove', error: err.message },
|
|
277
|
-
bubbles: true,
|
|
278
|
-
})
|
|
279
|
-
);
|
|
356
|
+
_.dispatchEvent(new CustomEvent('gwp:error', { detail: { action: 'remove', error: err.message }, bubbles: true }));
|
|
280
357
|
}
|
|
281
358
|
}
|
|
282
359
|
|
|
283
360
|
getState() {
|
|
361
|
+
const _ = this;
|
|
362
|
+
const convertedThreshold = _.#getConvertedThreshold();
|
|
284
363
|
return {
|
|
285
|
-
currentAmount:
|
|
286
|
-
threshold:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
364
|
+
currentAmount: _.#currentAmount,
|
|
365
|
+
threshold: _.#threshold,
|
|
366
|
+
convertedThreshold,
|
|
367
|
+
variantId: _.#variantId,
|
|
368
|
+
isActive: _.#isActive,
|
|
369
|
+
isAdded: _.#isAdded,
|
|
370
|
+
promoEnded: _.#promoEnded,
|
|
371
|
+
productAvailable: _.#productAvailable,
|
|
372
|
+
isDisabled: _.#isDisabled,
|
|
373
|
+
remainingAmount: Math.max(0, convertedThreshold - _.#currentAmount),
|
|
374
|
+
currencyRate: parseFloat(window.Shopify?.currency?.rate) || 1,
|
|
292
375
|
};
|
|
293
376
|
}
|
|
294
377
|
|
|
@@ -310,18 +393,25 @@ class GiftWithPurchase extends HTMLElement {
|
|
|
310
393
|
get promoEnded() {
|
|
311
394
|
return this.#promoEnded;
|
|
312
395
|
}
|
|
396
|
+
get productAvailable() {
|
|
397
|
+
return this.#productAvailable;
|
|
398
|
+
}
|
|
399
|
+
get isDisabled() {
|
|
400
|
+
return this.#isDisabled;
|
|
401
|
+
}
|
|
313
402
|
|
|
314
|
-
// Public setter methods for programmatic control
|
|
315
403
|
setCurrentAmount(amount) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
404
|
+
const _ = this;
|
|
405
|
+
_.#currentAmount = parseFloat(amount) || 0;
|
|
406
|
+
_.#updateState({ items: [] });
|
|
407
|
+
_.#updateMessages();
|
|
319
408
|
}
|
|
320
409
|
|
|
321
410
|
setThreshold(threshold) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
411
|
+
const _ = this;
|
|
412
|
+
_.#threshold = parseFloat(threshold) || 0;
|
|
413
|
+
_.#updateState({ items: [] });
|
|
414
|
+
_.#updateMessages();
|
|
325
415
|
}
|
|
326
416
|
|
|
327
417
|
setVariantId(variantId) {
|