import { useState, useEffect, useLayoutEffect, useRef } from "react"

import { addPropertyControls, ControlType } from "framer"


// ─── Palette (matches CVThankYouPage) ──────────────────────────────────────────

const COLORS = {

red: "#E53E2F",

redHover: "#CF3526",

redTint: "#FBEEED",

sage: "#E8F2EF",

sageText: "#3D8C7A",

dark: "#111111",

muted: "#6B6B6B",

border: "rgba(0,0,0,0.1)",

paper: "#FAFAF7",

surface: "#F7F7F5",

}


// ─── Container-based mobile detection (same as thank-you page) ──────────────────

function useContainerIsMobile(ref) {

const [isMobile, setIsMobile] = useState(false)


useLayoutEffect(() => {

if (!ref.current) return

const width = ref.current.getBoundingClientRect().width

if (width > 0) setIsMobile(width < 768)

}, [ref])


useEffect(() => {

if (!ref.current) return

const observer = new ResizeObserver((entries) => {

for (const entry of entries) {

setIsMobile(entry.contentRect.width < 768)

}

})

observer.observe(ref.current)

return () => observer.disconnect()

}, [ref])


return isMobile

}


// ─── UTM passthrough (same storage key as the rest of the funnel) ───────────────

const UTM_KEYS = [

"utm_source",

"utm_medium",

"utm_campaign",

"utm_content",

"utm_term",

"gclid",

"fbclid",

]

const UTM_STORAGE_KEY = "fsd_utm_data"


function getStoredUTMs() {

if (typeof window === "undefined") return {}

try {

const raw = sessionStorage.getItem(UTM_STORAGE_KEY)

return raw ? JSON.parse(raw) : {}

} catch (err) {

return {}

}

}


// ─── GTM dataLayer helper ───────────────────────────────────────────────────────

function pushDataLayer(event, payload) {

if (typeof window === "undefined") return

window.dataLayer = window.dataLayer || []

window.dataLayer.push({

event,

...getStoredUTMs(), // pass through all stored UTM params

...payload,

})

}


// ─── Optional email prefill from ?email= (if the main form passes it through) ───

function getEmailParam() {

if (typeof window === "undefined") return ""

try {

return new URLSearchParams(window.location.search).get("email") || ""

} catch (err) {

return ""

}

}


function isValidEmail(v) {

return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((v || "").trim())

}


// ─── Small checkmark (matches thank-you page style, scaled down) ───────────────

function MiniCheck({ size = 56 }) {

return (

<div

style={{

width: size,

height: size,

borderRadius: "50%",

background: COLORS.red,

display: "flex",

alignItems: "center",

justifyContent: "center",

color: "#fff",

fontSize: size * 0.5,

fontWeight: 600,

margin: "0 auto",

boxShadow: "0 8px 32px rgba(229,62,47,0.18)",

}}

>

</div>

)

}


// ─── Main component ────────────────────────────────────────────────────────────

