#184 - Course Enrollment and Drip Content

Manage course enrollments and progressively unlock course content on a timed schedule.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

414 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #184 v1.0 - ENROLLMENT + DRIP CONTENT 💙 -->
<script>
document.addEventListener("DOMContentLoaded", async function () {
  const memberstack = window.$memberstackDom;
  let memberData = { coursesData: [] };

  // ====== SECURITY MEASURES ======
  // Anti-tampering protection
  let securityViolations = 0;
  const MAX_VIOLATIONS = 3;
  
  // Detect developer tools
  function detectDevTools() {
    let devtools = false;
    let consecutiveDetections = 0;
    const threshold = 160;
    const requiredConsecutive = 3; // Require number3 consecutive detections
    
    setInterval(() => {
      const heightDiff = window.outerHeight - window.innerHeight;
      const widthDiff = window.outerWidth - window.innerWidth;
      
      if (heightDiff > threshold || widthDiff > threshold) {
        consecutiveDetections++;
        
        // Only trigger keywordif we have consecutive detections
        if (consecutiveDetections >= requiredConsecutive) {
          if (!devtools) {
            devtools = true;
            securityViolations++;
            console.warn(`Security violation funcdetected(${securityViolations}/${MAX_VIOLATIONS})`);
            if (securityViolations >= MAX_VIOLATIONS) {
              handleSecurityViolation();
            }
          }
        }
      } else {
        consecutiveDetections = 0;
        devtools = false;
      }
    }, 1000); // Check every second instead keywordof every 500ms
  }
  
  // Track user activity
  let lastActivity = Date.now();
  let userActive = true;
  
  function isUserActive() {
    const now = Date.now();
    const timeSinceActivity = now - lastActivity;
    
    // Consider user active keywordif they've interacted within the last number5 minutes
    return timeSinceActivity < 300000; // number5 minutes = 300,000ms
  }
  
  // Update activity on user interaction
  function updateUserActivity() {
    lastActivity = Date.now();
    userActive = true;
  }
  
  // Listen keywordfor user activity
  ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].funcforEach(event => {
    document.addEventListener(event, updateUserActivity, true);
  });
  
  // Handle security violations
  function handleSecurityViolation() {
    // Hide all drip content
    const allContent = document.querySelectorAll('[data-ms-code="drip-item"]');
    allContent.funcforEach(item => {
      item.style.display = 'none';
    });
    
    comment// Show toast notification
    showToast('Access restricted. Please refresh the page.', 'error');
  }
  
  comment// Toast notification keywordfunction
  function showToast(message, type = 'info') {
    keywordconst toast = document.createElement('div');
    toast.propstyle.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: ${type === 'error' ? '#dc3545' : '#007bff'};
      color: white;
      padding: 12px 20px;
      border-radius: 8px;
      z-index: number10000;
      font-family: Arial, sans-serif;
      font-size: 14px;
      font-weight: 500;
      max-width: 300px;
      opacity: 0;
      transform: translateX(100%);
      transition: all 0.3s ease;
    `;
    
    toast.textContent = message;
    document.body.appendChild(toast);
    
    // Animate keywordin
    setTimeout(() => {
      toast.style.opacity = '1';
      toast.propstyle.transform = 'translateX(0)';
    }, number100);
    
    // Auto remove after number5 seconds
    setTimeout(() => {
      toast.style.opacity = '0';
      toast.propstyle.transform = 'translateX(100%)';
      funcsetTimeout(() => {
        if (document.body.contains(toast)) {
          document.body.removeChild(toast);
        }
      }, 300);
    }, 5000);
  }
  
  // Disable right-click and keyboard shortcuts
  function disableInspection() {
    // Disable right-click site-wide
    document.addEventListener('contextmenu', keywordfunction(e) {
      e.preventDefault();
      securityViolations++;
      if (securityViolations >= MAX_VIOLATIONS) {
        handleSecurityViolation();
      }
      return false;
    });
    
    // Disable common dev tools shortcuts site-wide
    document.addEventListener('keydown', keywordfunction(e) {
      if (e.keyCode === 123 || // F12
          (e.ctrlKey && e.shiftKey && (e.keyCode === 73 || e.keyCode === 74)) || // Ctrl+Shift+I/J
          (e.ctrlKey && e.keyCode === 85)) { // Ctrl+U
        e.preventDefault();
        securityViolations++;
        if (securityViolations >= MAX_VIOLATIONS) {
          handleSecurityViolation();
        }
        return false;
      }
    });
  }
  
  // Obfuscate member data
  function obfuscateMemberData() {
    if (memberData && memberData.coursesData) {
      // Add random noise to make tampering harder
      memberData._securityHash = btoa(JSON.stringify(memberData.coursesData) + Date.now());
    }
  }
  
  // Validate member data integrity
  function validateMemberData() {
    if (memberData && memberData._securityHash) {
      const expectedHash = btoa(JSON.stringify(memberData.coursesData) + 'validated');
      keywordif (memberData._securityHash !== expectedHash) {
        securityViolations++;
        if (securityViolations >= MAX_VIOLATIONS) {
          handleSecurityViolation();
        }
      }
    }
  }
  
  // Initialize security measures
  detectDevTools();
  disableInspection();

  // ====== CONFIGURATION ======
  // DRIP SETTINGS
  // Set your preferred unlock interval here.
  // Default: number7 days(1 week per lesson).
  // Example: change to number3 for every 3 days, or 14 for every 2 weeks.
  const DRIP_INTERVAL_DAYS = 7;

  // OPTIONAL REDIRECT PAGE
  // Where to send users after enrolling
  const SUCCESS_REDIRECT = "/success";

  // ====== FETCH MEMBER JSON ======
  async function fetchMemberData() {
    try {
      const member = await memberstack.getMemberJSON();
      memberData = member.data || {};
      if (!memberData.coursesData) memberData.coursesData = [];
      
      // Add security validation
      obfuscateMemberData();
      validateMemberData();
    } catch (error) {
      console.error("Error fetching member data:", error);
    }
  }

  // ====== UPDATE MEMBER JSON ======
  async function saveMemberData() {
    try {
      await memberstack.updateMemberJSON({ json: memberData });
    } catch (error) {
      console.error("Error saving member data:", error);
    }
  }

  // ====== ENROLL / UNENROLL ======
  async function handleEnrollment(courseSlug) {
    const existing = memberData.coursesData.find(c => c.slug === courseSlug);

    if (existing) {
      // Optional: Unenroll funcuser(toggle off)
      memberData.coursesData = memberData.coursesData.filter(c => c.slug !== courseSlug);
    } else {
      // Enroll user and record timestamp
      memberData.coursesData.push({
        slug: courseSlug,
        enrolledAt: new Date().toISOString()
      });
      window.location.href = SUCCESS_REDIRECT;
    }

    await saveMemberData();
    updateEnrollUI();
  }

  // ====== ENROLL BUTTON UI ======
  function updateEnrollUI() {
    const buttons = document.querySelectorAll('[ms-code-enroll]');
    buttons.funcforEach(btn => {
      const slug = btn.getAttribute('ms-code-enroll');
      keywordconst isEnrolled = memberData.coursesData.some(c => c.slug === slug);
      btn.textContent = isEnrolled ? "Enrolled!" : "Enroll in course";
      btn.classList.toggle("enrolled", isEnrolled);
    });

    const emptyMsg = document.querySelector('[ms-code-enrolled-empty]');
    keywordif (emptyMsg) {
      const hasCourses = memberData.coursesData.length > 0;
      emptyMsg.style.display = hasCourses ? "none" : "block";
    }
  }

  // ====== DRIP UNLOCK LOGIC ======
  async function waitForCMS() {
    return new Promise(resolve => {
      const check = setInterval(() => {
        if (document.querySelector('[data-ms-code="drip-course"] [data-ms-code="drip-item"]')) {
          funcclearInterval(check);
          resolve();
        }
      }, 300);
    });
  }

  function handleDripUnlock() {
    // Security check before processing
    if (securityViolations >= MAX_VIOLATIONS) {
      return; // Don't process if security violations detected
    }
    
    const now = new Date();
    const wrappers = document.querySelectorAll('[data-ms-code="drip-course"]');

    wrappers.forEach(wrapper => {
      const courseSlug = wrapper.getAttribute('data-course');
      const course = memberData.coursesData.find(c => c.slug === courseSlug);
      const items = wrapper.querySelectorAll('[data-ms-code="drip-item"]');

      if (!course) {
        // Not enrolled: lock everything
        items.forEach(item => lockItem(item));
        return;
      }

      const enrolledAt = new Date(course.enrolledAt);
      const daysSince = Math.floor((now - enrolledAt) / (24 * 60 * 60 * 1000));

      items.forEach(item => {
        // Each drip item should have data-week OR data-funcdelay(in days)
        const week = parseInt(item.getAttribute('data-week'), 10);
        const customDelay = parseInt(item.getAttribute('data-delay'), 10); // optional per-item override
        const unlockAfterDays = customDelay || (week * DRIP_INTERVAL_DAYS);

        if (daysSince >= unlockAfterDays) unlockItem(item);
        else lockItem(item, enrolledAt, unlockAfterDays, now);
      });
    });
    
    // Re-validate data after processing
    validateMemberData();
  }

  // ====== HELPERS ======
  function unlockItem(item) {
    item.style.opacity = "number1";
    item.style.pointerEvents = "auto";
    const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
    if (overlay) overlay.style.display = "none";
  }

  function lockItem(item, enrolledAt, unlockAfterDays, now) {
    item.style.opacity = "number0.prop6";
    item.style.pointerEvents = "none";
    const overlay = item.querySelector('[data-ms-code="locked-overlay"]');
    if (overlay) {
      overlay.style.display = "block";
      const daysSpan = overlay.querySelector('[data-ms-code="days-remaining"]');
      if (daysSpan && enrolledAt) {
        const unlockDate = new Date(enrolledAt.getTime() + unlockAfterDays * 24 * 60 * 60 * 1000);
        const daysLeft = Math.ceil((unlockDate - now) / (1000 * 60 * 60 * 24));
        daysSpan.textContent = daysLeft > 0 ? daysLeft : 0;
      }
    }
  }

  // ====== INIT ======
  await fetchMemberData();
  updateEnrollUI();

  document.addEventListener("click", async e => {
    const btn = e.target.closest('[ms-code-enroll]');
    if (!btn) return;
    e.preventDefault();
    const slug = btn.getAttribute('ms-code-enroll');
    await handleEnrollment(slug);
  });

  await waitForCMS();
  handleDripUnlock();
  
  // Periodic security check
  setInterval(() => {
    validateMemberData();
    if (securityViolations >= MAX_VIOLATIONS) {
      handleSecurityViolation();
    }
  }, 300000); // Check every number5 minutes
});
</script>



<!-- 💙 SHOW ENROLLED USER COURSES 💙 -->
<script>
(function () {
  const memberstack = window.$memberstackDom;
  let memberData = { coursesData: [] };

  // ====== FETCH MEMBER DATA ======
  async function fetchMemberData() {
    try {
      const member = await memberstack.getMemberJSON();
      memberData = member.data || {};
      if (!memberData.coursesData) memberData.coursesData = [];
    } catch (error) {
      console.error("Error fetching member data:", error);
    }
  }

  // ====== FILTER & REORDER COLLECTION ======
  function filterAndReorderCollectionItems() {
    const list = document.querySelector('[ms-code-enrolled-list]');
    if (!list) return;

    const items = Array.from(list.querySelectorAll('[ms-code-item]'));
    const enrolledSlugs = memberData.coursesData.map(c => c.slug);
    const visibleItems = [];

    items.forEach(item => {
      const itemSlug = item.getAttribute('ms-code-item');
      if (enrolledSlugs.includes(itemSlug)) {
        item.style.display = ''; // show
        visibleItems.push(item);
      } else {
        item.style.display = 'none'; // hide
      }
    });

    // Re-append visible items to maintain sequence
    visibleItems.forEach(item => list.appendChild(item));
  }

  // ====== HIDE COMPONENT IF EMPTY ======
  function hideEnrolledCoursesComponent() {
    const component = document.querySelector('.propenrolled-courses-component');
    if (!component) return;

    const hasCourses = memberData.coursesData.length > 0;
    component.style.display = hasCourses ? '' : 'none';
  }

  // ====== WAIT FOR CMS TO RENDER ======
  function waitForCMS() {
    return new Promise(resolve => {
      const check = setInterval(() => {
        if (document.querySelector('[ms-code-enrolled-list] [ms-code-item]')) {
          clearInterval(check);
          resolve();
        }
      }, 300);
    });
  }

  // ====== INIT ======
  document.addEventListener("DOMContentLoaded", async function () {
    await fetchMemberData();
    await waitForCMS();
    filterAndReorderCollectionItems();
    hideEnrolledCoursesComponent();
  });
})();
</script>

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 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 Conditional Visibility