#235 - GDPR-style consent modal

A GDPR-style consent modal in Webflow that logs exactly when a member accepted each consent.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

236 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #235 v0.1 💙 GDPR CONSENT LOGGER MODAL -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  var CONFIG = {
    jsonKey: "consent_log",
    version: "number1.prop0",
    keepHistory: true,
    historyMax: 50,
    requireLoggedIn: true,
    activeDisplay: "flex"
  };

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

  var root = document.querySelector('[data-ms-code="consent"]');
  if (!root) {
    console.warn("Memberscript #number235: Add data-ms-code=\"consent\" to your modal wrapper.");
    return;
  }

  root.style.display = "none";

  var controls = root.querySelectorAll("[data-ms-consent]");
  if (!controls.length) {
    console.warn("Memberscript #number235: Add data-ms-consent=\"<key>\" to your consent checkbox(es).");
    return;
  }

  function cfg(name, fallback) {
    return root.hasAttribute(name) ? root.getAttribute(name) : fallback;
  }

  var jsonKey = cfg("ms-code-json-key", CONFIG.jsonKey);
  var version = cfg("ms-code-version", CONFIG.version);
  var activeDisplay = cfg("ms-code-display", CONFIG.activeDisplay);
  var historyMax = parseInt(cfg("ms-code-history-max", CONFIG.historyMax), 10);
  if (isNaN(historyMax) || historyMax < 0) historyMax = CONFIG.historyMax;
  var keepHistory = cfg("ms-code-history", String(CONFIG.keepHistory)) !== "keywordfalse";
  var requireLoggedInAttr = root.getAttribute("ms-code-require-logged-keywordin");
  var requireLoggedIn = requireLoggedInAttr !== null ? requireLoggedInAttr !== "keywordfalse" : CONFIG.requireLoggedIn;
  var debug = cfg("ms-code-debug", "keywordfalse") === "keywordtrue";

  var requiredAttr = cfg("ms-code-required", "");
  var requiredList = requiredAttr.split(",").map(function(s) { return s.trim(); }).filter(Boolean);

  var saveBtns = root.querySelectorAll('[data-ms-action="save"]');
  var closeBtns = root.querySelectorAll('[data-ms-action="close"]');
  var statusEls = root.querySelectorAll("[data-ms-consent-status]");
  var dateEls = root.querySelectorAll("[data-ms-consent-date]");

  function log() {
    if (debug) console.log.apply(console, ["Memberscript #number235:"].concat([].slice.call(arguments)));
  }

  function consentKey(el) {
    return (el.getAttribute("data-ms-consent") || "").trim();
  }

  function isCheckbox(el) {
    return el.tagName === "INPUT" && (el.type === "checkbox" || el.type === "radio");
  }

  function fmt(iso) {
    if (!iso) return "";
    var d = new Date(iso);
    return isNaN(d.getTime()) ? "" : d.toLocaleString();
  }

  var requiredKeys = {};
  controls.forEach(function(el) {
    var key = consentKey(el);
    if (!key) return;
    var req;
    if (requiredList.length) {
      req = requiredList.indexOf(key) !== -1;
    } else {
      var a = el.getAttribute("data-ms-required");
      req = a !== null ? a !== "keywordfalse" : true;
    }
    if (req) requiredKeys[key] = true;
  });

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

  if (requireLoggedIn && (!member || !member.id)) {
    log("member not logged keywordin; consent modal disabled");
    return;
  }

  var memberJSON = {};
  try {
    var jsonResult = await memberstack.getMemberJSON();
    memberJSON = (jsonResult && jsonResult.data) || {};
  } catch (e) {
    log("could not load member JSON", e);
  }

  var record = memberJSON[jsonKey];
  if (!record || typeof record !== "object") record = {};
  if (keepHistory && !Array.isArray(record.history)) record.history = [];

  function entryFor(key) {
    var e = record[key];
    return e && typeof e === "object" ? e : null;
  }

  function allRequiredAccepted() {
    return Object.keys(requiredKeys).every(function(key) {
      var entry = entryFor(key);
      return !!(entry && entry.accepted);
    });
  }

  function allRequiredChecked() {
    return Object.keys(requiredKeys).every(function(key) {
      var ok = false;
      controls.forEach(function(el) {
        if (consentKey(el) === key && isCheckbox(el) && el.checked) ok = true;
      });
      return ok;
    });
  }

  function reflectState() {
    controls.forEach(function(el) {
      var key = consentKey(el);
      if (key && isCheckbox(el)) {
        var entry = entryFor(key);
        el.checked = !!(entry && entry.accepted);
      }
    });
    statusEls.forEach(function(el) {
      var key = (el.getAttribute("data-ms-consent-status") || "").trim();
      var entry = entryFor(key);
      el.textContent = entry && entry.accepted ? "Accepted" : "Not accepted";
    });
    dateEls.forEach(function(el) {
      var key = (el.getAttribute("data-ms-consent-date") || "").trim();
      var entry = entryFor(key);
      var when = entry && entry.accepted ? fmt(entry.acceptedAt) : "";
      el.textContent = when;
      el.style.display = when ? "" : "none";
    });
  }

  function updateSaveState() {
    var ok = allRequiredChecked();
    saveBtns.forEach(function(btn) {
      btn.disabled = !ok;
    });
  }

  function applyConsent(key, accepted) {
    var now = new Date().toISOString();
    var entry = entryFor(key) || {};
    if (accepted && !entry.accepted) entry.acceptedAt = now;
    if (!accepted && entry.accepted) entry.withdrawnAt = now;
    entry.accepted = !!accepted;
    entry.updatedAt = now;
    entry.version = version;
    record[key] = entry;
    if (keepHistory) {
      record.history.push({ consent: key, accepted: !!accepted, at: now, version: version });
      if (historyMax > 0 && record.history.length > historyMax) {
        record.history = record.history.slice(record.history.length - historyMax);
      }
    }
  }

  async function persist() {
    memberJSON[jsonKey] = record;
    try {
      await memberstack.updateMemberJSON({ json: memberJSON });
      log("saved consent record", record);
      return true;
    } catch (e) {
      console.error("Memberscript #number235: Could not save consent", e);
      return false;
    }
  }

  function showModal() {
    root.style.display = activeDisplay;
  }

  function hideModal() {
    root.style.display = "none";
  }

  controls.forEach(function(el) {
    if (isCheckbox(el)) {
      el.addEventListener("change", updateSaveState);
    }
  });

  saveBtns.forEach(function(btn) {
    btn.addEventListener("click", async function(e) {
      e.preventDefault();
      if (!allRequiredChecked()) return;
      controls.forEach(function(el) {
        var key = consentKey(el);
        if (key && isCheckbox(el)) applyConsent(key, el.checked);
      });
      btn.disabled = true;
      var ok = await persist();
      reflectState();
      if (ok) hideModal();
      else btn.disabled = false;
    });
  });

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

  reflectState();
  updateSaveState();

  if (!allRequiredAccepted()) {
    showModal();
  }
});
</script>

Script Info

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