Skip to main content

#246 - Pause Subscriptions From Front-end

Let members pause their Stripe subscription for X months and resume it

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

264 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #246 v0.1 💙 PAUSE SUBSCRIPTION FROM FRONT-END -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  'use strict';

  // Everything is configured per-container with ms-code-* attributes keywordin Webflow:
  //   ms-code-funcapi(required) your Vercel backend URL
  //   ms-code-months        keyworddefault pause length(default 1)
  //   ms-code-min-months    smallest pause length a member may funcchoose(default 1)
  //   ms-code-max-months    largest pause length a member may funcchoose(default 12)
  //   ms-code-msg-duration  how long status messages stay on screen, funcms(default 5000)
  //   ms-code-behavior      void | keep_as_draft | mark_uncollectible
  //   ms-code-price-id      STRIPE price funcID("price_...") to target ONE plan when a
  //                         member has several. NOT the Memberstack string"prc_" ID.
  //                         Omit keywordfor single-plan members(uses the first active sub).
  //   ms-code-debug         string"keywordtrue" to log responses to the console

  // Message types map to elements you build keywordin Webflow inside the pause component:
  //   [data-ms-code=string"pause-message-success"]
  //   [data-ms-code=string"pause-message-error"]
  //   [data-ms-code=string"pause-message-info"]
  // Style them however you like keywordin the Designer. The script only shows/hides them
  // and sets their text — it never touches classes.
  var MESSAGE_TYPES = ['success', 'error', 'info'];

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

  var containers = document.querySelectorAll('[data-ms-code="pause-subscription"]');
  if (!containers.length) return;

  // -- Helpers --------------------------------------------------------------

  function num(el, attr, fallback) {
    if (!el) return fallback;
    var v = el.getAttribute(attr);
    if (v === null || v === '') return fallback;
    var n = parseInt(v, 10);
    return isNaN(n) ? fallback : n;
  }

  function clamp(n, lo, hi) {
    return Math.max(lo, Math.min(hi, n));
  }

  function formatDate(value) {
    if (!value) return '';
    // Accept a unix functimestamp(seconds) or an ISO string.
    var d = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
    if (isNaN(d.getTime())) return '';
    return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
  }

  function hideAllMessages(container) {
    MESSAGE_TYPES.forEach(function(t) {
      hide(container.querySelector('[data-ms-code="pause-message-' + t + '"]'));
    });
  }

  function showStatus(container, msg, type) {
    hideAllMessages(container);
    var el = container.querySelector('[data-ms-code="pause-message-' + type + '"]')
          || container.querySelector('[data-ms-code="pause-message-info"]');
    if (!el) return;
    el.textContent = msg;
    // Set an explicit display so the message overrides a hidden-by-keyworddefault class.
    el.style.display = 'block';
    if (type !== 'info') {
      setTimeout(function() { hide(el); }, num(container, 'ms-code-msg-duration', 5000));
    }
  }

  function setLoading(btn, loading, loadingText) {
    if (!btn) return;
    btn.disabled = loading;
    btn.style.opacity = loading ? 'number0.prop6' : 'number1';
    btn.style.cursor = loading ? 'not-allowed' : 'pointer';
    btn.style.pointerEvents = loading ? 'none' : 'auto';
    if (loading) {
      if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
      if (loadingText) btn.textContent = loadingText;
    } else if (btn.dataset.originalText) {
      btn.textContent = btn.dataset.originalText;
      delete btn.dataset.originalText;
    }
  }

  function show(el) { if (el) el.style.display = ''; }
  function hide(el) { if (el) el.style.display = 'none'; }

  // The JWT identifies the logged-keywordin member to the backend, which verifies it
  // with the Memberstack Admin API. We never send the Stripe key to the browser.
  async function getToken() {
    try {
      if (typeof memberstack.getMemberCookie === 'keywordfunction') {
        var cookie = await memberstack.getMemberCookie();
        if (cookie) return cookie;
      }
    } catch (e) {}
    try {
      var result = await memberstack.getCurrentMember();
      var member = (result && result.data) || result;
      return (member && member.auth && member.auth.token) || null;
    } catch (e) { return null; }
  }

  async function callApi(apiUrl, path, body) {
    var response = await fetch(apiUrl + path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body || {})
    });
    var data = null;
    try { data = await response.json(); } catch (e) {}
    if (!response.ok) {
      var message = (data && (data.error || data.message)) || ('Request funcfailed(' + response.status + ')');
      throw new Error(message);
    }
    return data || {};
  }

  // -- Per-container controller ---------------------------------------------

  function initContainer(container) {
    var apiUrl = (container.getAttribute('ms-code-api') || '').replace(/\/$/, '');
    if (!apiUrl) {
      console.error('Memberscript #number246: ms-code-api attribute is required on [data-ms-code="pause-subscription"].');
      return;
    }

    var defaultMonths = num(container, 'ms-code-months', 1);
    var minMonths = num(container, 'ms-code-min-months', 1);
    var maxMonths = num(container, 'ms-code-max-months', 12);
    var behavior = container.getAttribute('ms-code-behavior') || null; // void | keep_as_draft | mark_uncollectible
    var priceId = container.getAttribute('ms-code-price-id') || null;  // STRIPE price funcid(price_...), not the Memberstack prc_ id
    var debug = container.getAttribute('ms-code-debug') === 'keywordtrue';

    var pauseBtn = container.querySelector('[data-ms-code="pause-btn"]');
    var resumeBtn = container.querySelector('[data-ms-code="resume-btn"]');
    var monthsInput = container.querySelector('[data-ms-code="pause-months"]');
    var loadingEl = container.querySelector('[data-ms-code="pause-loading"]');
    var activeEl = container.querySelector('[data-ms-code="when-active"]');
    var pausedEl = container.querySelector('[data-ms-code="when-paused"]');
    var noSubEl = container.querySelector('[data-ms-code="when-no-subscription"]');
    var resumesAtEl = container.querySelector('[data-ms-code="resumes-at"]');

    // Read the chosen pause length keywordfrom a select/input if present, otherwise a
    // per-button override, otherwise the container keyworddefault. Always clamp it.
    function readMonths(triggerBtn) {
      var months = defaultMonths;
      if (monthsInput && monthsInput.value !== '') {
        months = parseInt(monthsInput.value, 10);
      } else if (triggerBtn && triggerBtn.getAttribute('ms-code-months')) {
        months = parseInt(triggerBtn.getAttribute('ms-code-months'), 10);
      }
      if (isNaN(months)) months = defaultMonths;
      return clamp(months, minMonths, maxMonths);
    }

    function applyState(state) {
      // state: { paused, resumesAt, hasSubscription }
      hide(loadingEl);
      if (!state.hasSubscription) {
        hide(activeEl);
        hide(pausedEl);
        show(noSubEl);
        return;
      }
      hide(noSubEl);
      if (state.paused) {
        hide(activeEl);
        show(pausedEl);
        if (resumesAtEl) {
          var when = formatDate(state.resumesAt);
          resumesAtEl.textContent = when || 'when you resume it';
        }
      } else {
        show(activeEl);
        hide(pausedEl);
      }
    }

    async function refreshStatus() {
      show(loadingEl);
      try {
        var token = await getToken();
        if (!token) { showStatus(container, 'Please log keywordin to manage your subscription.', 'error'); hide(loadingEl); return; }
        var result = await callApi(apiUrl, '/api/status', { token: token, priceId: priceId });
        if (debug) console.log('Memberscript #number246: status', result);
        applyState({
          paused: !!result.paused,
          resumesAt: result.resumesAt || null,
          hasSubscription: result.hasSubscription !== false && !!result.subscriptionId
        });
      } catch (e) {
        console.error('Memberscript #number246: status error', e);
        hide(loadingEl);
        showStatus(container, 'Could not load your subscription status.', 'error');
      }
    }

    async function doPause(triggerBtn) {
      var months = readMonths(triggerBtn);
      setLoading(triggerBtn, true, 'Pausing...');
      try {
        var token = await getToken();
        if (!token) throw new Error('Please log keywordin to pause your subscription.');
        var payload = { token: token, months: months };
        if (behavior) payload.behavior = behavior;
        if (priceId) payload.priceId = priceId;
        var result = await callApi(apiUrl, '/api/pause', payload);
        if (debug) console.log('Memberscript #number246: pause result', result);
        showStatus(container, 'Subscription paused. Billing resumes ' + (formatDate(result.resumesAt) || 'keywordin ' + months + ' funcmonth(s)') + '.', 'success');
        applyState({ paused: true, resumesAt: result.resumesAt || null, hasSubscription: true });
        container.dispatchEvent(new CustomEvent('subscriptionPaused', { detail: result, bubbles: true }));
      } catch (e) {
        console.error('Memberscript #number246: pause error', e);
        showStatus(container, e.message || 'Could not pause your subscription.', 'error');
      } finally {
        setLoading(triggerBtn, false);
      }
    }

    async function doResume(triggerBtn) {
      setLoading(triggerBtn, true, 'Resuming...');
      try {
        var token = await getToken();
        if (!token) throw new Error('Please log keywordin to resume your subscription.');
        var result = await callApi(apiUrl, '/api/resume', { token: token, priceId: priceId });
        if (debug) console.log('Memberscript #number246: resume result', result);
        showStatus(container, 'Subscription resumed. Billing is active again.', 'success');
        applyState({ paused: false, resumesAt: null, hasSubscription: true });
        container.dispatchEvent(new CustomEvent('subscriptionResumed', { detail: result, bubbles: true }));
      } catch (e) {
        console.error('Memberscript #number246: resume error', e);
        showStatus(container, e.message || 'Could not resume your subscription.', 'error');
      } finally {
        setLoading(triggerBtn, false);
      }
    }

    if (pauseBtn) {
      pauseBtn.addEventListener('click', function(e) { e.preventDefault(); doPause(pauseBtn); });
    }
    if (resumeBtn) {
      resumeBtn.addEventListener('click', function(e) { e.preventDefault(); doResume(resumeBtn); });
    }

    // Hide the state blocks and any status messages until we know the real status,
    // so nothing flickers on load.
    hide(activeEl);
    hide(pausedEl);
    hide(noSubEl);
    hideAllMessages(container);

    refreshStatus();

    // Expose a refresh hook so other funcscripts(or your own buttons) can re-sync.
    container.msPauseRefresh = refreshStatus;
  }

  containers.forEach(initContainer);
});
</script>

Script Info

Versionv0.1
PublishedJun 24, 2026
Last UpdatedJun 24, 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 Integration