@magic-spells/gift-with-purchase 0.2.0 → 2.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.
@@ -1,297 +1,408 @@
1
+ //#region src/gift-with-purchase.js
1
2
  /**
2
- * Gift With Purchase Component - automatically adds/removes gift when cart threshold is met
3
- * Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates
4
- */
5
- class GiftWithPurchase extends HTMLElement {
6
- // private fields
3
+ * Gift With Purchase Component - automatically adds/removes gift when cart threshold is met
4
+ * Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates
5
+ */
6
+ var GiftWithPurchase = class extends HTMLElement {
7
7
  #threshold = 0;
8
8
  #currentAmount = 0;
9
9
  #variantId = null;
10
10
  #isActive = false;
11
11
  #isAdded = false;
12
12
  #promoEnded = false;
13
- #cartDialog = null;
14
- #boundHandleCartDataChange = null; // pre-bound listener ref for clean-up
15
- #debounceTimer = null; // debouncing cart updates
16
- #messageAbove = null; // message when threshold is met
17
- #messageBelow = null; // message when below threshold
18
-
13
+ #productAvailable = true;
14
+ #isDisabled = false;
15
+ #cartPanel = null;
16
+ #handlers = {};
17
+ #debounceTimer = null;
18
+ #attachRetryTimer = null;
19
+ #isMutating = false;
20
+ #missedUpdate = false;
21
+ #messageAbove = null;
22
+ #messageBelow = null;
23
+ #moneyFormat = null;
19
24
  static get observedAttributes() {
20
- return ['threshold', 'current', 'variant-id', 'promo-ended', 'message-above', 'message-below'];
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
-
23
36
  constructor() {
24
37
  super();
25
- // read initial attributes once
26
- this.#threshold = parseFloat(this.getAttribute('threshold')) || 0;
27
- this.#currentAmount = parseFloat(this.getAttribute('current')) || 0;
28
- this.#variantId = this.getAttribute('variant-id');
29
- this.#promoEnded = this.hasAttribute('promo-ended');
30
- this.#messageAbove = this.getAttribute('message-above');
31
- this.#messageBelow = this.getAttribute('message-below');
32
- this.#boundHandleCartDataChange = this.#handleCartDataChange.bind(this);
33
- }
34
-
38
+ const _ = this;
39
+ _.#threshold = parseFloat(_.getAttribute("threshold")) || 0;
40
+ _.#currentAmount = parseFloat(_.getAttribute("current")) || 0;
41
+ _.#variantId = _.getAttribute("variant-id");
42
+ _.#promoEnded = _.hasAttribute("promo-ended");
43
+ _.#productAvailable = _.#parseBooleanValue(_.getAttribute("product-available"), true);
44
+ _.#messageAbove = _.getAttribute("message-above");
45
+ _.#messageBelow = _.getAttribute("message-below");
46
+ _.#moneyFormat = _.getAttribute("money-format");
47
+ _.#handlers = { cartDataChange: _.#handleCartDataChange.bind(_) };
48
+ }
35
49
  connectedCallback() {
36
- this.#render();
37
- this.#attachListeners();
50
+ const _ = this;
51
+ _.#calculateInitialState();
52
+ _.#render();
53
+ _.#updateVisualState();
54
+ _.#attachListeners();
55
+ if (_.#isDisabled && (!_.#cartPanel || _.#cartPanel.hasAttribute("manual"))) _.#updateState(null);
56
+ }
57
+ #calculateInitialState() {
58
+ const _ = this;
59
+ const convertedThreshold = _.#getConvertedThreshold();
60
+ _.#isDisabled = _.#promoEnded || !_.#productAvailable;
61
+ _.#isActive = _.#currentAmount >= convertedThreshold && !_.#isDisabled;
38
62
  }
39
-
40
63
  disconnectedCallback() {
41
- if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
42
- if (this.#cartDialog)
43
- this.#cartDialog.removeEventListener(
44
- 'cart-dialog:data-changed',
45
- this.#boundHandleCartDataChange
46
- );
47
- }
48
-
64
+ const _ = this;
65
+ if (_.#debounceTimer) clearTimeout(_.#debounceTimer);
66
+ if (_.#attachRetryTimer) clearTimeout(_.#attachRetryTimer);
67
+ if (_.#cartPanel) _.#cartPanel.removeEventListener("cart-panel:data-changed", _.#handlers.cartDataChange);
68
+ }
69
+ attributeChangedCallback(name, oldValue, newValue) {
70
+ const _ = this;
71
+ if (oldValue === newValue) return;
72
+ switch (name) {
73
+ case "threshold":
74
+ _.#threshold = parseFloat(newValue) || 0;
75
+ break;
76
+ case "current":
77
+ _.#currentAmount = parseFloat(newValue) || 0;
78
+ break;
79
+ case "variant-id":
80
+ _.#variantId = newValue;
81
+ break;
82
+ case "promo-ended":
83
+ _.#promoEnded = newValue !== null;
84
+ break;
85
+ case "product-available":
86
+ _.#productAvailable = _.#parseBooleanValue(newValue, true);
87
+ break;
88
+ case "message-above":
89
+ _.#messageAbove = newValue;
90
+ break;
91
+ case "message-below":
92
+ _.#messageBelow = newValue;
93
+ break;
94
+ case "money-format": _.#moneyFormat = newValue;
95
+ }
96
+ if (!_.isConnected) return;
97
+ const wasDisabled = _.#isDisabled;
98
+ _.#calculateInitialState();
99
+ _.#updateVisualState();
100
+ _.#updateMessages();
101
+ if (_.#isDisabled && !wasDisabled) _.#updateState(null);
102
+ }
49
103
  #render() {
50
- this.classList.add('gift-with-purchase');
104
+ this.classList.add("gift-with-purchase");
51
105
  this.#renderMessages();
52
106
  }
53
-
54
107
  #renderMessages() {
55
- // Look for existing message element with data-content-gwp-message
56
108
  this.#updateMessages();
57
109
  }
58
-
110
+ #formatMoney(amount) {
111
+ if (!this.#moneyFormat) return amount.toFixed(2).replace(/\.00$/, "");
112
+ const amountFixed = amount.toFixed(2);
113
+ const amountNoDecimals = Math.round(amount).toString();
114
+ const amountWithComma = amountFixed.replace(".", ",");
115
+ const amountNoDecimalsWithComma = amountNoDecimals.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
116
+ return this.#moneyFormat.replace(/\{\{\s*amount_no_decimals_with_comma_separator\s*\}\}/g, amountNoDecimalsWithComma).replace(/\{\{\s*amount_with_comma_separator\s*\}\}/g, amountWithComma).replace(/\{\{\s*amount_no_decimals\s*\}\}/g, amountNoDecimals).replace(/\{\{\s*amount\s*\}\}/g, amountFixed);
117
+ }
118
+ #getConvertedThreshold() {
119
+ const rate = parseFloat(window.Shopify?.currency?.rate) || 1;
120
+ return this.#threshold * rate;
121
+ }
122
+ #parseBooleanValue(value, defaultValue = true) {
123
+ if (value === null || typeof value === "undefined") return defaultValue;
124
+ if (value === "") return true;
125
+ const normalized = String(value).trim().toLowerCase();
126
+ if (normalized === "false" || normalized === "0") return false;
127
+ if (normalized === "true" || normalized === "1") return true;
128
+ return defaultValue;
129
+ }
59
130
  #updateMessages() {
60
- const messageEl = this.querySelector('[data-content-gwp-message]');
131
+ const _ = this;
132
+ const messageEl = _.querySelector("[data-content-gwp-message]");
61
133
  if (!messageEl) return;
62
-
63
- let message = '';
64
-
65
- // console.log('updateMessages - this.#isActive', this.#isActive);
66
-
67
- if (this.#isActive && this.#messageAbove) {
68
- // set message to above threshold message
69
- message = this.#messageAbove;
70
- } else if (!this.#isActive && this.#messageBelow) {
71
- // set message to below threshold message
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);
134
+ let message = "";
135
+ if (_.#isActive && _.#messageAbove) message = _.#messageAbove;
136
+ else if (!_.#isActive && _.#messageBelow) {
137
+ const remaining = _.#getConvertedThreshold() - _.#currentAmount;
138
+ const formattedAmount = _.#formatMoney(remaining);
139
+ message = _.#messageBelow.replace(/\[\s*amount\s*\]/g, formattedAmount).replace(/\[amount\]/g, formattedAmount);
77
140
  }
78
-
79
141
  messageEl.textContent = message;
80
- messageEl.style.display = message ? 'block' : 'none';
142
+ messageEl.style.display = message ? "block" : "none";
81
143
  }
82
-
83
144
  #attachListeners() {
84
- // Look for cart-dialog element when attaching listeners (more reliable timing)
85
- this.#cartDialog = this.closest('cart-dialog');
86
-
87
- if (this.#cartDialog) {
88
- // console.log('cartDialog exists and is attaching events');
89
- this.#cartDialog.addEventListener(
90
- 'cart-dialog:data-changed',
91
- this.#boundHandleCartDataChange
92
- );
93
- } else {
94
- // Try again after a short delay in case the DOM isn't fully ready
95
- setTimeout(() => {
96
- // console.log('cartDialog DIDNT exist and we waited to attach events');
97
- this.#cartDialog = this.closest('cart-dialog');
98
- if (this.#cartDialog) {
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
- }
106
- }, 100);
107
- }
145
+ const _ = this;
146
+ _.#cartPanel = _.closest("cart-panel");
147
+ if (_.#cartPanel) _.#cartPanel.addEventListener("cart-panel:data-changed", _.#handlers.cartDataChange);
148
+ else _.#attachRetryTimer = setTimeout(() => {
149
+ _.#attachRetryTimer = null;
150
+ _.#cartPanel = _.closest("cart-panel");
151
+ if (_.#cartPanel) _.#cartPanel.addEventListener("cart-panel:data-changed", _.#handlers.cartDataChange);
152
+ else console.error("GWP - cart-panel still not found after delay");
153
+ }, 100);
154
+ }
155
+ /**
156
+ * Ask the parent cart-panel to re-fetch and re-render the cart.
157
+ * refreshCart() is the public API across every published cart-panel (0.3.x - 2.x);
158
+ * getCartAndRefresh() is only tried as a fallback for custom panels written against
159
+ * the name earlier versions of this component mistakenly called. Missing methods are
160
+ * ignored rather than thrown so a successful add/remove never reports a spurious error.
161
+ */
162
+ #refreshCartPanel() {
163
+ const panel = this.#cartPanel;
164
+ if (!panel) return;
165
+ if (typeof panel.refreshCart === "function") panel.refreshCart();
166
+ else if (typeof panel.getCartAndRefresh === "function") panel.getCartAndRefresh();
108
167
  }
