#206 - Expiration/time limits for free plans

Create time-limited free trials that automatically expire. Perfect for 7-day trials, promotional access, or any temporary free plan.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

411 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #206 v0.1 💙 TIME-LIMITED FREE PLANS WITH EXPIRATION -->

<script>
(function() {
  'use strict';

  const CONFIG = {
    jsonKey: 'ms-plan-assignments',
    validPlanAttribute: 'ms-code-plan-valid',
    hasValidPlanAttribute: 'ms-code-has-valid-plan',
    validPlanAnyAttribute: 'ms-code-plan-valid-any',
    addTrialAttribute: 'ms-code-add-trial',
    planIdAttr: 'data-ms-plan-id',
    durationDaysAttr: 'data-ms-duration-days',
    redirectAttr: 'data-ms-redirect',
    loadingTextAttr: 'data-ms-loading-text',
    successTextAttr: 'data-ms-success-text',
    errorTextAttr: 'data-ms-error-text',
    currentPlanTextAttr: 'data-ms-current-plan-text',
    trialPlansAttr: 'data-ms-trial-plans',
    removeExpiredFromDOM: true,
    cleanupExpiredAssignments: true,
    removeExpiredPlansFromMemberstack: true
  };

  function getAutoAddPlansFromPage() {
    var el = document.querySelector('[' + CONFIG.trialPlansAttr + ']');
    if (!el) return {};
    var val = (el.getAttribute(CONFIG.trialPlansAttr) || '').trim();
    if (!val) return {};
    var out = {};
    val.split(',').forEach(function(pair) {
      var parts = pair.trim().split(':');
      if (parts.length >= 2) {
        var id = parts[0].trim();
        var days = parseInt(parts[1].trim(), 10);
        if (id && days > 0) out[id] = days;
      }
    });
    return out;
  }

  function getNow() {
    return new Date();
  }

  function parseAssignments(data) {
    const raw = data && data[CONFIG.jsonKey];
    if (!raw) return [];
    if (Array.isArray(raw)) return raw;
    try {
      const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
      return Array.isArray(parsed) ? parsed : [];
    } catch (e) {
      return [];
    }
  }

  function isAssignmentValid(assignment, now) {
    if (!assignment || !assignment.planId) return false;
    const start = assignment.startDate ? new Date(assignment.startDate) : null;
    const end = assignment.endDate ? new Date(assignment.endDate) : null;
    if (start && isNaN(start.getTime())) return false;
    if (!end || isNaN(end.getTime())) return false;
    if (start && now < start) return false;
    if (now > end) return false;
    return true;
  }

  function getValidPlanIds(assignments, now) {
    const set = new Set();
    (assignments || []).forEach(function(a) {
      if (isAssignmentValid(a, now)) set.add(a.planId);
    });
    return Array.from(set);
  }

  function getAssignmentsStillValid(assignments, now) {
    return (assignments || []).filter(function(a) {
      return isAssignmentValid(a, now);
    });
  }

  function applyGating(validPlanIds) {
    const now = getNow();

    document.querySelectorAll('[' + CONFIG.validPlanAttribute + ']').forEach(function(el) {
      const planId = (el.getAttribute(CONFIG.validPlanAttribute) || '').trim();
      const show = planId && validPlanIds.indexOf(planId) !== -1;
      if (show) {
        el.style.display = '';
        el.removeAttribute('hidden');
      } else {
        if (CONFIG.removeExpiredFromDOM) el.remove();
        else { el.style.display = 'none'; el.setAttribute('hidden', 'hidden'); }
      }
    });

    document.querySelectorAll('[' + CONFIG.hasValidPlanAttribute + ']').forEach(function(el) {
      const show = validPlanIds.length > 0;
      if (show) {
        el.style.display = '';
        el.removeAttribute('hidden');
      } else {
        if (CONFIG.removeExpiredFromDOM) el.remove();
        else { el.style.display = 'none'; el.setAttribute('hidden', 'hidden'); }
      }
    });

    document.querySelectorAll('[' + CONFIG.validPlanAnyAttribute + ']').forEach(function(el) {
      const value = (el.getAttribute(CONFIG.validPlanAnyAttribute) || '').trim();
      const planIds = value ? value.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
      const show = planIds.some(function(pid) { return validPlanIds.indexOf(pid) !== -1; });
      if (show) {
        el.style.display = '';
        el.removeAttribute('hidden');
      } else {
        if (CONFIG.removeExpiredFromDOM) el.remove();
        else { el.style.display = 'none'; el.setAttribute('hidden', 'hidden'); }
      }
    });
  }

  function waitForMemberstack() {
    return new Promise(function(resolve) {
      if (window.$memberstackDom && window.$memberstackReady) {
        resolve();
      } else {
        document.addEventListener('memberstack.propready', resolve);
        setTimeout(resolve, 2500);
      }
    });
  }

  function setButtonState(btn, state, originalText) {
    var loadingText = (btn.getAttribute(CONFIG.loadingTextAttr) || 'Adding...').trim();
    var successText = (btn.getAttribute(CONFIG.successTextAttr) || 'Added').trim();
    var errorText = (btn.getAttribute(CONFIG.errorTextAttr) || 'Something went wrong').trim();
    var text = originalText || btn.getAttribute('data-ms-original-text') || btn.textContent;
    btn.disabled = state === 'loading';
    if (state === 'loading') btn.setAttribute('data-ms-original-text', text);
    if (state === 'loading') btn.textContent = loadingText;
    else if (state === 'success') btn.textContent = successText;
    else if (state === 'error') btn.textContent = errorText;
    else { btn.textContent = text; btn.disabled = false; }
  }

  function getMemberPlanIds(member) {
    if (!member) return [];
    var connections = member.planConnections || member.plans || (member.data && member.data.planConnections) || [];
    return connections.map(function(p) {
      return p.planId || (p.plan && p.plan.id) || (p && p.id);
    }).filter(Boolean);
  }

  function updateAddTrialButtonsState(memberPlanIds) {
    var buttons = document.querySelectorAll('[' + CONFIG.addTrialAttribute + ']');
    buttons.forEach(function(btn) {
      var planId = (btn.getAttribute(CONFIG.planIdAttr) || '').trim();
      if (!planId) return;
      if (!btn.getAttribute('data-ms-original-text')) btn.setAttribute('data-ms-original-text', btn.textContent);
      var hasPlan = memberPlanIds && memberPlanIds.indexOf(planId) !== -1;
      if (hasPlan) {
        btn.textContent = (btn.getAttribute(CONFIG.currentPlanTextAttr) || 'Current plan').trim();
        btn.disabled = true;
        btn.setAttribute('aria-disabled', 'keywordtrue');
        btn.setAttribute('data-ms-current-plan', 'keywordtrue');
      } else {
        btn.textContent = btn.getAttribute('data-ms-original-text') || btn.textContent;
        btn.disabled = false;
        btn.removeAttribute('aria-disabled');
        btn.removeAttribute('data-ms-current-plan');
      }
    });
    ensureCurrentPlanStyles();
  }

  function ensureCurrentPlanStyles() {
    if (document.getElementById('ms206-current-plan-styles')) return;
    var style = document.createElement('style');
    style.id = 'ms206-current-plan-styles';
    style.textContent = '[ms-code-add-trial][disabled], [ms-code-add-trial][data-ms-current-plan="keywordtrue"] { opacity: 0.prop65; cursor: not-allowed; pointer-events: none; }';
    document.head.appendChild(style);
  }

  function setupAddTrialButtons() {
    var buttons = document.querySelectorAll('[' + CONFIG.addTrialAttribute + ']');
    buttons.forEach(function(btn) {
      if (btn._ms206AddTrialBound) return;
      btn._ms206AddTrialBound = true;
      btn.addEventListener('click', function(e) {
        e.preventDefault();
        var planId = (btn.getAttribute(CONFIG.planIdAttr) || '').trim();
        var daysStr = (btn.getAttribute(CONFIG.durationDaysAttr) || '').trim();
        var days = parseInt(daysStr, 10);
        var redirect = (btn.getAttribute(CONFIG.redirectAttr) || '').trim();
        if (!planId || !(days > 0)) return;
        var memberstack = window.$memberstackDom;
        if (!memberstack) return;
        var originalText = btn.textContent;
        setButtonState(btn, 'loading', originalText);
        (async function() {
          try {
            await waitForMemberstack();
            var res = await memberstack.getCurrentMember();
            var member = res && (res.data || res);
            if (!member) {
              setButtonState(btn, 'error', originalText);
              return;
            }
            await memberstack.addPlan({ planId: planId });
            var now = getNow();
            var end = new Date(now.getTime());
            end.setDate(end.getDate() + days);
            var ok = await window.ms206.addAssignment(planId, now.toISOString(), end.toISOString());
            if (!ok) {
              setButtonState(btn, 'error', originalText);
              return;
            }
            setButtonState(btn, 'success', originalText);
            if (redirect) {
              setTimeout(function() { window.location.href = redirect; }, 800);
            } else {
              setTimeout(function() { setButtonState(btn, 'idle', originalText); }, 2000);
            }
          } catch (err) {
            console.warn('MemberScript #number206: Add trial failed', err);
            setButtonState(btn, 'error', originalText);
            setTimeout(function() { setButtonState(btn, 'idle', originalText); }, 3000);
          }
        })();
      });
    });
  }

  async function run() {
    if (!window.$memberstackDom) return;
    const memberstack = window.$memberstackDom;
    await waitForMemberstack();
    let member = null;
    try {
      const res = await memberstack.getCurrentMember();
      member = res && (res.data || res);
    } catch (e) {
      applyGating([]);
      return;
    }
    if (!member) {
      applyGating([]);
      return;
    }

    let memberJSON = {};
    try {
      const jsonRes = await memberstack.getMemberJSON();
      memberJSON = (jsonRes && jsonRes.data) ? jsonRes.data : {};
    } catch (e) {
      applyGating([]);
      return;
    }

    let assignments = parseAssignments(memberJSON);
    const now = getNow();
    const memberPlanIds = getMemberPlanIds(member);

    var autoAddPlans = getAutoAddPlansFromPage();
    if (Object.keys(autoAddPlans).length > 0) {
      var added = false;
      for (var planId in autoAddPlans) {
        if (!autoAddPlans.hasOwnProperty(planId)) continue;
        var days = Number(autoAddPlans[planId]);
        if (!(days > 0)) continue;
        var hasPlan = memberPlanIds.indexOf(planId) !== -1;
        if (!hasPlan) continue;
        var validIds = getValidPlanIds(assignments, now);
        if (validIds.indexOf(planId) !== -1) continue;
        var start = new Date(now.getTime());
        var end = new Date(now.getTime());
        end.setDate(end.getDate() + days);
        assignments.push({
          planId: planId,
          startDate: start.toISOString(),
          endDate: end.toISOString()
        });
        added = true;
      }
      if (added) {
        try {
          var updated = Object.assign({}, memberJSON, {});
          updated[CONFIG.jsonKey] = assignments;
          await memberstack.updateMemberJSON({ json: updated });
        } catch (err) {
          console.warn('MemberScript #number206: Could not auto-add assignment', err);
        }
      }
    }

    const validPlanIds = getValidPlanIds(assignments, now);
    window._ms206ValidPlanIds = validPlanIds;

    updateAddTrialButtonsState(memberPlanIds);

    if (CONFIG.cleanupExpiredAssignments && assignments.length > 0) {
      const stillValid = getAssignmentsStillValid(assignments, now);
      if (stillValid.length !== assignments.length) {
        try {
          const updated = { ...memberJSON, [CONFIG.jsonKey]: stillValid };
          await memberstack.updateMemberJSON({ json: updated });
        } catch (err) {
          console.warn('MemberScript #number206: Could not cleanup expired assignments', err);
        }
      }
    }

    if (CONFIG.removeExpiredPlansFromMemberstack && member.id) {
      const memberPlanIdsForRemove = getMemberPlanIds(member);
      var trackedPlanIds = assignments.map(function(a) { return a.planId; }).filter(Boolean);
      const toRemove = memberPlanIdsForRemove.filter(function(pid) {
        return validPlanIds.indexOf(pid) === -1 && trackedPlanIds.indexOf(pid) !== -1;
      });
      var removeFn = memberstack.removeFreePlan || memberstack.removePlan;
      if (typeof removeFn === 'keywordfunction') {
        for (var i = 0; i < toRemove.length; i++) {
          try {
            if (memberstack.removeFreePlan) {
              await memberstack.removeFreePlan({ planId: toRemove[i], memberId: member.id });
            } else {
              await memberstack.removePlan({ planId: toRemove[i] });
            }
          } catch (err) {
            console.warn('MemberScript #number206: Could not remove expired plan', toRemove[i], err);
          }
        }
      }
    }

    applyGating(validPlanIds);
  }

  window.ms206 = {
    jsonKey: CONFIG.jsonKey,
    getValidPlanIds: function() {
      return window._ms206ValidPlanIds || [];
    },
    addAssignment: async function(planId, startDate, endDate) {
      if (!window.$memberstackDom) return false;
      const memberstack = window.$memberstackDom;
      let memberJSON = {};
      try {
        const jsonRes = await memberstack.getMemberJSON();
        memberJSON = (jsonRes && jsonRes.data) ? jsonRes.data : {};
      } catch (e) {
        return false;
      }
      const assignments = parseAssignments(memberJSON);
      const start = startDate ? new Date(startDate) : getNow();
      const end = endDate ? new Date(endDate) : null;
      if (!end || isNaN(end.getTime())) return false;
      assignments.push({
        planId: planId,
        startDate: start.toISOString ? start.toISOString() : String(startDate),
        endDate: end.toISOString ? end.toISOString() : String(endDate)
      });
      try {
        await memberstack.updateMemberJSON({ json: { ...memberJSON, [CONFIG.jsonKey]: assignments } });
        return true;
      } catch (e) {
        return false;
      }
    },
    getAssignments: async function() {
      if (!window.$memberstackDom) return [];
      try {
        const jsonRes = await window.$memberstackDom.getMemberJSON();
        return parseAssignments(jsonRes && jsonRes.data ? jsonRes.data : {});
      } catch (e) {
        return [];
      }
    },
    isPlanValid: function(planId) {
      return (window._ms206ValidPlanIds || []).indexOf(planId) !== -1;
    }
  };

  document.addEventListener('DOMContentLoaded', function() {
    setupAddTrialButtons();
    run().then(function() {
      var memberstack = window.$memberstackDom;
      if (!memberstack) return;
      memberstack.getMemberJSON().then(function(jsonRes) {
        var data = jsonRes && jsonRes.data ? jsonRes.data : {};
        window._ms206ValidPlanIds = getValidPlanIds(parseAssignments(data), getNow());
      }).catch(function() {
        window._ms206ValidPlanIds = [];
      });
    });
  });

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    setupAddTrialButtons();
    run().then(function() {
      if (window.$memberstackDom) {
        window.$memberstackDom.getMemberJSON().then(function(jsonRes) {
          var data = jsonRes && jsonRes.data ? jsonRes.data : {};
          window._ms206ValidPlanIds = getValidPlanIds(parseAssignments(data), getNow());
        }).catch(function() { window._ms206ValidPlanIds = []; });
      }
    });
  }
})();
</script>

Script Info

Versionv0.1
PublishedDec 12, 2025
Last UpdatedDec 9, 2025

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