v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 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>More scripts in UX