checkout: re-add link-customer + redirect to /account, fix success/account routes
Restores the /api/link-customer endpoint that was lost in the stash. It runs right before stripe.confirmPayment() and: - searches Stripe for an existing Customer by email - creates one if missing (URL-encodes + and @ so Gmail aliases work) - attaches the Customer to the live PaymentIntent - upserts the Supabase waitlist row with stripe_customer_id + plan Stripe locks customer on a Charge once set, so the webhook handler is the second-line backstop for any race where confirmPayment beats the link call. Also: change return_url from /marketplace/success to /account?welcome=1 so buyers land where they need to be, and switch /marketplace/success and /account from str_eq to str_starts_with so Stripe-appended query strings (?payment_intent=...&redirect_status=succeeded) don't 404.
This commit is contained in:
+18
-1
@@ -703,6 +703,9 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture the PI id so we can attach a Customer at submit time
|
||||
window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : '');
|
||||
|
||||
waitForStripe(function() {
|
||||
stripe = Stripe(STRIPE_PK);
|
||||
|
||||
@@ -815,10 +818,24 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
setLoading(true);
|
||||
document.getElementById('payment-message').style.display = 'none';
|
||||
|
||||
// Link a Stripe Customer to this PaymentIntent and to the Supabase waitlist row
|
||||
// (so the buyer shows up as a named customer, not Guest, and the account page
|
||||
// can find their plan via stripe_customer_id). Non-blocking - if it fails, the
|
||||
// webhook still links them server-side after payment_intent.succeeded fires.
|
||||
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 })
|
||||
});
|
||||
} catch(e) { /* non-blocking */ }
|
||||
}
|
||||
|
||||
var result = await stripe.confirmPayment({
|
||||
elements: elements,
|
||||
confirmParams: {
|
||||
return_url: window.location.origin + '/marketplace/success',
|
||||
return_url: window.location.origin + '/account?welcome=1',
|
||||
payment_method_data: {
|
||||
billing_details: { name: name, email: email }
|
||||
},
|
||||
|
||||
+61
-50
@@ -288,54 +288,7 @@ body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;bac
|
||||
<a href=\"https://neurontechnologies.ai\" class=\"cta-btn\">Try Neuron ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var CARD_ID = '" + id + "';
|
||||
var CARD_URL = '" + card_url + "';
|
||||
var voted = JSON.parse(localStorage.getItem('neuron_voted') || '{}');
|
||||
var upBtn = document.getElementById('vote-up');
|
||||
var downBtn = document.getElementById('vote-down');
|
||||
var scoreEl = document.getElementById('vote-score');
|
||||
|
||||
fetch('/api/vote-count/' + CARD_ID).then(function(r){return r.json();}).then(function(d){
|
||||
if (scoreEl) scoreEl.textContent = (d.score || 0);
|
||||
}).catch(function(){});
|
||||
|
||||
if (voted[CARD_ID] === 'up') { upBtn.classList.add('voted-up'); downBtn.classList.add('voted-down'); }
|
||||
else if (voted[CARD_ID] === 'down') { downBtn.classList.add('voted-down'); upBtn.classList.add('voted-up'); }
|
||||
|
||||
function doVote(dir) {
|
||||
if (voted[CARD_ID]) return;
|
||||
voted[CARD_ID] = dir;
|
||||
localStorage.setItem('neuron_voted', JSON.stringify(voted));
|
||||
if (dir === 'up') { upBtn.classList.add('voted-up'); downBtn.classList.add('voted-down'); }
|
||||
else { downBtn.classList.add('voted-down'); upBtn.classList.add('voted-up'); }
|
||||
var prev = parseInt(scoreEl.textContent) || 0;
|
||||
scoreEl.textContent = prev + (dir === 'up' ? 1 : -1);
|
||||
fetch('/api/vote', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:CARD_ID,direction:dir})});
|
||||
}
|
||||
upBtn.onclick = function() { doVote('up'); };
|
||||
downBtn.onclick = function() { doVote('down'); };
|
||||
|
||||
window.copyForPlatform = function(platform, btn) {
|
||||
var label = platform === 'tiktok' ? 'Copied — paste into TikTok' : 'Copied — paste into Snapchat';
|
||||
navigator.clipboard.writeText(CARD_URL).then(function() {
|
||||
var orig = btn.textContent.trim();
|
||||
btn.classList.add('copied');
|
||||
var svgEl = btn.querySelector('svg');
|
||||
var svgHtml = svgEl ? svgEl.outerHTML : '';
|
||||
btn.innerHTML = svgHtml + ' ' + label;
|
||||
setTimeout(function() {
|
||||
btn.classList.remove('copied');
|
||||
btn.innerHTML = svgHtml + ' ' + (platform === 'tiktok' ? 'TikTok' : 'Snapchat');
|
||||
}, 2500);
|
||||
}).catch(function() {
|
||||
// fallback: prompt
|
||||
window.prompt('Copy this link:', CARD_URL);
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.id=\"" + id + "\";window.NEURON_CFG.card_url=\"" + card_url + "\";</script><script src=\"/assets/js/94727a87c328.js\" defer></script>
|
||||
</body>
|
||||
</html>"
|
||||
}
|
||||
@@ -472,6 +425,61 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
return response
|
||||
}
|
||||
|
||||
// ── Link Stripe Customer to PaymentIntent + Supabase waitlist row ────────
|
||||
// Called from checkout.js right before stripe.confirmPayment. Three jobs:
|
||||
// 1. Find or create a Stripe Customer for this email (so they aren't a Guest)
|
||||
// 2. Attach the Customer to the live PaymentIntent
|
||||
// 3. Stamp stripe_customer_id + plan on the Supabase waitlist row (upsert by email)
|
||||
// Non-fatal: if any step errors, we still return 200 so the buyer can complete
|
||||
// payment. The Stripe webhook ( /api/webhooks/stripe ) re-links server-side on
|
||||
// payment_intent.succeeded as a backstop.
|
||||
if str_eq(path, "/api/link-customer") {
|
||||
let stripe_key: String = state_get("__stripe_secret_key__")
|
||||
if str_eq(stripe_key, "") {
|
||||
return "{\"linked\":false,\"error\":\"stripe_not_configured\"}"
|
||||
}
|
||||
let lc_pi_id: String = json_get_string(body, "pi_id")
|
||||
let lc_email: String = json_get_string(body, "email")
|
||||
let lc_name: String = json_get_string(body, "name")
|
||||
let lc_plan: String = json_get_string(body, "plan")
|
||||
if str_eq(lc_pi_id, "") || str_eq(lc_email, "") {
|
||||
return "{\"linked\":false,\"error\":\"missing_pi_or_email\"}"
|
||||
}
|
||||
let lc_auth: String = "Bearer " + stripe_key
|
||||
// URL-encode email so + and @ survive the form-body
|
||||
let lc_email_enc: String = str_replace(str_replace(lc_email, "@", "%40"), "+", "%2B")
|
||||
let lc_name_enc: String = str_replace(lc_name, " ", "%20")
|
||||
|
||||
// 1. Search existing customers by email
|
||||
let lc_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + lc_email_enc + "%22&limit=1"
|
||||
let lc_search: String = http_get_auth(lc_search_url, lc_auth)
|
||||
let lc_cus_id: String = json_get_string(lc_search, "id")
|
||||
|
||||
// 2. If none, create one
|
||||
if str_eq(lc_cus_id, "") {
|
||||
let lc_create_body: String = "email=" + lc_email_enc + "&name=" + lc_name_enc + "&metadata[plan]=" + lc_plan
|
||||
let lc_create: String = http_post_form_auth("https://api.stripe.com/v1/customers", lc_create_body, lc_auth)
|
||||
let lc_cus_id = json_get_string(lc_create, "id")
|
||||
}
|
||||
|
||||
// 3. Attach customer + receipt_email to the PaymentIntent
|
||||
if !str_eq(lc_cus_id, "") {
|
||||
let lc_attach_body: String = "customer=" + lc_cus_id + "&receipt_email=" + lc_email_enc
|
||||
let lc_attach_url: String = "https://api.stripe.com/v1/payment_intents/" + lc_pi_id
|
||||
let lc_attach: String = http_post_form_auth(lc_attach_url, lc_attach_body, lc_auth)
|
||||
}
|
||||
|
||||
// 4. Upsert the Supabase waitlist row so /account can find this purchase by email
|
||||
let sb_url: String = state_get("__supabase_project_url__")
|
||||
let sb_key: String = state_get("__supabase_service_key__")
|
||||
if !str_eq(sb_url, "") && !str_eq(sb_key, "") && !str_eq(lc_cus_id, "") {
|
||||
let lc_row: String = "{\"email\":\"" + lc_email + "\",\"name\":\"" + lc_name + "\",\"stripe_customer_id\":\"" + lc_cus_id + "\",\"plan\":\"" + lc_plan + "\"}"
|
||||
let _wl_resp: String = supabase_insert(sb_url, sb_key, "waitlist?on_conflict=email", lc_row)
|
||||
}
|
||||
|
||||
return "{\"linked\":true,\"customer_id\":\"" + lc_cus_id + "\"}"
|
||||
}
|
||||
|
||||
// ── Health check ──────────────────────────────────────────────────────────
|
||||
if str_eq(path, "/api/health") {
|
||||
return "{\"status\":\"ok\",\"service\":\"neuron-web\"}"
|
||||
@@ -921,7 +929,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
// ── Checkout success ──────────────────────────────────────────────────────
|
||||
if str_eq(path, "/marketplace/success") {
|
||||
// Stripe appends ?payment_intent=...&payment_intent_client_secret=...&redirect_status=succeeded
|
||||
// to the return_url. Use prefix match so the query string does not 404.
|
||||
if str_starts_with(path, "/marketplace/success") {
|
||||
let badge_html: String = founding_badge(get_sold())
|
||||
let badge_css: String = founding_badge_css()
|
||||
return page_open() + badge_css + "
|
||||
@@ -941,7 +951,8 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
// ── Account dashboard ─────────────────────────────────────────────────────
|
||||
if str_eq(path, "/account") {
|
||||
// Use prefix match so OAuth/email-confirmation redirects with query strings still hit.
|
||||
if str_starts_with(path, "/account") {
|
||||
let sb_url: String = state_get("__supabase_project_url__")
|
||||
let sb_anon: String = state_get("__supabase_anon_key__")
|
||||
return account_page(sb_url, sb_anon)
|
||||
|
||||
Reference in New Issue
Block a user