implement free plan age verification via Stripe SetupIntent; personalize soul demo greeting with user name and timezone
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m49s

This commit is contained in:
2026-05-11 02:03:39 -05:00
parent ac2d00d653
commit c966f2b455
7 changed files with 167 additions and 74 deletions
+5 -15
View File
@@ -79,7 +79,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let plan_desc: String = if is_founding {
"Pay once. Neuron inference when it launches - priced below the major APIs. No subscription, ever."
} else { if is_free {
"Start building your memory. No card required."
"Start building your memory. A card verifies you're 18+. You won't be charged."
} else {
"Full access. Bring your own API keys or use Neuron Inference when it launches - Q3 2026."
} }
@@ -142,7 +142,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let auth_heading: String = if is_free {
el_p("class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\"", "Create your account.")
+ el_p("class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\"", "No card required. Your account is free, forever.")
+ el_p("class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\"", "Create your account. We'll ask for a card to verify your age - you won't be charged.")
} else {
el_p("class=\"label\" style=\"margin-bottom: 1.25rem;\"", "Sign in (optional)")
+ el_p("class=\"checkout-auth-hint\"", "Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.")
@@ -201,15 +201,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
// Free-tier success panel
let free_success: String = if is_free {
el_div(
"id=\"free-success\" style=\"display:none; text-align:center; padding: 2.5rem 1rem;\"",
el_div("style=\"font-size:2.5rem; margin-bottom:1.25rem;\"", "✓")
+ el_p("class=\"label\" style=\"margin-bottom:.75rem; color:var(--navy);\"", "You're in.")
+ el_p("class=\"checkout-auth-hint\" style=\"margin-bottom:2rem;\"", "Your free account is ready. Download Neuron to get started.")
+ el_a("/marketplace", "class=\"checkout-submit\" style=\"display:inline-block; text-decoration:none; padding:.875rem 2rem;\"", "Go to your account →")
)
} else { "" }
let free_success: String = ""
// Payment section
@@ -281,7 +273,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
+ "<path d=\"M4 7l1.5 1.5L8.5 5\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>"
+ "</svg>"
let submit_label: String = if is_free { "Reserve free tier &#8594;" } else { "Complete purchase &#8594;" }
let submit_label: String = if is_free { "Verify age &amp; get started &#8594;" } else { "Complete purchase &#8594;" }
let payment_form: String = el_form(
"id=\"payment-form\" autocomplete=\"on\"",
@@ -347,9 +339,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let cfg_js: String = "window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan=\"" + plan + "\";window.NEURON_CFG.pub_key=\"" + pub_key + "\";"
let cfg_script: String = el_script_inline(cfg_js)
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
let free_init_script: String = if is_free {
el_script_inline("document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});")
} else { "" }
let free_init_script: String = ""
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
}
+68 -9
View File
@@ -14,6 +14,10 @@ fn main() -> Void {
var turnstileVerified = false;
var isOpen = false;
var MAX = 10;
var _userName = '';
var _userTimezone = (typeof Intl !== 'undefined' && Intl.DateTimeFormat)
? Intl.DateTimeFormat().resolvedOptions().timeZone
: '';
// ── Supabase auth state ───────────────────────────────────────────────────
var supabaseClient = null;
@@ -45,6 +49,11 @@ fn main() -> Void {
}
function _onWidgetAuthenticated() {
// Capture user name for personalized greeting
if (_supabaseSession && _supabaseSession.user) {
var _meta = _supabaseSession.user.user_metadata || {};
_userName = _meta.full_name || _meta.name || _supabaseSession.user.email || '';
}
var authPane = document.getElementById('neuron-demo-auth');
var gate = document.getElementById('neuron-demo-gate');
var msgs = document.getElementById('neuron-demo-messages');
@@ -60,10 +69,8 @@ fn main() -> Void {
if (session && session.messages && session.messages.length > 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
} else if (!session.greeted) {
addMsg('ai', 'Hey. What is on your mind?', true);
session.greeted = true;
saveSession(session);
}
_sendIntroGreeting();
}
}
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
@@ -247,10 +254,61 @@ fn main() -> Void {
}
}
function _timeOfDay() {
var h = new Date().getHours();
if (h < 12) return 'morning';
if (h < 17) return 'afternoon';
if (h < 21) return 'evening';
return 'night';
}
function _sendIntroGreeting() {
if (session.greeted) return;
session.greeted = true;
saveSession(session);
var accessToken = (_supabaseSession && _supabaseSession.access_token) ? _supabaseSession.access_token : '';
fetch('/api/demo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: '__intro_phase1__',
history: [],
cf_token: '',
uid: session.uid || '',
access_token: accessToken,
user_name: _userName,
user_timezone: _userTimezone,
time_of_day: _timeOfDay(),
is_return: session.count > 0 ? 'true' : 'false',
activated_nodes: [],
engram_node_count: 0,
questions_remaining: MAX,
is_last_question: false
})
})
.then(function(r) { return r.json(); })
.then(function(d) {
var reply = d.response || d.reply || d.message || '';
if (reply) {
addMsg('ai', reply, true);
session.messages = session.messages || [];
session.messages.push({ role: 'ai', text: reply });
saveSession(session);
} else {
addMsg('ai', 'Hey. What\'s on your mind?', true);
}
})
.catch(function() {
addMsg('ai', 'Hey. What\'s on your mind?', true);
});
}
window.neuronDemoReset = function() {
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
clearSession();
session = { messages: [], count: 0, context: '' };
session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
saveSession(session);
msgCount = 0;
var msgs = document.getElementById('neuron-demo-messages');
if (msgs) msgs.innerHTML = '';
@@ -258,7 +316,7 @@ fn main() -> Void {
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
var btn = document.getElementById('neuron-demo-send');
if (btn) btn.disabled = false;
addMsg('ai', 'Hey. What is on your mind?', true);
_sendIntroGreeting();
};
window.neuronDemoToggle = function() {
@@ -277,9 +335,7 @@ fn main() -> Void {
if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; }
}
} else if (!session.greeted) {
addMsg('ai', 'Hey. What is on your mind?', true);
session.greeted = true;
saveSession(session);
_sendIntroGreeting();
}
}
var input = document.getElementById('neuron-demo-text');
@@ -315,8 +371,8 @@ fn main() -> Void {
if (gate) gate.style.display = 'none';
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
addMsg('ai', 'Hey. What is on your mind?', true);
updateCountdown();
_sendIntroGreeting();
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
@@ -454,6 +510,9 @@ fn main() -> Void {
cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '',
uid: session.uid || '',
access_token: accessToken,
user_name: _userName,
user_timezone: _userTimezone,
time_of_day: _timeOfDay(),
activated_nodes: activated_nodes,
engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0,
questions_remaining: questionsRemaining,
+5 -14
View File
@@ -36,15 +36,8 @@ fn main() -> Void {
if (user && user.id) { window._neuronSupaId = user.id; }
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (isFree) {
// Free plan: show the success panel (user is signed in or just signed up)
var freeSuccess = document.getElementById('free-success');
if (freeSuccess) freeSuccess.style.display = '';
} else {
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
}
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
if (user) {
var badge = document.getElementById('auth-badge');
@@ -65,11 +58,9 @@ fn main() -> Void {
if (emailEl) emailEl.value = user.email;
}
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);
}
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() {
+28 -5
View File
@@ -666,10 +666,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
let timing: String = json_get_string(body, "timing")
if str_eq(timing, "") { let timing = "now" }
// Free tier: no card required. Return immediately no Stripe interaction.
if str_eq(plan, "free") {
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") {
let current_sold: Int = get_sold()
@@ -701,6 +697,25 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
}
// Free tier: creates a SetupIntent for age verification (18+ requirement).
// No charge but the user must provide a valid payment method.
if str_eq(plan, "free") {
let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification"
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
let free_si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
free_si_body,
auth_header)
if str_starts_with(free_si_resp, "{") {
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return free_si_resp
}
// 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") {
@@ -1390,6 +1405,14 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
let qrem_safe: String = if str_eq(qrem_str, "") { "10" } else { qrem_str }
let is_last_str: String = json_get(body, "is_last_question")
let is_last_safe: String = if str_eq(is_last_str, "true") { "true" } else { "false" }
let user_name_raw: String = json_get_string(body, "user_name")
let user_tz_raw: String = json_get_string(body, "user_timezone")
let tod_raw: String = json_get_string(body, "time_of_day")
let is_return_raw: String = json_get_string(body, "is_return")
let user_name_safe: String = str_replace(str_replace(user_name_raw, "\\", "\\\\"), "\"", "\\\"")
let user_tz_safe: String = str_replace(str_replace(user_tz_raw, "\\", "\\\\"), "\"", "\\\"")
let tod_safe: String = str_replace(str_replace(tod_raw, "\\", "\\\\"), "\"", "\\\"")
let is_return_safe: String = if str_eq(is_return_raw, "true") { "true" } else { "false" }
// Look up the configured chat model from public.neuron_config
// (Phase 1 runtime config store). 60s TTL caching, falls back
// to the hardcoded default on Supabase miss / error.
@@ -1398,7 +1421,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
// Build inner content with history and engram context for thread context.
// soul-demo unwraps payload from the dharma envelope, then reads
// model with json_get(body, "model") - so this propagates end to end.
let inner: String = "{\"event_type\":\"chat\",\"payload\":{\"message\":\"" + msg_safe + "\",\"history\":" + hist_safe + ",\"an\":" + an_safe + ",\"ec\":" + ec_safe + ",\"questions_remaining\":" + qrem_safe + ",\"is_last_question\":" + is_last_safe + ",\"model\":\"" + model_safe + "\"}}"
let inner: String = "{\"event_type\":\"chat\",\"payload\":{\"message\":\"" + msg_safe + "\",\"history\":" + hist_safe + ",\"an\":" + an_safe + ",\"ec\":" + ec_safe + ",\"questions_remaining\":" + qrem_safe + ",\"is_last_turn\":" + is_last_safe + ",\"model\":\"" + model_safe + "\",\"user_name\":\"" + user_name_safe + "\",\"user_timezone\":\"" + user_tz_safe + "\",\"time_of_day\":\"" + tod_safe + "\",\"is_return\":\"" + is_return_safe + "\"}}"
// Escape inner for the outer content field
let inner_safe: String = str_replace(str_replace(inner, "\\", "\\\\"), "\"", "\\\"")
// Build dharma envelope with per-user channel