Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00e62bb010 |
@@ -194,7 +194,6 @@ jobs:
|
|||||||
--image "$IMAGE" \
|
--image "$IMAGE" \
|
||||||
--region us-central1 \
|
--region us-central1 \
|
||||||
--project neuron-785695 \
|
--project neuron-785695 \
|
||||||
--execution-environment gen2 \
|
|
||||||
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
||||||
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
||||||
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
||||||
@@ -212,7 +211,6 @@ jobs:
|
|||||||
|
|
||||||
gcloud run services update marketing-stage \
|
gcloud run services update marketing-stage \
|
||||||
--region us-central1 --project neuron-785695 \
|
--region us-central1 --project neuron-785695 \
|
||||||
--execution-environment gen2 \
|
|
||||||
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
|
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
|
||||||
--quiet
|
--quiet
|
||||||
|
|
||||||
@@ -250,7 +248,6 @@ jobs:
|
|||||||
--image "$IMAGE" \
|
--image "$IMAGE" \
|
||||||
--region "$region" \
|
--region "$region" \
|
||||||
--project neuron-785695 \
|
--project neuron-785695 \
|
||||||
--execution-environment gen2 \
|
|
||||||
--quiet
|
--quiet
|
||||||
}
|
}
|
||||||
deploy us-central1 marketing-prod-us &
|
deploy us-central1 marketing-prod-us &
|
||||||
|
|||||||
@@ -29,10 +29,3 @@ src/assets/js/
|
|||||||
!dist/soul-demo.c
|
!dist/soul-demo.c
|
||||||
!dist/entrypoint.sh
|
!dist/entrypoint.sh
|
||||||
!dist/engram-snapshot.json
|
!dist/engram-snapshot.json
|
||||||
!dist/Dockerfile.soul-demo
|
|
||||||
!dist/k3s-soul-demo.yaml
|
|
||||||
|
|
||||||
# Build artifacts produced by the soul-demo packaging step in build-stage.sh
|
|
||||||
dist/soul-demo
|
|
||||||
dist/soul-demo-snapshot.json
|
|
||||||
dist/soul-demo-image.tar
|
|
||||||
|
|||||||
+2
-23
@@ -15,7 +15,6 @@ FROM debian:bookworm-slim AS builder
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
curl \
|
|
||||||
libcurl4-openssl-dev \
|
libcurl4-openssl-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@@ -56,10 +55,6 @@ RUN cc -O2 -rdynamic \
|
|||||||
soul-demo.c vessel_stubs.c el_runtime.o \
|
soul-demo.c vessel_stubs.c el_runtime.o \
|
||||||
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
||||||
|
|
||||||
# ── Download k3s binary ───────────────────────────────────────────────────────
|
|
||||||
RUN curl -fL https://github.com/k3s-io/k3s/releases/download/v1.32.4%2Bk3s1/k3s -o /usr/local/bin/k3s \
|
|
||||||
&& chmod +x /usr/local/bin/k3s
|
|
||||||
|
|
||||||
# ── Stage 2: runtime image ────────────────────────────────────────────────────
|
# ── Stage 2: runtime image ────────────────────────────────────────────────────
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -72,24 +67,11 @@ RUN apt-get update \
|
|||||||
&& groupadd -r landing && useradd -r -g landing landing \
|
&& groupadd -r landing && useradd -r -g landing landing \
|
||||||
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
||||||
&& mkdir -p /srv/soul/engram-demo \
|
&& mkdir -p /srv/soul/engram-demo \
|
||||||
&& chown -R landing:landing /srv/landing /srv/soul \
|
&& chown -R landing:landing /srv/landing /srv/soul
|
||||||
&& mkdir -p /var/lib/rancher/k3s /tmp/k3s \
|
|
||||||
&& chown -R landing:landing /var/lib/rancher /tmp/k3s
|
|
||||||
|
|
||||||
COPY --from=builder /build/neuron-web /usr/local/bin/neuron-web
|
COPY --from=builder /build/neuron-web /usr/local/bin/neuron-web
|
||||||
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
|
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
|
||||||
|
|
||||||
# k3s binary
|
|
||||||
COPY --from=builder /usr/local/bin/k3s /usr/local/bin/k3s
|
|
||||||
|
|
||||||
# soul-demo OCI image tar — k3s imports this at startup (no registry needed)
|
|
||||||
RUN mkdir -p /var/lib/rancher/k3s/agent/images
|
|
||||||
COPY dist/soul-demo-image.tar /var/lib/rancher/k3s/agent/images/soul-demo.tar
|
|
||||||
|
|
||||||
# k3s manifests — auto-applied when k3s starts
|
|
||||||
RUN mkdir -p /var/lib/rancher/k3s/server/manifests
|
|
||||||
COPY dist/k3s-soul-demo.yaml /var/lib/rancher/k3s/server/manifests/soul-demo.yaml
|
|
||||||
|
|
||||||
# Engram snapshot — baked in so soul has memory from cold start
|
# Engram snapshot — baked in so soul has memory from cold start
|
||||||
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
||||||
|
|
||||||
@@ -112,11 +94,8 @@ ENV LANDING_ROOT=/srv/landing
|
|||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
ENV NEURON_HOME=/srv/soul/engram-demo
|
ENV NEURON_HOME=/srv/soul/engram-demo
|
||||||
ENV NEURON_PORT=7772
|
ENV NEURON_PORT=7772
|
||||||
ENV K3S_DATA_DIR=/var/lib/rancher/k3s
|
|
||||||
ENV KUBECONFIG=/var/lib/rancher/k3s/server/cred/admin.kubeconfig
|
|
||||||
|
|
||||||
# k3s requires root to create network namespaces and mount cgroups.
|
USER landing
|
||||||
# Cloud Run gen2 sandbox is the security boundary here.
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["/usr/local/bin/entrypoint.sh"]
|
CMD ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|||||||
@@ -114,22 +114,4 @@ docker build \
|
|||||||
-t "marketing:${TAG}" \
|
-t "marketing:${TAG}" \
|
||||||
.
|
.
|
||||||
|
|
||||||
# Extract soul-demo binary and engram snapshot from built image
|
|
||||||
echo "==> Packaging soul-demo:local image for k3s..."
|
|
||||||
CONTAINER_ID=$(docker create "marketing:${TAG}")
|
|
||||||
docker cp "${CONTAINER_ID}:/usr/local/bin/soul-demo" dist/soul-demo
|
|
||||||
docker cp "${CONTAINER_ID}:/srv/soul/engram-demo/snapshot.json" dist/soul-demo-snapshot.json 2>/dev/null || true
|
|
||||||
docker rm "${CONTAINER_ID}"
|
|
||||||
|
|
||||||
# Build minimal soul-demo container image
|
|
||||||
cp dist/soul-demo-snapshot.json dist/engram-snapshot.json 2>/dev/null || true
|
|
||||||
docker build \
|
|
||||||
-f dist/Dockerfile.soul-demo \
|
|
||||||
-t soul-demo:local \
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Save as OCI tar for k3s to import at startup
|
|
||||||
docker save soul-demo:local -o dist/soul-demo-image.tar
|
|
||||||
echo "==> soul-demo:local image saved ($(du -sh dist/soul-demo-image.tar | cut -f1))"
|
|
||||||
|
|
||||||
echo "==> Done. marketing:${TAG} built."
|
echo "==> Done. marketing:${TAG} built."
|
||||||
|
|||||||
Vendored
-13
@@ -1,13 +0,0 @@
|
|||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends libcurl4 libssl3 ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& groupadd -r landing && useradd -r -g landing landing \
|
|
||||||
&& mkdir -p /srv/soul/engram-demo \
|
|
||||||
&& chown -R landing:landing /srv/soul
|
|
||||||
COPY soul-demo /usr/local/bin/soul-demo
|
|
||||||
COPY engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
|
||||||
ENV NEURON_HOME=/srv/soul/engram-demo
|
|
||||||
ENV NEURON_PORT=7772
|
|
||||||
USER landing
|
|
||||||
CMD ["/usr/local/bin/soul-demo"]
|
|
||||||
Vendored
+15
-29
@@ -1,35 +1,21 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
set -e
|
# entrypoint.sh — Start soul-demo then neuron-web in the same container.
|
||||||
|
#
|
||||||
|
# soul-demo runs in the background on :7772 (localhost only, not exposed).
|
||||||
|
# neuron-web runs in the foreground on :8080 (Cloud Run health checks this).
|
||||||
|
# If neuron-web exits, the container exits. Soul crashing is non-fatal —
|
||||||
|
# chat will return "demo soul not responding" but the page stays up.
|
||||||
|
|
||||||
echo "[entrypoint] Starting k3s server (embedded soul-demo orchestrator)..."
|
set -euo pipefail
|
||||||
|
|
||||||
# k3s server — single-node mode, disable unused components
|
echo "[entrypoint] starting soul-demo on :7772"
|
||||||
# --disable traefik,servicelb: we don't need an ingress or LB
|
/usr/local/bin/soul-demo &
|
||||||
# --disable metrics-server: saves ~50MB RAM
|
SOUL_PID=$!
|
||||||
# --write-kubeconfig-mode=644: allow non-root reads
|
|
||||||
# --data-dir: use the pre-chowned dir
|
|
||||||
k3s server \
|
|
||||||
--disable traefik \
|
|
||||||
--disable servicelb \
|
|
||||||
--disable metrics-server \
|
|
||||||
--write-kubeconfig-mode=644 \
|
|
||||||
--data-dir /var/lib/rancher/k3s \
|
|
||||||
--node-name soul-node &
|
|
||||||
|
|
||||||
K3S_PID=$!
|
# Give the soul a few seconds to load its engram and seed safety nodes
|
||||||
|
sleep 4
|
||||||
|
|
||||||
echo "[entrypoint] Waiting for k3s to become ready..."
|
echo "[entrypoint] soul-demo started (pid=$SOUL_PID)"
|
||||||
until k3s kubectl get nodes --no-headers 2>/dev/null | grep -q "Ready"; do
|
echo "[entrypoint] starting neuron-web on :${PORT:-8080}"
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "[entrypoint] k3s ready. soul-demo Deployment will be applied automatically from manifests."
|
|
||||||
|
|
||||||
# Wait for soul-demo pod to be Running before starting neuron-web
|
|
||||||
echo "[entrypoint] Waiting for soul-demo pod..."
|
|
||||||
until k3s kubectl get pods -l app=soul-demo --no-headers 2>/dev/null | grep -q "Running"; do
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
echo "[entrypoint] soul-demo is running."
|
|
||||||
|
|
||||||
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080}..."
|
|
||||||
exec /usr/local/bin/neuron-web
|
exec /usr/local/bin/neuron-web
|
||||||
|
|||||||
Vendored
-90
@@ -1,90 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: soul-demo
|
|
||||||
namespace: default
|
|
||||||
labels:
|
|
||||||
app: soul-demo
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: soul-demo
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: soul-demo
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: soul-demo
|
|
||||||
image: soul-demo:local
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- containerPort: 7772
|
|
||||||
env:
|
|
||||||
- name: NEURON_HOME
|
|
||||||
value: /srv/soul/engram-demo
|
|
||||||
- name: NEURON_PORT
|
|
||||||
value: "7772"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 250m
|
|
||||||
memory: 256Mi
|
|
||||||
limits:
|
|
||||||
cpu: 1000m
|
|
||||||
memory: 512Mi
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 7772
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 15
|
|
||||||
failureThreshold: 3
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 7772
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
volumeMounts:
|
|
||||||
- name: engram-data
|
|
||||||
mountPath: /srv/soul/engram-demo
|
|
||||||
volumes:
|
|
||||||
- name: engram-data
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: soul-demo
|
|
||||||
namespace: default
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
selector:
|
|
||||||
app: soul-demo
|
|
||||||
ports:
|
|
||||||
- port: 7772
|
|
||||||
targetPort: 7772
|
|
||||||
nodePort: 7772
|
|
||||||
protocol: TCP
|
|
||||||
---
|
|
||||||
apiVersion: autoscaling/v2
|
|
||||||
kind: HorizontalPodAutoscaler
|
|
||||||
metadata:
|
|
||||||
name: soul-demo
|
|
||||||
namespace: default
|
|
||||||
spec:
|
|
||||||
scaleTargetRef:
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
name: soul-demo
|
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 8
|
|
||||||
metrics:
|
|
||||||
- type: Resource
|
|
||||||
resource:
|
|
||||||
name: cpu
|
|
||||||
target:
|
|
||||||
type: Utilization
|
|
||||||
averageUtilization: 80
|
|
||||||
+11
-1
@@ -97,7 +97,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
|||||||
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
|
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
|
||||||
" + (if is_free { "
|
" + (if is_free { "
|
||||||
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
|
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
|
||||||
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No charge today. Add your card to reserve your spot - you won't be billed until you upgrade.</p>
|
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No card required. Your account is free, forever.</p>
|
||||||
" } else { "
|
" } else { "
|
||||||
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
|
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
|
||||||
<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.</p>
|
<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.</p>
|
||||||
@@ -135,6 +135,16 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Free-tier success panel: shown after account creation, no card needed -->
|
||||||
|
" + (if is_free { "
|
||||||
|
<div id=\"free-success\" style=\"display:none; text-align:center; padding: 2.5rem 1rem;\">
|
||||||
|
<div style=\"font-size:2.5rem; margin-bottom:1.25rem;\">✓</div>
|
||||||
|
<p class=\"label\" style=\"margin-bottom:.75rem; color:var(--navy);\">You're in.</p>
|
||||||
|
<p class=\"checkout-auth-hint\" style=\"margin-bottom:2rem;\">Your free account is ready. Download Neuron to get started.</p>
|
||||||
|
<a href=\"/marketplace\" class=\"checkout-submit\" style=\"display:inline-block; text-decoration:none; padding:.875rem 2rem;\">Go to your account →</a>
|
||||||
|
</div>
|
||||||
|
" } else { "" }) + "
|
||||||
|
|
||||||
<!-- Payment form (visible immediately - no auth wall) -->
|
<!-- Payment form (visible immediately - no auth wall) -->
|
||||||
<div id=\"payment-section\" " + (if is_free { "style=\"display:none;\"" } else { "" }) + ">
|
<div id=\"payment-section\" " + (if is_free { "style=\"display:none;\"" } else { "" }) + ">
|
||||||
<div id=\"auth-badge\" style=\"display:none; margin-bottom: 1.5rem;\"></div>
|
<div id=\"auth-badge\" style=\"display:none; margin-bottom: 1.5rem;\"></div>
|
||||||
|
|||||||
+10
-5
@@ -33,8 +33,11 @@ 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 payment = document.getElementById('payment-section');
|
var isFree = (window.NEURON_CFG || {}).plan === 'free';
|
||||||
if (payment) payment.style.display = '';
|
if (!isFree) {
|
||||||
|
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');
|
||||||
@@ -55,9 +58,11 @@ fn main() -> Void {
|
|||||||
if (emailEl) emailEl.value = user.email;
|
if (emailEl) emailEl.value = user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
var userEmail = user ? (user.email || '') : '';
|
if (!isFree) {
|
||||||
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
var userEmail = user ? (user.email || '') : '';
|
||||||
if (typeof initStripe === 'function') initStripe(userEmail, userName);
|
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
||||||
|
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistingSession() {
|
function checkExistingSession() {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
// checkout-free.el -- Free plan: reveal payment section after auth completes.
|
// checkout-free.el -- Free plan: show success panel after auth completes.
|
||||||
// Watches the auth-badge element; when it becomes visible, shows payment-section.
|
// Watches the auth-badge element; when it becomes visible, hides the auth
|
||||||
|
// section and shows the free-success panel. No card required for free tier.
|
||||||
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
||||||
|
|
||||||
fn main() -> Void {
|
fn main() -> Void {
|
||||||
native_js("(function() {
|
native_js("(function() {
|
||||||
var pay = document.getElementById('payment-section');
|
var success = document.getElementById('free-success');
|
||||||
if (!pay) return;
|
var auth = document.getElementById('auth-section');
|
||||||
|
if (!success) return;
|
||||||
var timer = setInterval(function() {
|
var timer = setInterval(function() {
|
||||||
var badge = document.getElementById('auth-badge');
|
var badge = document.getElementById('auth-badge');
|
||||||
if (badge && badge.offsetParent !== null) {
|
if (badge && badge.offsetParent !== null) {
|
||||||
pay.style.display = '';
|
if (auth) auth.style.display = 'none';
|
||||||
|
success.style.display = '';
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|||||||
@@ -31,8 +31,13 @@ fn main() -> Void {
|
|||||||
if (spinner) spinner.style.display = loading ? '' : 'none';
|
if (spinner) spinner.style.display = loading ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Free plan has no payment form — bail out entirely.
|
||||||
|
if (str_eq(PLAN, 'free')) return;
|
||||||
|
|
||||||
window._neuronMode = 'payment';
|
window._neuronMode = 'payment';
|
||||||
var paymentEl = null;
|
var paymentEl = null;
|
||||||
|
var userEmail = '';
|
||||||
|
var userName = '';
|
||||||
|
|
||||||
function appearance() {
|
function appearance() {
|
||||||
return {
|
return {
|
||||||
@@ -80,7 +85,7 @@ fn main() -> Void {
|
|||||||
return fetch('/api/payment-intent', {
|
return fetch('/api/payment-intent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ plan: PLAN, timing: timing })
|
body: JSON.stringify({ plan: PLAN, timing: timing, email: userEmail, name: userName })
|
||||||
})
|
})
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -117,11 +122,17 @@ fn main() -> Void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAndMount();
|
// Don't init Stripe at page load — wait for auth.
|
||||||
var tNow = document.getElementById('timing-now');
|
// checkout-auth.el calls window.initStripe(email, name) after sign-in.
|
||||||
var tLater = document.getElementById('timing-later');
|
window.initStripe = function(email, name) {
|
||||||
if (tNow) tNow.addEventListener('change', fetchAndMount);
|
userEmail = email || '';
|
||||||
if (tLater) tLater.addEventListener('change', fetchAndMount);
|
userName = name || '';
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
var form = document.getElementById('payment-form');
|
var form = document.getElementById('payment-form');
|
||||||
if (form) form.addEventListener('submit', async function(e) {
|
if (form) form.addEventListener('submit', async function(e) {
|
||||||
|
|||||||
+25
-16
@@ -584,23 +584,9 @@ fn handle_request_inner(method: String, path: String, body: String) -> 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: SetupIntent — save card details without charging.
|
// Free tier: no card required. Return immediately — no Stripe interaction.
|
||||||
// Card is stored on a Stripe Customer; billing begins only if the
|
|
||||||
// user later upgrades to a paid plan.
|
|
||||||
if str_eq(plan, "free") {
|
if str_eq(plan, "free") {
|
||||||
let si_body: String = "automatic_payment_methods[enabled]=true"
|
return "{\"plan\":\"free\",\"free\":true,\"no_payment_required\":true}"
|
||||||
+ "&usage=off_session"
|
|
||||||
+ "&metadata[plan]=free"
|
|
||||||
let auth_header: String = "Bearer " + stripe_key
|
|
||||||
let si_resp: String = http_post_form_auth(
|
|
||||||
"https://api.stripe.com/v1/setup_intents",
|
|
||||||
si_body,
|
|
||||||
auth_header)
|
|
||||||
if str_starts_with(si_resp, "{") {
|
|
||||||
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
|
|
||||||
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
|
|
||||||
}
|
|
||||||
return si_resp
|
|
||||||
}
|
}
|
||||||
// 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") {
|
||||||
@@ -612,6 +598,27 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
let auth_header: String = "Bearer " + stripe_key
|
let auth_header: String = "Bearer " + stripe_key
|
||||||
|
|
||||||
|
// Find-or-create Stripe Customer by email upfront so every intent
|
||||||
|
// is attached to an existing customer — prevents duplicate customers.
|
||||||
|
let pi_email: String = json_get_string(body, "email")
|
||||||
|
let pi_name: String = json_get_string(body, "name")
|
||||||
|
let pi_cus_id: String = ""
|
||||||
|
if !str_eq(pi_email, "") {
|
||||||
|
let pi_email_enc: String = str_replace(str_replace(pi_email, "@", "%40"), "+", "%2B")
|
||||||
|
let pi_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + pi_email_enc + "%22&limit=1"
|
||||||
|
let pi_search: String = http_get_auth(pi_search_url, auth_header)
|
||||||
|
let pi_cus_id = json_get_string(pi_search, "id")
|
||||||
|
if str_eq(pi_cus_id, "") {
|
||||||
|
let pi_name_enc: String = str_replace(pi_name, " ", "%20")
|
||||||
|
let pi_cus_body: String = "email=" + pi_email_enc
|
||||||
|
+ "&name=" + pi_name_enc
|
||||||
|
+ "&metadata[plan]=" + plan
|
||||||
|
+ "&metadata[source]=neuron-checkout"
|
||||||
|
let pi_cus_resp: String = http_post_form_auth("https://api.stripe.com/v1/customers", pi_cus_body, auth_header)
|
||||||
|
let pi_cus_id = json_get_string(pi_cus_resp, "id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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") {
|
||||||
@@ -620,6 +627,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
|
|||||||
+ "&metadata[plan]=" + plan
|
+ "&metadata[plan]=" + plan
|
||||||
+ "&metadata[hold_until]=launch"
|
+ "&metadata[hold_until]=launch"
|
||||||
+ "&metadata[launch_target]=2026-09-01"
|
+ "&metadata[launch_target]=2026-09-01"
|
||||||
|
let si_body = if !str_eq(pi_cus_id, "") { si_body + "&customer=" + pi_cus_id } else { si_body }
|
||||||
let si_resp: String = http_post_form_auth(
|
let si_resp: String = http_post_form_auth(
|
||||||
"https://api.stripe.com/v1/setup_intents",
|
"https://api.stripe.com/v1/setup_intents",
|
||||||
si_body,
|
si_body,
|
||||||
@@ -642,6 +650,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
|
|||||||
+ "&automatic_payment_methods[enabled]=true"
|
+ "&automatic_payment_methods[enabled]=true"
|
||||||
+ "&metadata[plan]=" + plan
|
+ "&metadata[plan]=" + plan
|
||||||
+ "&metadata[timing]=" + timing
|
+ "&metadata[timing]=" + timing
|
||||||
|
let pi_body = if !str_eq(pi_cus_id, "") { pi_body + "&customer=" + pi_cus_id } else { pi_body }
|
||||||
let response: String = http_post_form_auth(
|
let response: String = http_post_form_auth(
|
||||||
"https://api.stripe.com/v1/payment_intents",
|
"https://api.stripe.com/v1/payment_intents",
|
||||||
pi_body,
|
pi_body,
|
||||||
|
|||||||
Reference in New Issue
Block a user