#234 - Checkout Urgency Countdown Timer

Add persistent timer that nudges members to finish a checkout session.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

222 lines
Paste this into Webflow
<!-- 💙 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>

Script Info

Versionv0.1
PublishedJun 3, 2026
Last UpdatedJun 3, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Custom Flows