/* FriendlyPhotons — Marketing site UI kit · Get Early Access modal (single-flow signup). */ const WL_CONFIG = { // Live backend (Replit, shipped 2026-06-17). CORS allowlisted for friendlyphotons.ai. apiUrl: "https://app.friendlyphotons.ai/api/waitlist", // Cloudflare Turnstile site key — WIRE-UP: set this (Mitch/Replit). Empty = widget skipped. turnstileSiteKey: "0x4AAAAAADNo7TE_fd0LJFDF", // Real POST fires only on these hosts; elsewhere (preview) it runs in demo mode. liveHosts: ["friendlyphotons.ai", "www.friendlyphotons.ai"] }; function WaitlistModal({ open, onClose }) { const [first, setFirst] = React.useState(""); const [email, setEmail] = React.useState(""); const [contributor, setContributor] = React.useState(false); const [phone, setPhone] = React.useState(""); const [sms, setSms] = React.useState(false); const [why, setWhy] = React.useState(""); const [hear, setHear] = React.useState(""); const [consent, setConsent] = React.useState(false); const [hp, setHp] = React.useState(""); const [done, setDone] = React.useState(false); const [err, setErr] = React.useState(""); const [loading, setLoading] = React.useState(false); const turnstileRef = React.useRef(null); const turnstileIdRef = React.useRef(null); // Explicit Turnstile render — conditional-mount modal misses Cloudflare's // one-shot auto-scan. Render on open with retry for async-defer script load; // cleanup on close. (Pattern from 2026-06-18 waitlist sprint, restored here // after CD's v5 bundle regressed the inline implicit version.) React.useEffect(() => { if (!open || !WL_CONFIG.turnstileSiteKey) return; let cancelled = false; const tryRender = () => { if (cancelled || turnstileIdRef.current) return; if (window.turnstile && turnstileRef.current) { try { turnstileIdRef.current = window.turnstile.render(turnstileRef.current, { sitekey: WL_CONFIG.turnstileSiteKey, }); } catch (_) {} } else { setTimeout(tryRender, 100); } }; tryRender(); return () => { cancelled = true; if (turnstileIdRef.current && window.turnstile) { try { window.turnstile.remove(turnstileIdRef.current); } catch (_) {} turnstileIdRef.current = null; } }; }, [open]); React.useEffect(() => { if (open) { setDone(false); setErr(""); setLoading(false); } }, [open]); if (!open) return null; const isLive = WL_CONFIG.liveHosts.indexOf(window.location.hostname) !== -1; const phoneOk = (() => { const d = phone.replace(/\D/g, ""); return d.length >= 10 && d.length <= 15; })(); const valid = first.trim().length > 0 && /.+@.+\..+/.test(email) && consent && (!contributor || (phoneOk && sms)); const submit = (e) => { e.preventDefault(); if (!valid || loading) return; setErr(""); const payload = { firstName: first.trim(), email: email.trim(), consent: consent, hasMeter: contributor }; if (contributor) { payload.phone = phone.trim(); payload.smsConsent = sms; } if (why.trim()) payload.signupNotes = why.trim(); if (hear) payload.referralSource = hear; if (hp) payload.website = hp; // honeypot — server rejects if filled if (window.turnstile && WL_CONFIG.turnstileSiteKey && turnstileIdRef.current) { try { const tok = window.turnstile.getResponse(turnstileIdRef.current); if (tok) payload.captchaToken = tok; } catch (_) {} } // Demo mode (off the live domain): show the flow without a real POST. if (!isLive || !WL_CONFIG.apiUrl) { setDone(true); return; } setLoading(true); fetch(WL_CONFIG.apiUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) .then((res) => res.json().catch(() => ({})).then((body) => ({ status: res.status, ok: res.ok, body }))) .then((r) => { if (r.ok) { setDone(true); return; } // any 2xx (incl. duplicate) -> same success setLoading(false); if (r.status === 429) setErr("Too many attempts — please try again in a moment."); else setErr((r.body && r.body.error) || "Something went wrong. Please try again."); if (window.turnstile && WL_CONFIG.turnstileSiteKey && turnstileIdRef.current) { try { window.turnstile.reset(turnstileIdRef.current); } catch (_) {} } }) .catch(() => { setLoading(false); setErr("Network error — please check your connection and try again."); }); }; const Check = () => ( ); return (
We'll email you the moment your early access is ready. Until then — the invisible world is waiting to be seen.