@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 CHANGED
@@ -8,7 +8,7 @@ A powerful, e-commerce web component for automatic gift-with-purchase threshold
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
14
  - 🔧 **Flexible Content** - Data attributes for dynamic image, title, and variant updates
@@ -27,8 +27,9 @@ npm install @magic-spells/gift-with-purchase
27
27
  threshold="75.00"
28
28
  current="45.00"
29
29
  variant-id="12345678"
30
+ money-format="${{amount}}"
30
31
  message-above="🎉 Congratulations! You've qualified for your FREE gift!"
31
- message-below="Add ${ amount } more to unlock your free gift! 🎁">
32
+ message-below="Add [amount] more to unlock your free gift! 🎁">
32
33
  <div class="gwp-product">
33
34
  <img src="gift-image.jpg" alt="Free Gift" />
34
35
  <div class="gwp-content">
@@ -46,21 +47,22 @@ npm install @magic-spells/gift-with-purchase
46
47
 
47
48
  ## Cart Integration
48
49
 
49
- The component automatically listens for cart data changes when placed inside a `<cart-dialog>` component from the `@magic-spells/cart-panel` package:
50
+ The component automatically listens for cart data changes when placed inside a `<cart-panel>` component from the `@magic-spells/cart-panel` package:
50
51
 
51
- ```html
52
- <cart-dialog>
52
+ ```liquid
53
+ <cart-panel>
53
54
  <gift-with-purchase
54
55
  threshold="75.00"
55
56
  variant-id="12345678"
57
+ money-format={{ shop.money_format | json }}
56
58
  message-above="🎉 Congratulations! You've qualified for your FREE gift!"
57
- message-below="Add ${ amount } more to unlock your free gift! 🎁">
59
+ message-below="Add [amount] more to unlock your free gift! 🎁">
58
60
  <!-- Gift content -->
59
61
  </gift-with-purchase>
60
- </cart-dialog>
62
+ </cart-panel>
61
63
  ```
62
64
 
63
- When the cart-dialog emits a `cart-dialog:data-changed` event (typically from Shopify cart updates), the gift component will automatically:
65
+ When the cart-panel emits a `cart-panel:data-changed` event (typically from Shopify cart updates), the gift component will automatically:
64
66
 
65
67
  - Update the current cart amount using `calculated_subtotal` for accurate threshold calculation
66
68
  - Check if the threshold is met (excludes other gifts and honors pricing exclusions)
@@ -73,42 +75,74 @@ The component uses intelligent threshold calculation:
73
75
  - **Uses `calculated_subtotal`** from cart-panel which properly handles item exclusions
74
76
  - **Excludes gifts**: Other gifts with purchase won't count toward this threshold
75
77
  - **Includes bundle items**: Hidden bundle components that should count are included
76
- - **Backwards compatible**: Falls back to `total_price` if `calculated_subtotal` unavailable
78
+ - **Multi-currency support**: Automatically converts threshold using `Shopify.currency.rate` for stores with multiple currencies
79
+ - **Requires cart-panel**: The component requires `calculated_subtotal` in cart events to function
77
80
 
78
81
  ## JavaScript API
79
82
 
80
83
  ```javascript
81
84
  const gwp = document.querySelector('gift-with-purchase');
82
85
 
83
- // Update cart amount
86
+ // Setters - update state programmatically
84
87
  gwp.setCurrentAmount(85.5);
85
-
86
- // Change threshold
87
88
  gwp.setThreshold(100.0);
88
-
89
- // Update variant ID
90
89
  gwp.setVariantId('87654321');
91
90
 
92
- // Get current state
91
+ // Get full state object
93
92
  const state = gwp.getState();
94
93
  console.log(state.isActive, state.isAdded, state.remainingAmount);
