
This block reads live subscription data, so it only runs inside a real customer portal. The screenshot shows what subscribers see.
What it does
Shows subscribers the reward on their next order, with the date it processes, and a punch-card journey of every milestone still ahead.
Why it exists
For merchants who give rewards on set orders, like a free gift on order 2 or a discount on order 4. Showing the next perk and the road after it makes the program exciting, and gives subscribers a reason to stay for one more order.
The code
Paste it as-is, or download punch-card-milestone-banner.html<!-- Skio Milestone Rewards: custom block. Edit only the CONFIG section in the script. -->
<div id="skms" hidden></div>
<style>
#skms{--bg1:#06303A;--bg2:#0B5566;--card:#0E4A58;--y:#FFD23F;--y2:#FFE680;--t:#38D6E8;--ink:#05252D;
--tx:#FFFFFF;--mute:#B8DDE4;--line:rgba(255,255,255,.16);
font-family:inherit;color:var(--tx);background:linear-gradient(140deg,var(--bg1),var(--bg2));border-radius:24px;
padding:28px 28px 22px;position:relative;overflow:hidden;isolation:isolate;box-sizing:border-box;
box-shadow:0 18px 50px -20px rgba(6,48,58,.65)}
#skms *{box-sizing:border-box}
#skms:before{content:"";position:absolute;inset:-35% -25% auto auto;width:65%;height:85%;z-index:-1;
background:radial-gradient(closest-side,rgba(255,210,63,.22),transparent);animation:skms-drift 10s ease-in-out infinite alternate}
#skms:after{content:"";position:absolute;inset:auto auto -40% -20%;width:60%;height:80%;z-index:-1;
background:radial-gradient(closest-side,rgba(56,214,232,.22),transparent);animation:skms-drift 12s ease-in-out infinite alternate-reverse}
#skms .sp{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--y2);box-shadow:0 0 10px var(--y);opacity:0;animation:skms-tw 4s ease-in-out infinite;z-index:-1}
#skms .hd{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:20px}
#skms h3{margin:0;font-size:27px;line-height:1.02;font-weight:900;letter-spacing:-.01em;text-transform:uppercase;color:var(--tx)}
#skms h3 span{display:block;color:var(--y)}
#skms .cnt{white-space:nowrap;font-size:13px;font-weight:700;color:var(--ink);background:var(--y);border-radius:99px;padding:6px 12px}
#skms .tabs{display:flex;gap:6px;flex-wrap:wrap;margin:-6px 0 14px}
#skms .tab{font:inherit;font-size:12px;font-weight:700;border:1px solid var(--line);background:transparent;border-radius:99px;padding:6px 12px;cursor:pointer;color:var(--mute)}
#skms .tab[aria-pressed=true]{background:var(--tx);border-color:var(--tx);color:var(--ink)}
/* hero */
#skms .hero{position:relative;display:flex;gap:20px;align-items:center;border-radius:18px;padding:20px 22px;
background:linear-gradient(135deg,rgba(255,255,255,.10),rgba(255,255,255,.04));border:1.5px solid rgba(255,210,63,.65);
box-shadow:0 0 0 1px rgba(255,210,63,.15),0 14px 34px -14px rgba(255,210,63,.45);overflow:hidden;animation:skms-rise .6s cubic-bezier(.2,.8,.2,1) both}
#skms .hero:after{content:"";position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.22) 50%,transparent 65%);
transform:translateX(-120%);animation:skms-shine 5.5s ease-in-out 1.2s infinite;pointer-events:none}
#skms .hero i.bar{position:absolute;left:0;right:0;bottom:0;height:3px;background:linear-gradient(90deg,var(--y),var(--t),var(--y2),var(--t));background-size:200% 100%;animation:skms-flow 4s linear infinite}
#skms .viz{flex:none;width:100px;height:100px;display:grid;place-items:center;position:relative}
#skms .viz:before{content:"";position:absolute;inset:-6px;border-radius:50%;background:radial-gradient(closest-side,rgba(255,210,63,.45),transparent);animation:skms-pulse 2.6s ease-in-out infinite}
#skms .viz img{max-width:90px;max-height:94px;object-fit:contain;position:relative;filter:drop-shadow(0 10px 12px rgba(0,0,0,.35));animation:skms-bob 3.2s ease-in-out infinite}
#skms .burst{position:relative;width:96px;height:96px;display:grid;place-items:center;color:var(--ink);font-weight:900;text-align:center;line-height:.9}
#skms .burst svg{position:absolute;inset:0;width:100%;height:100%;animation:skms-spin 18s linear infinite;filter:drop-shadow(0 6px 14px rgba(255,210,63,.35))}
#skms .burst b{position:relative;font-size:27px;letter-spacing:-.02em}
#skms .burst small{display:block;font-size:12px;letter-spacing:.12em;margin-top:3px}
#skms .eb{font-size:11px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--y)}
#skms .tt{font-size:25px;font-weight:900;line-height:1.1;margin:5px 0 5px;letter-spacing:-.01em;color:var(--tx)}
#skms .sb{font-size:14px;color:var(--mute);margin:0 0 12px}
#skms .chip{display:inline-flex;align-items:center;gap:8px;background:rgba(5,37,45,.55);border:1px solid var(--line);border-radius:99px;padding:6px 12px;font-size:13px;color:var(--tx)}
#skms .chip:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--y);animation:skms-ping 1.8s infinite}
/* journey */
#skms .jl{margin:24px 0 10px;font-size:11px;font-weight:800;letter-spacing:.16em;color:var(--mute)}
#skms .trk{margin:0;padding:36px 0 0;display:flex}
#skms .st{flex:1;position:relative;text-align:center;min-width:0;animation:skms-pop .5s cubic-bezier(.3,1.4,.5,1) both;animation-delay:calc(var(--i)*90ms + .25s)}
#skms .st:not(:last-child):before,#skms .st:not(:last-child):after{content:"";position:absolute;top:20px;left:50%;width:100%;height:4px;background:var(--line);border-radius:3px}
#skms .st:not(:last-child):after{background:linear-gradient(90deg,var(--y),var(--t));transform-origin:left;transform:scaleX(0);box-shadow:0 0 12px rgba(255,210,63,.5)}
#skms .st.seg:after{animation:skms-fill 1.1s cubic-bezier(.6,0,.2,1) .7s both}
#skms .dot{position:relative;z-index:1;width:44px;height:44px;margin:0 auto;border-radius:50%;display:grid;place-items:center;
background:var(--card);border:2px solid rgba(255,255,255,.35);overflow:hidden;color:var(--tx)}
#skms .dot img{width:30px;height:30px;object-fit:contain}
#skms .dot.pc{font-size:12px;font-weight:900}
#skms .st.done .dot{background:var(--y);border-color:var(--y);color:var(--ink);font-weight:900}
#skms .st.now .dot{border-color:var(--y);box-shadow:0 0 0 6px rgba(255,210,63,.18),0 0 24px rgba(255,210,63,.6);animation:skms-glow 2.2s ease-in-out infinite;overflow:visible}
#skms .st.now .dot.pc{background:var(--y);color:var(--ink)}
#skms .tip{position:absolute;top:-36px;left:50%;transform:translateX(-50%);background:var(--y);color:var(--ink);font-size:11px;font-weight:800;padding:4px 10px;border-radius:7px;white-space:nowrap;animation:skms-bob 2.4s ease-in-out infinite}
#skms .tip:after{content:"";position:absolute;left:50%;bottom:-4px;width:8px;height:8px;background:var(--y);transform:translateX(-50%) rotate(45deg)}
#skms .on{display:block;margin-top:10px;font-size:11px;font-weight:800;letter-spacing:.1em;color:var(--mute);text-transform:uppercase}
#skms .rw{display:block;font-size:13px;margin-top:2px;line-height:1.25;padding:0 2px;color:var(--tx)}
#skms .st.now .on{color:var(--y)}
#skms .st.now .rw{font-weight:800}
#skms .ft{margin:18px 0 0;font-size:12px;color:var(--mute);text-align:center}
#skms .viz .mini{position:absolute;right:-6px;bottom:-4px;width:46px;height:46px;z-index:2}
#skms .viz .mini b{font-size:13px}
#skms .viz .mini small{font-size:7px;margin-top:1px}
#skms .dot .more{position:absolute;right:-6px;top:-6px;min-width:18px;height:18px;border-radius:9px;background:var(--t);color:var(--ink);font-size:10px;font-weight:900;display:grid;place-items:center;padding:0 4px;z-index:2}
#skms .st .dot{overflow:visible}
#skms .dot img{border-radius:50%}
#skms .cf{position:absolute;width:7px;height:11px;border-radius:2px;pointer-events:none;z-index:3;animation:skms-cf 1.3s cubic-bezier(.2,.7,.4,1) forwards}
/* mobile: vertical journey */
@media (max-width:560px){
#skms{padding:20px 16px 16px;border-radius:20px}
#skms h3{font-size:22px}
#skms .hero{flex-direction:column;text-align:center;gap:10px;padding:18px 14px}
#skms .tt{font-size:22px}
#skms .trk{flex-direction:column;padding:4px 0 0}
#skms .st{display:grid;grid-template-columns:44px 1fr;column-gap:14px;text-align:left;padding-bottom:16px;align-items:center}
#skms .st:not(:last-child):before,#skms .st:not(:last-child):after{top:44px;left:20px;width:4px;height:calc(100% - 44px)}
#skms .st:not(:last-child):after{background:linear-gradient(180deg,var(--y),var(--t));transform-origin:top;transform:scaleY(0)}
#skms .st.seg:after{animation-name:skms-fillv}
#skms .dot{grid-row:span 2;margin:0}
#skms .on{margin:0}
#skms .tip{display:none}
}
@keyframes skms-rise{from{opacity:0;transform:translateY(14px) scale(.98)}}
@keyframes skms-pop{from{opacity:0;transform:translateY(10px) scale(.85)}}
@keyframes skms-shine{0%,60%{transform:translateX(-120%)}100%{transform:translateX(120%)}}
@keyframes skms-flow{to{background-position:-200% 0}}
@keyframes skms-bob{50%{translate:0 -4px}}
@keyframes skms-spin{to{transform:rotate(360deg)}}
@keyframes skms-pulse{50%{transform:scale(1.12);opacity:.7}}
@keyframes skms-glow{50%{box-shadow:0 0 0 10px rgba(255,210,63,.08),0 0 32px rgba(255,210,63,.75)}}
@keyframes skms-ping{0%{box-shadow:0 0 0 0 rgba(255,210,63,.7)}70%,100%{box-shadow:0 0 0 8px rgba(255,210,63,0)}}
@keyframes skms-fill{from{transform:scaleX(0)}to{transform:scaleX(var(--p,1))}}
@keyframes skms-fillv{from{transform:scaleY(0)}to{transform:scaleY(var(--p,1))}}
@keyframes skms-drift{to{transform:translate(-12%,10%)}}
@keyframes skms-tw{0%,100%{opacity:0;transform:scale(.4)}50%{opacity:.9;transform:scale(1)}}
@keyframes skms-cf{0%{opacity:1;transform:translate(0,0) rotate(0)}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
@media (prefers-reduced-motion:reduce){#skms *,#skms:before,#skms:after,#skms *:before,#skms *:after{animation:none!important}#skms .st.seg:after{transform:scaleX(var(--p,1))}}
@media (prefers-reduced-motion:reduce) and (max-width:560px){#skms .st.seg:after{transform:scaleY(var(--p,1))}}
</style>
<script>
(function () {
/* ═══════════════════════════ CONFIG: EDIT ME ═══════════════════════════ */
var CONFIG = {
theme: {
bg1: '#06303A', // card gradient start (deep teal)
bg2: '#0B5566', // card gradient end (teal blue)
yellow: '#FFD23F', // primary accent
teal: '#38D6E8', // secondary accent
ink: '#05252D' // dark text used on yellow
},
copy: {
headlineAccent: 'Reach milestones',
headline: '& unlock rewards.',
nextLabel: 'With your next order',
footer: 'No action needed. Rewards apply automatically while your subscription stays active.',
giftSub: 'Added to your order automatically, on us.',
discountSub: 'Taken off your subscription order automatically.'
},
orderOffset: 1, // next order # = cyclesCompleted + orderOffset
confetti: true, // one small burst per visit when the next order earns a reward
/* One entry per ACTIVE Surprise & Delight rule. Several rules can share an order number.
order → the rule's "order to apply"
ok → statuses the rule allows, e.g. ['ACTIVE','FAILED'], or 'ANY' (no status condition)
img → free product image | off → discount label, e.g. '20%' or '$10'
needsProduct → optional: only show if the subscription has a product whose title contains one of these */
rules: [
{ order: 2, ok: 'ANY', title: 'A free gift worth $100', short: 'Free $100 gift',
img: 'https://cdn.shopify.com/s/files/1/0738/3964/0873/files/Screenshot2023-06-21at2.10.45PM.png?v=1687371078&width=240' },
{ order: 2, ok: ['ACTIVE', 'FAILED'], title: 'An extra 20% off', short: '20% off', off: '20%' },
{ order: 4, ok: ['ACTIVE'], title: '$10 off your order', short: '$10 off', off: '$10', tone: 'teal' },
{ order: 6, ok: ['ACTIVE'], title: 'Free Premium Exfoliant', short: 'Free exfoliant',
img: 'https://cdn.shopify.com/s/files/1/0738/3964/0873/products/ScreenShot2023-04-04at9.21.46AM.png?v=1680614538&width=240' }
]
};
/* ═══════════════════════ END CONFIG: no edits below ═══════════════════════ */
var root = document.getElementById('skms');
if (!root) return;
var T = CONFIG.theme, C = CONFIG.copy;
root.style.cssText = '--bg1:' + T.bg1 + ';--bg2:' + T.bg2 + ';--y:' + T.yellow + ';--t:' + T.teal + ';--ink:' + T.ink;
var STAR = 'M50 0L59.1 10L71.7 5L75.6 17.9L89.1 18.8L86.9 32.2L98.7 38.9L91 50L98.7 61.1L86.9 67.8L89.1 81.2L75.6 82.1L71.7 95L59.1 90L50 100L40.9 90L28.3 95L24.4 82.1L10.9 81.2L13.1 67.8L1.3 61.1L9 50L1.3 38.9L13.1 32.2L10.9 18.8L24.4 17.9L28.3 5L40.9 10Z';
var Q = 'query SkmsSubs{Subscriptions(order_by:{nextBillingDate:asc}){id status cyclesCompleted nextBillingDate ' +
'SubscriptionLines(where:{removedAt:{_is_null:true}}){ProductVariant{Product{title}}}}}';
var subs = [], picked = null, all = null;
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
function nextOf(s) { return (s.cyclesCompleted || 0) + CONFIG.orderOffset; }
function titles(s) {
return (s.SubscriptionLines || []).map(function (l) { return (((l || {}).ProductVariant || {}).Product || {}).title || ''; });
}
function qualifies(r, s) {
var st = r.ok === 'ANY' ? s.status !== 'CANCELLED' : (r.ok || ['ACTIVE']).indexOf(s.status) > -1;
if (!st) return false;
if (!r.needsProduct || !r.needsProduct.length) return true;
var t = titles(s).join(' | ').toLowerCase();
return r.needsProduct.some(function (p) { return t.indexOf(String(p).toLowerCase()) > -1; });
}
// eligible rules grouped into milestones: [{order, rewards:[...]}]
function milestones(s) {
var n = nextOf(s), map = {};
CONFIG.rules.forEach(function (r) {
if (r.order >= n && qualifies(r, s)) (map[r.order] = map[r.order] || []).push(r);
});
return Object.keys(map).map(Number).sort(function (a, b) { return a - b; }).map(function (o) {
var rw = map[o].slice().sort(function (a, b) { return (b.img ? 1 : 0) - (a.img ? 1 : 0); }); // gifts first
return { order: o, rewards: rw };
});
}
function burst(r, cls) {
var fill = r.tone === 'teal' ? T.teal : T.yellow;
return '<div class="burst' + (cls || '') + '"><svg viewBox="0 0 100 100"><path d="' + STAR + '" fill="' + fill + '"/></svg><b>' + esc(r.off) + '<small>OFF</small></b></div>';
}
function bigViz(m) {
var g = m.rewards.filter(function (r) { return r.img; })[0], d = m.rewards.filter(function (r) { return r.off; })[0];
if (g) return '<img src="' + esc(g.img) + '" alt="" loading="lazy">' + (d ? burst(d, ' mini') : '');
return burst(d);
}
function dotViz(m) {
var r = m.rewards[0], extra = m.rewards.length - 1;
return (r.img ? '<img src="' + esc(r.img) + '" alt="" loading="lazy">' : esc(r.off)) + (extra ? '<span class="more">+' + extra + '</span>' : '');
}
function joinTitles(m) {
return m.rewards.map(function (r, i) { return i ? r.title.charAt(0).toLowerCase() + r.title.slice(1) : r.title; }).join(' + ');
}
function subline(m) {
if (m.rewards.length > 1) return 'Both applied to your order automatically.';
var r = m.rewards[0];
return r.sub || (r.off ? C.discountSub : C.giftSub);
}
function when(iso) {
if (!iso) return '';
var d = new Date(iso), days = Math.ceil((d - new Date()) / 864e5);
var lbl = d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
return 'Processes ' + lbl + ' · ' + (days <= 0 ? 'today' : days === 1 ? 'tomorrow' : 'in ' + days + ' days');
}
function confetti(host, v) {
if (!CONFIG.confetti) return;
try { if (sessionStorage.getItem('skms-cf')) return; sessionStorage.setItem('skms-cf', 1); } catch (e) {}
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var cols = [T.yellow, T.teal, '#FFFFFF', '#FFE680'];
for (var i = 0; i < 24; i++) {
var p = document.createElement('i'), a = Math.random() * 6.28, d = 60 + Math.random() * 90;
p.className = 'cf';
p.style.cssText = 'left:' + (v.offsetLeft + 48) + 'px;top:' + (v.offsetTop + 48) + 'px;background:' + cols[i % 4] +
';--x:' + Math.cos(a) * d + 'px;--y:' + (Math.sin(a) * d - 30) + 'px;--r:' + (Math.random() * 720 - 360) + 'deg;animation-delay:' + (Math.random() * .15 + .5) + 's';
host.appendChild(p);
}
setTimeout(function () { [].forEach.call(host.querySelectorAll('.cf'), function (n) { n.remove(); }); }, 2200);
}
function render() {
var s = picked, ms = milestones(s), next = nextOf(s);
var m0 = ms[0], isNext = m0.order === next, togo = m0.order - next;
var count = ms.reduce(function (a, m) { return a + m.rewards.length; }, 0);
var tabs = subs.length > 1 ? '<div class="tabs">' + subs.map(function (x) {
return '<button class="tab" data-id="' + esc(x.id) + '" aria-pressed="' + (x === s) + '">' + esc(titles(x)[0] || 'Subscription') + '</button>';
}).join('') + '</div>' : '';
var last = Math.max(1, next - 1);
var steps = '<div class="st done seg" style="--i:0;--p:' + (isNext ? 1 : (1 / (togo + 1)).toFixed(3)) + '"><span class="dot">✓</span>' +
'<span class="on">Order ' + last + '</span><span class="rw">' + (last <= 1 ? 'Joined' : 'Completed') + '</span></div>' +
ms.map(function (m, i) {
var now = i === 0;
var tip = now ? '<span class="tip">' + (isNext ? 'Next order' : togo + ' to go') + '</span>' : '';
var lbl = m.rewards.map(function (r) { return r.short || r.title; }).join(' + ');
return '<div class="st' + (now ? ' now' : '') + '" style="--i:' + (i + 1) + '"><span class="dot' + (m.rewards[0].img ? '' : ' pc') + '">' + dotViz(m) + tip + '</span>' +
'<span class="on">Order ' + m.order + '</span><span class="rw">' + esc(lbl) + '</span></div>';
}).join('');
var sparks = [[8, 18, 0], [62, 9, 1.3], [88, 52, 2.4], [30, 88, 3.1]].map(function (p) {
return '<i class="sp" style="left:' + p[0] + '%;top:' + p[1] + '%;animation-delay:' + p[2] + 's"></i>';
}).join('');
root.innerHTML = sparks +
'<div class="hd"><h3><span>' + esc(C.headlineAccent) + '</span>' + esc(C.headline) + '</h3>' +
'<div class="cnt">' + count + ' reward' + (count > 1 ? 's' : '') + ' ahead</div></div>' + tabs +
'<div class="hero"><div class="viz">' + bigViz(m0) + '</div><div>' +
'<div class="eb">' + (isNext ? esc(C.nextLabel) : 'Unlocks in ' + togo + ' order' + (togo > 1 ? 's' : '')) + '</div>' +
'<div class="tt">' + esc(joinTitles(m0)) + '</div><p class="sb">' + esc(subline(m0)) + '</p>' +
'<span class="chip">' + (isNext ? esc(when(s.nextBillingDate)) : 'Unlocks on order ' + m0.order + ' · stay subscribed') + '</span>' +
'</div><i class="bar"></i></div>' +
'<div class="jl">YOUR JOURNEY</div><div class="trk">' + steps + '</div>' +
'<p class="ft">' + esc(C.footer) + '</p>';
root.hidden = false;
if (isNext) confetti(root.querySelector('.hero'), root.querySelector('.viz'));
}
function choose() {
var ok = subs.filter(function (s) { return milestones(s).length; });
var m = location.pathname.match(/subscriptions\/([0-9a-f-]{36})/i);
var onPage = m && subs.filter(function (s) { return s.id === m[1]; })[0];
if (m) { picked = onPage && milestones(onPage).length ? onPage : null; subs = picked ? [picked] : []; }
else { subs = ok; picked = ok[0] || null; }
if (!picked) { root.hidden = true; return; }
render();
}
root.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('.tab');
if (!b) return;
picked = subs.filter(function (s) { return s.id === b.getAttribute('data-id'); })[0] || picked;
render();
});
function api() {
return new Promise(function (res, rej) {
var t = 0, iv = setInterval(function () {
var f = window.fetchSkioAPI || (window.Skio && window.Skio.graphQLRequest && function (q) { return window.Skio.graphQLRequest({ query: q }); });
if (f) { clearInterval(iv); Promise.resolve(f(Q)).then(res, rej); }
else if (++t > 40) { clearInterval(iv); rej('no api'); }
}, 150);
});
}
function load() {
var demo = /[?&]skms_demo=(\d+)/.exec(location.search);
if (demo) {
var pm = location.pathname.match(/subscriptions\/([0-9a-f-]{36})/i);
all = [{ id: pm ? pm[1] : 'demo', status: 'ACTIVE', cyclesCompleted: +demo[1], nextBillingDate: new Date(Date.now() + 6 * 864e5).toISOString(), SubscriptionLines: [] }];
subs = all.slice(); return choose();
}
api().then(function (r) {
all = ((r && r.data) || r || {}).Subscriptions || [];
subs = all.slice(); choose();
}).catch(function () { root.hidden = true; });
}
load();
var path = location.pathname;
setInterval(function () { if (location.pathname !== path) { path = location.pathname; if (all) { subs = all.slice(); choose(); } } }, 800);
})();
</script>
How to install
- Copy the code above, or download the .html file.
- Go to
Storefront→Customer Portal→Branding→Layoutand clickAdd custom extension. - Pick
HTML with embedded CSS and JavaScriptand paste the whole file into the code editor. - Swap anything the file asks you to, like
[BRAND], product names or Quick Action IDs. The AI prompt below can walk you through it. - Save, click
+to add it to the layout, thenPublish. Check it on a test subscription’s magic link with?forcePreview=true, since some scripts don’t run in Preview.
Edit only the CONFIG section: add one entry per active Surprise & Delight rule (order number, allowed subscription statuses, title, and a product image or discount label), then set your colors and copy. Replace both product images, which point at the original brand's Shopify files. It reads subscriptions through Skio's portal API, so it needs a Skio customer portal.
Customize it with AI
Paste this prompt into Claude Code or Codex. It asks for the code and your brand and product details, then writes a version for your store.
I want to add a custom extension from customizeyourportal.xyz to my subscription customer portal. Walk me through customizing it for my brand, then write the finished code. EXTENSION Name: Punch Card Milestone Banner (No. 17) Page: /blocks/punch-card-milestone-banner What it does: Shows subscribers the reward on their next order, with the date it processes, and a punch-card journey of every milestone still ahead. My portal app: Skio WHAT THE BUILDER SAYS TO CUSTOMIZE Edit only the CONFIG section: add one entry per active Surprise & Delight rule (order number, allowed subscription statuses, title, and a product image or discount label), then set your colors and copy. Replace both product images, which point at the original brand's Shopify files. It reads subscriptions through Skio's portal API, so it needs a Skio customer portal. HOW TO WORK 1. Ask me to paste the full code (the "Copy code" button on the page) or to give you the path to the downloaded punch-card-milestone-banner.html. Wait until you have it. 2. Read the whole file. List every value that is specific to the original brand or needs my input: brand name and [PLACEHOLDERS], product and variant names or IDs, prices, discount codes and reward amounts, Quick Action IDs or tokens, store domains and API endpoints, colors, fonts, logos and images, and any copy that names the original brand, products or offers. Include everything in the builder's notes above. 3. Ask me for those values in one numbered list, with the current value next to each, so I can answer or say "keep". If you can open web pages, offer to pull my brand colors, fonts, logo and product names from my store's URL. 4. Don't invent stats, reviews, IDs, prices or claims. If I don't have a value, leave a clearly marked TODO in the code and tell me. 5. Write the customized version as punch-card-milestone-banner-<my-brand>.html. Change only what my brand needs. Keep the logic, behavior and structure the same, and keep it one self-contained file (HTML, <style> and <script>). 6. Finish with a short list of what you changed, any TODOs left, and these install steps for Skio: 1. Go to `Storefront` → `Customer Portal` → `Branding` → `Layout` and click `Add custom extension`. 2. Pick `HTML with embedded CSS and JavaScript` and paste the whole file into the code editor. 3. Save, click `+` to add it to the layout, then `Publish`. Check it on a test subscription’s magic link with `?forcePreview=true`, since some scripts don’t run in Preview.
Blocks are shared as-is by the people who built them. Read the code before you add it to your portal, and test it with a test subscriber first.
