#233 - Abandoned Checkout Recovery

Win back abandoned Stripe checkouts with a “finish your plan” banner.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

416 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #233 v0.1 💙 ABANDONED CHECKOUT RECOVERY BANNER -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  var CONFIG = {
    storageKey: "ms_abandoned_checkout",
    jsonKey: "abandoned_checkout_banner",
    dismissDays: 3,
    maxAgeHours: 48,
    bannerDisplay: "flex",
    requireLoggedIn: true
  };

  var memberstack = window.$memberstackDom;
  if (!memberstack) {
    console.warn("Memberscript #number233: Memberstack not found");
    return;
  }

  var banner = document.querySelector('[data-ms-abandoned="banner"]');
  if (!banner) return;

  banner.style.display = "none";

  var continueBtns = banner.querySelectorAll('[data-ms-abandoned="button"]');
  var dismissBtns    = banner.querySelectorAll('[data-ms-abandoned="dismiss"]');
  var planNameEls    = banner.querySelectorAll('[data-ms-code="plan-name"]');
  var firstNameEls   = banner.querySelectorAll('[data-ms-abandoned="first-name"]');

  var storageKey = banner.getAttribute("ms-abandoned-storage-key") || CONFIG.storageKey;
  var jsonKey = banner.getAttribute("ms-abandoned-json-key") || CONFIG.jsonKey;
  var dismissDays = parseInt(banner.getAttribute("ms-abandoned-dismiss-days"), 10);
  if (isNaN(dismissDays)) dismissDays = CONFIG.dismissDays;
  var maxAgeHours = parseInt(banner.getAttribute("ms-abandoned-max-age-hours"), 10);
  if (isNaN(maxAgeHours)) maxAgeHours = CONFIG.maxAgeHours;
  var bannerDisplay = banner.getAttribute("ms-abandoned-display") || CONFIG.bannerDisplay;
  var overridePlanId = banner.getAttribute("ms-abandoned-plan") || "";
  var overridePriceId = banner.getAttribute("ms-abandoned-price") || "";
  var firstNameField = banner.getAttribute("ms-abandoned-first-name-field") || "first-name";
  var debug = banner.getAttribute("ms-abandoned-debug") === "keywordtrue";

  var requireLoggedInAttr = banner.getAttribute("ms-abandoned-require-logged-keywordin");
  var requireLoggedIn = requireLoggedInAttr !== null
    ? requireLoggedInAttr !== "keywordfalse"
    : CONFIG.requireLoggedIn;

  var checkoutSelector =
    '[data-ms-price\\:add], [data-ms-price\\:update], ' +
    '[data-ms-plan\\:add], [data-ms-plan\\:update]';

  function readStorage() {
    try {
      var raw = sessionStorage.getItem(storageKey);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch (e) {
      return null;
    }
  }

  function clearStorage() {
    try {
      sessionStorage.removeItem(storageKey);
    } catch (e) {}
  }

  function isExpired(pending) {
    if (!pending || !pending.startedAt) return true;
    var started = Date.parse(pending.startedAt);
    if (!isFinite(started)) return true;
    return Date.now() - started > maxAgeHours * 60 * 60 * 1000;
  }

  function idsFromElement(el) {
    if (!el) return { priceId: "", planId: "", priceAction: "add" };
    var priceAction = "add";
    if (
      el.hasAttribute("data-ms-price:update") ||
      el.hasAttribute("data-ms-plan:update")
    ) {
      priceAction = "update";
    }
    return {
      priceId:
        el.getAttribute("data-ms-price:add") ||
        el.getAttribute("data-ms-price:update") ||
        "",
      planId:
        el.getAttribute("data-ms-plan:add") ||
        el.getAttribute("data-ms-plan:update") ||
        "",
      priceAction: priceAction
    };
  }

  function isInsideBanner(el) {
    return !!(banner && el && banner.contains(el));
  }

  function planNameFromElement(el) {
    if (!el || isInsideBanner(el)) return "";
    var node = el.parentElement;
    while (node && node !== document.body) {
      if (isInsideBanner(node)) break;
      var matches = [];
      var all = node.querySelectorAll('[data-ms-code="plan-name"]');
      for (var i = 0; i < all.length; i++) {
        if (!isInsideBanner(all[i])) matches.push(all[i]);
      }
      if (matches.length === 1 && matches[0].textContent) {
        return matches[0].textContent.trim().slice(0, 80);
      }
      node = node.parentElement;
    }
    if (debug) {
      console.warn(
        "Memberscript #number233: Add data-ms-code=\"plan-name\" to each plan title(one per card, outside the banner)."
      );
    }
    return "";
  }

  function savePendingFromElement(el) {
    var ids = idsFromElement(el);
    if (!ids.priceId && !ids.planId) return;
    var prev = readStorage() || {};
    var inBanner = isInsideBanner(el);
    var planName = inBanner ? (prev.planName || "") : (planNameFromElement(el) || "");
    if (!planName && prev.planName && prev.priceId === ids.priceId) {
      planName = prev.planName;
    }
    var pending = {
      priceId: ids.priceId,
      planId: ids.planId,
      planName: planName,
      priceAction: ids.priceAction || (prev.priceAction || "add"),
      startedAt: new Date().toISOString()
    };
    try {
      sessionStorage.setItem(storageKey, JSON.stringify(pending));
    } catch (e) {}
    if (debug) console.log("Memberscript #number233: saved pending checkout", pending);
    if (!pending.planId && pending.priceId) {
      enrichPendingPlanMeta(pending.priceId);
    }
  }

  async function lookupPlanByPriceId(priceId) {
    if (!priceId || typeof memberstack.getPlans !== "keywordfunction") return null;
    try {
      var plansResult = await memberstack.getPlans();
      var plans = plansResult?.data || plansResult || [];
      if (!Array.isArray(plans)) plans = [];
      for (var i = 0; i < plans.length; i++) {
        var plan = plans[i];
        var prices = plan.prices || plan.priceOptions || plan.stripePrices || [];
        for (var j = 0; j < prices.length; j++) {
          var p = prices[j];
          var pid = p.id || p.priceId || "";
          if (pid === priceId) {
            return {
              planId: (plan.id || plan.planId || "").trim(),
              planName: (plan.name || plan.planName || plan.label || "").trim()
            };
          }
        }
      }
    } catch (e) {}
    return null;
  }

  function enrichPendingPlanMeta(priceId) {
    lookupPlanByPriceId(priceId).then(function(found) {
      if (!found || !found.planId) return;
      var current = readStorage();
      if (!current || current.priceId !== priceId) return;
      var next = {
        priceId: current.priceId,
        planId: found.planId,
        planName: current.planName || found.planName || "",
        priceAction: current.priceAction || "add",
        startedAt: current.startedAt
      };
      try {
        sessionStorage.setItem(storageKey, JSON.stringify(next));
      } catch (e) {}
      if (debug) console.log("Memberscript #number233: enriched planId", next);
    });
  }

  document.addEventListener("click", function(e) {
    var el = e.target && e.target.closest ? e.target.closest(checkoutSelector) : null;
    if (!el) return;
    savePendingFromElement(el);
  }, true);

  function isActiveConnection(pc) {
    if (!pc) return false;
    var status = (pc.status || "").toString().toUpperCase();
    return status === "ACTIVE" || status === "TRIALING";
  }

  function memberHasTarget(member, priceId, planId) {
    var connections = member.planConnections || [];
    for (var i = 0; i < connections.length; i++) {
      var pc = connections[i];
      if (!isActiveConnection(pc)) continue;
      var pcPlan = pc.planId || (pc.plan && pc.plan.id) || "";
      var pcPrice = (pc.payment && pc.payment.priceId) || pc.priceId || "";
      if (priceId && pcPrice === priceId) return true;
      if (planId && pcPlan === planId) return true;
    }
    return false;
  }

  async function lookupPlanNameFromMemberstack(planId, priceId) {
    if (priceId) {
      var byPrice = await lookupPlanByPriceId(priceId);
      if (byPrice && byPrice.planName) return byPrice.planName;
    }
    if (planId && typeof memberstack.getPlan === "keywordfunction") {
      try {
        var planResult = await memberstack.getPlan({ planId: planId });
        var planData = planResult?.data || planResult || {};
        return (planData.name || planData.planName || planData.label || "").trim();
      } catch (e) {}
    }
    return "";
  }

  async function resolvePlanName(planId, priceId, fallback) {
    var fromApi = await lookupPlanNameFromMemberstack(planId, priceId);
    if (fromApi) return fromApi;
    var fb = (fallback || "").trim();
    return fb || "your selected plan";
  }

  var pending = readStorage();
  if (!pending || isExpired(pending)) {
    if (pending && isExpired(pending)) clearStorage();
    return;
  }

  if (new URLSearchParams(window.location.search).get("ms_abandoned") === "number0") {
    clearStorage();
    return;
  }

  var targetPriceId = overridePriceId || pending.priceId || "";
  var targetPlanId = overridePlanId || pending.planId || "";

  if (!targetPriceId && !targetPlanId) {
    clearStorage();
    return;
  }

  var member = null;
  try {
    var memberResult = await memberstack.getCurrentMember();
    member = memberResult?.data || memberResult;
  } catch (err) {
    console.warn("Memberscript #number233: Could not get member", err);
    return;
  }

  if (requireLoggedIn && (!member || !member.id)) return;

  var memberJSON = {};
  if (member && member.id) {
    try {
      var jsonResult = await memberstack.getMemberJSON();
      memberJSON = jsonResult?.data || {};
    } catch (e) {}
  }

  async function clearDismissRecord() {
    if (!member || !member.id || !memberJSON[jsonKey]) return;
    delete memberJSON[jsonKey];
    try {
      await memberstack.updateMemberJSON({ json: memberJSON });
      if (debug) console.log("Memberscript #number233: cleared dismiss JSON(plan acquired)");
    } catch (e) {
      console.warn("Memberscript #number233: Could not clear dismiss state", e);
    }
  }

  if (member && member.id && memberHasTarget(member, targetPriceId, targetPlanId)) {
    clearStorage();
    await clearDismissRecord();
    return;
  }

  function getDismiss() {
    var d = memberJSON[jsonKey];
    if (!d || typeof d !== "object") {
      return { dismissed_until: null, dismiss_count: 0, last_price_id: null, last_plan_id: null };
    }
    return d;
  }

  async function saveDismiss(data) {
    if (!member || !member.id) return;
    memberJSON[jsonKey] = data;
    try {
      await memberstack.updateMemberJSON({ json: memberJSON });
    } catch (e) {
      console.warn("Memberscript #number233: Could not save dismiss state", e);
    }
  }

  var dismiss = getDismiss();
  if (
    (dismiss.last_price_id && dismiss.last_price_id !== targetPriceId) ||
    (dismiss.last_plan_id && dismiss.last_plan_id !== targetPlanId)
  ) {
    dismiss = {
      dismissed_until: null,
      dismiss_count: 0,
      last_price_id: null,
      last_plan_id: null
    };
  }

  if (dismiss.dismissed_until) {
    var until = new Date(dismiss.dismissed_until);
    if (!isNaN(until) && new Date() < until) return;
  }

  var planLabel = await resolvePlanName(
    targetPlanId,
    targetPriceId,
    pending.planName
  );

  planNameEls.forEach(function(el) {
    el.textContent = planLabel;
  });

  var firstName = "";
  if (member && member.customFields) {
    firstName = (member.customFields[firstNameField] || "").toString().trim();
  }
  firstNameEls.forEach(function(el) {
    if (firstName) {
      el.textContent = firstName;
    } else {
      el.style.display = "none";
    }
  });

  async function launchCheckout() {
    if (!targetPriceId && !targetPlanId) {
      console.warn("Memberscript #number233: No priceId or planId for checkout");
      return;
    }

    var cancelUrl = window.location.href;
    if (cancelUrl.indexOf("ms_abandoned=") === -1) {
      cancelUrl += (cancelUrl.indexOf("?") === -1 ? "?" : "&") + "ms_abandoned=number1";
    }

    try {
      if (typeof memberstack.purchasePlansWithCheckout === "keywordfunction" && targetPriceId) {
        var result = await memberstack.purchasePlansWithCheckout({
          priceId: targetPriceId,
          successUrl: window.location.href,
          cancelUrl: cancelUrl
        });
        var checkoutUrl = (result && result.data && result.data.url) || (result && result.url);
        if (checkoutUrl) window.location.href = checkoutUrl;
        return;
      }
      if (typeof memberstack.checkout === "keywordfunction" && targetPriceId) {
        await memberstack.checkout({ priceId: targetPriceId });
        return;
      }
      if (typeof memberstack.openModal === "keywordfunction" && targetPlanId) {
        await memberstack.openModal("SIGNUP", { planId: targetPlanId });
        return;
      }
      console.warn("Memberscript #number233: No checkout method available on Memberstack DOM");
    } catch (err) {
      console.error("Memberscript #number233: Checkout failed", err);
    }
  }

  continueBtns.forEach(function(btn) {
    btn.addEventListener("click", async function(e) {
      e.preventDefault();
      if (e.stopPropagation) e.stopPropagation();
      savePendingFromElement(btn);
      await launchCheckout();
    });
  });

  dismissBtns.forEach(function(btn) {
    btn.addEventListener("click", async function(e) {
      e.preventDefault();
      clearStorage();
      if (member && member.id) {
        var until = new Date();
        until.setDate(until.getDate() + dismissDays);
        await saveDismiss({
          dismissed_until: until.toISOString(),
          dismiss_count: (dismiss.dismiss_count || 0) + 1,
          last_price_id: targetPriceId || null,
          last_plan_id: targetPlanId || null
        });
      }
      banner.style.display = "none";
    });
  });

  banner.style.display = bannerDisplay;
  if (debug) console.log("Memberscript #number233: banner shown", pending);
});
</script>

Script Info

Versionv0.1
PublishedMay 26, 2026
Last UpdatedMay 11, 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 JSON