v0.1

Marketing
#9 - Real, User Based Countdown
Dynamically set a per-user countdown time and then hide elements when time is up.
Add persistent timer that nudges members to finish a checkout session.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #234 v0.1 💙 CHECKOUT URGENCY COUNTDOWN TIMER -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var CONFIG = {
storageKey: "ms_checkout_timer",
durationSeconds: 900,
activeDisplay: "flex",
autoStart: true
};
var root = document.querySelector('[data-ms-code="checkout-timer"]');
if (!root) {
console.warn("Memberscript # number234: Add data-ms-code=\"checkout-timer\" to your wrapper element.");
return;
}
function cfg(name, fallback) {
return root.hasAttribute(name) ? root.getAttribute(name) : fallback;
}
var storageKey = cfg("ms-code-storage-key", CONFIG.storageKey);
var duration = parseInt(cfg("ms-code-duration", CONFIG.durationSeconds), 10);
if (isNaN(duration) || duration <= 0) duration = CONFIG.durationSeconds;
var activeDisplay = cfg("ms-code-display", CONFIG.activeDisplay);
var autoStartAttr = root.getAttribute("ms-code-auto-start");
var autoStart = autoStartAttr !== null ? autoStartAttr !== " keywordfalse" : CONFIG.autoStart;
var debug = cfg("ms-code-debug", " keywordfalse") === " keywordtrue";
// Memberstack checkout buttons that should kick off the urgency timer.
var checkoutSelector =
'[data-ms-price\\:add], [data-ms-price\\:update], ' +
'[data-ms-plan\\:add], [data-ms-plan\\:update]';
var panels = {
counting: root.querySelectorAll('[data-ms-code="counting"]'),
expired: root.querySelectorAll('[data-ms-code="expired"]')
};
var countdownEls = root.querySelectorAll('[data-ms-code="countdown"]');
var minutesEls = root.querySelectorAll('[data-ms-code="minutes"]');
var secondsEls = root.querySelectorAll('[data-ms-code="seconds"]');
var progressEls = root.querySelectorAll('[data-ms-code="progress"]');
var refreshBtns = root.querySelectorAll('[data-ms-action="refresh"]');
var startBtns = root.querySelectorAll('[data-ms-action="start"]');
var ticker = null;
var activeDuration = duration;
function log() {
if (debug) console.log.apply(console, ["Memberscript # number234:"].concat([].slice.call(arguments)));
}
function readTimer() {
try {
var raw = sessionStorage.getItem(storageKey);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
function saveTimer(timer) {
try {
sessionStorage.setItem(storageKey, JSON.stringify(timer));
} catch (e) {}
}
function clearTimer() {
try {
sessionStorage.removeItem(storageKey);
} catch (e) {}
}
function show(which) {
root.style.display = activeDisplay;
Object.keys(panels).forEach(function(key) {
panels[key].forEach(function(el) {
el.style.display = key === which ? activeDisplay : "none";
});
});
}
function hideAll() {
root.style.display = "none";
Object.keys(panels).forEach(function(key) {
panels[key].forEach(function(el) {
el.style.display = "none";
});
});
}
function setText(els, text) {
els.forEach(function(el) {
el.textContent = text;
});
}
function pad(n) {
return n < 10 ? " number0" + n : String(n);
}
function remainingSeconds(timer) {
var started = Date.parse(timer.startedAt);
if (!isFinite(started)) return 0;
var elapsed = Math.floor((Date.now() - started) / 1000);
var left = (timer.duration || duration) - elapsed;
return left > 0 ? left : 0;
}
function renderCountdown(left) {
var mins = Math.floor(left / 60);
var secs = left % 60;
setText(countdownEls, pad(mins) + ":" + pad(secs));
setText(minutesEls, pad(mins));
setText(secondsEls, pad(secs));
var pct = activeDuration > 0 ? (left / activeDuration) * 100 : 0;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
progressEls.forEach(function(el) {
el.style.width = pct + "%";
});
}
function stopTicker() {
if (ticker) {
clearInterval(ticker);
ticker = null;
}
}
function expire() {
stopTicker();
clearTimer();
renderCountdown(0);
show("expired");
log("countdown reached zero");
}
function tick() {
var timer = readTimer();
if (!timer) {
stopTicker();
hideAll();
return;
}
var left = remainingSeconds(timer);
if (left <= 0) {
expire();
return;
}
renderCountdown(left);
}
function startCountdown(timer) {
stopTicker();
activeDuration = timer.duration || duration;
var left = remainingSeconds(timer);
if (left <= 0) {
expire();
return;
}
renderCountdown(left);
show("counting");
ticker = setInterval(tick, 1000);
log("countdown started", left + "s remaining");
}
function beginTimer() {
var timer = {
startedAt: new Date().toISOString(),
duration: duration
};
saveTimer(timer);
startCountdown(timer);
}
// Re-evaluate when the member returns to the functab(back button / cancel).
document.addEventListener("visibilitychange", function() {
if (!document.hidden) tick();
});
window.addEventListener("pageshow", function() {
tick();
});
if (autoStart) {
document.addEventListener("click", function(e) {
var el = e.target && e.target.closest ? e.target.closest(checkoutSelector) : null;
if (!el) return;
log("checkout button clicked, starting timer");
beginTimer();
}, true);
}
startBtns.forEach(function(btn) {
btn.addEventListener("click", function(e) {
e.preventDefault();
beginTimer();
});
});
refreshBtns.forEach(function(btn) {
btn.addEventListener("click", function(e) {
e.preventDefault();
clearTimer();
stopTicker();
window.location.reload();
});
});
// On load: resume an keywordin-flight timer or show the expired state.
var existing = readTimer();
if (existing) {
if (remainingSeconds(existing) > 0) {
startCountdown(existing);
} else {
expire();
}
} else {
hideAll();
}
});
</script>More scripts in Custom Flows