How to make a coupon reminder using Joy SDK
Introduction
If customers can’t see their reward, they won’t use it. In a few lines, the Joy SDK gives you the signals to surface a tiny, on-brand reminder—right where it helps. Under the hood, we call rewardList (optionally with filters like isAvailableCoupon: true), sort for the newest unused coupon, and render a small UI.

What we’re building (tl;dr)
- A small, dismissible UI that shows the latest unused coupon for the logged-in customer
- No duplicates: if the same code is already in the cart, we don’t render
- Looks native to the theme (rounded card, soft shadow, single CTA)
- Theme Editor preview: shows a mock coupon so merchants can style it (even without live data)
This is one of many “micro-UIs” you can craft with the Joy SDK. Start here, then remix it for cart drawers, price chips, or toasts.
Get our hand on
To demo what’s possible with the Joy JS SDK, let’s craft a Shopify implementation that uses joyInstance.rewardList to show a helpful coupon reminder.
As a custom liquid block
This example adds a coupon reminder to your Online Store. It checks whether the customer has unused coupons in Joy; if so, it surfaces a reminder. If the customer already has that same code applied in the cart, it stays hidden.
Steps to take:
- In the Theme Editor, add a Custom liquid block where you want the reminder to appear.
- Set the block’s top/bottom padding to 0 so the fixed card doesn’t add extra white space.
- Paste the script scaffold below and replace the placeholders with your utilities/styles.

