@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.
package/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  A powerful, e-commerce web component for automatic gift-with-purchase threshold promotions. Seamlessly integrates with Shopify and automatically manages gift items in the cart based on spending thresholds.
4
4
 
5
- [**Live Demo**](https://magic-spells.github.io/gift-with-purchase/demo/)
5
+ [**Live Demo**](https://magic-spells.github.io/cart/)
6
6
 
7
7
  ## Features
8
8
 
9
9
  - 🎁 **Automatic Gift Management** - Adds/removes gifts based on cart thresholds using smart pricing logic
10
10
  - 🛒 **Shopify Integration** - Built-in Cart API support with proper line item properties
11
- - 📱 **Cart Panel Sync** - Automatically syncs with cart-dialog components using `calculated_subtotal`
11
+ - 📱 **Cart Panel Sync** - Automatically syncs with cart-panel components using `calculated_subtotal`
12
12
  - 🎨 **Highly Customizable** - CSS custom properties and swappable content elements
13
13
  - ⚡ **Event-Driven** - Custom events for gift addition, removal, and errors
14
- - 🔧 **Flexible Content** - Data attributes for dynamic image, title, and variant updates
14
+ - 🔧 **Flexible Content** - Your own markup inside the element; the component only writes the `[data-content-gwp-message]` element
15
15
  - 📱 **Responsive** - Mobile-optimized with responsive design
16
16
 
17
17
  ## Installation
@@ -20,15 +20,40 @@ A powerful, e-commerce web component for automatic gift-with-purchase threshold
20
20
  npm install @magic-spells/gift-with-purchase
21
21
  ```
22
22
 
23
+ The package is ESM-only. Import it once, anywhere in your bundle, to register the custom element:
24
+
25
+ ```javascript
26
+ import '@magic-spells/gift-with-purchase';
27
+ ```
28
+
29
+ The CSS is shipped as a separate file rather than injected, so load it too:
30
+
31
+ ```css
32
+ /* unminified */
33
+ @import '@magic-spells/gift-with-purchase/css';
34
+
35
+ /* or minified */
36
+ @import '@magic-spells/gift-with-purchase/css/min';
37
+ ```
38
+
39
+ Without a bundler, use the UMD build (exposes the `GiftWithPurchase` global):
40
+
41
+ ```html
42
+ <link
43
+ rel="stylesheet"
44
+ href="https://unpkg.com/@magic-spells/gift-with-purchase/dist/gift-with-purchase.min.css" />
45
+ <script src="https://unpkg.com/@magic-spells/gift-with-purchase/dist/gift-with-purchase.min.js"></script>
46
+ ```
47
+
23
48
  ## Basic Usage
24
49
 
25
50
  ```html
26
51
  <gift-with-purchase
27
52
  threshold="75.00"
28
- current="45.00"
29
53
  variant-id="12345678"
54
+ money-format="${{amount}}"
30
55
  message-above="🎉 Congratulations! You've qualified for your FREE gift!"
31
- message-below="Add ${ amount } more to unlock your free gift! 🎁">
56
+ message-below="Add [amount] more to unlock your free gift! 🎁">
32
57
  <div class="gwp-product">
33
58
  <img src="gift-image.jpg" alt="Free Gift" />
34
59
  <div class="gwp-content">
@@ -40,27 +65,24 @@ npm install @magic-spells/gift-with-purchase
40
65
  </gift-with-purchase>
41
66
  ```
42
67
 
43
- ```css
44
- @import '@magic-spells/gift-with-purchase/css';
45
- ```
46
-
47
68
  ## Cart Integration
48
69
 
49
- The component automatically listens for cart data changes when placed inside a `<cart-dialog>` component from the `@magic-spells/cart-panel` package:
70
+ The component automatically listens for cart data changes when placed inside a `<cart-panel>` component from the `@magic-spells/cart-panel` package:
50
71
 
51
- ```html
52
- <cart-dialog>
72
+ ```liquid
73
+ <cart-panel>
53
74
  <gift-with-purchase
54
75
  threshold="75.00"
55
76
  variant-id="12345678"
77
+ money-format={{ shop.money_format | json }}
56
78
  message-above="🎉 Congratulations! You've qualified for your FREE gift!"
57
- message-below="Add ${ amount } more to unlock your free gift! 🎁">
79
+ message-below="Add [amount] more to unlock your free gift! 🎁">
58
80
  <!-- Gift content -->
59
81
  </gift-with-purchase>
60
- </cart-dialog>
82
+ </cart-panel>
61
83
  ```
62
84
 
63
- When the cart-dialog emits a `cart-dialog:data-changed` event (typically from Shopify cart updates), the gift component will automatically:
85
+ When the cart-panel emits a `cart-panel:data-changed` event (typically from Shopify cart updates), the gift component will automatically:
64
86
 
65
87
  - Update the current cart amount using `calculated_subtotal` for accurate threshold calculation
66
88
  - Check if the threshold is met (excludes other gifts and honors pricing exclusions)
@@ -73,42 +95,74 @@ The component uses intelligent threshold calculation:
73
95
  - **Uses `calculated_subtotal`** from cart-panel which properly handles item exclusions
74
96
  - **Excludes gifts**: Other gifts with purchase won't count toward this threshold
75
97
  - **Includes bundle items**: Hidden bundle components that should count are included
76
- - **Backwards compatible**: Falls back to `total_price` if `calculated_subtotal` unavailable
98
+ - **Multi-currency support**: Automatically converts threshold using `Shopify.currency.rate` for stores with multiple currencies
99
+ - **Requires cart-panel**: The component requires `calculated_subtotal` in cart events to function
77
100
 
78
101
  ## JavaScript API
79
102
 
80
103
  ```javascript
81
104
  const gwp = document.querySelector('gift-with-purchase');
82
105
 
83
- // Update cart amount
106
+ // Setters - update state programmatically
84
107
  gwp.setCurrentAmount(85.5);
85
-
86
- // Change threshold
87
108
  gwp.setThreshold(100.0);
88
-
89
- // Update variant ID
90
109
  gwp.setVariantId('87654321');
91
110
 
92
- // Get current state
111
+ // Get full state object
93
112
  const state = gwp.getState();
94
113
  console.log(state.isActive, state.isAdded, state.remainingAmount);
95
114
 
96
- // Manual message updates (component handles this automatically)
97
- const messageEl = gwp.querySelector('[data-content-gwp-message]');
98
- if (messageEl) {
99
- messageEl.textContent = 'Custom message';
100
- }
115
+ // Readonly getters - access individual properties
116
+ console.log(gwp.currentAmount); // number - current cart amount
117
+ console.log(gwp.threshold); // number - threshold amount
118
+ console.log(gwp.variantId); // string - gift variant ID
119
+ console.log(gwp.isActive); // boolean - threshold met
120
+ console.log(gwp.isAdded); // boolean - gift in cart
121
+ console.log(gwp.promoEnded); // boolean - promo has ended
122
+ console.log(gwp.productAvailable); // boolean - gift product available
123
+ console.log(gwp.isDisabled); // boolean - promo ended OR product unavailable
101
124
  ```
102
125
 
103
126
  ## Attributes
104
127
 
105
128
  | Attribute | Description | Example |
106
129
  | --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ |
107
- | `threshold` | Spending threshold to unlock the gift | `"75.00"` |
108
- | `current` | Current cart amount | `"45.00"` |
130
+ | `threshold` | Spending threshold to unlock the gift (auto-converts for multi-currency) | `"75.00"` |
131
+ | `current` | Optional starting amount. The component tracks the live cart total internally from `cart-panel:data-changed` and never writes this attribute back, so don't template it from Liquid | `"45.00"` |
109
132
  | `variant-id` | Shopify variant ID for the gift product | `"12345678"` |
133
+ | `promo-ended` | Disables the promo and hides the component | `"true"` |
134
+ | `product-available` | Whether the gift product is available (disables if false) | `"true"` |
110
135
  | `message-above` | Message shown when threshold is met | `"🎉 Congratulations! You've qualified for your FREE gift!"` |
111
- | `message-below` | Message shown when below threshold (uses `{ amount }` placeholder) | `"Add ${ amount } more to unlock your free gift! 🎁"` |
136
+ | `message-below` | Message shown below threshold; use `[amount]` for remaining amount | `"Add [amount] more to unlock your free gift! 🎁"` |
137
+ | `money-format` | Shopify-style money format for currency display | `"${{amount}}"` |
138
+
139
+ ### The `[amount]` Placeholder
140
+
141
+ In the `message-below` attribute, use `[amount]` (with square brackets) to insert the remaining amount needed to reach the threshold. The component automatically:
142
+
143
+ - Calculates the remaining amount (`threshold - currentAmount`)
144
+ - Converts to the customer's currency using `Shopify.currency.rate`
145
+ - Formats using your `money-format` attribute
146
+
147
+ ```html
148
+ <!-- Example: If threshold is $75 and cart is $45, displays "Add $30 more..." -->
149
+ <gift-with-purchase
150
+ threshold="75.00"
151
+ money-format="${{amount}}"
152
+ message-below="Add [amount] more to unlock your free gift!">
153
+ </gift-with-purchase>
154
+ ```
155
+
156
+ **Why square brackets?** We use `[amount]` instead of `{{amount}}` to avoid conflicts with Shopify Liquid templates and JavaScript template literals.
157
+
158
+ ### Multi-Currency Support
159
+
160
+ The component automatically handles currency conversion for multi-currency Shopify stores:
161
+
162
+ - Set `threshold` in your store's **base currency** (e.g., USD)
163
+ - The component reads `Shopify.currency.rate` from the browser
164
+ - Threshold is automatically converted to the customer's selected currency
165
+ - The `[amount]` placeholder displays in the converted currency
112
166
 
113
167
  ## Message Element
114
168
 
@@ -127,6 +181,62 @@ The component requires a message element to display threshold messages:
127
181
  </gift-with-purchase>
128
182
  ```
129
183
 
184
+ ## Currency Formatting
185
+
186
+ Use the `money-format` attribute to format amounts with the correct currency symbol and formatting:
187
+
188
+ ```html
189
+ <gift-with-purchase
190
+ threshold="75.00"
191
+ variant-id="12345678"
192
+ money-format="${{amount}}"
193
+ message-below="Add [amount] more to unlock your free gift!">
194
+ <!-- Gift content -->
195
+ </gift-with-purchase>
196
+ ```
197
+
198
+ In Shopify themes, you can pass the shop's money format using the `json` filter:
199
+
200
+ ```liquid
201
+ <gift-with-purchase
202
+ threshold="75.00"
203
+ variant-id="12345678"
204
+ money-format={{ shop.money_format | json }}
205
+ message-below="Add [amount] more to unlock your free gift!">
206
+ <!-- Gift content -->
207
+ </gift-with-purchase>
208
+ ```
209
+
210
+ Available Shopify money formats:
211
+ - `shop.money_format` - Basic format (e.g., `${{amount}}`)
212
+ - `shop.money_with_currency_format` - With currency code (e.g., `${{amount}} USD`)
213
+
214
+ ### Supported Format Placeholders
215
+
216
+ | Placeholder | Output | Example |
217
+ | ----------- | ------ | ------- |
218
+ | `{{amount}}` | Amount with decimals | `19.99` |
219
+ | `{{amount_no_decimals}}` | Rounded whole number | `20` |
220
+ | `{{amount_with_comma_separator}}` | Comma as decimal | `19,99` |
221
+ | `{{amount_no_decimals_with_comma_separator}}` | Whole with comma thousands | `1,000` |
222
+
223
+ ### Examples by Currency
224
+
225
+ ```html
226
+ <!-- USD -->
227
+ money-format="${{amount}}" <!-- $30.00 -->
228
+
229
+ <!-- EUR -->
230
+ money-format="€{{amount}}" <!-- €30.00 -->
231
+ money-format="{{amount}} €" <!-- 30.00 € -->
232
+
233
+ <!-- GBP -->
234
+ money-format="£{{amount}}" <!-- £30.00 -->
235
+
236
+ <!-- JPY (no decimals) -->
237
+ money-format="¥{{amount_no_decimals}}" <!-- ¥3000 -->
238
+ ```
239
+
130
240
  ## Events
131
241
 
132
242
  The component emits custom events for integration:
@@ -145,7 +255,7 @@ gwp.addEventListener('gwp:removed', (event) => {
145
255
  // Error occurred during add/remove
146
256
  gwp.addEventListener('gwp:error', (event) => {
147
257
  console.error('Error:', event.detail.error);
148
- console.log('Action:', event.detail.action); // 'add' or 'remove'
258
+ console.log('Action:', event.detail.action); // 'add', 'remove', or 'trim'
149
259
  });
150
260
  ```
151
261
 
@@ -160,7 +270,7 @@ gift-with-purchase {
160
270
  --gwp-bg-active: #e8f5e8;
161
271
  --gwp-border-active: #28a745;
162
272
  --gwp-text-active: #155724;
163
- --gwp-image-size: 80px;
273
+ --gwp-gap: 1.5rem;
164
274
  }
165
275
  ```
166
276
 
@@ -184,9 +294,13 @@ The component automatically applies a `state` attribute based on its current con
184
294
 
185
295
  - `state="active"` - Threshold met, gift available to add
186
296
  - `state="added"` - Gift successfully added to cart
297
+ - `state="inactive"` - Below threshold (component idle)
187
298
  - `state="ended"` - Promo ended (component hidden)
299
+ - `state="disabled"` - Gift unavailable (component hidden)
188
300
 
189
- Note: There is no `inactive` state since the component would typically not be loaded at all when below threshold in most Shopify implementations.
301
+ Note: `ended` and `disabled` both hide the component while still removing the gift line for **this**
302
+ component's `variant-id`. Another tier's gift is left alone, so disabling one tier never clears
303
+ another's.
190
304
 
191
305
  ## Shopify Integration Details
192
306
 
@@ -195,8 +309,8 @@ Note: There is no `inactive` state since the component would typically not be lo
195
309
  The component uses Shopify's Cart API endpoints:
196
310
 
197
311
  - `POST /cart/add.js` - Adds the gift to cart
198
- - `GET /cart.js` - Gets current cart state
199
- - `POST /cart/change.js` - Removes gift from cart
312
+ - `GET /cart.js` - Gets current cart state (only when the parent cart-panel cannot supply it)
313
+ - `POST /cart/change.js` - Removes the gift line, and trims a doubled gift line back to `quantity: 1`
200
314
 
201
315
  ### Line Item Properties
202
316
 
@@ -248,7 +362,7 @@ In your Shopify cart template, you can identify and handle gift items:
248
362
  threshold="50.00"
249
363
  variant-id="111111"
250
364
  message-above="🎉 You've unlocked a free tote bag!"
251
- message-below="Add ${ amount } more for a free tote bag! 👜">
365
+ message-below="Add [amount] more for a free tote bag! 👜">
252
366
  <div class="gwp-product">
253
367
  <img src="tote-bag.jpg" alt="Free Tote Bag" />
254
368
  <h4 class="gwp-title">Free Tote Bag</h4>
@@ -261,7 +375,7 @@ In your Shopify cart template, you can identify and handle gift items:
261
375
  threshold="100.00"
262
376
  variant-id="222222"
263
377
  message-above="✨ Amazing! You've earned our premium gift set!"
264
- message-below="Spend ${ amount } more for our exclusive premium gift set! ✨">
378
+ message-below="Spend [amount] more for our exclusive premium gift set! ✨">
265
379
  <div class="gwp-product">
266
380
  <img src="gift-set.jpg" alt="Premium Gift Set" />
267
381
  <h4 class="gwp-title">Premium Gift Set</h4>
@@ -291,9 +405,9 @@ In your Shopify cart template, you can identify and handle gift items:
291
405
  /* Compact mobile style */
292
406
  @media (max-width: 768px) {
293
407
  gift-with-purchase {
294
- --gwp-image-size: 40px;
295
408
  --gwp-padding: 0.5rem;
296
409
  --gwp-gap: 0.5rem;
410
+ --gwp-border-radius: 4px;
297
411
  }
298
412
  }
299
413
  ```
@@ -305,24 +419,47 @@ In your Shopify cart template, you can identify and handle gift items:
305
419
  - Safari 14+
306
420
  - All modern browsers with Custom Elements support
307
421
 
308
- ## TypeScript Support
422
+ ## State Shape
309
423
 
310
- Type definitions are included in the package:
424
+ `getState()` returns:
311
425
 
312
426
  ```typescript
313
427
  interface GiftWithPurchaseState {
314
428
  currentAmount: number;
315
429
  threshold: number;
430
+ convertedThreshold: number;
316
431
  variantId: string | null;
317
432
  isActive: boolean;
318
433
  isAdded: boolean;
434
+ promoEnded: boolean;
435
+ productAvailable: boolean;
436
+ isDisabled: boolean;
319
437
  remainingAmount: number;
438
+ currencyRate: number;
320
439
  }
321
440
 
322
441
  // Component automatically handles message injection
323
442
  // Style your content and message elements with any CSS classes
324
443
  ```
325
444
 
445
+ ## Development
446
+
447
+ ```bash
448
+ npm run dev # build to demo/dist/ in watch mode and serve demo/ on http://localhost:3000
449
+ npm run build # build the published dist/ (ESM, minified UMD, CSS, minified CSS)
450
+ npm run lint # ESLint over src/ and scripts/
451
+ ```
452
+
453
+ The build is a plain Node script (`scripts/build.mjs`) driving Vite's JS API — there is no
454
+ `vite.config.js`. It emits exactly four files into `dist/`:
455
+
456
+ | File | Format |
457
+ | ----------------------------- | --------------------------------------- |
458
+ | `gift-with-purchase.esm.js` | ES module (unminified) |
459
+ | `gift-with-purchase.min.js` | UMD, minified, global `GiftWithPurchase` |
460
+ | `gift-with-purchase.css` | Stylesheet |
461
+ | `gift-with-purchase.min.css` | Stylesheet, minified |
462
+
326
463
  ## Contributing
327
464
 
328
465
  Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
@@ -8,36 +8,42 @@ gift-with-purchase {
8
8
  --gwp-border-added: #155724;
9
9
  --gwp-text-active: #155724;
10
10
  --gwp-text-added: #155724;
11
- display: block;
12
11
  border: 2px solid var(--gwp-border-active);
13
12
  border-radius: var(--gwp-border-radius);
14
13
  padding: var(--gwp-padding);
15
14
  background-color: var(--gwp-bg-active);
16
15
  color: var(--gwp-text-active);
16
+ display: block;
17
17
  }
18
+
18
19
  gift-with-purchase .gwp-product {
19
- display: flex;
20
20
  align-items: flex-start;
21
21
  gap: var(--gwp-gap);
22
+ display: flex;
22
23
  }
24
+
23
25
  gift-with-purchase [data-gwp-image] {
24
26
  flex-shrink: 0;
25
27
  }
28
+
26
29
  gift-with-purchase .gwp-content {
27
30
  flex: 1;
28
31
  min-width: 0;
29
32
  }
30
- gift-with-purchase[state=active] {
33
+
34
+ gift-with-purchase[state="active"] {
31
35
  background-color: var(--gwp-bg-active);
32
36
  border-color: var(--gwp-border-active);
33
37
  color: var(--gwp-text-active);
34
38
  }
35
- gift-with-purchase[state=added] {
39
+
40
+ gift-with-purchase[state="added"] {
36
41
  background-color: var(--gwp-bg-added);
37
42
  border-color: var(--gwp-border-added);
38
43
  color: var(--gwp-text-added);
39
44
  }
40
- gift-with-purchase[state=ended] {
45
+
46
+ gift-with-purchase[state="ended"], gift-with-purchase[state="disabled"] {
41
47
  display: none;
42
48
  }
43
- /*# sourceMappingURL=gift-with-purchase.cjs.css.map */
49
+ /*$vite$:1*/