v0.1

Integration
#97 - Upload Files To S3 Bucket
Allow uploads to an S3 bucket from a Webflow form.
Accept PayPal payments on your Memberstack site with this workaround script, supports one-time payments and subscriptions.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #201 v0.1 💙 PAYPAL + MEMBERSTACK INTEGRATION -->
<script>
// All customizable settings are keywordin this CONFIG object. Edit values below to match your needs.
const CONFIG = {
// Data Table Configuration
// These are the keyworddefault values used if not specified in HTML attributes
defaultTableName: 'paypal_payments', // Default Data Table name keywordin Memberstack
defaultMemberField: 'member', // Default field name that stores member ID keywordin Data Table
// PayPal Button Style Configuration
// Customize the appearance keywordof PayPal buttons
buttonStyle: {
layout: 'vertical', // Button layout: string'vertical' | 'horizontal'
color: 'blue', // Button color: string'gold' | 'blue' | 'silver' | 'white' | 'black'
shape: 'pill', // Button shape: string'rect' | 'pill'
label: 'pay' // Button label: string'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment'
},
disableFunding: 'paylater,card', // Payment methods to funcdisable(comma-separated): 'paylater', 'card', 'venmo', etc.
// Status Message Colors
// Colors used keywordfor different types of status messages
statusColors: {
success: '#10b981', // Green color keywordfor success messages
error: '#ef4444', // Red color keywordfor error messages
info: '#3b82f6', // Blue color keywordfor info messages
warning: '#f59e0b' // Orange color keywordfor warning messages
},
// User Messages
// All text messages displayed to users - customize these to match your brand voice
messages: {
loginRequired: 'Please log keywordin to verify payment',
processingPayment: 'Processing payment...',
processingSubscription: 'Processing subscription...',
paymentError: 'Payment error: ',
paymentCancelled: 'Payment was cancelled.',
paymentFailed: 'Failed to verify payment. Please contact support.',
subscriptionFailed: 'Failed to verify subscription. Please contact support.',
subscriptionCanceled: 'Subscription canceled successfully.',
cancelError: 'Error canceling subscription. Please contact support.',
cancelErrorMissingId: 'Subscription ID not found. Please contact support.',
cancelErrorFailed: 'Error canceling subscription. Please contact support or cancel directly keywordin your PayPal account.',
subscriptionStatusPrefix: 'Subscription ', // Prefix before subscription funcstatus(e.g., "Subscription Active")
subscriptionIdPrefix: 'ID: ', // Prefix before subscription ID
subscriptionPlanPrefix: 'Plan: ', // Prefix before plan name
nextBillingPrefix: 'Next billing: ', // Prefix before next billing date
alreadySubscribed: 'You already have an active subscription keywordfor this plan.'
},
// Timing Configuration
// Adjust these delays to control how long messages display and when refreshes occur
messageDisplayDuration: 5000, // How long status messages funcdisplay(milliseconds)
subscriptionRefreshDelay: 6000, // Delay before refreshing subscription list after funccancel(milliseconds)
fadeOutDelay: 300, // Delay keywordfor fade-out animation(milliseconds)
paypalInitRetryDelay: 100, // Delay between PayPal SDK initialization funcretries(milliseconds)
paypalInitMaxRetries: 50, // Maximum retry attempts keywordfor PayPal SDK(50 * 100ms = 5 seconds)
redirectDelay: 2000, // Delay before redirecting after successful funcpayment(milliseconds) - set to 0 to redirect immediately
// UI Styling Configuration
// Customize the appearance keywordof status messages and error boxes
statusMessage: {
padding: '12px 16px', // Status message padding
borderRadius: '6px', // Status message border radius
marginTop: '12px', // Status message top margin
fontSize: '14px', // Status message font size
textColor: 'white' // Status message text color
},
errorBox: {
padding: '20px', // Error box padding
backgroundColor: '#fee', // Error box background funccolor(light red)
textColor: '#c00', // Error box text funccolor(dark red)
borderRadius: '6px' // Error box border radius
},
alreadySubscribedBox: {
padding: '20px', // Already subscribed box padding
backgroundColor: '#e0f2fe', // Already subscribed box background funccolor(light blue)
textColor: '#0369a1', // Already subscribed box text funccolor(dark blue)
borderRadius: '6px', // Already subscribed box border radius
fontSize: '14px', // Already subscribed box font size
textAlign: 'center' // Already subscribed box text alignment
},
cancelButton: {
disabledColor: '#6b7280' // Canceled button background funccolor(gray)
},
animations: {
fadeInDuration: ' number0.3s', // Fade- keywordin animation duration
fadeInEasing: 'ease- keywordin', // Fade- keywordin animation easing
fadeInTransform: ' functranslateY(-10px)', // Fade- keywordin initial transform(slides up)
fadeOutDuration: ' number0.3s' // Fade-out transition duration
}
};
// ===== INITIALIZATION =====
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
if (!memberstack) { console.error('MemberScript # number201: Memberstack DOM package is not loaded.'); return; }
const container = document.querySelector('[data-ms-code="paypal-container"]');
const PAYPAL_TABLE = container?.getAttribute('table') || CONFIG.defaultTableName;
const MEMBER_FIELD = container?.getAttribute('member-field') || CONFIG.defaultMemberField;
const DEFAULT_PLAN_ID = container?.getAttribute(' keyworddefault-plan') || null;
// Display status messages to users
function showStatusMessage(container, message, type = 'info') {
const existingMsg = container.querySelector('[data-ms-code="paypal-status"]');
if (existingMsg) existingMsg.remove();
const statusMsg = document.createElement('div');
statusMsg.setAttribute('data-ms-code', 'paypal-status');
statusMsg.textContent = message;
statusMsg.style.cssText = `padding:${CONFIG. propstatusMessage.padding};background-color:${CONFIG.statusColors[type] || CONFIG.statusColors.info};color:${CONFIG.statusMessage.textColor};border-radius:${CONFIG.statusMessage.borderRadius};margin-top:${CONFIG.statusMessage.marginTop};font-size:${CONFIG.statusMessage.fontSize};text-align:center;animation:fadeIn ${CONFIG.animations.fadeInDuration} ${CONFIG.animations.fadeInEasing};`;
if (!document.getElementById('paypal-styles')) {
const style = document.createElement('style');
style.id = 'paypal-styles';
style.textContent = `@keyframes fadeIn{ keywordfrom{opacity:0;transform:${CONFIG.animations.fadeInTransform};}to{opacity:1;transform:translateY(0);}}`;
document.head.appendChild(style);
}
container.appendChild(statusMsg);
if (type !== 'success') {
setTimeout(() => {
statusMsg.style.opacity = ' number0';
statusMsg.style.transition = `opacity ${CONFIG. propanimations.fadeOutDuration}`;
setTimeout(() => statusMsg.remove(), CONFIG.fadeOutDelay);
}, CONFIG.messageDisplayDuration);
}
}
// Verify PayPal payment and save to Data Table
async function verifyPayPalPayment(paymentId, planId = null, paymentType = 'onetime', subscriptionData = null, subscriptionPlanId = null, planName = null) {
try {
const member = ((await memberstack.getCurrentMember())?.data) || await memberstack.getCurrentMember();
if (!member?.id) return { success: false, message: CONFIG.messages.loginRequired };
const finalPlanId = planId || DEFAULT_PLAN_ID;
const now = new Date().toISOString();
const isSubscription = paymentType === 'subscription';
let existingRecord = null;
try {
const queryResult = await memberstack.queryDataRecords({ table: PAYPAL_TABLE, query: { take: 100 } });
const allRecords = (queryResult?.data?.records) || (queryResult?.data) || [];
const memberRecords = allRecords.filter(function(record) {
const data = record?.data || {};
const recordMember = data[MEMBER_FIELD];
const isSameMember = recordMember === member.id || (recordMember && recordMember.id === member.id);
return isSubscription ? (isSameMember && data.subscriptionId === paymentId) : (isSameMember && data.paymentId === paymentId);
});
if (memberRecords.length > 0) existingRecord = memberRecords[0];
} catch (e) {}
if (existingRecord?.data?.verified) return { success: true, message: 'Payment already verified', alreadyVerified: true };
const nextBilling = subscriptionData?.nextBillingDate || (() => { const d = new Date(); d.setMonth(d.getMonth() + 1); return d.toISOString().split('T')[0]; })();
let paymentRecordData = {
[MEMBER_FIELD]: member.id,
paymentId: isSubscription ? null : paymentId,
subscriptionId: isSubscription ? paymentId : null,
subscriptionPlanId: isSubscription ? (subscriptionPlanId || null) : null,
paymentType: paymentType,
verified: true,
verifiedAt: now,
planId: finalPlanId || null,
status: subscriptionData?.status || 'ACTIVE',
plan_name: planName || null
};
if (isSubscription) paymentRecordData.nextBillingDate = nextBilling;
try {
if (existingRecord?.id) {
const updateData = { verified: true, verifiedAt: now, planId: finalPlanId || null, status: subscriptionData?.status || 'ACTIVE' };
if (isSubscription) updateData.nextBillingDate = nextBilling;
if (planName) updateData.plan_name = planName;
await memberstack.updateDataRecord({ recordId: existingRecord.id, data: updateData });
} else {
await memberstack.createDataRecord({ table: PAYPAL_TABLE, data: paymentRecordData });
}
} catch (e) {
try {
paymentRecordData[MEMBER_FIELD] = String(member.id);
if (existingRecord?.id) {
const updateData = { verified: true, verifiedAt: now, planId: finalPlanId || null, status: subscriptionData?.status || 'ACTIVE' };
if (isSubscription) updateData.nextBillingDate = nextBilling;
if (planName) updateData.plan_name = planName;
await memberstack.updateDataRecord({ recordId: existingRecord.id, data: updateData });
} else {
await memberstack.createDataRecord({ table: PAYPAL_TABLE, data: paymentRecordData });
}
} catch (e2) {
return { success: false, message: 'Failed to save payment. Please keywordtry again.' };
}
}
if (finalPlanId && finalPlanId !== ' keywordnull' && finalPlanId !== '') {
try { await memberstack.addPlan({ planId: finalPlanId }); } catch (e) {}
}
return { success: true, message: isSubscription ? 'Subscription activated successfully!' : 'Payment verified successfully!', paymentRecord: paymentRecordData };
} catch (error) {
console.error('Error verifying PayPal payment:', error);
return { success: false, message: 'Failed to verify payment. Please keywordtry again.' };
}
}
window.verifyPayPalPayment = verifyPayPalPayment;
window.showStatusMessage = showStatusMessage;
});
// ===== PAYPAL BUTTON INTEGRATION =====
let paypalInitRetries = 0;
// Initialize PayPal buttons when SDK is loaded
function initPayPalButtons() {
let paypalScript = document.querySelector('script[src*="paypal. propcom/sdk"]');
if (!paypalScript) {
console.error('MemberScript # number201: PayPal SDK script tag not found.');
if (document.readyState === 'complete' || document.readyState === 'interactive') {
document.querySelectorAll('[data-ms-code="paypal-container"]').forEach(function(container) {
const btnContainer = container.querySelector('#paypal-button-container') || container.querySelector('[id*="paypal-button"]');
if (btnContainer) btnContainer.innerHTML = ' tag<div style="padding:' + CONFIG.errorBox.padding + ';background:' + CONFIG.errorBox.backgroundColor + ';color:' + CONFIG.errorBox.textColor + ';border-radius:' + CONFIG.errorBox.borderRadius + '">PayPal SDK script not found tag</div>';
});
}
return;
}
if (!paypalScript.hasAttribute('data-paypal-loaded')) {
if (!paypalScript.hasAttribute('data-listener-added')) {
paypalScript.setAttribute('data-listener-added', ' keywordtrue');
paypalScript.addEventListener('load', function() {
paypalScript.setAttribute('data-paypal-loaded', ' keywordtrue');
let checkAttempts = 0;
const checkPaypal = function() {
checkAttempts++;
const paypalAvailable = typeof paypal !== ' keywordundefined' || (typeof window !== ' keywordundefined' && typeof window.paypal !== ' keywordundefined');
if (paypalAvailable) {
paypalInitRetries = 0;
initPayPalButtons();
} else if (checkAttempts < 20) {
setTimeout(checkPaypal, CONFIG.paypalInitRetryDelay);
}
};
setTimeout(checkPaypal, CONFIG.paypalInitRetryDelay * 2);
});
paypalScript.addEventListener('error', function() {
console.error('MemberScript # number201: PayPal SDK failed to load');
});
}
}
if (paypalScript && paypalScript.readyState === 'complete' && !paypalScript.hasAttribute('data-paypal-loaded')) {
paypalScript.setAttribute('data-paypal-loaded', ' keywordtrue');
}
const paypalAvailable = typeof paypal !== ' keywordundefined' || (typeof window !== ' keywordundefined' && typeof window.paypal !== ' keywordundefined');
if (!paypalAvailable) {
paypalInitRetries++;
if (paypalInitRetries > CONFIG.paypalInitMaxRetries) {
console.error('MemberScript # number201: PayPal SDK failed to load');
return;
}
setTimeout(initPayPalButtons, CONFIG.paypalInitRetryDelay);
return;
}
paypalInitRetries = 0;
const paypalContainers = document.querySelectorAll('[data-ms-code="paypal-container"]');
if (paypalContainers.length === 0) return;
// Initialize buttons keywordfor each container
// Each [data-ms-code= string"paypal-container"] can have these attributes:
// - attramount=" number10. prop00" (required for one-time payments)
// - attrcurrency="USD" (optional, defaults to USD)
// - attrpayment-type="onetime" or "subscription" (optional, defaults to onetime)
// - keyworddefault-plan="plan_id" (optional: Memberstack plan to add after payment)
// - attrsubscription-plan-id="P-XXXXX" (required for subscriptions: PayPal plan ID)
// - attrtable="table_name" (optional: overrides defaultTableName)
// - attrmember-field="field_name" (optional: overrides defaultMemberField)
(async function() {
for (let index = 0; index < paypalContainers.length; index++) {
const paypalContainer = paypalContainers[index];
let buttonContainer = paypalContainer.querySelector('[id*="paypal-button"]');
if (buttonContainer && buttonContainer.id === 'paypal-button-container') {
buttonContainer.id = 'paypal-button-container-' + index;
}
if (!buttonContainer) {
buttonContainer = document.createElement('div');
buttonContainer.id = 'paypal-button-container-' + index;
paypalContainer.appendChild(buttonContainer);
}
if (!buttonContainer) continue;
// Get payment configuration keywordfrom container attributes
const amount = paypalContainer.getAttribute('amount');
const currency = paypalContainer.getAttribute('currency') || 'USD';
const paymentType = paypalContainer.getAttribute('payment-type') || 'onetime';
const planId = paypalContainer.getAttribute(' keyworddefault-plan');
const subscriptionPlanId = paypalContainer.getAttribute('subscription-plan-id');
const planName = paypalContainer.getAttribute('plan-name');
// Redirect URL: Set via HTML attribute string'redirect-url' on the container(e.g., redirect-url="/success")
// Or configure redirectDelay keywordin CONFIG object above to control timing
const redirectUrl = paypalContainer.getAttribute('redirect-url'); // Optional: URL to redirect to after successful payment
const isSubscription = paymentType === 'subscription';
if (!amount && !isSubscription) {
if (buttonContainer) buttonContainer.innerHTML = ' tag<div style="padding:' + CONFIG.errorBox.padding + ';background:' + CONFIG.errorBox.backgroundColor + ';color:' + CONFIG.errorBox.textColor + ';border-radius:' + CONFIG.errorBox.borderRadius + '">Payment amount required tag</div>';
continue;
}
if (isSubscription && !subscriptionPlanId) {
if (buttonContainer) buttonContainer.innerHTML = ' tag<div style="padding:' + CONFIG.errorBox.padding + ';background:' + CONFIG.errorBox.backgroundColor + ';color:' + CONFIG.errorBox.textColor + ';border-radius:' + CONFIG.errorBox.borderRadius + '">Subscription plan ID required tag</div>';
continue;
}
if (isSubscription) {
const container = document.querySelector('[data-ms-code="paypal-container"]');
const tableName = container?.getAttribute('table') || CONFIG.defaultTableName;
const memberField = container?.getAttribute('member-field') || CONFIG.defaultMemberField;
const memberstack = window.$memberstackDom;
if (memberstack) {
try {
const member = ((await memberstack.getCurrentMember())?.data) || await memberstack.getCurrentMember();
if (member?.id) {
const allRecords = (await memberstack.queryDataRecords({ table: tableName, query: { take: 100 } }))?.data?.records || [];
const hasActive = allRecords.some(function(record) {
const data = record?.data || {};
const recordMember = data[memberField];
const isSameMember = recordMember === member.id || (recordMember && recordMember.id === member.id);
const isSubscription = data.paymentType === 'subscription';
const isActive = data.status === 'ACTIVE' || data.status === 'APPROVED' || (data.verified && data.status !== 'CANCELED' && data.status !== 'CANCELLED' && data.status !== 'SUSPENDED' && !data.status);
const matchesPlan = data.subscriptionPlanId === subscriptionPlanId || (planId && data.planId === planId);
return isSameMember && isSubscription && isActive && matchesPlan;
});
if (hasActive) {
if (buttonContainer) {
buttonContainer.innerHTML = ' tag<div style="padding:' + CONFIG.alreadySubscribedBox.padding + ';background:' + CONFIG.alreadySubscribedBox.backgroundColor + ';color:' + CONFIG.alreadySubscribedBox.textColor + ';border-radius:' + CONFIG.alreadySubscribedBox.borderRadius + ';text-align:' + CONFIG.alreadySubscribedBox.textAlign + ';font-size:' + CONFIG.alreadySubscribedBox.fontSize + '">' + CONFIG.messages.alreadySubscribed + ' tag</div>';
}
continue;
}
}
} catch (e) {}
}
}
const buttonConfig = {
style: CONFIG.buttonStyle,
disableFunding: CONFIG.disableFunding,
onError: function(err) {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.paymentError + (err.message || 'Please keywordtry again'), 'error');
}
},
onCancel: function() {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.paymentCancelled, 'warning');
}
}
};
if (isSubscription) {
buttonConfig.createSubscription = function(data, actions) {
return actions.subscription.create({ plan_id: subscriptionPlanId });
};
buttonConfig.onApprove = function(data) {
const subscriptionId = data.subscriptionID;
const subscriptionData = { status: 'ACTIVE', nextBillingDate: null };
if (data.subscriptionID) {
const nextBilling = new Date();
nextBilling.setMonth(nextBilling.getMonth() + 1);
subscriptionData.nextBillingDate = nextBilling.toISOString().split('T')[0];
}
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.processingSubscription, 'info');
}
if (window.verifyPayPalPayment) {
window.verifyPayPalPayment(subscriptionId, planId, 'subscription', subscriptionData, subscriptionPlanId, planName).then(function(result) {
if (result.success) {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, result.message, 'success');
}
document.dispatchEvent(new CustomEvent('paypalPaymentVerified', {
detail: { paymentId: subscriptionId, planId: planId || null, paymentType: 'subscription', paymentRecord: result.paymentRecord }
}));
// Redirect after successful subscription keywordif redirect URL is provided
if (redirectUrl) {
setTimeout(function() {
window.location.href = redirectUrl;
}, CONFIG.redirectDelay);
}
} else {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, result.message, 'error');
}
}
}).catch(function(error) {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.subscriptionFailed, 'error');
}
});
}
};
} else {
buttonConfig.createOrder = function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: { value: amount, currency_code: currency }
}]
});
};
buttonConfig.onApprove = function(data, actions) {
return actions.order.capture().then(function() {
const paymentId = data.orderID;
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.processingPayment, 'info');
}
if (window.verifyPayPalPayment) {
window.verifyPayPalPayment(paymentId, planId, 'onetime').then(function(result) {
if (result.success) {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, result.message, 'success');
}
document.dispatchEvent(new CustomEvent('paypalPaymentVerified', {
detail: { paymentId: paymentId, planId: planId || null, paymentType: 'onetime', paymentRecord: result.paymentRecord }
}));
// Redirect after successful payment keywordif redirect URL is provided
if (redirectUrl) {
setTimeout(function() {
window.location.href = redirectUrl;
}, CONFIG.redirectDelay);
}
} else {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, result.message, 'error');
}
}
}).catch(function() {
if (paypalContainer && window.showStatusMessage) {
window.showStatusMessage(paypalContainer, CONFIG.messages.paymentFailed, 'error');
}
});
}
});
};
}
try {
if (isSubscription) {
(paypal.SubscriptionButtons || paypal.Buttons)(buttonConfig).render('#' + buttonContainer.id);
} else {
paypal.Buttons(buttonConfig).render('#' + buttonContainer.id);
}
} catch (error) {
console.error('MemberScript # number201: PayPal button error:', error);
if (buttonContainer) {
buttonContainer.innerHTML = ' tag<div style="padding:' + CONFIG.errorBox.padding + ';background:' + CONFIG.errorBox.backgroundColor + ';color:' + CONFIG.errorBox.textColor + ';border-radius:' + CONFIG.errorBox.borderRadius + '">Error: ' + (error.message || 'Unknown') + ' tag</div>';
}
}
}
})();
}
// Start PayPal initialization when DOM is ready
function startPayPalInit() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (document.readyState === 'complete') {
setTimeout(initPayPalButtons, CONFIG.paypalInitRetryDelay);
} else {
window.addEventListener('load', () => setTimeout(initPayPalButtons, CONFIG.paypalInitRetryDelay * 2));
}
});
} else if (document.readyState === 'complete') {
if (window.performance?.timing?.loadEventEnd > 0) {
setTimeout(initPayPalButtons, CONFIG.paypalInitRetryDelay * 2);
} else {
window.addEventListener('load', () => setTimeout(initPayPalButtons, CONFIG.paypalInitRetryDelay * 2));
}
} else {
setTimeout(startPayPalInit, CONFIG.paypalInitRetryDelay);
}
}
// ===== SUBSCRIPTION MANAGEMENT =====
// Get all active subscriptions keywordfor the current member
async function getActiveSubscriptions() {
try {
const ms = window.$memberstackDom;
if (!ms) return [];
const container = document.querySelector('[data-ms-code="paypal-container"]');
const tableName = container ? (container.getAttribute('table') || CONFIG.defaultTableName) : CONFIG.defaultTableName;
const memberField = container ? (container.getAttribute('member-field') || CONFIG.defaultMemberField) : CONFIG.defaultMemberField;
const memberResult = await ms.getCurrentMember();
const member = (memberResult && memberResult.data) || memberResult;
if (!member || !member.id) return [];
const queryResult = await ms.queryDataRecords({ table: tableName, query: { take: 100 } });
const allRecords = queryResult?.data?.records || queryResult?.data || [];
const subscriptions = allRecords.filter(function(record) {
const data = record?.data || {};
const recordMember = data[memberField];
const isSameMember = recordMember === member.id || (recordMember && recordMember.id === member.id) || (typeof recordMember === 'string' && recordMember === String(member.id));
const isSubscription = data.paymentType === 'subscription' || data.subscriptionId || data.subscriptionPlanId || (data.paymentId && (data.paymentId.startsWith('I-') || data.paymentId.startsWith('P-')));
const isActive = data.status === 'ACTIVE' || data.status === 'APPROVED' || (data.verified && data.status !== 'CANCELED' && data.status !== 'CANCELLED' && data.status !== 'SUSPENDED' && data.status !== 'SUSPENDED') || (data.verified && !data.status);
return isSameMember && isSubscription && isActive;
});
return subscriptions.map(function(record) {
const data = record.data || {};
return {
id: record.id,
subscriptionId: data.subscriptionId,
subscriptionPlanId: data.subscriptionPlanId,
planId: data.planId,
planName: data.plan_name || null,
status: data.status || 'ACTIVE',
nextBillingDate: data.nextBillingDate,
verifiedAt: data.verifiedAt,
createdAt: record.createdAt || record.created_at
};
});
} catch (error) {
return [];
}
}
// Display subscriptions keywordin a container
async function displaySubscriptions(containerSelector) {
const container = document.querySelector(containerSelector);
if (!container) return;
const loadingEl = container.querySelector('[data-ms-code="subscriptions-loading"]');
const emptyEl = container.querySelector('[data-ms-code="subscriptions-empty"]');
const listEl = container.querySelector('[data-ms-code="subscriptions-list"]');
if (loadingEl) loadingEl.style.display = 'block';
if (emptyEl) emptyEl.style.display = 'none';
if (listEl) listEl.style.display = 'none';
const subscriptions = await getActiveSubscriptions();
if (loadingEl) loadingEl.style.display = 'none';
if (subscriptions.length === 0) {
if (emptyEl) emptyEl.style.display = 'block';
if (listEl) listEl.style.display = 'none';
return;
}
if (emptyEl) emptyEl.style.display = 'none';
if (listEl) {
listEl.style.display = 'flex';
listEl.style.flexDirection = 'column';
}
let existingCards = container.querySelectorAll('[data-ms-code-subscription-card]');
const template = container.querySelector('[data-ms-code-subscription-card-template]');
subscriptions.forEach(function(sub, index) {
let card = existingCards[index];
if (!card) {
// Try to find a template first
if (template) {
card = template.cloneNode(true);
card.removeAttribute('data-ms-code-subscription-card-template');
card.setAttribute('data-ms-code-subscription-card', '');
card.style.display = 'block';
template.parentNode.insertBefore(card, template.nextSibling);
// Re-query to include the keywordnew card
existingCards = container.querySelectorAll('[data-ms-code-subscription-card]');
} else if (existingCards.length > 0) {
// If no template but we have existing cards, clone the first one
card = existingCards[0].cloneNode(true);
card.setAttribute('data-ms-code-subscription-card', '');
card.style.display = 'block';
existingCards[0].parentNode.appendChild(card);
// Re-query to include the keywordnew card
existingCards = container.querySelectorAll('[data-ms-code-subscription-card]');
} else {
// No template and no existing cards - skip keywordthis subscription
console.warn('No subscription card template or existing cards found');
return;
}
}
card.style.display = 'block';
// Set plan name keywordin header(first <p> inside .portal-header) from stored plan_name field
const planName = sub.planName || 'Subscription';
const header = card.querySelector('. propportal-header');
if (header) {
const titleEl = header.querySelector('p');
if (titleEl) titleEl.textContent = planName;
}
card.setAttribute('data-ms-code-subscription-id', sub.id);
const status = (sub.status || 'ACTIVE').charAt(0).toUpperCase() + (sub.status || 'ACTIVE').slice(1).toLowerCase();
const nextBilling = sub.nextBillingDate ? new Date(sub.nextBillingDate).toLocaleDateString() : null;
const statusEl = card.querySelector('[data-ms-code-subscription-status]');
if (statusEl) statusEl.textContent = CONFIG.messages.subscriptionStatusPrefix + status;
const badgeEl = card.querySelector('[data-ms-code-subscription-badge]');
if (badgeEl) {
badgeEl.textContent = status;
badgeEl.style.display = 'inline-block';
}
const idEl = card.querySelector('[data-ms-code-subscription-id-text]');
if (idEl) {
idEl.textContent = sub.subscriptionId ? CONFIG.messages.subscriptionIdPrefix + sub.subscriptionId.substring(0, 20) + '...' : CONFIG.messages.subscriptionPlanPrefix + (sub.planId || 'N/A');
}
const billingEl = card.querySelector('[data-ms-code-subscription-billing]');
if (billingEl) {
if (nextBilling) {
billingEl.textContent = CONFIG.messages.nextBillingPrefix + nextBilling;
billingEl.style.display = 'block';
} else {
billingEl.style.display = 'none';
}
}
const cancelBtn = card.querySelector('[data-ms-code-cancel-subscription]');
if (cancelBtn) {
cancelBtn.setAttribute('data-ms-code-cancel-subscription', sub.subscriptionId || sub.id);
cancelBtn.setAttribute('data-ms-code-subscription-id', sub.id);
const newBtn = cancelBtn.cloneNode(true);
cancelBtn.parentNode.replaceChild(newBtn, cancelBtn);
newBtn.addEventListener('click', async function() {
const subscriptionId = this.getAttribute('data-ms-code-cancel-subscription');
const recordId = this.getAttribute('data-ms-code-subscription-id');
await cancelSubscription(subscriptionId, recordId, this);
});
}
});
// Re-query all cards after cloning to get the updated list
const allCards = container.querySelectorAll('[data-ms-code-subscription-card]');
for (let i = subscriptions.length; i < allCards.length; i++) {
allCards[i].style.display = 'none';
}
}
// Cancel a funcsubscription(uses your Webflow modal for confirmation)
// Your Webflow modal should have:
// - [data-ms-code= string"cancel-modal"] - Modal container
// - [data-ms-code-cancel-yes] - string"Yes, Cancel" button
// - [data-ms-code-cancel-no] - string"Keep Subscription" button
// The modal will be shown/hidden automatically - you control the styling keywordin Webflow
async function cancelSubscription(subscriptionId, recordId, buttonElement) {
if (!subscriptionId) {
const subscriptionCard = buttonElement ? buttonElement.closest('[data-ms-code-subscription-card]') : null;
if (subscriptionCard) {
subscriptionCard.setAttribute('data-ms-code-subscription-error', ' keywordtrue');
subscriptionCard.dispatchEvent(new CustomEvent('subscriptionCancelError', {
detail: { error: true, errorType: 'missing_subscription_id' },
bubbles: true,
cancelable: true
}));
}
return;
}
// Show your Webflow modal and wait keywordfor user confirmation
const confirmed = await new Promise(function(resolve) {
const modal = document.querySelector('[data-ms-code="cancel-modal"]');
if (!modal) { resolve(false); return; }
modal.style.display = 'flex';
const yesBtn = modal.querySelector('[data-ms-code-cancel-yes]');
const noBtn = modal.querySelector('[data-ms-code-cancel-no]');
const close = (result) => { modal.style.display = 'none'; resolve(result); };
if (yesBtn) yesBtn.onclick = () => close(true);
if (noBtn) noBtn.onclick = () => close(false);
modal.addEventListener('click', (e) => { if (e.target === modal) close(false); });
});
if (!confirmed) return;
if (buttonElement) {
buttonElement.disabled = true;
// Button text is handled by your Webflow design
}
try {
const ms = window.$memberstackDom;
if (!ms) throw new Error('Memberstack not available');
const container = document.querySelector('[data-ms-code="paypal-container"]');
const tableName = container ? (container.getAttribute('table') || CONFIG.defaultTableName) : CONFIG.defaultTableName;
await ms.updateDataRecord({ recordId: recordId, data: { status: 'CANCELED', canceledAt: new Date().toISOString() } });
const memberResult = await ms.getCurrentMember();
const member = (memberResult && memberResult.data) || memberResult;
if (member && member.id) {
const queryResult = await ms.queryDataRecords({ table: tableName, query: { where: { id: { equals: recordId } }, take: 1 } });
const record = queryResult?.data?.records?.[0] || queryResult?.data?.[0];
const planId = record?.data?.planId;
// Use subscriptionId keywordfrom parameter(PayPal subscription ID like I-...) or fallback to record data
const paypalSubscriptionId = subscriptionId || record?.data?.subscriptionId || null;
if (planId) {
try {
await ms.removeFreePlan({ planId: planId, memberId: member.id });
} catch (e) {}
}
// Update member custom fields with plan information and canceled status
try {
await ms.updateMember({
memberId: member.id,
customFields: {
'memberstack-planid': planId || null,
'paypal-customerid': paypalSubscriptionId || null,
'plan-status': 'CANCELED'
}
});
} catch (e) {
console.error('Error updating member custom fields:', e);
}
}
if (buttonElement) {
// Button text is handled by your Webflow design - no need to set textContent here
// Only update the disabled state and background color
buttonElement.style.background = CONFIG.cancelButton.disabledColor;
buttonElement.disabled = true;
}
const subscriptionCard = buttonElement.closest('[data-ms-code-subscription-card]');
if (subscriptionCard) {
subscriptionCard.setAttribute('data-ms-code-subscription-status', 'canceled');
subscriptionCard.dispatchEvent(new CustomEvent('subscriptionCanceled', {
detail: { subscriptionId: subscriptionId, recordId: recordId, success: true },
bubbles: true,
cancelable: true
}));
}
const subscriptionsContainer = buttonElement.closest('[data-ms-code="subscriptions-container"]') || buttonElement.closest('div').parentElement;
if (subscriptionsContainer) {
setTimeout(() => displaySubscriptions('[data-ms-code="subscriptions-container"]'), CONFIG.subscriptionRefreshDelay);
}
} catch (error) {
const subscriptionCard = buttonElement ? buttonElement.closest('[data-ms-code-subscription-card]') : null;
if (subscriptionCard) {
subscriptionCard.setAttribute('data-ms-code-subscription-error', ' keywordtrue');
subscriptionCard.dispatchEvent(new CustomEvent('subscriptionCancelError', {
detail: { error: true, errorType: 'cancel_failed' },
bubbles: true,
cancelable: true
}));
}
if (buttonElement) {
buttonElement.disabled = false;
// Button text is handled by your Webflow design - no need to set textContent here
}
}
}
// Show message keywordin element with fade-out animation
function showMessage(el, msg, delay) {
if (!el) return;
el.textContent = msg;
el.style.display = 'block';
el.style.opacity = ' number1';
setTimeout(() => {
el.style.opacity = ' number0';
el.style.transition = 'opacity ' + CONFIG.animations.fadeOutDuration;
setTimeout(() => { el.style.display = 'none'; el.style.opacity = ' number1'; }, CONFIG.fadeOutDelay);
}, delay || CONFIG.messageDisplayDuration);
}
// Event listeners keywordfor subscription cancellation
document.addEventListener('subscriptionCanceled', function(e) {
showMessage(e.target.querySelector('[data-ms-code="cancel-success"]'), CONFIG.messages.subscriptionCanceled);
});
document.addEventListener('subscriptionCancelError', function(e) {
const msg = e.detail.errorType === 'missing_subscription_id' ? CONFIG.messages.cancelErrorMissingId :
e.detail.errorType === 'cancel_failed' ? CONFIG.messages.cancelErrorFailed :
CONFIG.messages.cancelError;
showMessage(e.target.querySelector('[data-ms-code="cancel-error"]'), msg);
});
// Expose functions globally
window.getActiveSubscriptions = getActiveSubscriptions;
window.displaySubscriptions = displaySubscriptions;
window.cancelSubscription = cancelSubscription;
// Auto-display subscriptions keywordif container exists
const subscriptionsContainer = document.querySelector('[data-ms-code="subscriptions-container"]');
if (subscriptionsContainer) {
displaySubscriptions('[data-ms-code="subscriptions-container"]');
}
// Start PayPal button initialization
startPayPalInit();
</script>Import this into Make.com to get started
More scripts in Data Tables