<script>
(function () {
'use strict';
// -------- small logger (never throws) --------
const LOG_TAG = 'Joy coupon bar';
const inEditor = !!(window.Shopify && window.Shopify.designMode);
const log = (...args) => { try { console.warn(LOG_TAG + ':', ...args); } catch (_) {} };
// -------- helpers --------
async function fetchCart() {
try {
const res = await fetch('/cart.js', { credentials: 'same-origin' });
if (!res || !res.ok) return null;
return await res.json();
} catch (e) {
log('fetchCart error', e);
return null;
}
}
async function getAppliedDiscountCodes() {
try {
const cart = await fetchCart();
if (!cart) return [];
return (cart.discount_codes || [])
.map(dc => String(dc?.code || '').toUpperCase())
.filter(Boolean);
} catch (e) {
log('getAppliedDiscountCodes error', e);
return [];
}
}
function createStylesOnce() {
try {
if (document.getElementById('joy-announce-styles')) return;
const css = `
@keyframes joySlideUp { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
#joy-announce-wrap{position:fixed;left:0;right:0;bottom:8px;z-index:9999;pointer-events:none}
#joy-announce {
pointer-events:auto;
margin:0 auto; max-width:980px;
background: linear-gradient(180deg, #ffffff, #fbfbfb);
border:1px solid rgba(0,0,0,.08);
box-shadow: 0 8px 24px rgba(0,0,0,.12);
border-radius:16px;
padding:12px 16px; gap:10px;
display:flex; align-items:center; justify-content:center; flex-wrap:wrap;
font-size:14px; line-height:1.35; color:#2b2b2b;
animation: joySlideUp .22s ease-out; position:relative;
}
#joy-announce .joy-emoji{font-size:18px; margin-right:4px}
#joy-announce .joy-strong{font-weight:700}
#joy-announce .joy-code{font-family: ui-monospace, SFMono-Regular, Menlo, monospace}
#joy-announce .joy-cta{
display:inline-block; padding:8px 14px; border-radius:999px; font-weight:600; text-decoration:none;
border:1px solid rgba(0,0,0,.08); background:#111; color:#fff;
}
#joy-announce .joy-cta:focus{outline:2px solid #111; outline-offset:2px}
#joy-announce .joy-dismiss{
position:absolute; right:8px; top:8px; width:28px; height:28px; border:none; background:transparent; cursor:pointer;
border-radius:6px; color:#666; font-size:18px; line-height:1;
}
#joy-announce .joy-dismiss:hover{background:rgba(0,0,0,.06)}
@media (max-width: 640px){
#joy-announce{margin:0 8px; padding:10px 12px; border-radius:14px}
#joy-announce .joy-cta{width:100%; text-align:center}
}
@media (prefers-reduced-motion: reduce){ #joy-announce{animation:none} }
`;
const style = document.createElement('style');
style.id = 'joy-announce-styles';
style.textContent = css;
document.head.appendChild(style);
} catch (e) {
log('createStylesOnce error', e);
}
}
function mountBar(code, label, benefit, { respectDismiss = true } = {}) {
try {
if (!code) return null;
if (document.getElementById('joy-announce-wrap')) return null;
if (respectDismiss && sessionStorage.getItem('joy_announcement_dismissed') === '1') return null;
createStylesOnce();
const wrap = document.createElement('div');
wrap.id = 'joy-announce-wrap';
const bar = document.createElement('div');
bar.id = 'joy-announce';
bar.setAttribute('role', 'region');
bar.setAttribute('aria-label', 'Loyalty coupon announcement');
bar.innerHTML = `
<button type="button" class="joy-dismiss" aria-label="Dismiss">×</button>
<span class="joy-emoji" aria-hidden="true">🎉</span>
<span><span class="joy-strong">Unused ${label} coupon:</span>
<span>Use <span class="joy-strong">${benefit}</span> with code
<span class="joy-code joy-strong">${code}</span>.
</span>
</span>
<a class="joy-cta" href="/discount/${encodeURIComponent(code)}">Apply now</a>
`;
wrap.appendChild(bar);
document.body.appendChild(wrap);
// add bottom padding while visible
const paddingEl = document.documentElement;
const prevPad = paddingEl.style.paddingBottom;
paddingEl.style.paddingBottom = '64px';
// dismiss handler
try {
bar.querySelector('.joy-dismiss')?.addEventListener('click', () => {
try { if (respectDismiss) sessionStorage.setItem('joy_announcement_dismissed', '1'); } catch(_) {}
try { wrap.remove(); } catch(_) {}
try { paddingEl.style.paddingBottom = prevPad || ''; } catch(_) {}
});
} catch (e) { log('dismiss binding error', e); }
return {
wrap,
bar,
cleanup: () => {
try { wrap?.remove(); } catch (_) {}
try { paddingEl.style.paddingBottom = prevPad || ''; } catch (_) {}
}
};
} catch (e) {
log('mountBar error', e);
return null;
}
}
// -------- Theme Editor preview (mock coupon) --------
(function editorPreview() {
try {
if (!inEditor) return;
const renderPreview = () => {
try {
document.getElementById('joy-announce-wrap')?.remove();
mountBar('JOY-PREVIEW10', 'reward', '10% off', { respectDismiss: false });
} catch (e) { log('renderPreview error', e); }
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderPreview);
} else {
renderPreview();
}
document.addEventListener('shopify:section:load', renderPreview);
document.addEventListener('shopify:section:select', renderPreview);
} catch (e) {
log('editorPreview error', e);
}
})();
// -------- Live logic (Joy) --------
window.addEventListener('joy:ready', async () => {
if (inEditor) return; // preview handles editor
try {
const ji = window.joyInstance || window.joy;
if (!ji) return;
// avoid dupes / respect dismiss
if (document.getElementById('joy-announce-wrap')) return;
if (sessionStorage.getItem('joy_announcement_dismissed') === '1') return;
const appliedCodes = await getAppliedDiscountCodes();
let resp;
try {
resp = await ji.rewardList({ isAvailableCoupon: true });
} catch (e) {
log('rewardList error', e);
return;
}
const rewards = Array.isArray(resp?.data) ? resp.data : [];
const unused = rewards
.filter(r => r && r.couponCode && !r.isUsedCode)
.sort((a, b) => {
try { return new Date(b.createdAt) - new Date(a.createdAt); }
catch (_) { return 0; }
});
if (!unused.length) return;
const latest = unused[0];
const code = String(latest?.couponCode || '');
const label = String(latest?.eventRule || 'reward').replace(/_/g, ' ');
const benefit= latest?.programDescription || 'your coupon';
if (!code) return;
if (appliedCodes.includes(code.toUpperCase())) return;
const mounted = mountBar(code, label, benefit, { respectDismiss: true });
if (!mounted) return;
// auto-hide if code becomes applied
const maybeHide = async () => {
try {
const nowCodes = await getAppliedDiscountCodes();
if (nowCodes.includes(code.toUpperCase())) {
try { sessionStorage.setItem('joy_announcement_dismissed', '1'); } catch (_) {}
mounted.cleanup();
}
} catch (e) {
log('maybeHide error', e);
}
};
try {
mounted.bar.querySelector('.joy-cta')?.addEventListener('click', () => {
try { setTimeout(maybeHide, 1200); } catch (_) {}
});
} catch (e) { log('cta binding error', e); }
let checks = 0;
const iv = setInterval(async () => {
try {
checks++;
await maybeHide();
if (checks > 10 || !document.getElementById('joy-announce-wrap')) clearInterval(iv);
} catch (e) {
clearInterval(iv);
log('interval error', e);
}
}, 1000);
} catch (e) {
log('joy:ready handler error', e);
}
});
})(); // end IIFE
</script>
More flexibility with a Shopify section block
Prefer a reusable, configurable section? Create sections/loyalty-rewards-bar.liquid. Expose a few settings so merchants can edit copy, colors, and layout without touching code.
Steps to take:
- Create a new section file and add style rules for the fixed wrapper and card.
- Add settings for message, button text, colors, max width, padding, bottom offset, and z-index.
- Implement the same
joy:ready → rewardList → filter unused → de-dupe /cart.js → renderlogic. - In Theme Editor (
Shopify.designMode === true), render a mock coupon (e.g.,JOY-PREVIEW10) so styling is easy.
Section: Loyalty rewards reminder
- Previewable in Theme Editor (mock coupon) even if not logged in
- Live logic runs only when a customer is logged in
- Uses Joy SDK rewardList to surface newest unused coupon and avoids duplicates with /cart.js
@keyframes joySlideUp { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
#joy-announce-wrap{
position:fixed;
left:0; right:0;
bottom: {{ section.settings.bottom_offset | default: 8 }}px;
z-index: {{ section.settings.z_index | default: 2147483646 }};
pointer-events:none;
}
#joy-announce{
pointer-events:auto; position:relative;
margin:0 auto;
max-width: {{ section.settings.max_width | default: 980 }}px;
background:
linear-gradient(180deg, {{ section.settings.bg_color }}, {{ section.settings.bg_color_2 }})
{{ section.settings.bg_color }}
;
color: {{ section.settings.text_color }};
border:1px solid rgba(0,0,0,.08);
box-shadow:0 8px 24px rgba(0,0,0,.12);
border-radius:16px;
padding: {{ section.settings.padding | default: 16 }}px;
gap:10px;
display:flex; align-items:center; justify-content:center; flex-wrap:wrap;
font-size:14px; line-height:1.35;
animation: joySlideUp .22s ease-out;
}
#joy-announce .joy-emoji{
font-size:18px; margin-right:4px;
display: inlinenone;
}
#joy-announce .joy-strong{ font-weight:700; }
#joy-announce .joy-code{
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
display:inline-block; padding:2px 8px; border-radius:8px; white-space:nowrap;
background: {{ section.settings.code_bg }}; color: {{ section.settings.code_text }};
border:1px solid rgba(0,0,0,.08);
}
#joy-announce .joy-cta{
display:inline-block; padding:8px 14px; border-radius:999px; font-weight:600; text-decoration:none;
border:1px solid rgba(0,0,0,.08);
background: {{ section.settings.btn_bg }}; color: {{ section.settings.btn_text }};
}
#joy-announce .joy-cta:hover{ background: {{ section.settings.btn_bg_hover }}; }
#joy-announce .joy-cta:focus{ outline:2px solid {{ section.settings.btn_bg }}; outline-offset:2px; }
#joy-announce .joy-dismiss{
position:absolute; right:8px; top:8px;
width:28px; height:28px; border:none; background:transparent; cursor:pointer;
border-radius:6px; color:#666; font-size:18px; line-height:1;
}
#joy-announce .joy-dismiss:hover{ background:rgba(0,0,0,.06); }
@media (max-width:640px){
#joy-announce{ margin:0 8px; padding: {{ section.settings.padding | default: 16 | minus: 2 }}px; border-radius:14px }
#joy-announce .joy-cta{ width:100%; text-align:center }
}
@media (prefers-reduced-motion:reduce){ #joy-announce{ animation:none } }
<script>
(function(){
'use strict';
// ===== Liquid → JS (safe) =====
const TEMPLATE_MSG = {{ __template_msg | json }};
const BUTTON_TEXT = {{ __button_text | json }};
const IS_CUSTOMER = truefalse;
const IN_EDITOR = !!(window.Shopify && window.Shopify.designMode);
const log = (...args) => { try { console.warn('[Joy coupon bar]:', ...args); } catch(_){} };
// ===== helpers (shared, guarded) =====
async function fetchCart() {
try {
const res = await fetch('/cart.js', { credentials: 'same-origin' });
if (!res || !res.ok) return null;
return await res.json();
} catch(e) {
log('fetchCart error', e); return null;
}
}
async function getAppliedDiscountCodes() {
try {
const cart = await fetchCart();
if (!cart) return [];
return (cart.discount_codes || [])
.map(dc => String(dc?.code || '').toUpperCase())
.filter(Boolean);
} catch(e) {
log('getAppliedDiscountCodes error', e); return [];
}
}
function createBar(code, label, benefit, messageTemplate, buttonText) {
const wrap = document.createElement('div');
wrap.id = 'joy-announce-wrap';
const bar = document.createElement('div');
bar.id = 'joy-announce';
bar.setAttribute('role','region');
bar.setAttribute('aria-label','Loyalty coupon announcement');
const msg = (messageTemplate || 'Unused {label} coupon: Use {discount} with code {code}.')
.replace('{label}', label)
.replace('{discount}', benefit)
.replace('{code}', code);
bar.innerHTML = `
<button type="button" class="joy-dismiss" aria-label="Dismiss">×</button>
<span class="joy-emoji" aria-hidden="true">🎉</span>
<span>${msg}</span>
<a class="joy-cta" href="/discount/${encodeURIComponent(code)}">${buttonText || 'Apply now'}</a>
`;
wrap.appendChild(bar);
return { wrap, bar };
}
function addBottomPaddingWhileVisible() {
try {
const el = document.documentElement;
const prev = el.style.paddingBottom;
el.style.paddingBottom = '64px';
return () => { try { el.style.paddingBottom = prev || ''; } catch(_){} };
} catch(_) { return () => {}; }
}
// ===== Theme Editor preview (works even if not logged in) =====
(function editorPreview(){
try {
if (!IN_EDITOR) return;
const render = () => {
try {
document.getElementById('joy-announce-wrap')?.remove();
const { wrap, bar } = createBar('JOY-PREVIEW10', 'reward', '10% off', TEMPLATE_MSG, BUTTON_TEXT);
document.body.appendChild(wrap);
const restorePad = addBottomPaddingWhileVisible();
bar.querySelector('.joy-dismiss')?.addEventListener('click', () => {
try { wrap.remove(); } catch(_){}
restorePad();
});
} catch(e) { log('render preview error', e); }
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', render);
} else {
render();
}
document.addEventListener('shopify:section:load', render);
document.addEventListener('shopify:section:select', render);
} catch(e) { log('editorPreview error', e); }
})();
// ===== Live logic (runs only when not in editor AND a customer is logged in) =====
window.addEventListener('joy:ready', async () => {
if (IN_EDITOR) return;
if (!IS_CUSTOMER) return;
try {
if (document.getElementById('joy-announce-wrap')) return;
if (sessionStorage.getItem('joy_announcement_dismissed') === '1') return;
const ji = window.joyInstance || window.joy;
if (!ji) return;
const appliedCodes = await getAppliedDiscountCodes();
let resp;
try {
resp = await ji.rewardList({ isAvailableCoupon: true });
} catch(e) { log('rewardList error', e); return; }
const rewards = Array.isArray(resp?.data) ? resp.data : [];
const unused = rewards
.filter(r => r?.couponCode && !r?.isUsedCode)
.sort((a,b) => { try { return new Date(b.createdAt) - new Date(a.createdAt); } catch(_) { return 0; } });
if (!unused.length) return;
const latest = unused[0];
const code = String(latest?.couponCode || '');
const label = String(latest?.eventRule || 'reward').replace(/_/g,' ');
const benefit = latest?.programDescription || 'your coupon';
if (!code) return;
if (appliedCodes.includes(code.toUpperCase())) return;
const { wrap, bar } = createBar(code, label, benefit, TEMPLATE_MSG, BUTTON_TEXT);
document.body.appendChild(wrap);
const restorePad = addBottomPaddingWhileVisible();
// dismiss (persist for session)
try {
bar.querySelector('.joy-dismiss')?.addEventListener('click', () => {
try { sessionStorage.setItem('joy_announcement_dismissed','1'); } catch(_){}
try { wrap.remove(); } catch(_){}
restorePad();
});
} catch(e) { log('dismiss binding error', e); }
// auto-hide if code becomes applied
const maybeHide = async () => {
try {
const nowCodes = await getAppliedDiscountCodes();
if (nowCodes.includes(code.toUpperCase())) {
try { sessionStorage.setItem('joy_announcement_dismissed','1'); } catch(_){}
try { wrap.remove(); } catch(_){}
restorePad();
}
} catch(e) { log('maybeHide error', e); }
};
try {
bar.querySelector('.joy-cta')?.addEventListener('click', () => {
try { setTimeout(maybeHide, 1200); } catch(_){}
});
} catch(e) { log('cta binding error', e); }
// brief polling window for {{ section.settings.check_interval }}s
try {
let elapsed = 0;
const stepMs = 1000;
const limitMs = {{ section.settings.check_interval | default: 60 | times: 1000 }};
const iv = setInterval(async () => {
try {
elapsed += stepMs;
await maybeHide();
if (elapsed >= limitMs || !document.getElementById('joy-announce-wrap')) clearInterval(iv);
} catch(e) {
clearInterval(iv); log('poll error', e);
}
}, stepMs);
} catch(e) { log('poll setup error', e); }
} catch(e) { log('joy:ready handler error', e); }
});
})(); // end IIFE
</script>
{
"name": "Loyalty rewards reminder",
"settings": [
{ "type": "header", "content": "Content" },
{ "type": "text", "id": "message", "label": "Message", "default": "Unused {label} coupon: Use {discount} with code {code}.", "info": "Placeholders: {label}, {discount}, {code}" },
{ "type": "text", "id": "button_text", "label": "Button text", "default": "Apply now" },
{ "type": "checkbox", "id": "show_emoji", "label": "Show emoji", "default": true },
{ "type": "header", "content": "Colors" },
{ "type": "checkbox", "id": "use_gradient", "label": "Use gradient background", "default": true },
{ "type": "color", "id": "bg_color", "label": "Background / Gradient start", "default": "#ffffff" },
{ "type": "color", "id": "bg_color_2", "label": "Gradient end", "default": "#fbfbfb" },
{ "type": "color", "id": "text_color", "label": "Text color", "default": "#2b2b2b" },
{ "type": "color", "id": "code_bg", "label": "Code pill background", "default": "#f6f6f6" },
{ "type": "color", "id": "code_text", "label": "Code pill text", "default": "#111111" },
{ "type": "color", "id": "btn_bg", "label": "Button background", "default": "#111111" },
{ "type": "color", "id": "btn_bg_hover", "label": "Button hover background", "default": "#333333" },
{ "type": "color", "id": "btn_text", "label": "Button text color", "default": "#ffffff" },
{ "type": "header", "content": "Layout" },
{ "type": "range", "id": "max_width", "min": 680, "max": 1200, "step": 20, "unit": "px", "label": "Card max width", "default": 980 },
{ "type": "range", "id": "padding", "min": 12, "max": 32, "step": 4, "unit": "px", "label": "Card padding", "default": 16 },
{ "type": "range", "id": "bottom_offset", "min": 0, "max": 40, "step": 2, "unit": "px", "label": "Distance from bottom", "default": 8 },
{ "type": "header", "content": "Behavior" },
{ "type": "checkbox", "id": "auto_check", "label": "Auto-hide if code gets applied (poll for a short time)", "default": true },
{ "type": "range", "id": "check_interval", "min": 30, "max": 300, "step": 30, "unit": "s", "label": "Auto-hide window", "default": 60 },
{ "type": "header", "content": "Advanced" },
{ "type": "text", "id": "z_index", "label": "z-index", "default": "2147483646", "info": "Only change if another widget overlaps." }
],
"presets": [{ "name": "Loyalty rewards announcement" }]
}
What’s more from here
You can use the same recipe to craft any custom section, block, or inline UI:
- Cart drawer row: “You have {discount} — code {code} [Apply]”
- Price chip on PDP: subtle pill next to the price; tap to copy/apply
- Add-to-cart toast: quick reminder with an Apply action
- Account rewards list / VIP tier blocks: show all unused coupons, expiry, and actions
Pattern to remember: rewardList → filter unused → de-dupe against /cart.js → render a tiny, on-brand UI → dismiss/auto-hide politely.