95
94
 
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
- }
95
+ // Readonly getters - access individual properties
96
+ console.log(gwp.currentAmount); // number - current cart amount
97
+ console.log(gwp.threshold); // number - threshold amount
98
+ console.log(gwp.variantId); // string - gift variant ID
99
+ console.log(gwp.isActive); // boolean - threshold met
100
+ console.log(gwp.isAdded); // boolean - gift in cart
101
+ console.log(gwp.promoEnded); // boolean - promo has ended
102
+ console.log(gwp.productAvailable); // boolean - gift product available
103
+ console.log(gwp.isDisabled); // boolean - promo ended OR product unavailable
101
104
  ```
102
105
 
103
106
  ## Attributes
104
107
 
105
108
  | Attribute | Description | Example |
106
109
  | --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ |
107
- | `threshold` | Spending threshold to unlock the gift | `"75.00"` |
108
- | `current` | Current cart amount | `"45.00"` |
110
+ | `threshold` | Spending threshold to unlock the gift (auto-converts for multi-currency) | `"75.00"` |
111
+ | `current` | Current cart amount (typically set automatically via cart-panel) | `"45.00"` |
109
112
  | `variant-id` | Shopify variant ID for the gift product | `"12345678"` |
113
+ | `promo-ended` | Disables the promo and hides the component | `"true"` |
114
+ | `product-available` | Whether the gift product is available (disables if false) | `"true"` |
110
115
  | `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! 🎁"` |
116
+ | `message-below` | Message shown below threshold; use `[amount]` for remaining amount | `"Add [amount] more to unlock your free gift! 🎁"` |
117
+ | `money-format` | Shopify-style money format for currency display | `"${{amount}}"` |
118
+
119
+ ### The `[amount]` Placeholder
120
+
121
+ In the `message-below` attribute, use `[amount]` (with square brackets) to insert the remaining amount needed to reach the threshold. The component automatically:
122
+
123
+ - Calculates the remaining amount (`threshold - currentAmount`)
124
+ - Converts to the customer's currency using `Shopify.currency.rate`
125
+ - Formats using your `money-format` attribute
126
+
127
+ ```html
128
+ <!-- Example: If threshold is $75 and cart is $45, displays "Add $30 more..." -->
129
+ <gift-with-purchase
130
+ threshold="75.00"
131
+ money-format="${{amount}}"
132
+ message-below="Add [amount] more to unlock your free gift!">
133
+ </gift-with-purchase>
134
+ ```
135
+
136
+ **Why square brackets?** We use `[amount]` instead of `{{amount}}` to avoid conflicts with Shopify Liquid templates and JavaScript template literals.
137
+
138
+ ### Multi-Currency Support
139
+
140
+ The component automatically handles currency conversion for multi-currency Shopify stores:
141
+
142
+ - Set `threshold` in your store's **base currency** (e.g., USD)
143
+ - The component reads `Shopify.currency.rate` from the browser
144
+ - Threshold is automatically converted to the customer's selected currency
145
+ - The `[amount]` placeholder displays in the converted currency
112
146
 
113
147
  ## Message Element
114
148
 
@@ -127,6 +161,62 @@ The component requires a message element to display threshold messages:
127
161
  </gift-with-purchase>
