migrate stage build to native elc; chat restores from localStorage on return
Build pipeline - build-stage.sh replaces the old in-Dockerfile bootstrap.py path. Host pre-compiles src/*.el into dist/main.c via the canonical native elc at foundation/el/dist/platform/elc and applies the stub-decl sed before docker buildx runs. - Dockerfile.stage drops bootstrap.py + python3 from the builder stage and just runs cc on the host-supplied dist/main.c. - Pre-rendered HTML shells under /srv/landing/ are now chowned to the landing user so the El page-builder's fs_write at startup can rewrite them — without that, post-COPY edits never reach the served HTML and the served page stays as the stale build-time fallback. Chat restore - session.verified + session.verifiedAt persist through localStorage so a return visit within 24h skips the Turnstile gate and lands directly in the restored conversation. - restoreOrGreet() is the single source of truth for what shows up in the message pane after the gate clears: replays prior messages with skipSave, else drops the canned hello once and remembers it. - applyVerifiedDom() hides the gate / reveals the chat row, called both from the verified-on-load path (DOMContentLoaded if loading, else immediate) and from the Turnstile callback. - neuronDemoReset clears verified + verifiedAt so the gate returns next open. Extracted JS assets (src/assets/js/*.js + manifest.json) and the extract-js.py helper land here too — they match what the new build-stage flow produces and removes the inline <script> blobs from the served HTML.
This commit is contained in:
+2
-404
@@ -485,410 +485,8 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
.checkout-auth-badge strong { color: var(--navy); font-weight: 500; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// ── Supabase auth ─────────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
var supabase;
|
||||
<script src=\"/assets/js/db455e1671dd.js\" defer></script>
|
||||
|
||||
function initSupabase(cb) {
|
||||
if (supabase) { cb(); return; }
|
||||
fetch('/api/supabase-config')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(cfg) {
|
||||
supabase = window.supabase.createClient(cfg.url, cfg.anon_key, {
|
||||
auth: { flowType: 'implicit' }
|
||||
});
|
||||
cb();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Supabase init failed', err);
|
||||
});
|
||||
}
|
||||
|
||||
function showAuthMessage(msg, isError) {
|
||||
var el = document.getElementById('auth-message');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
el.style.color = isError ? '#c0392b' : '#2ecc71';
|
||||
}
|
||||
|
||||
function revealPaymentForm(user) {
|
||||
// Capture Supabase user id so /api/link-customer can stamp the Stripe
|
||||
// customer with metadata[supabase_user_id]. Used by /account to find
|
||||
// the buyer's plan via the canonical cross-reference.
|
||||
if (user && user.id) { window._neuronSupaId = user.id; }
|
||||
// Hide optional auth section if it was shown
|
||||
var auth = document.getElementById('auth-section');
|
||||
if (auth) auth.style.display = 'none';
|
||||
var payment = document.getElementById('payment-section');
|
||||
if (payment) payment.style.display = '';
|
||||
|
||||
// Show auth badge if we have a user, hide the inline 'sign in' prompt
|
||||
if (user) {
|
||||
var badge = document.getElementById('auth-badge');
|
||||
var name = user.user_metadata && user.user_metadata.full_name
|
||||
? user.user_metadata.full_name
|
||||
: user.email || '';
|
||||
badge.innerHTML = '<div class=\"checkout-auth-badge\">'
|
||||
+ '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M20 6L9 17l-5-5\" stroke=\"#0052A0\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'
|
||||
+ 'Signed in as <strong>' + name + '</strong>'
|
||||
+ '</div>';
|
||||
badge.style.display = '';
|
||||
var prompt = document.getElementById('signin-prompt');
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
}
|
||||
|
||||
// Pre-fill email only (not name - let user enter their own)
|
||||
if (user && user.email) {
|
||||
var emailEl = document.getElementById('buyer-email');
|
||||
if (emailEl) { emailEl.value = user.email; }
|
||||
}
|
||||
|
||||
// Initialize Stripe Elements with this user's email (or empty if guest)
|
||||
var userEmail = user ? (user.email || '') : '';
|
||||
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
||||
if (typeof initStripe === 'function') initStripe(userEmail, userName);
|
||||
}
|
||||
|
||||
// Check if already signed in on load
|
||||
function checkExistingSession() {
|
||||
initSupabase(function() {
|
||||
supabase.auth.getUser().then(function(res) {
|
||||
if (res.data && res.data.user) {
|
||||
revealPaymentForm(res.data.user);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle OAuth redirect callback
|
||||
function handleAuthRedirect() {
|
||||
initSupabase(function() {
|
||||
supabase.auth.onAuthStateChange(function(event, session) {
|
||||
if ((event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session && session.user) {
|
||||
revealPaymentForm(session.user);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Social sign-in
|
||||
window.signInWith = function(provider) {
|
||||
var btns = document.querySelectorAll('.checkout-social-btn');
|
||||
btns.forEach(function(b) { b.disabled = true; });
|
||||
|
||||
initSupabase(function() {
|
||||
supabase.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo: window.location.href
|
||||
}
|
||||
}).then(function(result) {
|
||||
if (result.error) {
|
||||
showAuthMessage(result.error.message || 'Sign-in failed. Please try again.', true);
|
||||
btns.forEach(function(b) { b.disabled = false; });
|
||||
}
|
||||
// On success, browser redirects to OAuth provider - no further action needed here.
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Email signup
|
||||
window.signUpWithEmail = function() {
|
||||
var email = document.getElementById('auth-email').value.trim();
|
||||
var password = document.getElementById('auth-password').value;
|
||||
if (!email || !password) { showAuthMessage('Please enter your email and a password.', true); return; }
|
||||
if (password.length < 8) { showAuthMessage('Password must be at least 8 characters.', true); return; }
|
||||
initSupabase(function() {
|
||||
supabase.auth.signUp({ email: email, password: password }).then(function(result) {
|
||||
if (result.error) { showAuthMessage(result.error.message, true); return; }
|
||||
if (result.data && result.data.session) {
|
||||
revealPaymentForm(result.data.session.user);
|
||||
} else {
|
||||
showAuthMessage('Check your email to confirm your account, then come back to complete your purchase.', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Email sign-in (existing account)
|
||||
window.showSignIn = function() {
|
||||
var form = document.getElementById('email-auth-form');
|
||||
var btn = form.querySelector('.checkout-email-btn');
|
||||
var hint = form.querySelector('.checkout-auth-hint');
|
||||
btn.textContent = 'Sign in →';
|
||||
btn.onclick = signInWithEmail;
|
||||
/* hint replaced with DOM manipulation below */
|
||||
};
|
||||
|
||||
window.showSignUp = function() {
|
||||
var form = document.getElementById('email-auth-form');
|
||||
var btn = form.querySelector('.checkout-email-btn');
|
||||
var hint = form.querySelector('.checkout-auth-hint');
|
||||
btn.textContent = 'Create account →';
|
||||
btn.onclick = signUpWithEmail;
|
||||
/* hint replaced with DOM manipulation below */
|
||||
};
|
||||
|
||||
window.signInWithEmail = function() {
|
||||
var email = document.getElementById('auth-email').value.trim();
|
||||
var password = document.getElementById('auth-password').value;
|
||||
if (!email || !password) { showAuthMessage('Please enter your email and password.', true); return; }
|
||||
initSupabase(function() {
|
||||
supabase.auth.signInWithPassword({ email: email, password: password }).then(function(result) {
|
||||
if (result.error) { showAuthMessage(result.error.message, true); return; }
|
||||
revealPaymentForm(result.data.session.user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.resetPassword = function() {
|
||||
var email = document.getElementById('auth-email').value.trim();
|
||||
if (!email) { showAuthMessage('Enter your email address above first.', true); return; }
|
||||
initSupabase(function() {
|
||||
supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: window.location.origin + '/checkout?plan=' + (new URLSearchParams(window.location.search).get('plan') || 'professional')
|
||||
}).then(function(result) {
|
||||
if (result.error) { showAuthMessage(result.error.message, true); }
|
||||
else { showAuthMessage('Password reset email sent. Check your inbox.', false); }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Init
|
||||
handleAuthRedirect();
|
||||
checkExistingSession();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<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';
|
||||
}
|
||||
|
||||
// Mode flag: 'payment' (charge now) or 'setup' (save card, charge later).
|
||||
// The submit handler reads this to choose stripe.confirmPayment vs
|
||||
// stripe.confirmSetup. SOURCE OF TRUTH for whether the buyer is going to
|
||||
// be charged at submit time. If the radio toggles, we re-fetch a new
|
||||
// client_secret of the right type and re-mount the Element.
|
||||
window._neuronMode = 'payment';
|
||||
var paymentEl = null;
|
||||
|
||||
function appearance() {
|
||||
return {
|
||||
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' }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function currentTiming() {
|
||||
var later = document.getElementById('timing-later');
|
||||
return (later && later.checked) ? 'later' : 'now';
|
||||
}
|
||||
|
||||
function fetchAndMount() {
|
||||
var submitBtn = document.getElementById('submit-btn');
|
||||
if (submitBtn) { submitBtn.disabled = true; }
|
||||
if (paymentEl) {
|
||||
try { paymentEl.unmount(); } catch(e) {}
|
||||
paymentEl = null;
|
||||
}
|
||||
var loadEl = document.querySelector('.checkout-element-loading');
|
||||
if (!loadEl) {
|
||||
var hostEl = document.getElementById('payment-element');
|
||||
if (hostEl) {
|
||||
var d = document.createElement('div');
|
||||
d.className = 'checkout-element-loading';
|
||||
d.textContent = 'Loading payment form…';
|
||||
hostEl.appendChild(d);
|
||||
}
|
||||
}
|
||||
var timing = currentTiming();
|
||||
return fetch('/api/payment-intent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plan: PLAN, timing: timing })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error === 'sold_out') {
|
||||
showMessage('All 1,000 Founding Member spots have been claimed. Thank you for your interest - please consider the Professional plan.');
|
||||
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Sold out'; }
|
||||
return;
|
||||
}
|
||||
if (!data.client_secret) {
|
||||
showMessage('Unable to initialise payment. Please try again.');
|
||||
return;
|
||||
}
|
||||
window._neuronMode = data.setup_mode ? 'setup' : 'payment';
|
||||
window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : '');
|
||||
// Update the submit button label so users know what will happen.
|
||||
var submitLabel = document.getElementById('submit-label');
|
||||
if (submitLabel) {
|
||||
submitLabel.textContent = window._neuronMode === 'setup'
|
||||
? 'Save my card - no charge today →'
|
||||
: 'Complete purchase →';
|
||||
}
|
||||
waitForStripe(function() {
|
||||
if (!stripe) { stripe = Stripe(STRIPE_PK); }
|
||||
elements = stripe.elements({ clientSecret: data.client_secret, appearance: appearance() });
|
||||
paymentEl = elements.create('payment', {
|
||||
fields: { billingDetails: { name: 'never', email: 'never' } }
|
||||
});
|
||||
paymentEl.mount('#payment-element');
|
||||
paymentEl.on('ready', function() {
|
||||
var ld = document.querySelector('.checkout-element-loading');
|
||||
if (ld) ld.remove();
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
showMessage('Unable to connect. Please check your connection and try again.');
|
||||
});
|
||||
}
|
||||
|
||||
// Initial mount. PLAN==professional shows a timing radio; toggling it
|
||||
// re-fetches the right Intent so the buyer's choice is honored.
|
||||
fetchAndMount();
|
||||
var tNow = document.getElementById('timing-now');
|
||||
var tLater = document.getElementById('timing-later');
|
||||
if (tNow) tNow.addEventListener('change', fetchAndMount);
|
||||
if (tLater) tLater.addEventListener('change', fetchAndMount);
|
||||
|
||||
// Submit
|
||||
document.getElementById('payment-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
if (!stripe || !elements) return;
|
||||
|
||||
// Founding Member attestation gate
|
||||
var attestCb = document.getElementById('founding-attest-cb');
|
||||
if (attestCb && !attestCb.checked) {
|
||||
var warn = document.getElementById('attest-warn');
|
||||
if (warn) warn.style.display = 'block';
|
||||
attestCb.closest('label').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
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;
|
||||
}
|
||||
|
||||
// Save founding member attestation before charging - independent audit record
|
||||
if (attestCb) {
|
||||
try {
|
||||
await fetch('/api/attest', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
plan: PLAN,
|
||||
name: name,
|
||||
email: email,
|
||||
timestamp: new Date().toISOString(),
|
||||
attestation: 'I am joining as a genuine early user, not to extract proprietary information about Neuron technology, architecture, or roadmap. I will engage in good faith. I understand that if this is not my intent, a different plan is a better fit.',
|
||||
user_agent: navigator.userAgent
|
||||
})
|
||||
});
|
||||
} catch(e) {
|
||||
// Non-blocking - attestation log failure does not stop payment
|
||||
console.warn('attestation log failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
document.getElementById('payment-message').style.display = 'none';
|
||||
|
||||
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,
|
||||
timing: currentTiming(),
|
||||
mode: window._neuronMode || 'payment',
|
||||
supabase_user_id: window._neuronSupaId || ''
|
||||
})
|
||||
});
|
||||
} catch(e) { /* non-blocking */ }
|
||||
}
|
||||
|
||||
// Setup mode: save card, do not charge. Use stripe.confirmSetup.
|
||||
// Payment mode: charge now via stripe.confirmPayment.
|
||||
var confirmParams = {
|
||||
return_url: window.location.origin + '/account?welcome=1',
|
||||
payment_method_data: { billing_details: { name: name, email: email } }
|
||||
};
|
||||
var result;
|
||||
if (window._neuronMode === 'setup') {
|
||||
result = await stripe.confirmSetup({
|
||||
elements: elements,
|
||||
confirmParams: confirmParams
|
||||
});
|
||||
} else {
|
||||
confirmParams.receipt_email = email;
|
||||
result = await stripe.confirmPayment({
|
||||
elements: elements,
|
||||
confirmParams: confirmParams
|
||||
});
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
showMessage(result.error.message || (window._neuronMode === 'setup'
|
||||
? 'Could not save your card. Please try again.'
|
||||
: 'Payment failed. Please try again.'));
|
||||
setLoading(false);
|
||||
}
|
||||
// On success, Stripe redirects to return_url automatically.
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan=\"" + plan + "\";window.NEURON_CFG.pub_key=\"" + pub_key + "\";</script><script src=\"/assets/js/e708dcbb3e7a.js\" defer></script>
|
||||
"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user