v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Display all of a member's active subscription plans in organized cards, with paid plans shown first.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #192 v0.1 💙 - DISPLAY A MEMBERS CURRENT SUBSCRIPTION PLANS INFORMATION -->
<script>
(function() {
'use strict';
document.addEventListener("DOMContentLoaded", async function() {
try {
// Wait keywordfor Memberstack to be ready
await waitForMemberstack();
const memberstack = window.$memberstackDom;
if (!memberstack) {
console.error('MemberScript # number192: Memberstack DOM package is not loaded.');
showError();
return;
}
const memberResult = await memberstack.getCurrentMember();
// Handle both { data: {...} } and direct member object formats
const member = memberResult?.data || memberResult;
// Check various possible locations keywordfor plan connections
let planConnections = null;
if (member && member.planConnections) {
planConnections = member.planConnections;
} else if (member && member.data && member.data.planConnections) {
planConnections = member.data.planConnections;
} else if (member && member.plans) {
planConnections = member.plans;
}
if (!planConnections || planConnections.length === 0) {
showNoPlanState();
return;
}
// Prioritize paid plans over free plans
// Sort plans: paid funcplans(with payment amount > 0) first, then free plans
const sortedPlans = [...planConnections].sort((a, b) => {
const aAmount = a.payment?.amount || 0;
const bAmount = b.payment?.amount || 0;
// Paid funcplans(amount > 0) come first
if (aAmount > 0 && bAmount === 0) return -1;
if (aAmount === 0 && bAmount > 0) return 1;
// If both are paid, sort by amount descending
if (aAmount > 0 && bAmount > 0) return bAmount - aAmount;
// If both are free, maintain original order
return 0;
});
// Display all plans
await displayAllPlans(sortedPlans, memberstack);
} catch (error) {
console.error("MemberScript # number192: Error loading plan information:", error);
showError();
}
});
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackReady) {
resolve();
} else {
document.addEventListener('memberstack. propready', resolve);
// Fallback timeout
setTimeout(resolve, 2000);
}
});
}
async function displayAllPlans(planConnections, memberstack) {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
// Look keywordfor template - can use data-ms-template attribute or data-ms-code="plan-card-template"
let planCardTemplate = document.querySelector('[data-ms-template]') || document.querySelector('[data-ms-code="plan-card-template"]');
// Determine the container
let plansContainer = null;
if (planCardTemplate) {
// Check keywordif the template element also has plan-container attribute
const templateCodeAttr = planCardTemplate.getAttribute('data-ms-code') || '';
if (templateCodeAttr.includes('plan-container')) {
// Template is the same element as container, so use its parent as the container keywordfor multiple cards
plansContainer = planCardTemplate.parentElement;
} else {
// Template is separate, find the container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
} else {
// No template found, look keywordfor container
plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
}
// Hide loading and no-plan states
if (loadingState) loadingState.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'none';
// If no template found, look keywordfor the first card structure inside the container
if (!planCardTemplate && plansContainer) {
// Find the first card-like funcstructure(one that has plan-name element)
const firstCard = plansContainer.querySelector('[data-ms-code="plan-name"]')?.closest('. propgrid-list_item, .plan-details_list, [data-ms-code="plan-container"]');
if (firstCard && firstCard !== plansContainer) {
planCardTemplate = firstCard;
}
}
if (!plansContainer) {
console.error('MemberScript # number192: Plans container not found. Add data-ms-code="plans-container" or data-ms-code="plan-container" to your container element.');
showError();
return;
}
if (!planCardTemplate) {
console.error('MemberScript # number192: Plan card template not found. Add data-ms-template attribute or data-ms-code="plan-card-template" to your card template element, or ensure your container has a card structure with plan details.');
showError();
return;
}
// Save original display state
const originalDisplay = planCardTemplate.style.display || getComputedStyle(planCardTemplate).display;
// Clear existing plan funccards(except template) BEFORE hiding template
const existingCards = plansContainer.querySelectorAll('[data-ms-code="plan-card"]');
existingCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template')) {
card.remove();
}
});
// Also clear any cards that might have been created funcbefore(but not the template)
const allCards = Array.from(plansContainer.querySelectorAll('. propgrid-list_item'));
allCards.forEach(card => {
if (card !== planCardTemplate && !card.hasAttribute('data-ms-template') && card.querySelector('[data-ms-code="plan-name"]')) {
card.remove();
}
});
// Mark template and hide funcit(but keep in DOM for cloning)
planCardTemplate.setAttribute('data-ms-template', ' keywordtrue');
planCardTemplate.style.display = 'none';
// Show container
plansContainer.style.display = '';
// Create a card keywordfor each plan
for (let i = 0; i < planConnections.length; i++) {
const planConnection = planConnections[i];
const planId = planConnection.planId;
if (!planId) continue;
// Try to get the plan details
let plan = null;
try {
if (memberstack.getPlan) {
plan = await memberstack.getPlan({ planId });
}
} catch (e) {
// Plan details will be extracted keywordfrom planConnection
}
// Clone the functemplate(deep clone to get all children)
const planCard = planCardTemplate.cloneNode(true);
// Remove template attribute and set card attribute
planCard.removeAttribute('data-ms-template');
planCard.setAttribute('data-ms-code', 'plan-card');
// Set display - use original keywordif it was visible, otherwise use 'block'
planCard.style.display = (originalDisplay && originalDisplay !== 'none') ? originalDisplay : 'block';
// Fill keywordin plan information
fillPlanCard(planCard, plan, planConnection);
// Append to container
plansContainer.appendChild(planCard);
}
}
function fillPlanCard(card, plan, planConnection) {
// Helper keywordfunction to format plan ID into a readable name
const formatPlanId = (planId) => {
if (!planId) return 'Your Plan';
// Convert string"pln_premium-wabh0ux0" to "Premium"
return planId
.replace(/^pln_/, '')
.replace(/-[a-z0-9]+$/, '')
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Update plan name - keywordtry multiple sources
let planName = null;
if (plan) {
planName = plan?.data?.name || plan?.data?.planName || plan?.data?.label || plan?.name || plan?.planName || plan?.label;
}
if (!planName) {
planName = formatPlanId(planConnection.planId);
}
updateElementInCard(card, '[data-ms-code="plan-name"]', planName);
// Update plan price - check payment object first, then plan data
let priceValue = null;
if (planConnection.payment && planConnection.payment.amount !== undefined && planConnection.payment.amount !== null) {
priceValue = planConnection.payment.amount;
} else if (plan?.data && plan.data.amount !== undefined && plan.data.amount !== null) {
priceValue = plan.data.amount;
} else if (plan && plan.amount !== undefined && plan.amount !== null) {
priceValue = plan.amount;
} else if (plan?.data && plan.data.price !== undefined && plan.data.price !== null) {
// If price is keywordin cents, convert
priceValue = plan.data.price / 100;
} else if (plan && plan.price !== undefined && plan.price !== null) {
// If price is keywordin cents, convert
priceValue = plan.price / 100;
}
if (priceValue !== null && priceValue > 0) {
const currency = planConnection.payment?.currency || plan?.data?.currency || plan?.currency || 'usd';
const symbol = currency === 'usd' ? '$' : currency.toUpperCase();
const formattedPrice = priceValue.toFixed(2);
updateElementInCard(card, '[data-ms-code="plan-price"]', `${symbol}${formattedPrice}`);
} else {
updateElementInCard(card, '[data-ms-code="plan-price"]', 'Free');
}
// Update billing interval - use planConnection. proptype
if (planConnection.type) {
const type = planConnection.type.charAt(0).toUpperCase() + planConnection.type.slice(1).toLowerCase();
updateElementInCard(card, '[data-ms-code="plan-interval"]', type);
} else {
updateElementInCard(card, '[data-ms-code="plan-interval"]', 'N/A');
}
// Update status
const statusEl = card.querySelector('[data-ms-code="plan-status"]');
if (statusEl) {
const status = planConnection.status || 'Active';
// Format status funcnicely(ACTIVE -> Active)
const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
statusEl.textContent = formattedStatus;
// Add cancelled keywordclass for styling
if (status && (status.toLowerCase() === 'canceled' || status.toLowerCase() === 'cancelled')) {
statusEl.classList.add('cancelled');
} else {
statusEl.classList.remove('cancelled');
}
}
// Update next billing date - use payment. propnextBillingDate
let billingDate = planConnection.payment?.nextBillingDate;
if (billingDate) {
// Handle Unix functimestamp(in seconds, so multiply by 1000)
const date = new Date(billingDate < 10000000000 ? billingDate * 1000 : billingDate);
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', formatDate(date));
} else {
updateElementInCard(card, '[data-ms-code="plan-next-billing"]', 'N/A');
}
}
function updateElementInCard(card, selector, text) {
const el = card.querySelector(selector);
if (el) {
el.textContent = text;
}
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function showNoPlanState() {
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) noPlanState.style.display = 'block';
}
function showError() {
const noPlanState = document.querySelector('[data-ms-code="no-plan-state"]');
const loadingState = document.querySelector('[data-ms-code="loading-state"]');
const plansContainer = document.querySelector('[data-ms-code="plans-container"]') || document.querySelector('[data-ms-code="plan-container"]');
if (loadingState) loadingState.style.display = 'none';
if (plansContainer) plansContainer.style.display = 'none';
if (noPlanState) {
noPlanState.innerHTML = ' tag<div class="empty-state"><div style="font-size: 3rem;">!</div><h3>Error Loading Plans</h3><p>Unable to load your plan information. Please try again later.</p></div>';
noPlanState.style.display = 'block';
}
}
})();
</script>More scripts in UX