#201 - PayPal + Memberstack Integration

Accept PayPal payments on your Memberstack site with this workaround script, supports one-time payments and subscriptions.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

749 lines
Paste this into Webflow
<!-- 💙 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 foundtag</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 requiredtag</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 requiredtag</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>

Make.com Blueprint

Download Blueprint

Import this into Make.com to get started

Download File
How to use:
  1. Download the JSON blueprint above
  2. Navigate to Make.com and create a new scenario
  3. Click the 3-dot menu and select Import Blueprint
  4. Upload your file and connect your accounts

Script Info

Versionv0.1
PublishedDec 12, 2025
Last UpdatedDec 9, 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 Data Tables