Fix free tier checkout and Stripe duplicate customers

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
This commit is contained in:
2026-05-07 01:00:51 -05:00
parent f0a6b55a13
commit 00e62bb010
5 changed files with 71 additions and 33 deletions
+10 -5
View File
@@ -33,8 +33,11 @@ fn main() -> Void {
if (user && user.id) { window._neuronSupaId = user.id; }
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree) {
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
}
if (user) {
var badge = document.getElementById('auth-badge');
@@ -55,9 +58,11 @@ fn main() -> Void {
if (emailEl) emailEl.value = user.email;
}
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof initStripe === 'function') initStripe(userEmail, userName);
if (!isFree) {
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
}
}
function checkExistingSession() {
+8 -5
View File
@@ -1,15 +1,18 @@
// checkout-free.el -- Free plan: reveal payment section after auth completes.
// Watches the auth-badge element; when it becomes visible, shows payment-section.
// checkout-free.el -- Free plan: show success panel after auth completes.
// Watches the auth-badge element; when it becomes visible, hides the auth
// section and shows the free-success panel. No card required for free tier.
// Compiled with: elc --target=js --bundle --minify --obfuscate
fn main() -> Void {
native_js("(function() {
var pay = document.getElementById('payment-section');
if (!pay) return;
var success = document.getElementById('free-success');
var auth = document.getElementById('auth-section');
if (!success) return;
var timer = setInterval(function() {
var badge = document.getElementById('auth-badge');
if (badge && badge.offsetParent !== null) {
pay.style.display = '';
if (auth) auth.style.display = 'none';
success.style.display = '';
clearInterval(timer);
}
}, 150);
+17 -6
View File
@@ -31,8 +31,13 @@ fn main() -> Void {
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 {
@@ -80,7 +85,7 @@ fn main() -> Void {
return fetch('/api/payment-intent', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ plan: PLAN, timing: timing })
body: JSON.stringify({ plan: PLAN, timing: timing, email: userEmail, name: userName })
})
.then(function(r) { return r.json(); })
.then(function(data) {
@@ -117,11 +122,17 @@ fn main() -> Void {
});
}
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);
// 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) {