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
+25 -16
View File
@@ -584,23 +584,9 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
let timing: String = json_get_string(body, "timing")
if str_eq(timing, "") { let timing = "now" }
// Free tier: SetupIntent save card details without charging.
// Card is stored on a Stripe Customer; billing begins only if the
// user later upgrades to a paid plan.
// Free tier: no card required. Return immediately no Stripe interaction.
if str_eq(plan, "free") {
let si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
let auth_header: String = "Bearer " + stripe_key
let si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
si_body,
auth_header)
if str_starts_with(si_resp, "{") {
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return si_resp
return "{\"plan\":\"free\",\"free\":true,\"no_payment_required\":true}"
}
// Hard cap: block founding checkouts when 1,000 spots are filled
if str_eq(plan, "founding") {
@@ -612,6 +598,27 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
let auth_header: String = "Bearer " + stripe_key
// Find-or-create Stripe Customer by email upfront so every intent
// is attached to an existing customer prevents duplicate customers.
let pi_email: String = json_get_string(body, "email")
let pi_name: String = json_get_string(body, "name")
let pi_cus_id: String = ""
if !str_eq(pi_email, "") {
let pi_email_enc: String = str_replace(str_replace(pi_email, "@", "%40"), "+", "%2B")
let pi_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + pi_email_enc + "%22&limit=1"
let pi_search: String = http_get_auth(pi_search_url, auth_header)
let pi_cus_id = json_get_string(pi_search, "id")
if str_eq(pi_cus_id, "") {
let pi_name_enc: String = str_replace(pi_name, " ", "%20")
let pi_cus_body: String = "email=" + pi_email_enc
+ "&name=" + pi_name_enc
+ "&metadata[plan]=" + plan
+ "&metadata[source]=neuron-checkout"
let pi_cus_resp: String = http_post_form_auth("https://api.stripe.com/v1/customers", pi_cus_body, auth_header)
let pi_cus_id = json_get_string(pi_cus_resp, "id")
}
}
// Setup-mode path: save payment method, do not charge. Only valid
// for Professional (Founding is one-shot lifetime, charges immediately).
if str_eq(plan, "professional") && str_eq(timing, "later") {
@@ -620,6 +627,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
+ "&metadata[plan]=" + plan
+ "&metadata[hold_until]=launch"
+ "&metadata[launch_target]=2026-09-01"
let si_body = if !str_eq(pi_cus_id, "") { si_body + "&customer=" + pi_cus_id } else { si_body }
let si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
si_body,
@@ -642,6 +650,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
+ "&automatic_payment_methods[enabled]=true"
+ "&metadata[plan]=" + plan
+ "&metadata[timing]=" + timing
let pi_body = if !str_eq(pi_cus_id, "") { pi_body + "&customer=" + pi_cus_id } else { pi_body }
let response: String = http_post_form_auth(
"https://api.stripe.com/v1/payment_intents",
pi_body,