export default function CVBudgetGate({

// Question step

questionEyebrow,

questionText,

yesLabel,

noLabel,

bookingUrl,

// Downsell step

downsellHeadline,

downsellBody,

emailPlaceholder,

downsellButton,

reconsiderText,

reassurance,

// Success step

successHeadline,

successBody,

// Plumbing

submitEndpoint,

qualifiedValue,

downsellValue,

}) {

const containerRef = useRef(null)

const isMobile = useContainerIsMobile(containerRef)


const [step, setStep] = useState("question") // question | downsell | success

const [email, setEmail] = useState("")

const [error, setError] = useState("")

const [submitting, setSubmitting] = useState(false)

const [hovered, setHovered] = useState("")


// Prefill email if the main form passed ?email= through

useEffect(() => {

const e = getEmailParam()

if (e) setEmail(e)

}, [])


// ── Handlers ──

function handleYes() {

pushDataLayer("budget_qualifier_answered", {

qualified: true,

qualifier_answer: "yes",

conversion_value: qualifiedValue,

conversion_currency: "USD",

})

if (typeof window !== "undefined" && bookingUrl) {

window.location.href = bookingUrl

}

}


function handleNo() {

pushDataLayer("budget_qualifier_answered", {

qualified: false,

qualifier_answer: "no",

})

setStep("downsell")

}


async function handleSubmit() {

const clean = email.trim()

if (!isValidEmail(clean)) {

setError("Enter a valid email so we can send the teardown.")

return

}

setError("")

setSubmitting(true)


const payload = {

email: clean,

source: "90day_budget_downsell",

offer: "free_teardown",

...getStoredUTMs(),

}


// Optional POST to an ESP / Zapier / webhook endpoint.

// If no endpoint is set, we still fire the dataLayer event and

// proceed — GTM becomes the capture path of record.

if (submitEndpoint) {

try {

await fetch(submitEndpoint, {

method: "POST",

headers: { "Content-Type": "application/json" },

body: JSON.stringify(payload),

})

} catch (err) {

// Don't block the user on a failed POST — capture via GTM instead.

console.error("Downsell endpoint POST failed:", err)

}

}


// Distinct event — intentionally NOT the main Contact conversion,

// so the downsell never pollutes the campaign's purchase/Contact signal.

pushDataLayer("downsell_teardown_requested", {

conversion_value: downsellValue,

conversion_currency: "USD",

invitee_email: clean,

})


setSubmitting(false)

setStep("success")

}


// ── Shared shells ──

const outer = {

fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",

width: "100%",

boxSizing: "border-box",

padding: isMobile ? "8px" : "12px",

display: "flex",

justifyContent: "center",

}

const card = {

width: "100%",

maxWidth: 600,

background: COLORS.paper,

border: `0.5px solid ${COLORS.border}`,

borderRadius: 16,

padding: isMobile ? "28px 22px" : "40px 36px",

boxSizing: "border-box",

}

const eyebrow = {

display: "inline-flex",

alignItems: "center",

gap: 8,

background: COLORS.sage,

color: COLORS.sageText,

fontSize: 11,

fontWeight: 600,

letterSpacing: "0.1em",

textTransform: "uppercase",

padding: "6px 14px",

borderRadius: 100,

marginBottom: 18,

}


// ── QUESTION STEP ──

if (step === "question") {

return (

<div ref={containerRef} style={outer}>

<div style={card}>

<div style={eyebrow}>{questionEyebrow}</div>

<h2

style={{

fontSize: isMobile ? 22 : 26,

fontWeight: 700,

lineHeight: 1.25,

color: COLORS.dark,

letterSpacing: "-0.02em",

margin: "0 0 24px",

}}

>

{questionText}

</h2>

<div

style={{

display: "flex",

flexDirection: isMobile ? "column" : "row",

gap: 12,

}}

>

{/* Yes — primary */}

<button

onClick={handleYes}

onMouseEnter={() => setHovered("yes")}

onMouseLeave={() => setHovered("")}

style={{

flex: 1,

cursor: "pointer",

border: "none",

borderRadius: 10,

padding: "16px 20px",

fontSize: 15,

fontWeight: 600,

fontFamily: "inherit",

color: "#fff",

background:

hovered === "yes"

? COLORS.redHover

: COLORS.red,

transition: "background 0.15s, transform 0.15s",

transform:

hovered === "yes"

? "translateY(-1px)"

: "none",

}}

>

{yesLabel}

</button>

{/* No — quiet secondary */}

<button

onClick={handleNo}

onMouseEnter={() => setHovered("no")}

onMouseLeave={() => setHovered("")}

style={{

flex: 1,

cursor: "pointer",

borderRadius: 10,

padding: "16px 20px",

fontSize: 15,

fontWeight: 600,

fontFamily: "inherit",

color: COLORS.muted,

background:

hovered === "no" ? COLORS.surface : "#fff",

border: `1px solid ${COLORS.border}`,

transition: "background 0.15s",

}}

>

{noLabel}

</button>

</div>

</div>

</div>

)

}


// ── DOWNSELL STEP ──

if (step === "downsell") {

return (

<div ref={containerRef} style={outer}>

<div style={card}>

<div style={eyebrow}>Let's start smaller</div>

<h2

style={{

fontSize: isMobile ? 24 : 30,

fontWeight: 700,

lineHeight: 1.15,

color: COLORS.dark,

letterSpacing: "-0.02em",

margin: "0 0 14px",

}}

>

{downsellHeadline}

</h2>

<p

style={{

fontSize: isMobile ? 15 : 16,

color: COLORS.muted,

lineHeight: 1.55,

margin: "0 0 24px",

}}

>

{downsellBody}

</p>


<div

style={{

display: "flex",

flexDirection: isMobile ? "column" : "row",

gap: 10,

}}

>

<input

type="email"

value={email}

placeholder={emailPlaceholder}

onChange={(e) => {

setEmail(e.target.value)

if (error) setError("")

}}

onKeyDown={(e) => {

if (e.key === "Enter") handleSubmit()

}}

style={{

flex: 1,

fontSize: 15,

fontFamily: "inherit",

color: COLORS.dark,

padding: "15px 16px",

borderRadius: 10,

border: `1px solid ${

error ? COLORS.red : COLORS.border

}`,

outline: "none",

background: "#fff",

boxSizing: "border-box",

}}

/>

<button

onClick={handleSubmit}

disabled={submitting}

onMouseEnter={() => setHovered("send")}

onMouseLeave={() => setHovered("")}

style={{

cursor: submitting ? "default" : "pointer",

border: "none",

borderRadius: 10,

padding: "15px 24px",

fontSize: 15,

fontWeight: 600,

fontFamily: "inherit",

color: "#fff",

whiteSpace: "nowrap",

opacity: submitting ? 0.7 : 1,

background:

hovered === "send" && !submitting

? COLORS.redHover

: COLORS.red,

transition: "background 0.15s",

}}

>

{submitting ? "Sending…" : `${downsellButton} →`}

</button>

</div>


{error && (

<div

style={{

fontSize: 13,

color: COLORS.red,

marginTop: 10,

}}

>

{error}

</div>

)}


{reassurance ? (

<div

style={{

fontSize: 12.5,

color: COLORS.muted,

marginTop: 14,

}}

>

{reassurance}

</div>

) : null}


{/* Recovery link for the "reflexive No" who could actually pay */}

{reconsiderText ? (

<div

style={{

marginTop: 22,

paddingTop: 18,

borderTop: `0.5px solid ${COLORS.border}`,

textAlign: "center",

}}

>

<span

onClick={handleYes}

style={{

fontSize: 13,

fontWeight: 600,

color: COLORS.muted,

cursor: "pointer",

borderBottom: `1px solid ${COLORS.border}`,

paddingBottom: 1,

}}

>

{reconsiderText} →

</span>

</div>

) : null}

</div>

</div>

)

}


// ── SUCCESS STEP ──

return (

<div ref={containerRef} style={outer}>

<div style={{ ...card, textAlign: "center" }}>

<div style={{ marginBottom: 20 }}>

<MiniCheck size={isMobile ? 52 : 56} />

</div>

<h2

style={{

fontSize: isMobile ? 24 : 28,

fontWeight: 700,

lineHeight: 1.15,

color: COLORS.dark,

letterSpacing: "-0.02em",

margin: "0 0 12px",

}}

>

{successHeadline}

</h2>

<p

style={{

fontSize: isMobile ? 15 : 16,

color: COLORS.muted,

lineHeight: 1.55,

margin: "0 auto",

maxWidth: 420,

}}

>

{successBody}

</p>

</div>

</div>

)

}


