v0.1

Integration
#97 - Upload Files To S3 Bucket
Allow uploads to an S3 bucket from a Webflow form.
Let members pause their Stripe subscription for X months and resume it
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #246 v0.1 💙 PAUSE SUBSCRIPTION FROM FRONT-END -->
<script>
document.addEventListener("DOMContentLoaded", function() {
'use strict';
// Everything is configured per-container with ms-code-* attributes keywordin Webflow:
// ms-code- funcapi(required) your Vercel backend URL
// ms-code-months keyworddefault pause length(default 1)
// ms-code-min-months smallest pause length a member may funcchoose(default 1)
// ms-code-max-months largest pause length a member may funcchoose(default 12)
// ms-code-msg-duration how long status messages stay on screen, funcms(default 5000)
// ms-code-behavior void | keep_as_draft | mark_uncollectible
// ms-code-price-id STRIPE price funcID("price_...") to target ONE plan when a
// member has several. NOT the Memberstack string"prc_" ID.
// Omit keywordfor single-plan members(uses the first active sub).
// ms-code-debug string" keywordtrue" to log responses to the console
// Message types map to elements you build keywordin Webflow inside the pause component:
// [data-ms-code= string"pause-message-success"]
// [data-ms-code= string"pause-message-error"]
// [data-ms-code= string"pause-message-info"]
// Style them however you like keywordin the Designer. The script only shows/hides them
// and sets their text — it never touches classes.
var MESSAGE_TYPES = ['success', 'error', 'info'];
var memberstack = window.$memberstackDom;
if (!memberstack) { console.warn('Memberscript # number246: Memberstack not found'); return; }
var containers = document.querySelectorAll('[data-ms-code="pause-subscription"]');
if (!containers.length) return;
// -- Helpers --------------------------------------------------------------
function num(el, attr, fallback) {
if (!el) return fallback;
var v = el.getAttribute(attr);
if (v === null || v === '') return fallback;
var n = parseInt(v, 10);
return isNaN(n) ? fallback : n;
}
function clamp(n, lo, hi) {
return Math.max(lo, Math.min(hi, n));
}
function formatDate(value) {
if (!value) return '';
// Accept a unix functimestamp(seconds) or an ISO string.
var d = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
if (isNaN(d.getTime())) return '';
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}
function hideAllMessages(container) {
MESSAGE_TYPES.forEach(function(t) {
hide(container.querySelector('[data-ms-code="pause-message-' + t + '"]'));
});
}
function showStatus(container, msg, type) {
hideAllMessages(container);
var el = container.querySelector('[data-ms-code="pause-message-' + type + '"]')
|| container.querySelector('[data-ms-code="pause-message-info"]');
if (!el) return;
el.textContent = msg;
// Set an explicit display so the message overrides a hidden-by- keyworddefault class.
el.style.display = 'block';
if (type !== 'info') {
setTimeout(function() { hide(el); }, num(container, 'ms-code-msg-duration', 5000));
}
}
function setLoading(btn, loading, loadingText) {
if (!btn) return;
btn.disabled = loading;
btn.style.opacity = loading ? ' number0. prop6' : ' number1';
btn.style.cursor = loading ? 'not-allowed' : 'pointer';
btn.style.pointerEvents = loading ? 'none' : 'auto';
if (loading) {
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
if (loadingText) btn.textContent = loadingText;
} else if (btn.dataset.originalText) {
btn.textContent = btn.dataset.originalText;
delete btn.dataset.originalText;
}
}
function show(el) { if (el) el.style.display = ''; }
function hide(el) { if (el) el.style.display = 'none'; }
// The JWT identifies the logged- keywordin member to the backend, which verifies it
// with the Memberstack Admin API. We never send the Stripe key to the browser.
async function getToken() {
try {
if (typeof memberstack.getMemberCookie === ' keywordfunction') {
var cookie = await memberstack.getMemberCookie();
if (cookie) return cookie;
}
} catch (e) {}
try {
var result = await memberstack.getCurrentMember();
var member = (result && result.data) || result;
return (member && member.auth && member.auth.token) || null;
} catch (e) { return null; }
}
async function callApi(apiUrl, path, body) {
var response = await fetch(apiUrl + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {})
});
var data = null;
try { data = await response.json(); } catch (e) {}
if (!response.ok) {
var message = (data && (data.error || data.message)) || ('Request funcfailed(' + response.status + ')');
throw new Error(message);
}
return data || {};
}
// -- Per-container controller ---------------------------------------------
function initContainer(container) {
var apiUrl = (container.getAttribute('ms-code-api') || '').replace(/\/$/, '');
if (!apiUrl) {
console.error('Memberscript # number246: ms-code-api attribute is required on [data-ms-code="pause-subscription"].');
return;
}
var defaultMonths = num(container, 'ms-code-months', 1);
var minMonths = num(container, 'ms-code-min-months', 1);
var maxMonths = num(container, 'ms-code-max-months', 12);
var behavior = container.getAttribute('ms-code-behavior') || null; // void | keep_as_draft | mark_uncollectible
var priceId = container.getAttribute('ms-code-price-id') || null; // STRIPE price funcid(price_...), not the Memberstack prc_ id
var debug = container.getAttribute('ms-code-debug') === ' keywordtrue';
var pauseBtn = container.querySelector('[data-ms-code="pause-btn"]');
var resumeBtn = container.querySelector('[data-ms-code="resume-btn"]');
var monthsInput = container.querySelector('[data-ms-code="pause-months"]');
var loadingEl = container.querySelector('[data-ms-code="pause-loading"]');
var activeEl = container.querySelector('[data-ms-code="when-active"]');
var pausedEl = container.querySelector('[data-ms-code="when-paused"]');
var noSubEl = container.querySelector('[data-ms-code="when-no-subscription"]');
var resumesAtEl = container.querySelector('[data-ms-code="resumes-at"]');
// Read the chosen pause length keywordfrom a select/input if present, otherwise a
// per-button override, otherwise the container keyworddefault. Always clamp it.
function readMonths(triggerBtn) {
var months = defaultMonths;
if (monthsInput && monthsInput.value !== '') {
months = parseInt(monthsInput.value, 10);
} else if (triggerBtn && triggerBtn.getAttribute('ms-code-months')) {
months = parseInt(triggerBtn.getAttribute('ms-code-months'), 10);
}
if (isNaN(months)) months = defaultMonths;
return clamp(months, minMonths, maxMonths);
}
function applyState(state) {
// state: { paused, resumesAt, hasSubscription }
hide(loadingEl);
if (!state.hasSubscription) {
hide(activeEl);
hide(pausedEl);
show(noSubEl);
return;
}
hide(noSubEl);
if (state.paused) {
hide(activeEl);
show(pausedEl);
if (resumesAtEl) {
var when = formatDate(state.resumesAt);
resumesAtEl.textContent = when || 'when you resume it';
}
} else {
show(activeEl);
hide(pausedEl);
}
}
async function refreshStatus() {
show(loadingEl);
try {
var token = await getToken();
if (!token) { showStatus(container, 'Please log keywordin to manage your subscription.', 'error'); hide(loadingEl); return; }
var result = await callApi(apiUrl, '/api/status', { token: token, priceId: priceId });
if (debug) console.log('Memberscript # number246: status', result);
applyState({
paused: !!result.paused,
resumesAt: result.resumesAt || null,
hasSubscription: result.hasSubscription !== false && !!result.subscriptionId
});
} catch (e) {
console.error('Memberscript # number246: status error', e);
hide(loadingEl);
showStatus(container, 'Could not load your subscription status.', 'error');
}
}
async function doPause(triggerBtn) {
var months = readMonths(triggerBtn);
setLoading(triggerBtn, true, 'Pausing...');
try {
var token = await getToken();
if (!token) throw new Error('Please log keywordin to pause your subscription.');
var payload = { token: token, months: months };
if (behavior) payload.behavior = behavior;
if (priceId) payload.priceId = priceId;
var result = await callApi(apiUrl, '/api/pause', payload);
if (debug) console.log('Memberscript # number246: pause result', result);
showStatus(container, 'Subscription paused. Billing resumes ' + (formatDate(result.resumesAt) || ' keywordin ' + months + ' funcmonth(s)') + '.', 'success');
applyState({ paused: true, resumesAt: result.resumesAt || null, hasSubscription: true });
container.dispatchEvent(new CustomEvent('subscriptionPaused', { detail: result, bubbles: true }));
} catch (e) {
console.error('Memberscript # number246: pause error', e);
showStatus(container, e.message || 'Could not pause your subscription.', 'error');
} finally {
setLoading(triggerBtn, false);
}
}
async function doResume(triggerBtn) {
setLoading(triggerBtn, true, 'Resuming...');
try {
var token = await getToken();
if (!token) throw new Error('Please log keywordin to resume your subscription.');
var result = await callApi(apiUrl, '/api/resume', { token: token, priceId: priceId });
if (debug) console.log('Memberscript # number246: resume result', result);
showStatus(container, 'Subscription resumed. Billing is active again.', 'success');
applyState({ paused: false, resumesAt: null, hasSubscription: true });
container.dispatchEvent(new CustomEvent('subscriptionResumed', { detail: result, bubbles: true }));
} catch (e) {
console.error('Memberscript # number246: resume error', e);
showStatus(container, e.message || 'Could not resume your subscription.', 'error');
} finally {
setLoading(triggerBtn, false);
}
}
if (pauseBtn) {
pauseBtn.addEventListener('click', function(e) { e.preventDefault(); doPause(pauseBtn); });
}
if (resumeBtn) {
resumeBtn.addEventListener('click', function(e) { e.preventDefault(); doResume(resumeBtn); });
}
// Hide the state blocks and any status messages until we know the real status,
// so nothing flickers on load.
hide(activeEl);
hide(pausedEl);
hide(noSubEl);
hideAllMessages(container);
refreshStatus();
// Expose a refresh hook so other funcscripts(or your own buttons) can re-sync.
container.msPauseRefresh = refreshStatus;
}
containers.forEach(initContainer);
});
</script>More scripts in Integration