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;
|
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() {
|
waitForStripe(function() {
|
||||||
stripe = Stripe(STRIPE_PK);
|
stripe = Stripe(STRIPE_PK);
|
||||||
|
|
||||||
@@ -815,10 +818,24 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
document.getElementById('payment-message').style.display = 'none';
|
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({
|
var result = await stripe.confirmPayment({
|
||||||
elements: elements,
|
elements: elements,
|
||||||
confirmParams: {
|
confirmParams: {
|
||||||
return_url: window.location.origin + '/marketplace/success',
|
return_url: window.location.origin + '/account?welcome=1',
|
||||||
payment_method_data: {
|
payment_method_data: {
|
||||||
billing_details: { name: name, email: email }
|
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>
|
<a href=\"https://neurontechnologies.ai\" class=\"cta-btn\">Try Neuron ↗</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
(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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>"
|
</html>"
|
||||||
}
|
}
|
||||||
@@ -472,6 +425,61 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
return response
|
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 ──────────────────────────────────────────────────────────
|
// ── Health check ──────────────────────────────────────────────────────────
|
||||||
if str_eq(path, "/api/health") {
|
if str_eq(path, "/api/health") {
|
||||||
return "{\"status\":\"ok\",\"service\":\"neuron-web\"}"
|
return "{\"status\":\"ok\",\"service\":\"neuron-web\"}"
|
||||||
@@ -921,7 +929,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Checkout success ──────────────────────────────────────────────────────
|
// ── 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_html: String = founding_badge(get_sold())
|
||||||
let badge_css: String = founding_badge_css()
|
let badge_css: String = founding_badge_css()
|
||||||
return page_open() + badge_css + "
|
return page_open() + badge_css + "
|
||||||
@@ -941,7 +951,8 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Account dashboard ─────────────────────────────────────────────────────
|
// ── 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_url: String = state_get("__supabase_project_url__")
|
||||||
let sb_anon: String = state_get("__supabase_anon_key__")
|
let sb_anon: String = state_get("__supabase_anon_key__")
|
||||||
return account_page(sb_url, sb_anon)
|
return account_page(sb_url, sb_anon)
|
||||||
|
|||||||
Reference in New Issue
Block a user