00e62bb010
Free tier: - checkout-stripe.el bails out immediately for plan=free (no Stripe init) - checkout-auth.el skips payment section reveal and initStripe for free plan - checkout-free.el shows #free-success panel after auth (no card ever shown) - /api/payment-intent returns early for free plan — no Stripe call Stripe dedup (all paid plans): - Stripe init now deferred to window.initStripe(email, name), called by checkout-auth.el after sign-in — email is known before intent is created - /api/payment-intent finds-or-creates Stripe Customer by email before creating the PaymentIntent/SetupIntent and attaches customer upfront - Eliminates the window between intent creation and /api/link-customer that was producing duplicate guest customers
212 lines
8.2 KiB
EmacsLisp
212 lines
8.2 KiB
EmacsLisp
// checkout-stripe.el -- Stripe Payment Element setup and form submission.
|
|
// Reads NEURON_CFG.plan and NEURON_CFG.pub_key from window.
|
|
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
|
//
|
|
// Required globals: window.NEURON_CFG.plan, window.NEURON_CFG.pub_key
|
|
// Required CDN: Stripe.js loaded before this script
|
|
|
|
fn main() -> Void {
|
|
native_js("(function() {
|
|
var cfg = window.NEURON_CFG || {};
|
|
var PLAN = cfg.plan || '';
|
|
var STRIPE_PK = cfg.pub_key || '';
|
|
var stripe, elements;
|
|
|
|
function waitForStripe(cb) {
|
|
if (window.Stripe) { cb(); return; }
|
|
setTimeout(function() { waitForStripe(cb); }, 50);
|
|
}
|
|
|
|
function showMessage(msg) {
|
|
var el = document.getElementById('payment-message');
|
|
if (el) { el.textContent = msg; el.style.display = 'block'; }
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
var btn = document.getElementById('submit-btn');
|
|
var label = document.getElementById('submit-label');
|
|
var spinner = document.getElementById('submit-spinner');
|
|
if (btn) btn.disabled = loading;
|
|
if (label) label.style.display = loading ? 'none' : '';
|
|
if (spinner) spinner.style.display = loading ? '' : 'none';
|
|
}
|
|
|
|
// Free plan has no payment form — bail out entirely.
|
|
if (str_eq(PLAN, 'free')) return;
|
|
|
|
window._neuronMode = 'payment';
|
|
var paymentEl = null;
|
|
var userEmail = '';
|
|
var userName = '';
|
|
|
|
function appearance() {
|
|
return {
|
|
theme: 'flat',
|
|
variables: {
|
|
colorPrimary: '#0052A0',
|
|
colorBackground: '#ffffff',
|
|
colorText: '#1A1A2E',
|
|
colorDanger: '#c0392b',
|
|
colorTextPlaceholder: '#9B9BAD',
|
|
borderRadius: '0px',
|
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
fontSizeBase: '15px',
|
|
fontWeightNormal: '300',
|
|
spacingUnit: '4px'
|
|
},
|
|
rules: {
|
|
'.Input': { border: '1px solid rgba(0,82,160,.22)', boxShadow: 'none', padding: '10px 14px' },
|
|
'.Input:focus': { border: '1px solid rgba(0,82,160,.6)', boxShadow: '0 0 0 3px rgba(0,82,160,.08)', outline: 'none' },
|
|
'.Label': { fontSize: '11px', fontWeight: '500', letterSpacing: '.06em', textTransform: 'uppercase', color: '#6B6B7E', marginBottom: '6px' },
|
|
'.Tab': { border: '1px solid rgba(0,82,160,.18)', boxShadow: 'none' },
|
|
'.Tab--selected':{ border: '1px solid rgba(0,82,160,.5)', boxShadow: '0 0 0 2px rgba(0,82,160,.12)' },
|
|
'.Error': { color: '#c0392b' }
|
|
}
|
|
};
|
|
}
|
|
|
|
function currentTiming() {
|
|
var later = document.getElementById('timing-later');
|
|
return (later && later.checked) ? 'later' : 'now';
|
|
}
|
|
|
|
function fetchAndMount() {
|
|
var submitBtn = document.getElementById('submit-btn');
|
|
if (submitBtn) submitBtn.disabled = true;
|
|
if (paymentEl) { try { paymentEl.unmount(); } catch(e) {} paymentEl = null; }
|
|
var hostEl = document.getElementById('payment-element');
|
|
if (hostEl && !document.querySelector('.checkout-element-loading')) {
|
|
var d = document.createElement('div');
|
|
d.className = 'checkout-element-loading';
|
|
d.textContent = 'Loading payment form...';
|
|
hostEl.appendChild(d);
|
|
}
|
|
var timing = currentTiming();
|
|
return fetch('/api/payment-intent', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ plan: PLAN, timing: timing, email: userEmail, name: userName })
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.error === 'sold_out') {
|
|
showMessage('All 1,000 Founding Member spots have been claimed. Thank you for your interest - please consider the Professional plan.');
|
|
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Sold out'; }
|
|
return;
|
|
}
|
|
if (!data.client_secret) { showMessage('Unable to initialise payment. Please try again.'); return; }
|
|
window._neuronMode = data.setup_mode ? 'setup' : 'payment';
|
|
window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : '');
|
|
var submitLabel = document.getElementById('submit-label');
|
|
if (submitLabel) {
|
|
submitLabel.textContent = window._neuronMode === 'setup'
|
|
? 'Save my card - no charge today →'
|
|
: 'Complete purchase →';
|
|
}
|
|
waitForStripe(function() {
|
|
if (!stripe) stripe = Stripe(STRIPE_PK);
|
|
elements = stripe.elements({ clientSecret: data.client_secret, appearance: appearance() });
|
|
paymentEl = elements.create('payment', {
|
|
fields: { billingDetails: { name: 'never', email: 'never' } }
|
|
});
|
|
paymentEl.mount('#payment-element');
|
|
paymentEl.on('ready', function() {
|
|
var ld = document.querySelector('.checkout-element-loading');
|
|
if (ld) ld.remove();
|
|
if (submitBtn) submitBtn.disabled = false;
|
|
});
|
|
});
|
|
})
|
|
.catch(function() {
|
|
showMessage('Unable to connect. Please check your connection and try again.');
|
|
});
|
|
}
|
|
|
|
// Don't init Stripe at page load — wait for auth.
|
|
// checkout-auth.el calls window.initStripe(email, name) after sign-in.
|
|
window.initStripe = function(email, name) {
|
|
userEmail = email || '';
|
|
userName = name || '';
|
|
fetchAndMount();
|
|
var tNow = document.getElementById('timing-now');
|
|
var tLater = document.getElementById('timing-later');
|
|
if (tNow) tNow.addEventListener('change', fetchAndMount);
|
|
if (tLater) tLater.addEventListener('change', fetchAndMount);
|
|
};
|
|
|
|
var form = document.getElementById('payment-form');
|
|
if (form) form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
if (!stripe || !elements) return;
|
|
|
|
var attestCb = document.getElementById('founding-attest-cb');
|
|
if (attestCb && !attestCb.checked) {
|
|
var warn = document.getElementById('attest-warn');
|
|
if (warn) warn.style.display = 'block';
|
|
attestCb.closest('label').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
return;
|
|
}
|
|
|
|
var name = document.getElementById('buyer-name').value.trim();
|
|
var email = document.getElementById('buyer-email').value.trim();
|
|
if (!name || !email) { showMessage('Please enter your name and email.'); return; }
|
|
|
|
if (attestCb) {
|
|
try {
|
|
await fetch('/api/attest', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
plan: PLAN, name: name, email: email,
|
|
timestamp: new Date().toISOString(),
|
|
attestation: 'I am joining as a genuine early user, not to extract proprietary information about Neuron technology, architecture, or roadmap. I will engage in good faith. I understand that if this is not my intent, a different plan is a better fit.',
|
|
user_agent: navigator.userAgent
|
|
})
|
|
});
|
|
} catch(e) {}
|
|
}
|
|
|
|
setLoading(true);
|
|
var pmsg = document.getElementById('payment-message');
|
|
if (pmsg) pmsg.style.display = 'none';
|
|
|
|
if (window._neuronPiId) {
|
|
try {
|
|
await fetch('/api/link-customer', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
pi_id: window._neuronPiId,
|
|
email: email,
|
|
name: name,
|
|
plan: PLAN,
|
|
timing: currentTiming(),
|
|
mode: window._neuronMode || 'payment',
|
|
supabase_user_id: window._neuronSupaId || ''
|
|
})
|
|
});
|
|
} catch(e) {}
|
|
}
|
|
|
|
var confirmParams = {
|
|
return_url: window.location.origin + '/account?welcome=1',
|
|
payment_method_data: { billing_details: { name: name, email: email } }
|
|
};
|
|
var result;
|
|
if (window._neuronMode === 'setup') {
|
|
result = await stripe.confirmSetup({ elements: elements, confirmParams: confirmParams });
|
|
} else {
|
|
confirmParams.receipt_email = email;
|
|
result = await stripe.confirmPayment({ elements: elements, confirmParams: confirmParams });
|
|
}
|
|
|
|
if (result.error) {
|
|
showMessage(result.error.message || (window._neuronMode === 'setup'
|
|
? 'Could not save your card. Please try again.'
|
|
: 'Payment failed. Please try again.'));
|
|
setLoading(false);
|
|
}
|
|
});
|
|
})()")
|
|
}
|