CVBudgetGate.defaultProps = {

questionEyebrow: "One last thing",

questionText:

"Our management fee starts at $5,000/month. Does that work for you?",

yesLabel: "Yes, that works",

noLabel: "Not right now",

bookingUrl: "/90-day/book",


downsellHeadline: "No problem. Let's start smaller.",

downsellBody:

"A $5K/month retainer isn't the right move for every brand, and forcing it would be a bad fit for both of us. But you came here because your CPA is a problem, so let's still help. We'll do a free teardown of your current ad account and creative, the same audit we run before taking on any client, and send you the first three things we'd change. No pitch, no obligation.",

emailPlaceholder: "you@yourbrand.com",

downsellButton: "Send me the teardown",

reconsiderText: "Actually, $5K works for us. Book the call",

reassurance: "We'll send it over within 2-3 business days.",


successHeadline: "On its way.",

successBody:

"Keep an eye on your inbox over the next 2-3 business days. We'll send the teardown and the first three things we'd change.",


submitEndpoint: "",

qualifiedValue: 7500,

downsellValue: 0,

}


addPropertyControls(CVBudgetGate, {

// ─── Question step ───

questionEyebrow: {

type: ControlType.String,

title: "Q · Eyebrow",

defaultValue: "One last thing",

},

questionText: {

type: ControlType.String,

title: "Q · Question",

defaultValue:

"Our management fee starts at $5,000/month. Does that work for you?",

displayTextArea: true,

},

yesLabel: {

type: ControlType.String,

title: "Q · Yes label",

defaultValue: "Yes, that works",

},

noLabel: {

type: ControlType.String,

title: "Q · No label",

defaultValue: "Not right now",

},

bookingUrl: {

type: ControlType.String,

title: "Q · Booking URL (Yes)",

description: "Where 'Yes' sends them — your Calendly/booking step",

defaultValue: "/90-day/book",

},


// ─── Downsell step ───

downsellHeadline: {

type: ControlType.String,

title: "DS · Headline",

defaultValue: "No problem. Let's start smaller.",

},

downsellBody: {

type: ControlType.String,

title: "DS · Body",

defaultValue:

"A $5K/month retainer isn't the right move for every brand, and forcing it would be a bad fit for both of us. But you came here because your CPA is a problem, so let's still help. We'll do a free teardown of your current ad account and creative, the same audit we run before taking on any client, and send you the first three things we'd change. No pitch, no obligation.",

displayTextArea: true,

},

emailPlaceholder: {

type: ControlType.String,

title: "DS · Email placeholder",

defaultValue: "you@yourbrand.com",

},

downsellButton: {

type: ControlType.String,

title: "DS · Button",

defaultValue: "Send me the teardown",

},

reconsiderText: {

type: ControlType.String,

title: "DS · Reconsider link",

description: "Recovery link for a reflexive 'No'. Leave blank to hide.",

defaultValue: "Actually, $5K works for us. Book the call",

},

reassurance: {

type: ControlType.String,

title: "DS · Reassurance line",

defaultValue: "We'll send it over within 2-3 business days.",

},


// ─── Success step ───

successHeadline: {

type: ControlType.String,

title: "OK · Headline",

defaultValue: "On its way.",

},

successBody: {

type: ControlType.String,

title: "OK · Body",

defaultValue:

"Keep an eye on your inbox over the next 2-3 business days. We'll send the teardown and the first three things we'd change.",

displayTextArea: true,

},


// ─── Plumbing ───

submitEndpoint: {

type: ControlType.String,

title: "Submit endpoint",

description:

"Optional POST URL (ESP / Zapier / webhook). Blank = GTM-only capture.",

defaultValue: "",

},

qualifiedValue: {

type: ControlType.Number,

title: "Qualified value (USD)",

description: "Fires with budget_qualifier_answered on 'Yes'",

defaultValue: 7500,

min: 0,

step: 100,

},

downsellValue: {

type: ControlType.Number,

title: "Downsell value (USD)",

description: "Fires with downsell_teardown_requested. Keep low/0.",

defaultValue: 0,

min: 0,

step: 50,

},

})

One last thing

Our management fee starts at $5,000/month. Does that work for you?