Add integrated checkout, child safety section, and content polish
- checkout.el: Custom Stripe Payment Element page at /checkout?plan=... - safety.el: Hard Bell and emergency routing architecture section - main.el: Wire checkout + safety, /api/payment-intent, STRIPE_PUBLISHABLE_KEY - enterprise_terms.el: Add nav bar (was missing, page had no navigation) - styles.el: Checkout buttons now link to integrated page, not hosted Stripe - El runtime: Auto-detect HTML responses, serve text/html vs application/json - footer.el: Wordmark image centered above "Built Different." tagline - environmental.el: Replace SQLite with custom on-device storage - enterprise.el: Replace SQLite with custom on-device storage - pricing.el: Local models degraded performance note near Ollama feature - about.el: "left home at fifteen", remove programming language mention
This commit is contained in:
+427
@@ -0,0 +1,427 @@
|
||||
// checkout.el — Integrated Stripe checkout page.
|
||||
//
|
||||
// Uses Stripe Payment Element (not hosted checkout) so the entire
|
||||
// purchase experience lives on neurontechnologies.ai. The page is
|
||||
// designed to match the Neuron brand — navy, clean, no Stripe chrome.
|
||||
//
|
||||
// Flow:
|
||||
// 1. GET /checkout?plan=founding|professional
|
||||
// 2. Page JS calls POST /api/payment-intent → { client_secret, publishable_key, plan }
|
||||
// 3. Stripe Payment Element mounts into #payment-element
|
||||
// 4. User fills name, email, card — submits
|
||||
// 5. stripe.confirmPayment() → redirects to /marketplace/success
|
||||
|
||||
fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
let is_founding: Bool = str_eq(plan, "founding")
|
||||
|
||||
let plan_name: String = if is_founding { "Founding Member" } else { "Professional" }
|
||||
let plan_price: String = if is_founding { "$199" } else { "$19 / month" }
|
||||
let plan_desc: String = if is_founding {
|
||||
"Pay once. Neuron inference forever. No subscription, ever."
|
||||
} else {
|
||||
"Full access. Neuron inference — cheaper than what you're paying now."
|
||||
}
|
||||
let plan_cadence: String = if is_founding { "one-time" } else { "billed monthly" }
|
||||
|
||||
let features_html: String = if is_founding {
|
||||
"<li>Neuron inference — priced below competitors, forever</li>
|
||||
<li>Everything in Professional — forever</li>
|
||||
<li>Never pay again — lifetime updates included</li>
|
||||
<li>Founding member badge in the app</li>
|
||||
<li>Private community — direct line to the team</li>
|
||||
<li>Shape the roadmap — your votes carry more weight</li>
|
||||
<li>Beta features before general release</li>
|
||||
<li>Name in the credits</li>"
|
||||
} else {
|
||||
"<li>Neuron inference — metered, priced below OpenAI and Anthropic</li>
|
||||
<li>Persistent memory — never resets</li>
|
||||
<li>Unlimited projects</li>
|
||||
<li>Full plugin marketplace</li>
|
||||
<li>IDE, Slack, and more integrations</li>
|
||||
<li>Early access to new features</li>"
|
||||
}
|
||||
|
||||
return "
|
||||
<nav id=\"nav\">
|
||||
<div class=\"nav-inner\">
|
||||
<a href=\"/\" class=\"nav-logo\" aria-label=\"Neuron home\"><img src=\"/assets/brand/neuron-wordmark-on-light.png\" srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" alt=\"Neuron\" height=\"28\"></a>
|
||||
<div class=\"nav-links\">
|
||||
<a href=\"/\" class=\"nav-link\">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main style=\"min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;\">
|
||||
<div class=\"checkout-shell\">
|
||||
|
||||
<!-- Left: order summary -->
|
||||
<div class=\"checkout-summary\">
|
||||
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Your order</p>
|
||||
|
||||
<div style=\"margin-bottom: 2rem;\">
|
||||
<p class=\"checkout-plan-name\">" + plan_name + "</p>
|
||||
<p class=\"checkout-plan-desc\">" + plan_desc + "</p>
|
||||
<div style=\"display: flex; align-items: baseline; gap: .5rem; margin-top: 1.25rem;\">
|
||||
<span class=\"checkout-price\">" + plan_price + "</span>
|
||||
<span class=\"checkout-cadence\">" + plan_cadence + "</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\"navy-line-left\" style=\"width: 3rem; margin-bottom: 1.75rem;\"></div>
|
||||
|
||||
<ul class=\"checkout-features\">
|
||||
" + features_html + "
|
||||
</ul>
|
||||
|
||||
<p class=\"checkout-guarantee\">
|
||||
Your data stays yours. Runs locally. No telemetry.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Right: payment form -->
|
||||
<div class=\"checkout-form-wrap\">
|
||||
<p class=\"label\" style=\"margin-bottom: 1.75rem;\">Payment</p>
|
||||
|
||||
<form id=\"payment-form\" autocomplete=\"on\">
|
||||
|
||||
<div class=\"checkout-field-group\">
|
||||
<div class=\"checkout-field\">
|
||||
<label for=\"buyer-name\" class=\"checkout-label\">Full name</label>
|
||||
<input id=\"buyer-name\" name=\"name\" type=\"text\" autocomplete=\"name\"
|
||||
class=\"checkout-input\" placeholder=\"Will Anderson\" required>
|
||||
</div>
|
||||
<div class=\"checkout-field\">
|
||||
<label for=\"buyer-email\" class=\"checkout-label\">Email</label>
|
||||
<input id=\"buyer-email\" name=\"email\" type=\"email\" autocomplete=\"email\"
|
||||
class=\"checkout-input\" placeholder=\"you@example.com\" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\"checkout-payment-element-wrap\">
|
||||
<div id=\"payment-element\">
|
||||
<!-- Stripe Payment Element mounts here -->
|
||||
<div class=\"checkout-element-loading\">Loading payment form…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id=\"payment-message\" class=\"checkout-message\" style=\"display:none;\"></div>
|
||||
|
||||
<button id=\"submit-btn\" class=\"checkout-submit\" type=\"submit\" disabled>
|
||||
<span id=\"submit-label\">Complete purchase →</span>
|
||||
<span id=\"submit-spinner\" style=\"display:none;\">Processing…</span>
|
||||
</button>
|
||||
|
||||
<p class=\"checkout-security\">
|
||||
<svg aria-hidden=\"true\" width=\"12\" height=\"14\" viewBox=\"0 0 12 14\" fill=\"none\">
|
||||
<path d=\"M6 0L0 2.5V7c0 3.3 2.5 6.3 6 7 3.5-.7 6-3.7 6-7V2.5L6 0z\" fill=\"currentColor\" opacity=\".35\"/>
|
||||
<path d=\"M4 7l1.5 1.5L8.5 5\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>
|
||||
</svg>
|
||||
Secured by Stripe · 256-bit TLS · PCI DSS compliant
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Stripe.js -->
|
||||
<script src=\"https://js.stripe.com/v3/\" async></script>
|
||||
|
||||
<style>
|
||||
.checkout-shell {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.checkout-shell { grid-template-columns: 1fr; gap: 3rem; }
|
||||
}
|
||||
.checkout-plan-name {
|
||||
font-family: var(--head);
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 600;
|
||||
color: var(--t1);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 .5rem;
|
||||
}
|
||||
.checkout-plan-desc {
|
||||
font-family: var(--body);
|
||||
font-weight: 300;
|
||||
font-size: .9375rem;
|
||||
color: var(--t2);
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
}
|
||||
.checkout-price {
|
||||
font-family: var(--head);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--t1);
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
}
|
||||
.checkout-cadence {
|
||||
font-family: var(--body);
|
||||
font-size: .8125rem;
|
||||
color: var(--t3);
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.checkout-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
}
|
||||
.checkout-features li {
|
||||
font-family: var(--body);
|
||||
font-weight: 300;
|
||||
font-size: .9rem;
|
||||
color: var(--t2);
|
||||
padding-left: 1.1rem;
|
||||
position: relative;
|
||||
}
|
||||
.checkout-features li::before {
|
||||
content: '–';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--navy);
|
||||
opacity: .6;
|
||||
}
|
||||
.checkout-guarantee {
|
||||
font-family: var(--body);
|
||||
font-weight: 300;
|
||||
font-size: .8125rem;
|
||||
color: var(--t3);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
.checkout-field-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@media (max-width: 480px) { .checkout-field-group { grid-template-columns: 1fr; } }
|
||||
.checkout-field { display: flex; flex-direction: column; gap: .4rem; }
|
||||
.checkout-label {
|
||||
font-family: var(--body);
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--t2);
|
||||
}
|
||||
.checkout-input {
|
||||
font-family: var(--body);
|
||||
font-size: .9375rem;
|
||||
font-weight: 300;
|
||||
color: var(--t1);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,82,160,.22);
|
||||
padding: .75rem 1rem;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.checkout-input::placeholder { color: var(--t3); }
|
||||
.checkout-input:focus { border-color: rgba(0,82,160,.6); box-shadow: 0 0 0 3px rgba(0,82,160,.08); }
|
||||
.checkout-payment-element-wrap {
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(0,82,160,.22);
|
||||
padding: 1.25rem;
|
||||
background: #fff;
|
||||
min-height: 80px;
|
||||
}
|
||||
.checkout-element-loading {
|
||||
font-family: var(--body);
|
||||
font-size: .875rem;
|
||||
color: var(--t3);
|
||||
}
|
||||
.checkout-message {
|
||||
font-family: var(--body);
|
||||
font-size: .875rem;
|
||||
color: #c0392b;
|
||||
margin-bottom: 1rem;
|
||||
padding: .75rem 1rem;
|
||||
background: rgba(192,57,43,.06);
|
||||
border-left: 2px solid rgba(192,57,43,.4);
|
||||
}
|
||||
.checkout-submit {
|
||||
width: 100%;
|
||||
padding: 1.05rem 2rem;
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
font-family: var(--body);
|
||||
font-size: .8125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background .2s, opacity .2s;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.checkout-submit:hover:not(:disabled) { background: #0078D4; }
|
||||
.checkout-submit:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.checkout-security {
|
||||
font-family: var(--body);
|
||||
font-size: .75rem;
|
||||
color: var(--t3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var PLAN = '" + plan + "';
|
||||
var STRIPE_PK = '" + pub_key + "';
|
||||
var stripe, elements;
|
||||
|
||||
// Wait for Stripe.js to load
|
||||
function waitForStripe(cb) {
|
||||
if (window.Stripe) { cb(); return; }
|
||||
setTimeout(function() { waitForStripe(cb); }, 50);
|
||||
}
|
||||
|
||||
function showMessage(msg) {
|
||||
var el = document.getElementById('payment-message');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
var btn = document.getElementById('submit-btn');
|
||||
var label = document.getElementById('submit-label');
|
||||
var spinner = document.getElementById('submit-spinner');
|
||||
btn.disabled = loading;
|
||||
label.style.display = loading ? 'none' : '';
|
||||
spinner.style.display = loading ? '' : 'none';
|
||||
}
|
||||
|
||||
// Fetch client secret from our server
|
||||
fetch('/api/payment-intent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plan: PLAN })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.client_secret) {
|
||||
showMessage('Unable to initialise payment. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
waitForStripe(function() {
|
||||
stripe = Stripe(STRIPE_PK);
|
||||
|
||||
elements = stripe.elements({
|
||||
clientSecret: data.client_secret,
|
||||
appearance: {
|
||||
theme: 'flat',
|
||||
variables: {
|
||||
colorPrimary: '#0052A0',
|
||||
colorBackground: '#ffffff',
|
||||
colorText: '#1A1A2E',
|
||||
colorDanger: '#c0392b',
|
||||
colorTextPlaceholder:'#9B9BAD',
|
||||
borderRadius: '0px',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontSizeBase: '15px',
|
||||
fontWeightNormal: '300',
|
||||
spacingUnit: '4px',
|
||||
},
|
||||
rules: {
|
||||
'.Input': {
|
||||
border: '1px solid rgba(0,82,160,.22)',
|
||||
boxShadow: 'none',
|
||||
padding: '10px 14px',
|
||||
},
|
||||
'.Input:focus': {
|
||||
border: '1px solid rgba(0,82,160,.6)',
|
||||
boxShadow: '0 0 0 3px rgba(0,82,160,.08)',
|
||||
outline: 'none',
|
||||
},
|
||||
'.Label': {
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
letterSpacing: '.06em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#6B6B7E',
|
||||
marginBottom: '6px',
|
||||
},
|
||||
'.Tab': {
|
||||
border: '1px solid rgba(0,82,160,.18)',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'.Tab--selected': {
|
||||
border: '1px solid rgba(0,82,160,.5)',
|
||||
boxShadow: '0 0 0 2px rgba(0,82,160,.12)',
|
||||
},
|
||||
'.Error': { color: '#c0392b' },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var paymentEl = elements.create('payment', {
|
||||
fields: { billingDetails: { name: 'never', email: 'never' } }
|
||||
});
|
||||
paymentEl.mount('#payment-element');
|
||||
|
||||
paymentEl.on('ready', function() {
|
||||
document.querySelector('.checkout-element-loading') &&
|
||||
document.querySelector('.checkout-element-loading').remove();
|
||||
document.getElementById('submit-btn').disabled = false;
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
showMessage('Unable to connect. Please check your connection and try again.');
|
||||
});
|
||||
|
||||
// Submit
|
||||
document.getElementById('payment-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
if (!stripe || !elements) return;
|
||||
|
||||
var name = document.getElementById('buyer-name').value.trim();
|
||||
var email = document.getElementById('buyer-email').value.trim();
|
||||
if (!name || !email) {
|
||||
showMessage('Please enter your name and email.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
document.getElementById('payment-message').style.display = 'none';
|
||||
|
||||
var result = await stripe.confirmPayment({
|
||||
elements: elements,
|
||||
confirmParams: {
|
||||
return_url: window.location.origin + '/marketplace/success',
|
||||
payment_method_data: {
|
||||
billing_details: { name: name, email: email }
|
||||
},
|
||||
receipt_email: email,
|
||||
}
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
showMessage(result.error.message || 'Payment failed. Please try again.');
|
||||
setLoading(false);
|
||||
}
|
||||
// On success, Stripe redirects to return_url automatically.
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
"
|
||||
}
|
||||
Reference in New Issue
Block a user