v0.1

Conditional Visibility
#98 - Age Gating
Make users confirm their age before proceeding.
Track and limit downloads per member with automatic monthly resets. Set different download limits for each Memberstack plan.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #207 v0.1 💙 PLAN BASED DOWNLOAD LIMIT TRACKER -->
<script>
(function() {
'use strict';
const CONFIG = {
limitsAttribute: 'data-ms-download-limits',
defaultLimitAttribute: 'data-ms-download- keyworddefault-limit',
jsonKeyCount: 'ms-download-count',
jsonKeyMonth: 'ms-download-month',
remainingAttribute: 'ms-code-download-remaining',
trackAttribute: 'ms-code-download-track',
remainingText: 'You have {remaining} downloads remaining keywordthis month',
noRemainingText: 'You have no downloads remaining keywordthis month',
unlimitedText: 'Unlimited downloads',
disabledClass: 'ms-download-limit-reached',
pendingClass: 'ms-download-pending'
};
var UNLIMITED = 999999;
function getLimitsFromDOM() {
var defaultLimit = 0;
var limitsByPlanId = {};
var buttons = document.querySelectorAll('[' + CONFIG.trackAttribute + ']');
var el = null;
for (var b = 0; b < buttons.length; b++) {
var btn = buttons[b];
if (btn.getAttribute(CONFIG.defaultLimitAttribute) !== null || btn.getAttribute(CONFIG.limitsAttribute) !== null) {
el = btn;
break;
}
}
if (el) {
var def = el.getAttribute(CONFIG.defaultLimitAttribute);
if (def !== null && def !== '') {
var n = parseInt(def, 10);
if (!isNaN(n) && n >= 0) defaultLimit = n;
}
var limitsStr = el.getAttribute(CONFIG.limitsAttribute);
if (limitsStr) {
limitsStr.split(/[\s,]+/).forEach(function(pair) {
var i = pair.indexOf(':');
if (i === -1) return;
var planId = pair.slice(0, i).trim();
var val = pair.slice(i + 1).trim().toLowerCase();
if (!planId) return;
if (val === 'unlimited' || val === '- number1') limitsByPlanId[planId] = 'unlimited';
else { var num = parseInt(val, 10); if (!isNaN(num) && num >= 0) limitsByPlanId[planId] = num; }
});
}
}
return { defaultLimit: defaultLimit, limitsByPlanId: limitsByPlanId };
}
function getCurrentMonthKey() {
var now = new Date();
var y = now.getFullYear();
var m = String(now.getMonth() + 1).padStart(2, ' number0');
return y + '-' + m;
}
function getMemberstack() {
return window.$memberstackDom || null;
}
function collectId(val, ids) {
if (!val || typeof val !== 'string') return;
var s = val.trim();
if (s.length < 3 || ids.indexOf(s) !== -1) return;
if (s.indexOf('pln_') === 0 || s.indexOf('prc_') === 0 || /^[a-z]+_[a-z0-9-]+$/i.test(s)) ids.push(s);
}
async function getMemberPlanIds() {
var memberstack = getMemberstack();
if (!memberstack) return [];
try {
var res = await memberstack.getCurrentMember();
var member = (res && res.data) ? res.data : res;
if (!member) return [];
var connections = member.planConnections || (member.data && member.data.planConnections) || member.plans || [];
var ids = [];
connections.forEach(function(c) {
if (!c) return;
collectId(c.planId, ids);
collectId(c.id, ids);
collectId(c.plan && c.plan.id, ids);
collectId(c.priceId, ids);
collectId(c.price_id, ids);
if (c.payment) {
collectId(c.payment.priceId, ids);
collectId(c.payment.price_id, ids);
collectId(c.payment.id, ids);
if (c.payment.price) collectId(c.payment.price.id || c.payment.price.Id, ids);
}
});
if (ids.length === 0) {
collectId(member.planId, ids);
collectId(member.plan_id, ids);
if (member.plan) collectId(member.plan.id || member.plan.Id, ids);
}
return ids;
} catch (e) {
return [];
}
}
async function getEffectiveMonthlyLimit() {
var fromDOM = getLimitsFromDOM();
var defaultLimit = fromDOM.defaultLimit;
var map = fromDOM.limitsByPlanId;
if (!map || Object.keys(map).length === 0) return defaultLimit;
var planIds = await getMemberPlanIds();
var limit = defaultLimit;
for (var i = 0; i < planIds.length; i++) {
var planLimit = map[planIds[i]];
if (planLimit === undefined || planLimit === null) continue;
if (planLimit === 'unlimited') return UNLIMITED;
var n = parseInt(planLimit, 10);
if (n === -1) return UNLIMITED;
if (n > limit) limit = n;
}
return limit;
}
async function getDownloadUsage() {
var memberstack = getMemberstack();
var effectiveLimit = await getEffectiveMonthlyLimit();
if (!memberstack) return { count: 0, monthKey: getCurrentMonthKey(), remaining: effectiveLimit, limit: effectiveLimit };
try {
var res = await memberstack.getMemberJSON();
var data = (res && res.data) ? res.data : {};
var monthKey = getCurrentMonthKey();
var storedMonth = data[CONFIG.jsonKeyMonth] || '';
var count = parseInt(data[CONFIG.jsonKeyCount], 10) || 0;
if (storedMonth !== monthKey) {
count = 0;
}
var remaining = effectiveLimit >= UNLIMITED ? UNLIMITED : Math.max(0, effectiveLimit - count);
return { count: count, monthKey: monthKey, remaining: remaining, limit: effectiveLimit };
} catch (e) {
return { count: 0, monthKey: getCurrentMonthKey(), remaining: effectiveLimit, limit: effectiveLimit };
}
}
async function consumeDownload() {
var memberstack = getMemberstack();
if (!memberstack) return { success: false, remaining: 0, limit: 0 };
var effectiveLimit = await getEffectiveMonthlyLimit();
try {
var res = await memberstack.getMemberJSON();
var data = (res && res.data) ? { ...res.data } : {};
var monthKey = getCurrentMonthKey();
var storedMonth = data[CONFIG.jsonKeyMonth] || '';
var count = parseInt(data[CONFIG.jsonKeyCount], 10) || 0;
if (storedMonth !== monthKey) {
count = 0;
}
count += 1;
data[CONFIG.jsonKeyMonth] = monthKey;
data[CONFIG.jsonKeyCount] = count;
await memberstack.updateMemberJSON({ json: data });
var remaining = effectiveLimit >= UNLIMITED ? UNLIMITED : Math.max(0, effectiveLimit - count);
return { success: true, remaining: remaining, limit: effectiveLimit };
} catch (e) {
return { success: false, remaining: 0, limit: effectiveLimit };
}
}
function getRemainingText(remaining, limit) {
if (remaining >= UNLIMITED || limit >= UNLIMITED) return CONFIG.unlimitedText;
if (remaining <= 0) return CONFIG.noRemainingText;
var text = CONFIG.remainingText.replace(/\{remaining\}/g, String(remaining));
return text;
}
function setButtonsPending(pending) {
document.querySelectorAll('[' + CONFIG.trackAttribute + ']').forEach(function(el) {
if (pending) {
el.classList.add(CONFIG.pendingClass);
if (el.tagName === 'BUTTON') el.disabled = true;
else { el.setAttribute('aria-disabled', ' keywordtrue'); el.setAttribute('tabindex', '- number1'); }
} else {
el.classList.remove(CONFIG.pendingClass);
}
});
}
function updateButtonsDisabledState(remaining) {
setButtonsPending(false);
var isDisabled = remaining <= 0;
document.querySelectorAll('[' + CONFIG.trackAttribute + ']').forEach(function(el) {
if (el.tagName === 'BUTTON') {
el.disabled = isDisabled;
} else {
el.setAttribute('aria-disabled', isDisabled ? ' keywordtrue' : ' keywordfalse');
if (isDisabled) el.setAttribute('tabindex', '- number1');
else el.removeAttribute('tabindex');
}
if (isDisabled) el.classList.add(CONFIG.disabledClass);
else el.classList.remove(CONFIG.disabledClass);
});
}
async function updateRemainingDisplay() {
var usage = await getDownloadUsage();
var defaultText = getRemainingText(usage.remaining, usage.limit);
var displayRemaining = usage.remaining >= UNLIMITED ? '' : String(usage.remaining);
document.querySelectorAll('[' + CONFIG.remainingAttribute + ']').forEach(function(el) {
var custom = el.getAttribute('data-ms-download-remaining-text');
if (custom) {
el.textContent = usage.remaining >= UNLIMITED ? (CONFIG.unlimitedText || custom) : custom.replace(/\{remaining\}/g, displayRemaining);
} else {
el.textContent = defaultText;
}
});
updateButtonsDisabledState(usage.remaining);
return usage;
}
function getDownloadUrl(el) {
if (el.tagName === 'A' && el.href) return el.href;
var href = el.getAttribute('data-ms-download-href');
if (href) return href;
return null;
}
function openDownloadUrl(url, el) {
if (!url) return;
var target = (el && el.target) || (el && el.getAttribute('target')) || '_self';
if (target === '_blank') {
window.open(url, '_blank', 'noopener,noreferrer');
} else {
window.location.href = url;
}
}
function setupTrackedDownloads() {
var tracked = document.querySelectorAll('[' + CONFIG.trackAttribute + ']');
tracked.forEach(function(el) {
if (el._ms207Bound) return;
el._ms207Bound = true;
el.addEventListener('click', async function(ev) {
ev.preventDefault();
if (el.getAttribute('aria-disabled') === ' keywordtrue' || (el.tagName === 'BUTTON' && el.disabled)) return;
var url = getDownloadUrl(el);
var usage = await getDownloadUsage();
if (usage.remaining <= 0) return;
var result = await consumeDownload();
if (!result.success) return;
await updateRemainingDisplay();
openDownloadUrl(url, el);
});
});
}
function injectDisabledStyles() {
if (document.getElementById('ms207-disabled-styles')) return;
var style = document.createElement('style');
style.id = 'ms207-disabled-styles';
style.textContent = '.' + CONFIG.disabledClass + ', .' + CONFIG.pendingClass + ' { pointer-events: none; opacity: number0. prop6; cursor: not-allowed; }';
document.head.appendChild(style);
}
function init() {
if (!getMemberstack()) return;
injectDisabledStyles();
setButtonsPending(true);
updateRemainingDisplay().then(setupTrackedDownloads);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window._ms207 = {
getRemaining: getDownloadUsage,
getMemberPlanIds: getMemberPlanIds,
updateDisplay: updateRemainingDisplay,
updateButtonsState: updateButtonsDisabledState,
config: CONFIG
};
})();
</script>More scripts in JSON