109
-
110
168
  #handleCartDataChange(event) {
169
+ const _ = this;
111
170
  const cart = event.detail;
112
- // console.log('GWP - handleCartDataChange cart: ', cart.calculated_subtotal, cart);
113
-
114
- if (!cart || typeof cart.calculated_subtotal === 'undefined') return;
115
- if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
116
-
117
- this.#debounceTimer = setTimeout(() => {
118
- this.#debounceTimer = null;
119
- this.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;
120
- this.#checkGiftInCart(cart);
121
- this.#updateState(cart);
171
+ if (!cart || typeof cart.calculated_subtotal === "undefined") return;
172
+ if (_.#debounceTimer) clearTimeout(_.#debounceTimer);
173
+ _.#debounceTimer = setTimeout(() => {
174
+ _.#debounceTimer = null;
175
+ if (_.#isMutating) {
176
+ _.#missedUpdate = true;
177
+ return;
178
+ }
179
+ _.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;
180
+ _.#checkGiftInCart(cart);
181
+ _.#updateState(cart);
122
182
  }, 300);
123
183
  }
124
-
125
- // checks to see if the gift is already in the cart
126
184
  #checkGiftInCart(cart) {
127
- if (!cart.items || !this.#variantId) {
128
- this.#isAdded = false;
129
- return;
185
+ const _ = this;
186
+ const giftLines = _.#getGiftLines(cart);
187
+ _.#isAdded = giftLines.length > 0;
188
+ const duplicate = giftLines.find((item) => item.quantity > 1);
189
+ if (duplicate) _.#trimGiftQuantity(duplicate);
190
+ }
191
+ async #trimGiftQuantity(item) {
192
+ const _ = this;
193
+ _.#isMutating = true;
194
+ try {
195
+ const res = await fetch("/cart/change.js", {
196
+ method: "POST",
197
+ credentials: "same-origin",
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ "X-Requested-With": "XMLHttpRequest"
201
+ },
202
+ body: JSON.stringify({
203
+ id: item.key,
204
+ quantity: 1
205
+ })
206
+ });
207
+ if (!res.ok) throw new Error(`http ${res.status}`);
208
+ _.#refreshCartPanel();
209
+ } catch (err) {
210
+ console.error("giftwithpurchase: quantity trim error", err);
211
+ _.dispatchEvent(new CustomEvent("gwp:error", {
212
+ detail: {
213
+ action: "trim",
214
+ error: err.message
215
+ },
216
+ bubbles: true
217
+ }));
218
+ } finally {
219
+ _.#isMutating = false;
220
+ _.#discardStaleCart();
130
221
  }
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
- }
139
-
140
- #updateState(cart) {
141
- // console.log('********** ---------- Updating state....');
142
- const wasActive = this.#isActive;
143
- this.#isActive = this.#currentAmount >= this.#threshold && !this.#promoEnded;
144
-
145
- // console.log('********** ---------- this.#isActive', this.#isActive);
146
-
147
- if (this.#promoEnded) {
148
- // remove GWP from cart
149
- this.#removeGiftFromCart(cart);
222
+ }
223
+ #getGiftLines(cart) {
224
+ const _ = this;
225
+ if (!cart?.items || !_.#variantId) return [];
226
+ return cart.items.filter((item) => {
227
+ if (item.properties?._gwp_item !== "true") return false;
228
+ return item.variant_id?.toString() === _.#variantId.toString();
229
+ });
230
+ }
231
+ /**
232
+ * Cart snapshots held across a mutation predate it - drop them and ask for fresh truth.
233
+ */
234
+ #discardStaleCart() {
235
+ const _ = this;
236
+ if (_.#debounceTimer) {
237
+ clearTimeout(_.#debounceTimer);
238
+ _.#debounceTimer = null;
239
+ _.#missedUpdate = true;
150
240
  }
151
-
152
- if (this.#isActive && !wasActive && !this.#isAdded && this.#variantId) {
153
- this.#addGiftToCart();
154
- } else if (!this.#isActive && wasActive && this.#isAdded && this.#variantId) {
155
- this.#removeGiftFromCart(cart);
241
+ if (!_.#missedUpdate) return;
242
+ _.#missedUpdate = false;
243
+ if (_.#cartPanel) _.#refreshCartPanel();
244
+ else _.#updateState(null);
245
+ }
246
+ #updateState(cart) {
247
+ const _ = this;
248
+ if (_.#isMutating) {
249
+ _.#missedUpdate = true;
250
+ return;
156
251
  }
157
-
158
- this.#updateVisualState();
159
- this.#updateMessages();
252
+ const convertedThreshold = _.#getConvertedThreshold();
253
+ _.#isDisabled = _.#promoEnded || !_.#productAvailable;
254
+ _.#isActive = _.#currentAmount >= convertedThreshold && !_.#isDisabled;
255
+ if (_.#isDisabled) _.#removeGiftFromCart(cart);
256
+ else if (_.#isActive && !_.#isAdded && _.#variantId) _.#addGiftToCart();
257
+ else if (!_.#isActive && _.#isAdded && _.#variantId) _.#removeGiftFromCart(cart);
258
+ _.#updateVisualState();
259
+ _.#updateMessages();
160
260
  }
161
-
162
261
  #updateVisualState() {
163
- if (this.#promoEnded) {
164
- this.setAttribute('state', 'ended');
165
- this.style.display = 'none';
262
+ const _ = this;
263
+ if (_.#promoEnded) {
264
+ _.setAttribute("state", "ended");
265
+ _.style.display = "none";
166
266
  return;
167
267
  }
168
-
169
- this.style.display = '';
170
- if (this.#isAdded) {
171
- this.setAttribute('state', 'added');
172
- } else if (this.#isActive) {
173
- this.setAttribute('state', 'active');
268
+ if (!_.#productAvailable) {
269
+ _.setAttribute("state", "disabled");
270
+ _.style.display = "none";
271
+ return;
174
272
  }
175
- // Note: no 'inactive' state since component wouldn't be loaded if inactive
273
+ _.style.display = "";
274
+ if (_.#isAdded) _.setAttribute("state", "added");
275
+ else if (_.#isActive) _.setAttribute("state", "active");
276
+ else _.setAttribute("state", "inactive");
176
277
  }
177
-
178
278
  async #addGiftToCart() {
279
+ const _ = this;
280
+ _.#isMutating = true;
179
281
  try {
180
- const res = await fetch('/cart/add.js', {
181
- method: 'POST',
182
- credentials: 'same-origin',
282
+ const res = await fetch("/cart/add.js", {
283
+ method: "POST",
284
+ credentials: "same-origin",
183
285
  headers: {
184
- 'Content-Type': 'application/json',
185
- 'X-Requested-With': 'XMLHttpRequest',
286
+ "Content-Type": "application/json",
287
+ "X-Requested-With": "XMLHttpRequest"
186
288
  },
187
- 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
- ],
199
- }),
289
+ body: JSON.stringify({ items: [{
290
+ id: _.#variantId,
291
+ quantity: 1,
292
+ properties: {
293
+ _gwp_item: "true",
294
+ _hide_in_cart: "true",
295
+ _ignore_price_in_subtotal: "true"
296
+ }
297
+ }] })
200
298
  });
201
299
  if (!res.ok) throw new Error(`http ${res.status}`);
202
300
  await res.json();
203
- this.#isAdded = true;
204
- this.dispatchEvent(
205
- new CustomEvent('gwp:added', {
206
- detail: { variantId: this.#variantId },
207
- bubbles: true,
208
- })
209
- );
301
+ _.#isAdded = true;
302
+ _.dispatchEvent(new CustomEvent("gwp:added", {
303
+ detail: { variantId: _.#variantId },
304
+ bubbles: true
305
+ }));
306
+ _.#refreshCartPanel();
210
307
  } catch (err) {
211
- console.error('giftwithpurchase: add error', err);
212
- this.dispatchEvent(
213
- new CustomEvent('gwp:error', {
214
- detail: { action: 'add', error: err.message },
215
- bubbles: true,
216
- })
217
- );
308
+ console.error("giftwithpurchase: add error", err);
309
+ _.dispatchEvent(new CustomEvent("gwp:error", {
310
+ detail: {
311
+ action: "add",
312
+ error: err.message
313
+ },
314
+ bubbles: true
315
+ }));
316
+ } finally {
317
+ _.#isMutating = false;
318
+ _.#discardStaleCart();
218
319
  }
219
320
  }
220
-
221
321
  async #removeGiftFromCart(cart) {
322
+ const _ = this;
323
+ _.#isMutating = true;
222
324
  try {
223
- // get all GWP items in the cart
224
- const giftLines = cart.items.filter(
225
- (lineItem) =>
226
- lineItem.variant_id.toString() === this.#variantId.toString() &&
227
- lineItem.properties?._gwp_item === 'true'
228
- );
229
-
230
- // exit if no items in the cart
325
+ if (!cart?.items) cart = await _.#fetchCart();
326
+ if (!cart?.items) return;
327
+ const giftLines = _.#getGiftLines(cart);
231
328
  if (!giftLines.length) {
232
- this.#isAdded = false;
329
+ _.#isAdded = false;
233
330
  return;
234
331
  }
235
-
236
- // remove all GWP items from the cart
237
- await this.#removeAllGiftItems(giftLines);
332
+ await _.#removeAllGiftItems(giftLines);
333
+ } finally {
334
+ _.#isMutating = false;
335
+ _.#discardStaleCart();
336
+ }
337
+ }
338
+ async #fetchCart() {
339
+ const _ = this;
340
+ try {
341
+ let cart;
342
+ if (typeof _.#cartPanel?.getCart === "function") cart = await _.#cartPanel.getCart();
343
+ else {
344
+ const res = await fetch("/cart.js", { credentials: "same-origin" });
345
+ if (!res.ok) throw new Error(`http ${res.status}`);
346
+ cart = await res.json();
347
+ }
348
+ return cart?.error ? null : cart;
238
349
  } catch (err) {
239
- console.error('giftwithpurchase: remove error', err);
240
- this.dispatchEvent(
241
- new CustomEvent('gwp:error', {
242
- detail: { action: 'remove', error: err.message },
243
- bubbles: true,
244
- })
245
- );
350
+ console.error("giftwithpurchase: cart fetch error", err);
351
+ return null;
246
352
  }
247
353
  }
248
-
249
354
  async #removeAllGiftItems(giftLines) {
355
+ const _ = this;
250
356
  try {
251
- await Promise.all(
252
- giftLines.map((giftItem) =>
253
- fetch('/cart/change.js', {
254
- method: 'POST',
255
- credentials: 'same-origin',
256
- headers: {
257
- 'Content-Type': 'application/json',
258
- 'X-Requested-With': 'XMLHttpRequest',
259
- },
260
- body: JSON.stringify({ id: giftItem.key, quantity: 0 }),
357
+ await Promise.all(giftLines.map(async (item) => {
358
+ const res = await fetch("/cart/change.js", {
359
+ method: "POST",
360
+ credentials: "same-origin",
361
+ headers: {
362
+ "Content-Type": "application/json",
363
+ "X-Requested-With": "XMLHttpRequest"
364
+ },
365
+ body: JSON.stringify({
366
+ id: item.key,
367
+ quantity: 0
261
368
  })
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,
270
- })
271
- );
369
+ });
370
+ if (!res.ok) throw new Error(`http ${res.status}`);
371
+ }));
372
+ _.#isAdded = false;
373
+ _.dispatchEvent(new CustomEvent("gwp:removed", {
374
+ detail: { variantId: _.#variantId },
375
+ bubbles: true
376
+ }));
377
+ _.#refreshCartPanel();
272
378
  } catch (err) {
273
- console.error('giftwithpurchase: bulk remove error', err);
274
- this.dispatchEvent(
275
- new CustomEvent('gwp:error', {
276
- detail: { action: 'remove', error: err.message },
277
- bubbles: true,
278
- })
279
- );
379
+ console.error("giftwithpurchase: bulk remove error", err);
380
+ _.dispatchEvent(new CustomEvent("gwp:error", {
381
+ detail: {
382
+ action: "remove",
383
+ error: err.message
384
+ },
385
+ bubbles: true
386
+ }));
280
387
  }
281
388
  }
282
-
283
389
  getState() {
390
+ const _ = this;
391
+ const convertedThreshold = _.#getConvertedThreshold();
284
392
  return {
285
- currentAmount: this.#currentAmount,
286
- threshold: this.#threshold,
287
- variantId: this.#variantId,
288
- isActive: this.#isActive,
289
- isAdded: this.#isAdded,
290
- promoEnded: this.#promoEnded,
291
- remainingAmount: Math.max(0, this.#threshold - this.#currentAmount),
393
+ currentAmount: _.#currentAmount,
394
+ threshold: _.#threshold,
395
+ convertedThreshold,
396
+ variantId: _.#variantId,
397
+ isActive: _.#isActive,
398
+ isAdded: _.#isAdded,
399
+ promoEnded: _.#promoEnded,
400
+ productAvailable: _.#productAvailable,
401
+ isDisabled: _.#isDisabled,
402
+ remainingAmount: Math.max(0, convertedThreshold - _.#currentAmount),
403
+ currencyRate: parseFloat(window.Shopify?.currency?.rate) || 1
292
404
  };
293
405
  }
294
-
295
406
  get currentAmount() {
296
407
  return this.#currentAmount;
297
408
  }
@@ -310,28 +421,28 @@ class GiftWithPurchase extends HTMLElement {
310
421
  get promoEnded() {
311
422
  return this.#promoEnded;
312
423
  }
313
-
314
- // Public setter methods for programmatic control
424
+ get productAvailable() {
425
+ return this.#productAvailable;
426
+ }
427
+ get isDisabled() {
428
+ return this.#isDisabled;
429
+ }
315
430
  setCurrentAmount(amount) {
316
- this.#currentAmount = parseFloat(amount) || 0;
317
- this.#updateState({ items: [] }); // Pass empty cart to avoid cart operations
318
- this.#updateMessages();
431
+ const _ = this;
432
+ _.#currentAmount = parseFloat(amount) || 0;
433
+ _.#updateState(null);
434
+ _.#updateMessages();
319
435
  }
320
-
321
436
  setThreshold(threshold) {
322
- this.#threshold = parseFloat(threshold) || 0;
323
- this.#updateState({ items: [] }); // Pass empty cart to avoid cart operations
324
- this.#updateMessages();
437
+ const _ = this;
438
+ _.#threshold = parseFloat(threshold) || 0;
439
+ _.#updateState(null);
440
+ _.#updateMessages();
325
441
  }
326
-
327
442
  setVariantId(variantId) {
328
443
  this.#variantId = variantId;
329
444
  }
330
- }
331
-
332
- if (!customElements.get('gift-with-purchase')) {
333
- customElements.define('gift-with-purchase', GiftWithPurchase);
334
- }
335
-
445
+ };
446
+ if (!customElements.get("gift-with-purchase")) customElements.define("gift-with-purchase", GiftWithPurchase);
447
+ //#endregion
336
448
  export { GiftWithPurchase };
337
- //# sourceMappingURL=gift-with-purchase.esm.js.map