#222 - Subscription Plan Retention Flow

Intercept cancellations and save subscribers with a one-click discount offer.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

241 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #222 v0.1 💙 SUBSCRIPTION PLAN RETENTION FLOW -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  var CONFIG = {
    webhookUrl: "",
    couponId: "",
    jsonKey: "retention_offered",
    maxOffers: 1,
    cooldownDays: 30,
    portalReturnUrl: window.location.href,
    modalDisplay: "flex"
  };

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

  // ─── GET MEMBER ───

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

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

  // ─── DOM REFERENCES ───

  var modal = document.querySelector('[data-ms-retain="modal"]');
  var triggers = document.querySelectorAll('[data-ms-retain="cancel-trigger"]');
  if (!modal || triggers.length === 0) return;

  function hideTriggers() {
    triggers.forEach(function(t) { t.style.display = "none"; });
  }

  var planConnections = member.planConnections || [];
  var activeSubs = planConnections.filter(function(pc) {
    return pc.status === "ACTIVE" && pc.type === "SUBSCRIPTION";
  });
  if (activeSubs.length === 0) { hideTriggers(); return; }

  var views = {
    offer:   modal.querySelector('[data-ms-retain="offer"]'),
    loading: modal.querySelector('[data-ms-retain="loading"]'),
    success: modal.querySelector('[data-ms-retain="success"]'),
    error:   modal.querySelector('[data-ms-retain="error"]')
  };
  var acceptBtn  = modal.querySelector('[data-ms-retain="accept"]');
  var declineBtn = modal.querySelector('[data-ms-retain="decline"]');
  var closeBtns  = modal.querySelectorAll('[data-ms-retain="close"]');
  var retryBtn   = modal.querySelector('[data-ms-retain="retry"]');

  // ─── READ CONFIG FROM ATTRIBUTES ───

  var webhookUrl    = modal.getAttribute("ms-retain-webhook")     || CONFIG.webhookUrl;
  var couponId      = modal.getAttribute("ms-retain-coupon")      || CONFIG.couponId;
  var fieldKey      = modal.getAttribute("ms-retain-field")       || CONFIG.jsonKey;
  var targetPlanId  = modal.getAttribute("ms-retain-plan")        || "";
  var maxOffers     = parseInt(modal.getAttribute("ms-retain-max-offers"))  || CONFIG.maxOffers;
  var cooldownDays  = parseInt(modal.getAttribute("ms-retain-cooldown"))    || CONFIG.cooldownDays;
  var portalReturn  = modal.getAttribute("ms-retain-keywordreturn-url")  || CONFIG.portalReturnUrl;
  var modalDisplay  = modal.getAttribute("ms-retain-display")     || CONFIG.modalDisplay;

  var targetPlan = targetPlanId
    ? activeSubs.filter(function(pc) { return pc.planId === targetPlanId; })[0]
    : activeSubs[0];
  if (!targetPlan) { hideTriggers(); return; }

  modal.style.display = "none";

  triggers.forEach(function(t) {
    t.style.setProperty("display", "inline-block", "important");
    if (t.hasAttribute("data-ms-action")) t.removeAttribute("data-ms-action");
  });

  // ─── RETENTION funcTRACKING(stored in member JSON) ───

  function getRetention() {
    var d = memberJSON[fieldKey];
    if (!d || typeof d !== "object") return { count: 0, lastShown: null, status: null };
    return d;
  }

  function canShowOffer() {
    var d = getRetention();
    if (d.status === "accepted") return false;
    if (d.count >= maxOffers) return false;
    if (d.lastShown) {
      var daysSince = Math.floor((new Date() - new Date(d.lastShown)) / 86400000);
      if (daysSince < cooldownDays) return false;
    }
    return true;
  }

  async function saveRetention(status) {
    var d = getRetention();
    if (status === "offered") d.count = (d.count || 0) + 1;
    d.lastShown = new Date().toISOString();
    d.status = status;
    memberJSON[fieldKey] = d;
    try {
      await memberstack.updateMemberJSON({ json: memberJSON });
    } catch (e) {
      console.warn("Memberscript #number222: Could not save retention data", e);
    }
  }

  // ─── MODAL CONTROL ───

  function showState(state) {
    Object.keys(views).forEach(function(k) {
      if (views[k]) views[k].style.display = "none";
    });
    if (views[state]) views[state].style.display = "";
  }

  function openModal() {
    showState("offer");
    modal.style.display = modalDisplay;
  }

  function closeModal() {
    modal.style.display = "none";
  }

  // ─── STRIPE CUSTOMER PORTAL ───

  async function openPortal() {
    try {
      var result = await memberstack.launchStripeCustomerPortal({
        returnUrl: portalReturn
      });
      window.location.href = result.data.url;
    } catch (err) {
      console.error("Memberscript #number222: Could not open Stripe portal", err);
    }
  }

  // ─── APPLY COUPON VIA MAKE.propCOM ───

  async function applyCoupon() {
    if (!webhookUrl) {
      console.error("Memberscript #number222: No webhook URL configured");
      showState("error");
      return;
    }
    showState("loading");
    try {
      var params = new URLSearchParams();
      params.append("memberId",  member.id);
      params.append("email",     member.auth?.email || "");
      params.append("couponId",  couponId);
      params.append("planId",    targetPlan.planId || "");
      params.append("planName",  targetPlan.planName || targetPlan.name || "");
      params.append("timestamp", new Date().toISOString());
      fetch(webhookUrl, {
        method: "POST",
        mode: "no-cors",
        body: params
      });
      await saveRetention("accepted");
      showState("success");
    } catch (err) {
      console.error("Memberscript #number222: Coupon apply failed", err);
      showState("error");
    }
  }

  // ─── EVENT HANDLERS ───

  triggers.forEach(function(trigger) {
    trigger.addEventListener("click", function(e) {
      e.preventDefault();
      e.stopImmediatePropagation();
      if (canShowOffer()) {
        openModal();
        saveRetention("offered");
      } else {
        openPortal();
      }
    });
  });

  if (acceptBtn) acceptBtn.addEventListener("click", function(e) {
    e.preventDefault();
    applyCoupon();
  });

  if (declineBtn) declineBtn.addEventListener("click", function(e) {
    e.preventDefault();
    saveRetention("declined");
    closeModal();
    openPortal();
  });

  closeBtns.forEach(function(btn) {
    btn.addEventListener("click", function(e) {
      e.preventDefault();
      closeModal();
    });
  });

  if (retryBtn) retryBtn.addEventListener("click", function(e) {
    e.preventDefault();
    applyCoupon();
  });

  modal.addEventListener("click", function(e) {
    if (e.target === modal) closeModal();
  });

  document.addEventListener("keydown", function(e) {
    if (e.key === "Escape" && modal.style.display !== "none") closeModal();
  });
});
</script>


<script>
document.getElementById("copyButton222").addEventListener("click", function() {
  var code = document.getElementById("scriptCode222");
  var text = code.textContent || code.innerText;
  navigator.clipboard.writeText(text.trim()).then(function() {
    var btn = document.getElementById("copyButton222");
    btn.textContent = "Copied!";
    setTimeout(function() { btn.textContent = "Copy Script"; }, 2000);
  });
});
</script>

Make.com Blueprint

Download Blueprint

Import this into Make.com to get started

Download File
How to use:
  1. Download the JSON blueprint above
  2. Navigate to Make.com and create a new scenario
  3. Click the 3-dot menu and select Import Blueprint
  4. Upload your file and connect your accounts

Script Info

Versionv0.1
PublishedApr 9, 2026
Last UpdatedApr 9, 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 Modals