@runwell/shopify-toolkit 0.1.0 → 0.2.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/modules/_shared/css-tokens/assets/runwell-tokens.css +14 -0
- package/modules/_shared/css-tokens/module.json +13 -0
- package/modules/cart-cross-sell/README.md +32 -0
- package/modules/cart-cross-sell/module.json +15 -0
- package/modules/cart-cross-sell/snippets/runwell-cart-xsell.liquid +40 -0
- package/modules/cart-freeship-progress/README.md +29 -0
- package/modules/cart-freeship-progress/module.json +16 -0
- package/modules/cart-freeship-progress/snippets/runwell-cart-freeship.liquid +27 -0
- package/modules/cart-usps/README.md +22 -0
- package/modules/cart-usps/module.json +17 -0
- package/modules/cart-usps/snippets/runwell-cart-usps.liquid +11 -0
- package/modules/editorial-hero/sections/runwell-video-hero.liquid +9 -3
- package/modules/gift-with-purchase/README.md +36 -0
- package/modules/gift-with-purchase/assets/runwell-gwp.js +42 -0
- package/modules/gift-with-purchase/module.json +32 -0
- package/modules/gift-with-purchase/snippets/runwell-gwp.liquid +30 -0
- package/modules/loyalty-tiers/README.md +45 -0
- package/modules/loyalty-tiers/module.json +40 -0
- package/modules/loyalty-tiers/sections/runwell-tier-card.liquid +86 -0
- package/modules/product-badges/README.md +35 -0
- package/modules/product-badges/module.json +16 -0
- package/modules/product-badges/snippets/runwell-product-badges.liquid +19 -0
- package/modules/quantity-breaks/README.md +33 -0
- package/modules/quantity-breaks/module.json +35 -0
- package/modules/quantity-breaks/snippets/runwell-quantity-breaks.liquid +28 -0
- package/modules/quick-view/README.md +36 -0
- package/modules/quick-view/assets/runwell-quickview.js +153 -0
- package/modules/quick-view/module.json +14 -0
- package/modules/quick-view/snippets/runwell-quickview-modal.liquid +14 -0
- package/modules/quick-view/snippets/runwell-quickview-trigger.liquid +19 -0
- package/modules/subscriptions/README.md +37 -0
- package/modules/subscriptions/module.json +36 -0
- package/modules/subscriptions/snippets/runwell-subscription-picker.liquid +35 -0
- package/modules/wishlist/README.md +48 -0
- package/modules/wishlist/assets/runwell-wishlist.js +112 -0
- package/modules/wishlist/module.json +25 -0
- package/modules/wishlist/sections/runwell-wishlist-page.liquid +35 -0
- package/modules/wishlist/snippets/runwell-wishlist-icon.liquid +17 -0
- package/modules/wishlist/templates/page.wishlist.json +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Runwell Shopify Toolkit: brand tokens.
|
|
2
|
+
Generated from runwell.config.json brand vars. Every other module
|
|
3
|
+
references these via var(--runwell-X). Do not hand-edit in client
|
|
4
|
+
themes; re-run runwell-shopify sync to update.
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--runwell-primary: {{brand.primary}};
|
|
8
|
+
--runwell-accent: {{brand.accent}};
|
|
9
|
+
--runwell-cream: {{brand.cream}};
|
|
10
|
+
--runwell-oat: {{brand.oat}};
|
|
11
|
+
--runwell-celadon: {{brand.celadon}};
|
|
12
|
+
--runwell-blue: {{brand.blue}};
|
|
13
|
+
--runwell-rain-forrest: {{brand.rain-forrest}};
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "_shared/css-tokens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "foundation",
|
|
5
|
+
"description": "CSS custom properties published as :root vars so every Runwell module pulls brand colors from one source. Always synced.",
|
|
6
|
+
"always_enabled": true,
|
|
7
|
+
"files": {
|
|
8
|
+
"assets": ["assets/runwell-tokens.css"]
|
|
9
|
+
},
|
|
10
|
+
"config": {
|
|
11
|
+
"schema": {}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# cart-cross-sell
|
|
2
|
+
|
|
3
|
+
Cart drawer cross-sell suggestion. Picks the first available product not already in cart and renders a one-click Add card.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `snippets/runwell-cart-xsell.liquid`
|
|
8
|
+
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
In `snippets/cart-drawer.liquid` body:
|
|
12
|
+
|
|
13
|
+
```liquid
|
|
14
|
+
{% render 'runwell-cart-xsell' %}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Config
|
|
18
|
+
|
|
19
|
+
| Key | Default | Notes |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `eyebrow` | `Pairs well with` | Tag above the suggestion |
|
|
22
|
+
| `cta_label` | `Add` | Button label |
|
|
23
|
+
|
|
24
|
+
## Replaces
|
|
25
|
+
|
|
26
|
+
Rebuy, One Click Upsell (OCU) pre-purchase display layer.
|
|
27
|
+
|
|
28
|
+
## Future variants
|
|
29
|
+
|
|
30
|
+
- `tag-based`: filter to products tagged complementary to cart contents
|
|
31
|
+
- `metafield-curated`: per-product complementary set in `lushi.complements` metafield
|
|
32
|
+
- `recommendations-api`: Shopify `recommendations.json?intent=complementary`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cart-cross-sell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "conversion",
|
|
5
|
+
"description": "Cart drawer cross-sell card. Suggests one related product not yet in cart with one-click Add. Replaces Rebuy / OneClickUpsell pre-purchase upsell.",
|
|
6
|
+
"files": {
|
|
7
|
+
"snippets": ["snippets/runwell-cart-xsell.liquid"]
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"schema": {
|
|
11
|
+
"eyebrow": { "type": "string", "default": "Pairs well with" },
|
|
12
|
+
"cta_label": { "type": "string", "default": "Add" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Runwell cart cross-sell. Surfaces the first available product not
|
|
3
|
+
already in cart. Replaces Rebuy/OneClickUpsell pre-purchase upsell
|
|
4
|
+
display. Render inside cart-drawer.liquid via:
|
|
5
|
+
{% render 'runwell-cart-xsell' %}
|
|
6
|
+
{%- endcomment -%}
|
|
7
|
+
|
|
8
|
+
{%- if cart.item_count > 0 -%}
|
|
9
|
+
{%- assign cart_handles = cart.items | map: 'handle' -%}
|
|
10
|
+
{%- assign suggestion = blank -%}
|
|
11
|
+
{%- for p in collections.all.products -%}
|
|
12
|
+
{%- unless cart_handles contains p.handle -%}
|
|
13
|
+
{%- if p.available -%}
|
|
14
|
+
{%- assign suggestion = p -%}
|
|
15
|
+
{%- break -%}
|
|
16
|
+
{%- endif -%}
|
|
17
|
+
{%- endunless -%}
|
|
18
|
+
{%- endfor -%}
|
|
19
|
+
{%- if suggestion != blank -%}
|
|
20
|
+
<div class="runwell-cart-xsell" data-runwell-xsell>
|
|
21
|
+
<p class="runwell-cart-xsell__eyebrow">{{config.eyebrow}}</p>
|
|
22
|
+
<div class="runwell-cart-xsell__row">
|
|
23
|
+
{%- if suggestion.featured_image -%}
|
|
24
|
+
<a href="{{ suggestion.url }}" class="runwell-cart-xsell__media" aria-hidden="true" tabindex="-1">
|
|
25
|
+
<img src="{{ suggestion.featured_image | image_url: width: 120 }}" alt="" width="60" height="60" loading="lazy">
|
|
26
|
+
</a>
|
|
27
|
+
{%- endif -%}
|
|
28
|
+
<div class="runwell-cart-xsell__body">
|
|
29
|
+
<a href="{{ suggestion.url }}" class="runwell-cart-xsell__title">{{ suggestion.title }}</a>
|
|
30
|
+
<span class="runwell-cart-xsell__price">{{ suggestion.price | money }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<form action="{{ routes.cart_add_url }}" method="post" enctype="multipart/form-data" class="runwell-cart-xsell__form">
|
|
33
|
+
<input type="hidden" name="id" value="{{ suggestion.selected_or_first_available_variant.id }}">
|
|
34
|
+
<input type="hidden" name="quantity" value="1">
|
|
35
|
+
<button type="submit" class="runwell-cart-xsell__cta">{{config.cta_label}}</button>
|
|
36
|
+
</form>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{%- endif -%}
|
|
40
|
+
{%- endif -%}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# cart-freeship-progress
|
|
2
|
+
|
|
3
|
+
Cart drawer free-shipping progress bar. Renders dynamic remaining-to-free-ship message + a CSS progress fill that updates with cart total.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `snippets/runwell-cart-freeship.liquid`
|
|
8
|
+
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
In `snippets/cart-drawer.liquid` (or wherever your cart drawer body is):
|
|
12
|
+
|
|
13
|
+
```liquid
|
|
14
|
+
{% render 'runwell-cart-freeship' %}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
CSS is part of `_shared/css-tokens` + the styles already present in the merchant theme stylesheet (look for `.runwell-freeship` rules in your CSS).
|
|
18
|
+
|
|
19
|
+
## Config
|
|
20
|
+
|
|
21
|
+
| Key | Default | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `threshold_cents` | `7500` | Threshold in cents (USD `$75 = 7500`). Adjust for currency. |
|
|
24
|
+
| `away_text` | `away from free shipping.` | Text appended after the dynamic remaining amount |
|
|
25
|
+
| `unlocked_message` | `You unlocked free shipping. <strong>Glow on.</strong>` | HTML shown when cart total clears the threshold |
|
|
26
|
+
|
|
27
|
+
## Replaces
|
|
28
|
+
|
|
29
|
+
Bold Free Shipping Manager, similar app features.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cart-freeship-progress",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "conversion",
|
|
5
|
+
"description": "Cart drawer free-shipping progress bar. Shows remaining $$ to free ship + visual progress fill. Replaces Bold Free Shipping Manager and similar app features.",
|
|
6
|
+
"files": {
|
|
7
|
+
"snippets": ["snippets/runwell-cart-freeship.liquid"]
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"schema": {
|
|
11
|
+
"threshold_cents": { "type": "number", "default": 7500, "label": "Free shipping threshold (cents)" },
|
|
12
|
+
"away_text": { "type": "string", "default": "away from free shipping.", "label": "Text after remaining amount" },
|
|
13
|
+
"unlocked_message": { "type": "string", "default": "You unlocked free shipping. <strong>Glow on.</strong>", "label": "HTML shown when threshold met" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Runwell cart free-shipping progress bar.
|
|
3
|
+
Renders inside cart-drawer.liquid via {% render 'runwell-cart-freeship' %}.
|
|
4
|
+
Replaces inline implementations and the freeship-bar feature in apps
|
|
5
|
+
like Vitals / Bold Free Shipping Manager.
|
|
6
|
+
{%- endcomment -%}
|
|
7
|
+
|
|
8
|
+
{%- assign runwell_freeship_threshold_cents = {{config.threshold_cents}} -%}
|
|
9
|
+
{%- assign runwell_remaining = runwell_freeship_threshold_cents | minus: cart.total_price -%}
|
|
10
|
+
<div class="runwell-freeship">
|
|
11
|
+
{%- if cart.item_count > 0 -%}
|
|
12
|
+
{%- if runwell_remaining > 0 -%}
|
|
13
|
+
<p class="runwell-freeship__msg">
|
|
14
|
+
You're <strong>{{ runwell_remaining | money }}</strong> {{config.away_text}}
|
|
15
|
+
</p>
|
|
16
|
+
{%- else -%}
|
|
17
|
+
<p class="runwell-freeship__msg runwell-freeship__msg--unlocked">
|
|
18
|
+
{{config.unlocked_message}}
|
|
19
|
+
</p>
|
|
20
|
+
{%- endif -%}
|
|
21
|
+
<div class="runwell-freeship__track" aria-hidden="true">
|
|
22
|
+
{%- assign pct = cart.total_price | times: 100.0 | divided_by: runwell_freeship_threshold_cents -%}
|
|
23
|
+
{%- if pct > 100 -%}{%- assign pct = 100 -%}{%- endif -%}
|
|
24
|
+
<div class="runwell-freeship__fill" style="width: {{ pct }}%"></div>
|
|
25
|
+
</div>
|
|
26
|
+
{%- endif -%}
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# cart-usps
|
|
2
|
+
|
|
3
|
+
Three brand promises listed at the bottom of the cart drawer. Reinforces trust at the moment of decision.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `snippets/runwell-cart-usps.liquid`
|
|
8
|
+
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
```liquid
|
|
12
|
+
{% render 'runwell-cart-usps' %}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Config
|
|
16
|
+
|
|
17
|
+
| Key | Default | Notes |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `icon` | `✓` | Bullet icon (HTML entity or emoji) |
|
|
20
|
+
| `usp_1` | `30-day satisfaction promise` | First USP |
|
|
21
|
+
| `usp_2` | `Editorially curated, not bulk-stocked` | Second USP |
|
|
22
|
+
| `usp_3` | `Every product paired with a journal post` | Third USP |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cart-usps",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "conversion",
|
|
5
|
+
"description": "Three brand promises listed at the bottom of the cart drawer. Native, no app.",
|
|
6
|
+
"files": {
|
|
7
|
+
"snippets": ["snippets/runwell-cart-usps.liquid"]
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"schema": {
|
|
11
|
+
"icon": { "type": "string", "default": "✓", "label": "Bullet icon (HTML entity or emoji)" },
|
|
12
|
+
"usp_1": { "type": "string", "default": "30-day satisfaction promise" },
|
|
13
|
+
"usp_2": { "type": "string", "default": "Editorially curated, not bulk-stocked" },
|
|
14
|
+
"usp_3": { "type": "string", "default": "Every product paired with a journal post" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Runwell cart USPs. Three short bullets at the bottom of the cart
|
|
3
|
+
drawer reinforcing brand promises. Render via:
|
|
4
|
+
{% render 'runwell-cart-usps' %}
|
|
5
|
+
{%- endcomment -%}
|
|
6
|
+
|
|
7
|
+
<ul class="runwell-cart-usps" aria-label="Brand promises">
|
|
8
|
+
<li><span aria-hidden="true">{{config.icon}}</span> {{config.usp_1}}</li>
|
|
9
|
+
<li><span aria-hidden="true">{{config.icon}}</span> {{config.usp_2}}</li>
|
|
10
|
+
<li><span aria-hidden="true">{{config.icon}}</span> {{config.usp_3}}</li>
|
|
11
|
+
</ul>
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
height="1080"
|
|
29
29
|
loading="eager"
|
|
30
30
|
>
|
|
31
|
-
{%-
|
|
32
|
-
{%- comment -%} Fallback to bundled
|
|
31
|
+
{%- elsif section.settings.fallback_asset != blank -%}
|
|
32
|
+
{%- comment -%} Fallback to bundled brand image so the hero is never blank pre-launch {%- endcomment -%}
|
|
33
33
|
<img
|
|
34
34
|
class="runwell-video-hero__video"
|
|
35
|
-
src="{{
|
|
35
|
+
src="{{ section.settings.fallback_asset | asset_url }}"
|
|
36
36
|
alt="{{ section.settings.heading | escape }}"
|
|
37
37
|
width="1920"
|
|
38
38
|
height="1080"
|
|
@@ -109,6 +109,12 @@
|
|
|
109
109
|
"id": "poster_image",
|
|
110
110
|
"label": "Poster image (and fallback)"
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
"type": "text",
|
|
114
|
+
"id": "fallback_asset",
|
|
115
|
+
"label": "Fallback asset filename",
|
|
116
|
+
"info": "Bundled theme asset filename (e.g. lushi-hero-bg.jpg). Used when video_url and poster_image are blank."
|
|
117
|
+
},
|
|
112
118
|
{
|
|
113
119
|
"type": "range",
|
|
114
120
|
"id": "min_height",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# gift-with-purchase
|
|
2
|
+
|
|
3
|
+
Auto-add a gift product to the cart when total clears a threshold. Shows progress message before unlock and confirmation after. Combine with a Shopify 100%-off automatic discount on the gift SKU so the line lands free at checkout.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `snippets/runwell-gwp.liquid`. Cart-drawer snippet rendering progress + unlock state.
|
|
8
|
+
- `assets/runwell-gwp.js`. Auto-adder. Posts to `/cart/add.js` once when threshold is met and gift is not yet in cart.
|
|
9
|
+
|
|
10
|
+
## How to use
|
|
11
|
+
|
|
12
|
+
1. Run admin steps in `module.json`: create the gift product, create the 100% off automatic discount targeting it
|
|
13
|
+
2. Render the snippet in `snippets/cart-drawer.liquid`:
|
|
14
|
+
```liquid
|
|
15
|
+
{% render 'runwell-gwp' %}
|
|
16
|
+
```
|
|
17
|
+
3. Sync: `runwell-shopify sync`
|
|
18
|
+
|
|
19
|
+
## Behaviour
|
|
20
|
+
|
|
21
|
+
- Cart total below threshold: shows "Spend $X more for [unlocked_message]"
|
|
22
|
+
- Cart total above threshold AND gift not yet in cart: auto-adds gift, shows "[unlocked_message]"
|
|
23
|
+
- Cart total above threshold AND gift in cart: shows "[unlocked_message]" only
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
| Key | Default | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `threshold_cents` | `10000` | $100 default. Adjust per AOV. |
|
|
30
|
+
| `gift_handle` | `free-gift` | Product handle for the auto-added gift |
|
|
31
|
+
| `unlocked_message` | `You earned a free gift.` | Shown when threshold met |
|
|
32
|
+
| `locked_message_suffix` | `a free gift.` | Appended to "Spend $X more for..." |
|
|
33
|
+
|
|
34
|
+
## Replaces
|
|
35
|
+
|
|
36
|
+
Free Gifts BOGO Plus, Bold Free Gifts, Tada free gift apps.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Runwell gift-with-purchase auto-adder. When the cart drawer renders
|
|
2
|
+
a [data-runwell-gwp] element AND the configured gift product is not
|
|
3
|
+
already in cart, this script POSTs to /cart/add.js to add it.
|
|
4
|
+
Quantity is locked to 1. The merchant should also create a
|
|
5
|
+
"Gift" discount in Shopify that makes the gift product 100% off when
|
|
6
|
+
cart total >= threshold (so the auto-added line is free). */
|
|
7
|
+
(function () {
|
|
8
|
+
if (typeof window === 'undefined') return;
|
|
9
|
+
|
|
10
|
+
function autoAddGift() {
|
|
11
|
+
var unlocked = document.querySelector('.runwell-gwp--unlocked');
|
|
12
|
+
if (!unlocked) return;
|
|
13
|
+
var giftHandle = unlocked.getAttribute('data-gift-handle');
|
|
14
|
+
if (!giftHandle) return;
|
|
15
|
+
if (window._runwellGwpAdded) return;
|
|
16
|
+
window._runwellGwpAdded = true;
|
|
17
|
+
|
|
18
|
+
fetch('/products/' + giftHandle + '.js')
|
|
19
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
20
|
+
.then(function (product) {
|
|
21
|
+
if (!product) return;
|
|
22
|
+
var variantId = product.variants && product.variants[0] && product.variants[0].id;
|
|
23
|
+
if (!variantId) return;
|
|
24
|
+
return fetch('/cart/add.js', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ id: variantId, quantity: 1, properties: { _runwell_gift: '1' } })
|
|
28
|
+
});
|
|
29
|
+
})
|
|
30
|
+
.then(function () {
|
|
31
|
+
if (typeof window.fetchCart === 'function') window.fetchCart();
|
|
32
|
+
document.dispatchEvent(new CustomEvent('runwell:cart:refresh'));
|
|
33
|
+
})
|
|
34
|
+
.catch(function () { window._runwellGwpAdded = false; });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (document.readyState === 'loading') {
|
|
38
|
+
document.addEventListener('DOMContentLoaded', autoAddGift);
|
|
39
|
+
} else {
|
|
40
|
+
autoAddGift();
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gift-with-purchase",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "conversion",
|
|
5
|
+
"description": "Auto-add a gift product to the cart when total clears a threshold. Shows progress message before unlock and confirmation after. Combine with a 100%-off discount on the gift SKU so it lands free.",
|
|
6
|
+
"files": {
|
|
7
|
+
"snippets": ["snippets/runwell-gwp.liquid"],
|
|
8
|
+
"assets": ["assets/runwell-gwp.js"]
|
|
9
|
+
},
|
|
10
|
+
"config": {
|
|
11
|
+
"schema": {
|
|
12
|
+
"threshold_cents": { "type": "number", "default": 10000, "label": "Spend threshold in cents (e.g. 10000 = $100)" },
|
|
13
|
+
"gift_handle": { "type": "string", "default": "free-gift", "label": "Product handle for the gift" },
|
|
14
|
+
"unlocked_message": { "type": "string", "default": "You earned a free gift." },
|
|
15
|
+
"locked_message_suffix": { "type": "string", "default": "a free gift." }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"admin_steps": [
|
|
19
|
+
{
|
|
20
|
+
"id": "create-gift-product",
|
|
21
|
+
"label": "Create the gift product",
|
|
22
|
+
"url": "https://admin.shopify.com/store/{store_handle}/products/new",
|
|
23
|
+
"summary": "Create a product matching the gift_handle config (default: free-gift). Set a baseline price (e.g. $10) so it shows value. Inventory: enough for forecast monthly volume."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "create-gift-discount",
|
|
27
|
+
"label": "Create the 100% off discount on the gift product",
|
|
28
|
+
"url": "https://admin.shopify.com/store/{store_handle}/discounts/new",
|
|
29
|
+
"summary": "Discount type: Amount off products. Code or Automatic: Automatic preferred. Value: 100%. Applies to: Specific products > select the gift product. Minimum requirement: Subtotal >= $100 (matching threshold_cents). Customer eligibility: All customers. So the auto-added gift line lands free at checkout."
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Runwell gift-with-purchase. Shows a "free gift unlocked" message in
|
|
3
|
+
the cart drawer when cart total clears the threshold. The actual gift
|
|
4
|
+
product gets auto-added via runwell-gwp.js (cart.js write).
|
|
5
|
+
Render inside cart-drawer.liquid:
|
|
6
|
+
|
|
7
|
+
{% render 'runwell-gwp' %}
|
|
8
|
+
{%- endcomment -%}
|
|
9
|
+
|
|
10
|
+
{%- assign threshold_cents = {{config.threshold_cents}} -%}
|
|
11
|
+
{%- assign gift_handle = '{{config.gift_handle}}' -%}
|
|
12
|
+
{%- assign cart_handles = cart.items | map: 'handle' -%}
|
|
13
|
+
|
|
14
|
+
{%- if cart.total_price >= threshold_cents -%}
|
|
15
|
+
<div class="runwell-gwp runwell-gwp--unlocked" data-runwell-gwp data-gift-handle="{{ gift_handle }}">
|
|
16
|
+
<p class="runwell-gwp__msg">{{config.unlocked_message}}</p>
|
|
17
|
+
{%- unless cart_handles contains gift_handle -%}
|
|
18
|
+
<p class="runwell-gwp__detail">Adding to your bag now.</p>
|
|
19
|
+
{%- endunless -%}
|
|
20
|
+
</div>
|
|
21
|
+
{%- elsif cart.item_count > 0 -%}
|
|
22
|
+
{%- assign remaining = threshold_cents | minus: cart.total_price -%}
|
|
23
|
+
<div class="runwell-gwp runwell-gwp--locked">
|
|
24
|
+
<p class="runwell-gwp__msg">
|
|
25
|
+
Spend <strong>{{ remaining | money }}</strong> more for {{config.locked_message_suffix}}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
{%- endif -%}
|
|
29
|
+
|
|
30
|
+
<script src="{{ 'runwell-gwp.js' | asset_url }}" defer="defer"></script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# loyalty-tiers
|
|
2
|
+
|
|
3
|
+
Lifetime-spend tier card on the customer account page. Combined with Shopify Flow auto-tagging and tag-targeted discounts, replaces Smile.io / Yotpo Loyalty / LoyaltyLion. App-free.
|
|
4
|
+
|
|
5
|
+
## Why tiers, not points
|
|
6
|
+
|
|
7
|
+
Points-based loyalty needs:
|
|
8
|
+
- Real-time balance updates (Shopify Flow runs are async, lagging 1 to 5 minutes)
|
|
9
|
+
- A custom redemption endpoint that issues unique discount codes
|
|
10
|
+
- Customer education that's expensive at low scale
|
|
11
|
+
|
|
12
|
+
Tiers deliver the same psychological lever (status + reward) using only Shopify primitives: `customer.total_spent`, customer tags, Shopify Flow, tag-targeted discounts.
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
- `sections/runwell-tier-card.liquid`. Tier card section that renders on the customer account template.
|
|
17
|
+
|
|
18
|
+
## How to use
|
|
19
|
+
|
|
20
|
+
1. Run the merchant admin steps in `module.json`:
|
|
21
|
+
- Create Shopify Flow workflows that auto-tag customers as their lifetime spend crosses the thresholds
|
|
22
|
+
- Create tier-targeted discount codes (TIER_INSIDER 10%, TIER_FOUNDER 15%)
|
|
23
|
+
- Add the tier card section to `templates/customers/account.json`
|
|
24
|
+
2. Sync: `runwell-shopify sync`
|
|
25
|
+
3. The tier card renders for any logged-in customer; tier auto-derives from `customer.total_spent`
|
|
26
|
+
|
|
27
|
+
## Default tiers
|
|
28
|
+
|
|
29
|
+
| Tier | Threshold | Perk |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Curator | $0 (everyone) | Free ship over $75, 30-day returns |
|
|
32
|
+
| Insider | $250 | 10% off code (auto-applied via tag), first-look on new arrivals |
|
|
33
|
+
| Founder | $750 | 15% off code, free shipping always, free sample |
|
|
34
|
+
|
|
35
|
+
## Config
|
|
36
|
+
|
|
37
|
+
All tier names, thresholds, and perk percentages are configurable per client. See `module.json` config schema.
|
|
38
|
+
|
|
39
|
+
## Replaces
|
|
40
|
+
|
|
41
|
+
Smile.io, Yotpo Loyalty, Stamped Loyalty, LoyaltyLion (display layer; rewards still issued via Shopify Discounts).
|
|
42
|
+
|
|
43
|
+
## Plan requirement
|
|
44
|
+
|
|
45
|
+
Works on **Shopify Basic and up**. Shopify Flow is included free on all plans (with a daily action limit).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loyalty-tiers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "customer",
|
|
5
|
+
"description": "Lifetime-spend tier card on the customer account page. Combined with Shopify Flow auto-tagging and tag-targeted discounts, replaces Smile.io / Yotpo Loyalty / LoyaltyLion. App-free.",
|
|
6
|
+
"files": {
|
|
7
|
+
"sections": ["sections/runwell-tier-card.liquid"]
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"schema": {
|
|
11
|
+
"tier_0_name": { "type": "string", "default": "Curator" },
|
|
12
|
+
"tier_1_name": { "type": "string", "default": "Insider" },
|
|
13
|
+
"tier_1_threshold": { "type": "number", "default": 250 },
|
|
14
|
+
"tier_1_perk_pct": { "type": "number", "default": 10 },
|
|
15
|
+
"tier_2_name": { "type": "string", "default": "Founder" },
|
|
16
|
+
"tier_2_threshold": { "type": "number", "default": 750 },
|
|
17
|
+
"tier_2_perk_pct": { "type": "number", "default": 15 }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"admin_steps": [
|
|
21
|
+
{
|
|
22
|
+
"id": "create-tier-tags-and-flows",
|
|
23
|
+
"label": "Create Shopify Flow workflows for tier auto-tagging",
|
|
24
|
+
"url": "https://admin.shopify.com/store/{store_handle}/apps",
|
|
25
|
+
"summary": "Apps > Shopify Flow > Create workflow. Trigger: Order paid. Filter: customer.amount_spent.amount >= 250. Action: Add customer tag runwell-tier-insider + send email. Repeat for >= 750 with runwell-tier-founder."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "create-tier-discounts",
|
|
29
|
+
"label": "Create tier-targeted discount codes",
|
|
30
|
+
"url": "https://admin.shopify.com/store/{store_handle}/discounts/new",
|
|
31
|
+
"summary": "Two discount codes: TIER_INSIDER (10%) targeting customers tagged runwell-tier-insider, TIER_FOUNDER (15%) targeting customers tagged runwell-tier-founder. Both: amount off order, sitewide, no min, unlimited uses, automatic per customer."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "add-tier-card-to-account",
|
|
35
|
+
"label": "Add the tier card to customer account",
|
|
36
|
+
"url": "https://admin.shopify.com/store/{store_handle}/themes",
|
|
37
|
+
"summary": "Edit the customer/account.json template (or via theme editor on the Customer account template). Add the Runwell tier card section before the order list."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Runwell loyalty tier card. Renders on the customer account page
|
|
3
|
+
showing current tier + progress to next tier. Drives spend tiers via
|
|
4
|
+
Shopify Flow workflows that auto-tag customers (see admin_steps).
|
|
5
|
+
Replaces Smile.io / Yotpo Loyalty / LoyaltyLion display layer.
|
|
6
|
+
{%- endcomment -%}
|
|
7
|
+
|
|
8
|
+
{%- if customer != blank -%}
|
|
9
|
+
{%- assign spend = customer.total_spent | divided_by: 100.0 -%}
|
|
10
|
+
{%- assign current_tier = section.settings.tier_0_name -%}
|
|
11
|
+
{%- assign current_perk_pct = section.settings.tier_0_perk -%}
|
|
12
|
+
{%- assign next_threshold = section.settings.tier_1_threshold -%}
|
|
13
|
+
{%- assign next_tier = section.settings.tier_1_name -%}
|
|
14
|
+
|
|
15
|
+
{%- if spend >= section.settings.tier_2_threshold -%}
|
|
16
|
+
{%- assign current_tier = section.settings.tier_2_name -%}
|
|
17
|
+
{%- assign current_perk_pct = section.settings.tier_2_perk -%}
|
|
18
|
+
{%- assign next_threshold = 0 -%}
|
|
19
|
+
{%- assign next_tier = blank -%}
|
|
20
|
+
{%- elsif spend >= section.settings.tier_1_threshold -%}
|
|
21
|
+
{%- assign current_tier = section.settings.tier_1_name -%}
|
|
22
|
+
{%- assign current_perk_pct = section.settings.tier_1_perk -%}
|
|
23
|
+
{%- assign next_threshold = section.settings.tier_2_threshold -%}
|
|
24
|
+
{%- assign next_tier = section.settings.tier_2_name -%}
|
|
25
|
+
{%- endif -%}
|
|
26
|
+
|
|
27
|
+
<section class="runwell-tier" aria-labelledby="runwell-tier-heading">
|
|
28
|
+
<div class="runwell-tier__inner">
|
|
29
|
+
<header class="runwell-tier__header">
|
|
30
|
+
<p class="runwell-tier__eyebrow">{{ section.settings.eyebrow }}</p>
|
|
31
|
+
<h2 id="runwell-tier-heading" class="runwell-tier__name">{{ current_tier }}</h2>
|
|
32
|
+
{%- if current_perk_pct > 0 -%}
|
|
33
|
+
<p class="runwell-tier__perk">{{ current_perk_pct }}% off every order, applied automatically at checkout.</p>
|
|
34
|
+
{%- endif -%}
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
{%- if next_tier != blank -%}
|
|
38
|
+
{%- assign remaining = next_threshold | minus: spend -%}
|
|
39
|
+
{%- assign pct = spend | times: 100.0 | divided_by: next_threshold -%}
|
|
40
|
+
{%- if pct > 100 -%}{%- assign pct = 100 -%}{%- endif -%}
|
|
41
|
+
<div class="runwell-tier__progress">
|
|
42
|
+
<p class="runwell-tier__progress-label">${{ remaining | round }} to <strong>{{ next_tier }}</strong></p>
|
|
43
|
+
<div class="runwell-tier__progress-track" aria-hidden="true">
|
|
44
|
+
<div class="runwell-tier__progress-fill" style="width: {{ pct }}%"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
{%- else -%}
|
|
48
|
+
<p class="runwell-tier__topped">You're at the top tier. Welcome to the inside.</p>
|
|
49
|
+
{%- endif -%}
|
|
50
|
+
|
|
51
|
+
<ul class="runwell-tier__perks-list">
|
|
52
|
+
<li>Free shipping over $75</li>
|
|
53
|
+
<li>30-day satisfaction promise</li>
|
|
54
|
+
{%- if customer.tags contains 'runwell-tier-insider' or customer.tags contains 'runwell-tier-founder' -%}
|
|
55
|
+
<li>First-look access on new arrivals</li>
|
|
56
|
+
{%- endif -%}
|
|
57
|
+
{%- if customer.tags contains 'runwell-tier-founder' -%}
|
|
58
|
+
<li>Free shipping always</li>
|
|
59
|
+
<li>Free sample with every order</li>
|
|
60
|
+
{%- endif -%}
|
|
61
|
+
</ul>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
{%- endif -%}
|
|
65
|
+
|
|
66
|
+
{% schema %}
|
|
67
|
+
{
|
|
68
|
+
"name": "Runwell tier card",
|
|
69
|
+
"tag": "section",
|
|
70
|
+
"class": "section-runwell-tier",
|
|
71
|
+
"settings": [
|
|
72
|
+
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Your tier" },
|
|
73
|
+
{ "type": "text", "id": "tier_0_name", "label": "Tier 0 name", "default": "Curator" },
|
|
74
|
+
{ "type": "number", "id": "tier_0_perk", "label": "Tier 0 perk %", "default": 0 },
|
|
75
|
+
{ "type": "text", "id": "tier_1_name", "label": "Tier 1 name", "default": "Insider" },
|
|
76
|
+
{ "type": "number", "id": "tier_1_threshold", "label": "Tier 1 spend threshold ($)", "default": 250 },
|
|
77
|
+
{ "type": "number", "id": "tier_1_perk", "label": "Tier 1 perk %", "default": 10 },
|
|
78
|
+
{ "type": "text", "id": "tier_2_name", "label": "Tier 2 name", "default": "Founder" },
|
|
79
|
+
{ "type": "number", "id": "tier_2_threshold", "label": "Tier 2 spend threshold ($)", "default": 750 },
|
|
80
|
+
{ "type": "number", "id": "tier_2_perk", "label": "Tier 2 perk %", "default": 15 }
|
|
81
|
+
],
|
|
82
|
+
"presets": [
|
|
83
|
+
{ "name": "Runwell tier card" }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
{% endschema %}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# product-badges
|
|
2
|
+
|
|
3
|
+
Tag-driven product card badges (Best seller / New / Editor's pick). Renders inside the card-product snippet.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `snippets/runwell-product-badges.liquid`
|
|
8
|
+
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
In `snippets/card-product.liquid` near the existing `card__badge` block:
|
|
12
|
+
|
|
13
|
+
```liquid
|
|
14
|
+
{% render 'runwell-product-badges', product: card_product %}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Tags
|
|
18
|
+
|
|
19
|
+
- Tag a product with `best-seller` to render the Best seller badge
|
|
20
|
+
- Tag with `new` for the New badge
|
|
21
|
+
- Tag with `editor-pick` for the Editor's pick badge
|
|
22
|
+
|
|
23
|
+
Tags are added in Shopify admin: Products > {product} > Tags.
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
| Key | Default | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `label_best_seller` | `Best seller` | Localise per locale |
|
|
30
|
+
| `label_new` | `New` | Localise per locale |
|
|
31
|
+
| `label_editor_pick` | `Editor's pick` | Localise per locale |
|
|
32
|
+
|
|
33
|
+
## Replaces
|
|
34
|
+
|
|
35
|
+
Badge widgets in Vitals, BSS, and similar.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "product-badges",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"category": "social-proof",
|
|
5
|
+
"description": "Tag-driven badges on product cards (Best seller, New, Editor's pick). Native, no app.",
|
|
6
|
+
"files": {
|
|
7
|
+
"snippets": ["snippets/runwell-product-badges.liquid"]
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"schema": {
|
|
11
|
+
"label_best_seller": { "type": "string", "default": "Best seller" },
|
|
12
|
+
"label_new": { "type": "string", "default": "New" },
|
|
13
|
+
"label_editor_pick": { "type": "string", "default": "Editor's pick" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|