128
162
  ```
129
163
 
164
+ ## Currency Formatting
165
+
166
+ Use the `money-format` attribute to format amounts with the correct currency symbol and formatting:
167
+
168
+ ```html
169
+ <gift-with-purchase
170
+ threshold="75.00"
171
+ variant-id="12345678"
172
+ money-format="${{amount}}"
173
+ message-below="Add [amount] more to unlock your free gift!">
174
+ <!-- Gift content -->
175
+ </gift-with-purchase>
176
+ ```
177
+
178
+ In Shopify themes, you can pass the shop's money format using the `json` filter:
179
+
180
+ ```liquid
181
+ <gift-with-purchase
182
+ threshold="75.00"
183
+ variant-id="12345678"
184
+ money-format={{ shop.money_format | json }}
185
+ message-below="Add [amount] more to unlock your free gift!">
186
+ <!-- Gift content -->
187
+ </gift-with-purchase>
188
+ ```
189
+
190
+ Available Shopify money formats:
191
+ - `shop.money_format` - Basic format (e.g., `${{amount}}`)
192
+ - `shop.money_with_currency_format` - With currency code (e.g., `${{amount}} USD`)
193
+
194
+ ### Supported Format Placeholders
195
+
196
+ | Placeholder | Output | Example |
197
+ | ----------- | ------ | ------- |
198
+ | `{{amount}}` | Amount with decimals | `19.99` |
199
+ | `{{amount_no_decimals}}` | Rounded whole number | `20` |
200
+ | `{{amount_with_comma_separator}}` | Comma as decimal | `19,99` |
201
+ | `{{amount_no_decimals_with_comma_separator}}` | Whole with comma thousands | `1,000` |
202
+
203
+ ### Examples by Currency
204
+
205
+ ```html
206
+ <!-- USD -->
207
+ money-format="${{amount}}" <!-- $30.00 -->
208
+
209
+ <!-- EUR -->
210
+ money-format="€{{amount}}" <!-- €30.00 -->
211
+ money-format="{{amount}} €" <!-- 30.00 € -->
212
+
213
+ <!-- GBP -->
214
+ money-format="£{{amount}}" <!-- £30.00 -->
215
+
216
+ <!-- JPY (no decimals) -->
217
+ money-format="¥{{amount_no_decimals}}" <!-- ¥3000 -->
218
+ ```
219
+
130
220
  ## Events
131
221
 
132
222
  The component emits custom events for integration:
@@ -184,9 +274,11 @@ The component automatically applies a `state` attribute based on its current con
184
274
 
185
275
  - `state="active"` - Threshold met, gift available to add
186
276
  - `state="added"` - Gift successfully added to cart
277
+ - `state="inactive"` - Below threshold (component idle)
187
278
  - `state="ended"` - Promo ended (component hidden)
279
+ - `state="disabled"` - Gift unavailable (component hidden)
188
280
 
189
- Note: There is no `inactive` state since the component would typically not be loaded at all when below threshold in most Shopify implementations.
281
+ Note: `ended` and `disabled` both hide the component while still removing any existing gift items.
190
282
 
191
283
  ## Shopify Integration Details
192
284
 
@@ -248,7 +340,7 @@ In your Shopify cart template, you can identify and handle gift items:
248
340
  threshold="50.00"
249
341
  variant-id="111111"
250
342
  message-above="🎉 You've unlocked a free tote bag!"
251
- message-below="Add ${ amount } more for a free tote bag! 👜">
343
+ message-below="Add [amount] more for a free tote bag! 👜">
252
344
  <div class="gwp-product">
253
345
  <img src="tote-bag.jpg" alt="Free Tote Bag" />
254
346
  <h4 class="gwp-title">Free Tote Bag</h4>
@@ -261,7 +353,7 @@ In your Shopify cart template, you can identify and handle gift items:
261
353
  threshold="100.00"
262
354
  variant-id="222222"
263
355
  message-above="✨ Amazing! You've earned our premium gift set!"
264
- message-below="Spend ${ amount } more for our exclusive premium gift set! ✨">
356
+ message-below="Spend [amount] more for our exclusive premium gift set! ✨">
265
357
  <div class="gwp-product">
266
358
  <img src="gift-set.jpg" alt="Premium Gift Set" />
267
359
  <h4 class="gwp-title">Premium Gift Set</h4>
@@ -313,10 +405,15 @@ Type definitions are included in the package:
313
405
  interface GiftWithPurchaseState {
314
406
  currentAmount: number;
315
407
  threshold: number;
408
+ convertedThreshold: number;
316
409
  variantId: string | null;
317
410
  isActive: boolean;
318
411
  isAdded: boolean;
412
+ promoEnded: boolean;
413
+ productAvailable: boolean;
414
+ isDisabled: boolean;
319
415
  remainingAmount: number;
416
+ currencyRate: number;
320
417
  }
321
418
 
322
419
  // Component automatically handles message injection
@@ -40,4 +40,7 @@ gift-with-purchase[state=added] {
40
40
  gift-with-purchase[state=ended] {
41
41
  display: none;
42
42
  }
43
+ gift-with-purchase[state=disabled] {
44
+ display: none;
45
+ }
43
46
  /*# sourceMappingURL=gift-with-purchase.cjs.css.map */
@@ -1 +1 @@
1
- {"version":3,"sources":["gift-with-purchase.scss"],"names":[],"mappings":"AAAA;EACE,wBAAwB;EACxB,mBAAmB;EACnB,eAAe;EACf,wBAAwB;EACxB,uBAAuB;EACvB,4BAA4B;EAC5B,2BAA2B;EAC3B,0BAA0B;EAC1B,yBAAyB;EACzB,cAAc;EACd,0CAA0C;EAC1C,uCAAuC;EACvC,2BAA2B;EAC3B,sCAAsC;EACtC,6BAA6B;AAC/B;AACA;EACE,aAAa;EACb,uBAAuB;EACvB,mBAAmB;AACrB;AACA;EACE,cAAc;AAChB;AACA;EACE,OAAO;EACP,YAAY;AACd;AACA;EACE,sCAAsC;EACtC,sCAAsC;EACtC,6BAA6B;AAC/B;AACA;EACE,qCAAqC;EACrC,qCAAqC;EACrC,4BAA4B;AAC9B;AACA;EACE,aAAa;AACf","file":"gift-with-purchase.cjs.css","sourcesContent":["gift-with-purchase {\n --gwp-border-radius: 8px;\n --gwp-padding: 1rem;\n --gwp-gap: 1rem;\n --gwp-bg-active: #e8f5e8;\n --gwp-bg-added: #d4edda;\n --gwp-border-active: #28a745;\n --gwp-border-added: #155724;\n --gwp-text-active: #155724;\n --gwp-text-added: #155724;\n display: block;\n border: 2px solid var(--gwp-border-active);\n border-radius: var(--gwp-border-radius);\n padding: var(--gwp-padding);\n background-color: var(--gwp-bg-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase .gwp-product {\n display: flex;\n align-items: flex-start;\n gap: var(--gwp-gap);\n}\ngift-with-purchase [data-gwp-image] {\n flex-shrink: 0;\n}\ngift-with-purchase .gwp-content {\n flex: 1;\n min-width: 0;\n}\ngift-with-purchase[state=active] {\n background-color: var(--gwp-bg-active);\n border-color: var(--gwp-border-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase[state=added] {\n background-color: var(--gwp-bg-added);\n border-color: var(--gwp-border-added);\n color: var(--gwp-text-added);\n}\ngift-with-purchase[state=ended] {\n display: none;\n}"]}
1
+ {"version":3,"sources":["gift-with-purchase.scss"],"names":[],"mappings":"AAAA;EACE,wBAAwB;EACxB,mBAAmB;EACnB,eAAe;EACf,wBAAwB;EACxB,uBAAuB;EACvB,4BAA4B;EAC5B,2BAA2B;EAC3B,0BAA0B;EAC1B,yBAAyB;EACzB,cAAc;EACd,0CAA0C;EAC1C,uCAAuC;EACvC,2BAA2B;EAC3B,sCAAsC;EACtC,6BAA6B;AAC/B;AACA;EACE,aAAa;EACb,uBAAuB;EACvB,mBAAmB;AACrB;AACA;EACE,cAAc;AAChB;AACA;EACE,OAAO;EACP,YAAY;AACd;AACA;EACE,sCAAsC;EACtC,sCAAsC;EACtC,6BAA6B;AAC/B;AACA;EACE,qCAAqC;EACrC,qCAAqC;EACrC,4BAA4B;AAC9B;AACA;EACE,aAAa;AACf;AACA;EACE,aAAa;AACf","file":"gift-with-purchase.cjs.css","sourcesContent":["gift-with-purchase {\n --gwp-border-radius: 8px;\n --gwp-padding: 1rem;\n --gwp-gap: 1rem;\n --gwp-bg-active: #e8f5e8;\n --gwp-bg-added: #d4edda;\n --gwp-border-active: #28a745;\n --gwp-border-added: #155724;\n --gwp-text-active: #155724;\n --gwp-text-added: #155724;\n display: block;\n border: 2px solid var(--gwp-border-active);\n border-radius: var(--gwp-border-radius);\n padding: var(--gwp-padding);\n background-color: var(--gwp-bg-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase .gwp-product {\n display: flex;\n align-items: flex-start;\n gap: var(--gwp-gap);\n}\ngift-with-purchase [data-gwp-image] {\n flex-shrink: 0;\n}\ngift-with-purchase .gwp-content {\n flex: 1;\n min-width: 0;\n}\ngift-with-purchase[state=active] {\n background-color: var(--gwp-bg-active);\n border-color: var(--gwp-border-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase[state=added] {\n background-color: var(--gwp-bg-added);\n border-color: var(--gwp-border-added);\n color: var(--gwp-text-added);\n}\ngift-with-purchase[state=ended] {\n display: none;\n}\ngift-with-purchase[state=disabled] {\n display: none;\n}"]}