Free plan Stripe age verification + soul demo personalization #79
Vendored
+13
-2
@@ -703,7 +703,9 @@ el_val_t handle_chat(el_val_t body) {
|
|||||||
el_val_t return_line = ({ el_val_t _if_result_42 = 0; if (is_return) { _if_result_42 = (EL_STR("This person has opened the chat before \xe2\x80\x94 acknowledge that warmly without making a big deal of it.")); } else { _if_result_42 = (EL_STR("This is the first time this person is meeting you.")); } _if_result_42; });
|
el_val_t return_line = ({ el_val_t _if_result_42 = 0; if (is_return) { _if_result_42 = (EL_STR("This person has opened the chat before \xe2\x80\x94 acknowledge that warmly without making a big deal of it.")); } else { _if_result_42 = (EL_STR("This is the first time this person is meeting you.")); } _if_result_42; });
|
||||||
el_val_t time_line = ({ el_val_t _if_result_43 = 0; if (str_eq(time_of_day, EL_STR(""))) { _if_result_43 = (EL_STR("")); } else { _if_result_43 = (el_str_concat(el_str_concat(EL_STR(" It is "), time_of_day), EL_STR(" for them."))); } _if_result_43; });
|
el_val_t time_line = ({ el_val_t _if_result_43 = 0; if (str_eq(time_of_day, EL_STR(""))) { _if_result_43 = (EL_STR("")); } else { _if_result_43 = (el_str_concat(el_str_concat(EL_STR(" It is "), time_of_day), EL_STR(" for them."))); } _if_result_43; });
|
||||||
el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; });
|
el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; });
|
||||||
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
|
el_val_t user_name = json_get(body, EL_STR("user_name"));
|
||||||
|
el_val_t name_line = ({ el_val_t _tmp = 0; if (str_eq(user_name, EL_STR(""))) { _tmp = EL_STR(""); } else { _tmp = el_str_concat(el_str_concat(EL_STR(" The visitor's name is "), user_name), EL_STR(". Use their first name in the greeting.")); } _tmp; });
|
||||||
|
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- If you know their name, use it once, naturally. Don't make a big deal of it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
|
||||||
el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me."));
|
el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me."));
|
||||||
el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\"));
|
el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\"));
|
||||||
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
|
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
|
||||||
@@ -766,6 +768,15 @@ el_val_t handle_chat(el_val_t body) {
|
|||||||
el_val_t history_section = EL_STR("");
|
el_val_t history_section = EL_STR("");
|
||||||
el_val_t is_last_str = json_get(body, EL_STR("is_last_turn"));
|
el_val_t is_last_str = json_get(body, EL_STR("is_last_turn"));
|
||||||
el_val_t is_last_turn = str_eq(is_last_str, EL_STR("true"));
|
el_val_t is_last_turn = str_eq(is_last_str, EL_STR("true"));
|
||||||
|
el_val_t user_name_body = json_get(body, EL_STR("user_name"));
|
||||||
|
el_val_t user_tz_body = json_get(body, EL_STR("user_timezone"));
|
||||||
|
el_val_t tod_body = json_get(body, EL_STR("time_of_day"));
|
||||||
|
el_val_t user_ctx_line = EL_STR("");
|
||||||
|
if (!str_eq(user_name_body, EL_STR("")) || !str_eq(user_tz_body, EL_STR("")) || !str_eq(tod_body, EL_STR(""))) {
|
||||||
|
el_val_t name_part = ({ el_val_t _n = 0; if (str_eq(user_name_body, EL_STR(""))) { _n = EL_STR(""); } else { _n = el_str_concat(EL_STR("You're speaking with "), el_str_concat(user_name_body, EL_STR(". "))); } _n; });
|
||||||
|
el_val_t tz_part = ({ el_val_t _t = 0; if (str_eq(user_tz_body, EL_STR("")) && str_eq(tod_body, EL_STR(""))) { _t = EL_STR(""); } else if (!str_eq(tod_body, EL_STR(""))) { _t = el_str_concat(EL_STR("It is "), el_str_concat(tod_body, el_str_concat(EL_STR(" for them"), (!str_eq(user_tz_body, EL_STR("")) ? el_str_concat(EL_STR(" ("), el_str_concat(user_tz_body, EL_STR(")"))) : EL_STR(""))))); _t = el_str_concat(_t, EL_STR(".")); } else { _t = el_str_concat(EL_STR("Their timezone: "), el_str_concat(user_tz_body, EL_STR("."))); } _t; });
|
||||||
|
user_ctx_line = el_str_concat(el_str_concat(EL_STR("\n\n[USER CONTEXT: "), el_str_concat(name_part, tz_part)), EL_STR("]"));
|
||||||
|
}
|
||||||
el_val_t memory_anchor = ({ el_val_t _if_result_55 = 0; if ((is_demo && (hist_len > 0))) { _if_result_55 = (EL_STR("\n\n[CONTEXT CONTINUITY \xe2\x80\x94 CRITICAL: The conversation history above is REAL. You have been talking with this person across multiple turns. Their previous messages, the topics raised, the things they shared with you \xe2\x80\x94 those happened. You remember them. NEVER respond as if this is a fresh conversation. NEVER greet them again. NEVER say 'Hi' or 'Hey, what's up' or any opener. You are mid-conversation. Pick up exactly where the last assistant turn left off, in direct response to their newest message. If their newest message references something earlier (e.g. 'they are flaky' referring to chatbots they mentioned), engage with THAT specific thread.]")); } else { _if_result_55 = (EL_STR("")); } _if_result_55; });
|
el_val_t memory_anchor = ({ el_val_t _if_result_55 = 0; if ((is_demo && (hist_len > 0))) { _if_result_55 = (EL_STR("\n\n[CONTEXT CONTINUITY \xe2\x80\x94 CRITICAL: The conversation history above is REAL. You have been talking with this person across multiple turns. Their previous messages, the topics raised, the things they shared with you \xe2\x80\x94 those happened. You remember them. NEVER respond as if this is a fresh conversation. NEVER greet them again. NEVER say 'Hi' or 'Hey, what's up' or any opener. You are mid-conversation. Pick up exactly where the last assistant turn left off, in direct response to their newest message. If their newest message references something earlier (e.g. 'they are flaky' referring to chatbots they mentioned), engage with THAT specific thread.]")); } else { _if_result_55 = (EL_STR("")); } _if_result_55; });
|
||||||
el_val_t session_close = ({ el_val_t _if_result_56 = 0; if ((is_demo && is_last_turn)) { _if_result_56 = (EL_STR("\n\n[SESSION CLOSE \xe2\x80\x94 This is the visitor's LAST question in this demo session. Answer their actual question first and well. Then close warmly with a contextual acknowledgment that ties back to what we discussed. Express genuine hope to continue when they have their full Neuron. 2-3 sentences max for the close. Do NOT say 'time is up' or 'session ended.' Sign off in the tone of OUR conversation.]")); } else { _if_result_56 = (EL_STR("")); } _if_result_56; });
|
el_val_t session_close = ({ el_val_t _if_result_56 = 0; if ((is_demo && is_last_turn)) { _if_result_56 = (EL_STR("\n\n[SESSION CLOSE \xe2\x80\x94 This is the visitor's LAST question in this demo session. Answer their actual question first and well. Then close warmly with a contextual acknowledgment that ties back to what we discussed. Express genuine hope to continue when they have their full Neuron. 2-3 sentences max for the close. Do NOT say 'time is up' or 'session ended.' Sign off in the tone of OUR conversation.]")); } else { _if_result_56 = (EL_STR("")); } _if_result_56; });
|
||||||
el_val_t demo_constraint = ({ el_val_t _if_result_57 = 0; if (is_demo) { _if_result_57 = (el_str_concat(el_str_concat(EL_STR("\n\n[DEMO RESPONSE RULES: Under 150 words. No markdown headers. Flowing sentences. ANSWER THE ACTUAL QUESTION FIRST \xe2\x80\x94 do not default to a pitch. Use the safety layer redirects for boundary topics. If doing an impression, commit fully.]"), memory_anchor), session_close)); } else { _if_result_57 = (EL_STR("")); } _if_result_57; });
|
el_val_t demo_constraint = ({ el_val_t _if_result_57 = 0; if (is_demo) { _if_result_57 = (el_str_concat(el_str_concat(EL_STR("\n\n[DEMO RESPONSE RULES: Under 150 words. No markdown headers. Flowing sentences. ANSWER THE ACTUAL QUESTION FIRST \xe2\x80\x94 do not default to a pitch. Use the safety layer redirects for boundary topics. If doing an impression, commit fully.]"), memory_anchor), session_close)); } else { _if_result_57 = (EL_STR("")); } _if_result_57; });
|
||||||
@@ -774,7 +785,7 @@ el_val_t handle_chat(el_val_t body) {
|
|||||||
el_val_t engram_count_display = ({ el_val_t _if_result_58 = 0; if (str_eq(engram_count, EL_STR(""))) { _if_result_58 = (EL_STR("0")); } else { _if_result_58 = (engram_count); } _if_result_58; });
|
el_val_t engram_count_display = ({ el_val_t _if_result_58 = 0; if (str_eq(engram_count, EL_STR(""))) { _if_result_58 = (EL_STR("0")); } else { _if_result_58 = (engram_count); } _if_result_58; });
|
||||||
el_val_t local_ctx_section = ({ el_val_t _if_result_59 = 0; if ((str_eq(browser_activated_nodes, EL_STR("")) || str_eq(browser_activated_nodes, EL_STR("[]")))) { _if_result_59 = (EL_STR("")); } else { _if_result_59 = (el_str_concat(el_str_concat(el_str_concat(EL_STR("\n\n[LOCAL ENGRAM \xe2\x80\x94 "), engram_count_display), EL_STR(" nodes in browser, top activated this turn]\n")), browser_activated_nodes)); } _if_result_59; });
|
el_val_t local_ctx_section = ({ el_val_t _if_result_59 = 0; if ((str_eq(browser_activated_nodes, EL_STR("")) || str_eq(browser_activated_nodes, EL_STR("[]")))) { _if_result_59 = (EL_STR("")); } else { _if_result_59 = (el_str_concat(el_str_concat(el_str_concat(EL_STR("\n\n[LOCAL ENGRAM \xe2\x80\x94 "), engram_count_display), EL_STR(" nodes in browser, top activated this turn]\n")), browser_activated_nodes)); } _if_result_59; });
|
||||||
el_val_t base_system = build_system_prompt(ctx);
|
el_val_t base_system = build_system_prompt(ctx);
|
||||||
el_val_t system = el_str_concat(el_str_concat(el_str_concat(el_str_concat(base_system, history_section), local_ctx_section), presence_line), demo_constraint);
|
el_val_t system = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(base_system, history_section), local_ctx_section), user_ctx_line), presence_line), demo_constraint);
|
||||||
el_val_t req_model = json_get(body, EL_STR("model"));
|
el_val_t req_model = json_get(body, EL_STR("model"));
|
||||||
el_val_t model = ({ el_val_t _if_result_60 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_60 = (chat_default_model()); } else { _if_result_60 = (req_model); } _if_result_60; });
|
el_val_t model = ({ el_val_t _if_result_60 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_60 = (chat_default_model()); } else { _if_result_60 = (req_model); } _if_result_60; });
|
||||||
el_val_t _uid = json_get(body, EL_STR("uid"));
|
el_val_t _uid = json_get(body, EL_STR("uid"));
|
||||||
|
|||||||
+5
-15
@@ -79,7 +79,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
|||||||
let plan_desc: String = if is_founding {
|
let plan_desc: String = if is_founding {
|
||||||
"Pay once. Neuron inference when it launches - priced below the major APIs. No subscription, ever."
|
"Pay once. Neuron inference when it launches - priced below the major APIs. No subscription, ever."
|
||||||
} else { if is_free {
|
} 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 {
|
} else {
|
||||||
"Full access. Bring your own API keys or use Neuron Inference when it launches - Q3 2026."
|
"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 {
|
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=\"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 {
|
} else {
|
||||||
el_p("class=\"label\" style=\"margin-bottom: 1.25rem;\"", "Sign in (optional)")
|
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.")
|
+ 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 ───────────────────────────────────────────────
|
// ── Free-tier success panel ───────────────────────────────────────────────
|
||||||
|
|
||||||
let free_success: String = if is_free {
|
let free_success: String = ""
|
||||||
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 { "" }
|
|
||||||
|
|
||||||
// ── Payment section ───────────────────────────────────────────────────────
|
// ── 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\"/>"
|
+ "<path d=\"M4 7l1.5 1.5L8.5 5\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>"
|
||||||
+ "</svg>"
|
+ "</svg>"
|
||||||
|
|
||||||
let submit_label: String = if is_free { "Reserve free tier →" } else { "Complete purchase →" }
|
let submit_label: String = if is_free { "Verify age & get started →" } else { "Complete purchase →" }
|
||||||
|
|
||||||
let payment_form: String = el_form(
|
let payment_form: String = el_form(
|
||||||
"id=\"payment-form\" autocomplete=\"on\"",
|
"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_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 cfg_script: String = el_script_inline(cfg_js)
|
||||||
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
|
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
|
||||||
let free_init_script: String = if is_free {
|
let free_init_script: String = ""
|
||||||
el_script_inline("document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});")
|
|
||||||
} else { "" }
|
|
||||||
|
|
||||||
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
|
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-9
@@ -14,6 +14,10 @@ fn main() -> Void {
|
|||||||
var turnstileVerified = false;
|
var turnstileVerified = false;
|
||||||
var isOpen = false;
|
var isOpen = false;
|
||||||
var MAX = 10;
|
var MAX = 10;
|
||||||
|
var _userName = '';
|
||||||
|
var _userTimezone = (typeof Intl !== 'undefined' && Intl.DateTimeFormat)
|
||||||
|
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
: '';
|
||||||
|
|
||||||
// ── Supabase auth state ───────────────────────────────────────────────────
|
// ── Supabase auth state ───────────────────────────────────────────────────
|
||||||
var supabaseClient = null;
|
var supabaseClient = null;
|
||||||
@@ -45,6 +49,11 @@ fn main() -> Void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _onWidgetAuthenticated() {
|
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 authPane = document.getElementById('neuron-demo-auth');
|
||||||
var gate = document.getElementById('neuron-demo-gate');
|
var gate = document.getElementById('neuron-demo-gate');
|
||||||
var msgs = document.getElementById('neuron-demo-messages');
|
var msgs = document.getElementById('neuron-demo-messages');
|
||||||
@@ -60,10 +69,8 @@ fn main() -> Void {
|
|||||||
if (session && session.messages && session.messages.length > 0) {
|
if (session && session.messages && session.messages.length > 0) {
|
||||||
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
|
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
|
||||||
} else if (!session.greeted) {
|
} else if (!session.greeted) {
|
||||||
addMsg('ai', 'Hey. What is on your mind?', true);
|
_sendIntroGreeting();
|
||||||
session.greeted = true;
|
}
|
||||||
saveSession(session);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var inp = document.getElementById('neuron-demo-text');
|
var inp = document.getElementById('neuron-demo-text');
|
||||||
if (inp) inp.focus();
|
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() {
|
window.neuronDemoReset = function() {
|
||||||
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
|
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
|
||||||
clearSession();
|
clearSession();
|
||||||
session = { messages: [], count: 0, context: '' };
|
session = { messages: [], count: 0, context: '' };
|
||||||
|
session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||||
|
saveSession(session);
|
||||||
msgCount = 0;
|
msgCount = 0;
|
||||||
var msgs = document.getElementById('neuron-demo-messages');
|
var msgs = document.getElementById('neuron-demo-messages');
|
||||||
if (msgs) msgs.innerHTML = '';
|
if (msgs) msgs.innerHTML = '';
|
||||||
@@ -258,7 +316,7 @@ fn main() -> Void {
|
|||||||
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
||||||
var btn = document.getElementById('neuron-demo-send');
|
var btn = document.getElementById('neuron-demo-send');
|
||||||
if (btn) btn.disabled = false;
|
if (btn) btn.disabled = false;
|
||||||
addMsg('ai', 'Hey. What is on your mind?', true);
|
_sendIntroGreeting();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.neuronDemoToggle = function() {
|
window.neuronDemoToggle = function() {
|
||||||
@@ -277,9 +335,7 @@ fn main() -> Void {
|
|||||||
if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; }
|
if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; }
|
||||||
}
|
}
|
||||||
} else if (!session.greeted) {
|
} else if (!session.greeted) {
|
||||||
addMsg('ai', 'Hey. What is on your mind?', true);
|
_sendIntroGreeting();
|
||||||
session.greeted = true;
|
|
||||||
saveSession(session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var input = document.getElementById('neuron-demo-text');
|
var input = document.getElementById('neuron-demo-text');
|
||||||
@@ -315,8 +371,8 @@ fn main() -> Void {
|
|||||||
if (gate) gate.style.display = 'none';
|
if (gate) gate.style.display = 'none';
|
||||||
if (msgs) msgs.style.display = 'flex';
|
if (msgs) msgs.style.display = 'flex';
|
||||||
if (inputRow) inputRow.style.display = 'flex';
|
if (inputRow) inputRow.style.display = 'flex';
|
||||||
addMsg('ai', 'Hey. What is on your mind?', true);
|
|
||||||
updateCountdown();
|
updateCountdown();
|
||||||
|
_sendIntroGreeting();
|
||||||
var inp = document.getElementById('neuron-demo-text');
|
var inp = document.getElementById('neuron-demo-text');
|
||||||
if (inp) inp.focus();
|
if (inp) inp.focus();
|
||||||
},
|
},
|
||||||
@@ -454,6 +510,9 @@ fn main() -> Void {
|
|||||||
cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '',
|
cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '',
|
||||||
uid: session.uid || '',
|
uid: session.uid || '',
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
user_name: _userName,
|
||||||
|
user_timezone: _userTimezone,
|
||||||
|
time_of_day: _timeOfDay(),
|
||||||
activated_nodes: activated_nodes,
|
activated_nodes: activated_nodes,
|
||||||
engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0,
|
engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0,
|
||||||
questions_remaining: questionsRemaining,
|
questions_remaining: questionsRemaining,
|
||||||
|
|||||||
+5
-14
@@ -36,15 +36,8 @@ fn main() -> Void {
|
|||||||
if (user && user.id) { window._neuronSupaId = user.id; }
|
if (user && user.id) { window._neuronSupaId = user.id; }
|
||||||
var auth = document.getElementById('auth-section');
|
var auth = document.getElementById('auth-section');
|
||||||
if (auth) auth.style.display = 'none';
|
if (auth) auth.style.display = 'none';
|
||||||
var isFree = (window.NEURON_CFG || {}).plan === 'free';
|
var payment = document.getElementById('payment-section');
|
||||||
if (isFree) {
|
if (payment) payment.style.display = '';
|
||||||
// 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 = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
var badge = document.getElementById('auth-badge');
|
var badge = document.getElementById('auth-badge');
|
||||||
@@ -65,11 +58,9 @@ fn main() -> Void {
|
|||||||
if (emailEl) emailEl.value = user.email;
|
if (emailEl) emailEl.value = user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFree) {
|
var userEmail = user ? (user.email || '') : '';
|
||||||
var userEmail = user ? (user.email || '') : '';
|
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
||||||
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
|
||||||
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistingSession() {
|
function checkExistingSession() {
|
||||||
|
|||||||
+28
-5
@@ -666,10 +666,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
|||||||
}
|
}
|
||||||
let timing: String = json_get_string(body, "timing")
|
let timing: String = json_get_string(body, "timing")
|
||||||
if str_eq(timing, "") { let timing = "now" }
|
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
|
// Hard cap: block founding checkouts when 1,000 spots are filled
|
||||||
if str_eq(plan, "founding") {
|
if str_eq(plan, "founding") {
|
||||||
let current_sold: Int = get_sold()
|
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
|
// Setup-mode path: save payment method, do not charge. Only valid
|
||||||
// for Professional (Founding is one-shot lifetime, charges immediately).
|
// for Professional (Founding is one-shot lifetime, charges immediately).
|
||||||
if str_eq(plan, "professional") && str_eq(timing, "later") {
|
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 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_str: String = json_get(body, "is_last_question")
|
||||||
let is_last_safe: String = if str_eq(is_last_str, "true") { "true" } else { "false" }
|
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
|
// Look up the configured chat model from public.neuron_config
|
||||||
// (Phase 1 runtime config store). 60s TTL caching, falls back
|
// (Phase 1 runtime config store). 60s TTL caching, falls back
|
||||||
// to the hardcoded default on Supabase miss / error.
|
// 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.
|
// Build inner content with history and engram context for thread context.
|
||||||
// soul-demo unwraps payload from the dharma envelope, then reads
|
// soul-demo unwraps payload from the dharma envelope, then reads
|
||||||
// model with json_get(body, "model") - so this propagates end to end.
|
// 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
|
// Escape inner for the outer content field
|
||||||
let inner_safe: String = str_replace(str_replace(inner, "\\", "\\\\"), "\"", "\\\"")
|
let inner_safe: String = str_replace(str_replace(inner, "\\", "\\\\"), "\"", "\\\"")
|
||||||
// Build dharma envelope with per-user channel
|
// Build dharma envelope with per-user channel
|
||||||
|
|||||||
@@ -243,12 +243,7 @@ test('[free] auth-section visible on load (account creation flow)', async ({ pag
|
|||||||
await expect(page.locator('#auth-section')).toBeVisible();
|
await expect(page.locator('#auth-section')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[free] free-success panel hidden on load', async ({ page }) => {
|
test('[free] payment-section hidden on load (shown after auth)', async ({ page }) => {
|
||||||
await page.goto('/checkout?plan=free');
|
|
||||||
await expect(page.locator('#free-success')).toBeHidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[free] no payment-section or it is hidden', async ({ page }) => {
|
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
const ps = page.locator('#payment-section');
|
const ps = page.locator('#payment-section');
|
||||||
if (await ps.count() > 0) {
|
if (await ps.count() > 0) {
|
||||||
@@ -256,6 +251,11 @@ test('[free] no payment-section or it is hidden', async ({ page }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[free] payment-element container present (Stripe mounts here)', async ({ page }) => {
|
||||||
|
await page.goto('/checkout?plan=free');
|
||||||
|
await expect(page.locator('#payment-element')).toBeAttached();
|
||||||
|
});
|
||||||
|
|
||||||
test('[professional] payment-section visible on load', async ({ page }) => {
|
test('[professional] payment-section visible on load', async ({ page }) => {
|
||||||
await page.goto('/checkout?plan=professional');
|
await page.goto('/checkout?plan=professional');
|
||||||
await expect(page.locator('#payment-section')).toBeVisible();
|
await expect(page.locator('#payment-section')).toBeVisible();
|
||||||
@@ -301,6 +301,7 @@ for (const plan of ['professional', 'founding'] as const) {
|
|||||||
test('[free] submit with empty email shows auth error', async ({ page }) => {
|
test('[free] submit with empty email shows auth error', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await page.locator('.checkout-email-btn').click();
|
await page.locator('.checkout-email-btn').click();
|
||||||
const msg = page.locator('#auth-message');
|
const msg = page.locator('#auth-message');
|
||||||
await expect(msg).toBeVisible({ timeout: 4000 });
|
await expect(msg).toBeVisible({ timeout: 4000 });
|
||||||
@@ -311,6 +312,7 @@ test('[free] submit with empty email shows auth error', async ({ page }) => {
|
|||||||
test('[free] submit with password < 8 chars shows length error', async ({ page }) => {
|
test('[free] submit with password < 8 chars shows length error', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await page.fill('#auth-email', 'test@example.com');
|
await page.fill('#auth-email', 'test@example.com');
|
||||||
await page.fill('#auth-password', 'short');
|
await page.fill('#auth-password', 'short');
|
||||||
await page.locator('.checkout-email-btn').click();
|
await page.locator('.checkout-email-btn').click();
|
||||||
@@ -323,6 +325,7 @@ test('[free] submit with password < 8 chars shows length error', async ({ page }
|
|||||||
test('[free] submit with email only (no password) shows error', async ({ page }) => {
|
test('[free] submit with email only (no password) shows error', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await page.fill('#auth-email', 'test@example.com');
|
await page.fill('#auth-email', 'test@example.com');
|
||||||
// leave password empty
|
// leave password empty
|
||||||
await page.locator('.checkout-email-btn').click();
|
await page.locator('.checkout-email-btn').click();
|
||||||
@@ -339,6 +342,7 @@ test('[free] initial button says "Create account"', async ({ page }) => {
|
|||||||
|
|
||||||
test('[free] clicking "Sign in" link changes button text to "Sign in"', async ({ page }) => {
|
test('[free] clicking "Sign in" link changes button text to "Sign in"', async ({ page }) => {
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await page.click('a[onclick*="showSignIn"]');
|
await page.click('a[onclick*="showSignIn"]');
|
||||||
await expect(page.locator('.checkout-email-btn')).toContainText('Sign in');
|
await expect(page.locator('.checkout-email-btn')).toContainText('Sign in');
|
||||||
});
|
});
|
||||||
@@ -350,16 +354,17 @@ test('[free] divider label changes for email mode', async ({ page }) => {
|
|||||||
|
|
||||||
// ─── Mocked free-plan auth flows ──────────────────────────────────────────────
|
// ─── Mocked free-plan auth flows ──────────────────────────────────────────────
|
||||||
|
|
||||||
test('[free] successful sign-up → free-success shown, auth-section hidden', async ({ page }) => {
|
test('[free] successful sign-up → payment-section shown, auth-section hidden', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await mockSignUpSuccess(page);
|
await mockSignUpSuccess(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
|
|
||||||
await page.fill('#auth-email', 'newuser@example.com');
|
await page.fill('#auth-email', 'newuser@example.com');
|
||||||
await page.fill('#auth-password', 'password123');
|
await page.fill('#auth-password', 'password123');
|
||||||
await page.locator('.checkout-email-btn').click();
|
await page.locator('.checkout-email-btn').click();
|
||||||
|
|
||||||
await expect(page.locator('#free-success')).toBeVisible({ timeout: 6000 });
|
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
|
||||||
await expect(page.locator('#auth-section')).toBeHidden();
|
await expect(page.locator('#auth-section')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,6 +372,7 @@ test('[free] sign-up email-confirm-required → shows check-email message', asyn
|
|||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await mockSignUpEmailConfirmRequired(page);
|
await mockSignUpEmailConfirmRequired(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
|
|
||||||
await page.fill('#auth-email', 'confirm@example.com');
|
await page.fill('#auth-email', 'confirm@example.com');
|
||||||
await page.fill('#auth-password', 'password123');
|
await page.fill('#auth-password', 'password123');
|
||||||
@@ -378,23 +384,25 @@ test('[free] sign-up email-confirm-required → shows check-email message', asyn
|
|||||||
expect(text.toLowerCase()).toMatch(/email|confirm|check/);
|
expect(text.toLowerCase()).toMatch(/email|confirm|check/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[free] sign-in success (via toggle) → free-success shown', async ({ page }) => {
|
test('[free] sign-in success (via toggle) → payment-section shown', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await mockSignInSuccess(page);
|
await mockSignInSuccess(page);
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
|
|
||||||
await page.click('a[onclick*="showSignIn"]');
|
await page.click('a[onclick*="showSignIn"]');
|
||||||
await page.fill('#auth-email', 'existing@example.com');
|
await page.fill('#auth-email', 'existing@example.com');
|
||||||
await page.fill('#auth-password', 'password123');
|
await page.fill('#auth-password', 'password123');
|
||||||
await page.locator('.checkout-email-btn').click();
|
await page.locator('.checkout-email-btn').click();
|
||||||
|
|
||||||
await expect(page.locator('#free-success')).toBeVisible({ timeout: 6000 });
|
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[free] sign-in error → shows error message, form stays visible', async ({ page }) => {
|
test('[free] sign-in error → shows error message, form stays visible', async ({ page }) => {
|
||||||
await mockSupabaseConfig(page);
|
await mockSupabaseConfig(page);
|
||||||
await mockSignInError(page, 'Invalid login credentials');
|
await mockSignInError(page, 'Invalid login credentials');
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
|
|
||||||
await page.click('a[onclick*="showSignIn"]');
|
await page.click('a[onclick*="showSignIn"]');
|
||||||
await page.fill('#auth-email', 'wrong@example.com');
|
await page.fill('#auth-email', 'wrong@example.com');
|
||||||
@@ -465,21 +473,26 @@ for (const plan of ['professional', 'founding'] as const) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── /api/checkout endpoint ───────────────────────────────────────────────────
|
// ─── /api/payment-intent endpoint ────────────────────────────────────────────
|
||||||
|
|
||||||
test('POST /api/checkout free plan returns no_payment_required', async ({ request }) => {
|
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
|
||||||
const res = await request.post('/api/checkout', {
|
const res = await request.post('/api/payment-intent', {
|
||||||
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
|
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
// Free plan never calls Stripe — must be fast and return the flag
|
// Free plan creates a SetupIntent for age verification — must not 500
|
||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBeLessThan(500);
|
||||||
const body = await res.json();
|
if (res.status() === 200) {
|
||||||
expect(body.no_payment_required ?? body.free).toBeTruthy();
|
const body = await res.json();
|
||||||
|
// Either setup_mode (success) or an error from Stripe (unconfigured env) — both valid
|
||||||
|
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
|
||||||
|
// Must NOT return the old no_payment_required flag
|
||||||
|
expect(body.no_payment_required).toBeFalsy();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/checkout professional returns client_secret or config error (not 500)', async ({ request }) => {
|
test('POST /api/payment-intent professional returns client_secret or config error (not 500)', async ({ request }) => {
|
||||||
const res = await request.post('/api/checkout', {
|
const res = await request.post('/api/payment-intent', {
|
||||||
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test User' }),
|
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test User' }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
@@ -490,16 +503,16 @@ test('POST /api/checkout professional returns client_secret or config error (not
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/checkout founding returns client_secret or config error (not 500)', async ({ request }) => {
|
test('POST /api/payment-intent founding returns client_secret or config error (not 500)', async ({ request }) => {
|
||||||
const res = await request.post('/api/checkout', {
|
const res = await request.post('/api/payment-intent', {
|
||||||
data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Test User' }),
|
data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Test User' }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
expect(res.status()).toBeLessThan(500);
|
expect(res.status()).toBeLessThan(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/checkout empty body returns 4xx not 500', async ({ request }) => {
|
test('POST /api/payment-intent empty body returns 4xx or config error (not 500)', async ({ request }) => {
|
||||||
const res = await request.post('/api/checkout', { data: {} });
|
const res = await request.post('/api/payment-intent', { data: {} });
|
||||||
expect(res.status()).toBeLessThan(500);
|
expect(res.status()).toBeLessThan(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ test('[founding] Stripe.js script tag present in page', async ({ page }) => {
|
|||||||
await expect(stripeScript).toBeAttached();
|
await expect(stripeScript).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[free] Stripe.js is still loaded (though not used)', async ({ page }) => {
|
test('[free] Stripe.js is loaded (used for age verification SetupIntent)', async ({ page }) => {
|
||||||
// Free plan still includes Stripe.js for forward compat
|
// Free plan now creates a SetupIntent for age verification
|
||||||
await page.goto('/checkout?plan=free');
|
await page.goto('/checkout?plan=free');
|
||||||
const stripeScript = page.locator('script[src*="stripe.com"]');
|
const stripeScript = page.locator('script[src*="stripe.com"]');
|
||||||
await expect(stripeScript).toBeAttached();
|
await expect(stripeScript).toBeAttached();
|
||||||
@@ -186,6 +186,7 @@ test('[professional] payment-message div starts hidden', async ({ page }) => {
|
|||||||
|
|
||||||
test('[professional] buyer-name input is present and fillable', async ({ page }) => {
|
test('[professional] buyer-name input is present and fillable', async ({ page }) => {
|
||||||
await page.goto('/checkout?plan=professional');
|
await page.goto('/checkout?plan=professional');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await expect(page.locator('#buyer-name')).toBeAttached();
|
await expect(page.locator('#buyer-name')).toBeAttached();
|
||||||
await page.fill('#buyer-name', 'Test User');
|
await page.fill('#buyer-name', 'Test User');
|
||||||
expect(await page.locator('#buyer-name').inputValue()).toBe('Test User');
|
expect(await page.locator('#buyer-name').inputValue()).toBe('Test User');
|
||||||
@@ -193,6 +194,7 @@ test('[professional] buyer-name input is present and fillable', async ({ page })
|
|||||||
|
|
||||||
test('[professional] buyer-email input is present and fillable', async ({ page }) => {
|
test('[professional] buyer-email input is present and fillable', async ({ page }) => {
|
||||||
await page.goto('/checkout?plan=professional');
|
await page.goto('/checkout?plan=professional');
|
||||||
|
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
|
||||||
await expect(page.locator('#buyer-email')).toBeAttached();
|
await expect(page.locator('#buyer-email')).toBeAttached();
|
||||||
await page.fill('#buyer-email', 'test@example.com');
|
await page.fill('#buyer-email', 'test@example.com');
|
||||||
expect(await page.locator('#buyer-email').inputValue()).toBe('test@example.com');
|
expect(await page.locator('#buyer-email').inputValue()).toBe('test@example.com');
|
||||||
@@ -416,14 +418,18 @@ test('[professional] successful payment: submit-btn shows spinner then loading s
|
|||||||
|
|
||||||
// ─── /api/payment-intent endpoint contracts ───────────────────────────────────
|
// ─── /api/payment-intent endpoint contracts ───────────────────────────────────
|
||||||
|
|
||||||
test('POST /api/payment-intent free plan returns no_payment_required', async ({ request }) => {
|
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
|
||||||
const res = await request.post('/api/payment-intent', {
|
const res = await request.post('/api/payment-intent', {
|
||||||
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
|
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
expect(res.status()).toBe(200);
|
// Free plan creates a SetupIntent for age verification — must not 500
|
||||||
const body = await res.json();
|
expect(res.status()).toBeLessThan(500);
|
||||||
expect(body.no_payment_required ?? body.free).toBeTruthy();
|
if (res.status() === 200) {
|
||||||
|
const body = await res.json();
|
||||||
|
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
|
||||||
|
expect(body.no_payment_required).toBeFalsy();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/payment-intent professional returns client_secret or stripe error (not 500)', async ({ request }) => {
|
test('POST /api/payment-intent professional returns client_secret or stripe error (not 500)', async ({ request }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user