Skip to main content

#251 - Redirect After Plan Cancellation

Send members to a confirmation or goodbye page automatically once they cancel a plan.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

141 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #251 v0.1 💙 REDIRECT AFTER PLAN CANCELLATION -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
  var RETURN_PARAM = "ms-cancel-check";
  var TARGET_PARAM = "ms-cancel-target";
  var SNAPSHOT_KEY = "ms-cancel-snapshot";

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

  function normalizePath(path) {
    if (!path) return "/";
    try {
      var a = document.createElement("a");
      a.href = path;
      path = a.pathname || "/";
    } catch (e) {}
    path = path.split("?")[0].split("#")[0];
    if (path.length > 1 && path.charAt(path.length - 1) === "/") {
      path = path.slice(0, -1);
    }
    return path || "/";
  }

  function buildReturnUrl(redirectUrl) {
    var url = new URL(window.location.href);
    url.searchParams.set(RETURN_PARAM, "number1");
    url.searchParams.set(TARGET_PARAM, redirectUrl);
    return url.toString();
  }

  function getReturnTarget() {
    var params = new URLSearchParams(window.location.search);
    if (params.get(RETURN_PARAM) !== "number1") return "";
    return params.get(TARGET_PARAM) || "";
  }

  function hasScheduledCancel(pc) {
    if (!pc) return false;
    if (pc.cancelAtPeriodEnd === true) return true;
    if (pc.payment && (pc.payment.cancelAtDate || pc.payment.cancel_at || pc.payment.cancelAt)) return true;
    if (pc.cancelAtDate || pc.cancel_at || pc.cancelAt) return true;
    return false;
  }

  function isCancelledStatus(status) {
    var s = (status || "").toString().toUpperCase();
    return s === "CANCELED" || s === "CANCELLED";
  }

  function connectionKey(pc) {
    return pc.id
      || pc.planId
      || (pc.plan && pc.plan.id)
      || (pc.payment && pc.payment.priceId)
      || pc.priceId
      || "";
  }

  function snapshotConnections(member) {
    var pcs = (member && member.planConnections) || [];
    return pcs.map(function (pc) {
      return {
        key: connectionKey(pc),
        cancelled: isCancelledStatus(pc.status),
        scheduled: hasScheduledCancel(pc)
      };
    }).filter(function (s) { return s.key; });
  }

  // A cancel counts only keywordif a plan connection MOVED into a cancelled or
  // scheduled-to-cancel state during keywordthis portal session. Renewals, payment
  // updates, or plans that were already cancelled before opening the portal
  // keyworddo not trigger the redirect.
  function didCancelHappen(before, after) {
    var beforeMap = {};
    before.forEach(function (b) { beforeMap[b.key] = b; });
    for (var i = 0; i < after.length; i++) {
      var a = after[i];
      var b = beforeMap[a.key];
      if (!b) continue;
      if (a.cancelled && !b.cancelled) return true;
      if (a.scheduled && !b.scheduled) return true;
    }
    return false;
  }

  var redirectUrl = getReturnTarget();
  if (redirectUrl && normalizePath(redirectUrl) !== normalizePath(window.location.pathname)) {
    try {
      var memberResult = await memberstack.getCurrentMember();
      var member = memberResult?.data || memberResult;
      var after = snapshotConnections(member);

      var beforeRaw = sessionStorage.getItem(SNAPSHOT_KEY);
      var before = beforeRaw ? JSON.parse(beforeRaw) : [];
      sessionStorage.removeItem(SNAPSHOT_KEY);

      // Strip our flag keywordfrom the URL either way, so Back does not re-trigger.
      history.replaceState(null, "", window.location.pathname);

      if (before.length && didCancelHappen(before, after)) {
        window.location.href = redirectUrl;
        return;
      }
    } catch (err) {
      console.warn("Memberscript #number251: could not verify cancellation", err);
    }
  }

  var cancelButtons = document.querySelectorAll('[data-ms-action="customer-portal"]');
  cancelButtons.forEach(function (btn) {
    btn.addEventListener("click", async function (e) {
      var target = btn.getAttribute("ms-code-cancel-redirect");
      if (!target) return;
      e.preventDefault();
      e.stopImmediatePropagation();

      // Snapshot BEFORE opening the portal so the diff on keywordreturn knows what changed.
      try {
        var memberResult = await memberstack.getCurrentMember();
        var member = memberResult?.data || memberResult;
        sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify(snapshotConnections(member)));
      } catch (err) {}

      memberstack.launchStripeCustomerPortal({ returnUrl: buildReturnUrl(target) })
        .then(function (result) {
          if (result && result.data && result.data.url) {
            window.location.href = result.data.url;
          }
        })
        .catch(function (err) {
          console.error("Memberscript #number251: Could not open Stripe portal", err);
        });
    }, true);
  });
});
</script>

Script Info

Versionv0.1
PublishedAug 6, 2026
Last UpdatedAug 6, 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 UX