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