Files
lynkeduppro-crm/public/square-checkout.js
tanweer919 a459fefb7f feat(landing): port the full marketing site into the CRM (static)
Copies the goutam-pages landing (React marketing home + /page/1..19 + assets)
into /public and adds beforeFiles rewrites so '/' serves the marketing home and
'/1'..'/19' + '/thanks' serve the static pages — reproducing the landing's
vercel.json clean URLs. /portal/* (login/register) is untouched.
2026-07-18 00:06:53 +05:30

643 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Multi-step checkout wizard for the "Claim Founders Lifetime Deal" button
// (.deal-btn). Opens as a modal and walks the visitor through:
//
// Step 1 Registration - contact + company + email + phone + sales reps +
// number of licenses (1-10) + business address (with
// API autofill) + SMS consent. Submitting captures the
// record server-side (POST /api/register) BEFORE any
// branch, so no lead is ever lost.
// Branch on license count:
// <= 5 Step 2 Cart + Terms - order summary (licenses x $2,000), the Deal
// Terms & Conditions, and an "I agree" checkbox. Then
// "Continue to Secure Payment" -> POST /api/checkout ->
// redirect to Stripe. After payment Stripe returns the
// buyer to /thanks.
// > 5 Sales screen - "you may be eligible for a special discount" +
// "Talk to Sales". No online payment (bulk orders are
// handled by the sales team).
//
// WHY A CUSTOM FORM AND NOT STRIPE'S PAGE:
// Customers could complete an Apple Pay purchase without giving us any contact
// details, so there was no way to onboard buyers or distribute licenses. Stripe
// hosted Checkout can't fix this (no way to hide the Apple Pay express button;
// custom_fields enforcement there is undocumented) and SMS consent legally needs
// our own disclaimer text. Collecting first makes the payment method irrelevant.
// The server re-validates everything (api/_lead.js) — this form is UX only.
//
// Event delegation is used so this works on the static pages and the React home
// page (where the button mounts after this script loads).
(function () {
// ---------------------------------------------------------------------------
// TODO(client): replace with the exact SMS consent disclaimer Justin provided.
// PLACEHOLDER wording, no legal pass. When it changes, bump SMS_CONSENT_VERSION
// in api/checkout.js AND api/register.js so the CRM records the agreed version.
// ---------------------------------------------------------------------------
var SMS_CONSENT_TEXT =
"I agree to receive SMS messages from Lynked Up Technologies about my Founders " +
"License, onboarding, and product updates at the phone number provided. Message " +
"frequency varies. Message and data rates may apply. Reply STOP to opt out or HELP " +
"for help.";
// TODO(client): confirm the sales inbox for >5-license (bulk) enquiries.
var SALES_EMAIL = "sales@lynkeduppro.com";
var SALES_REP_BUCKETS = ["1", "2-5", "6-10", "11-25", "26-50", "51+"];
// DISPLAY ONLY — the server owns the real charge (api/checkout.js). Keep in sync.
var UNIT_PRICE = 2000; // USD per license
var MAX_LICENSES = 10; // dropdown ceiling; must match api/_lead.js MAX_LICENSES
var SELF_SERVE_MAX = 5; // <= this pays online; above -> sales. Match api/_lead.js
// Deal Terms & Conditions shown on the cart step. Mirrors the accordion text on
// the pricing sections. TODO(client): keep in sync with Justin's final legal T&C.
var TERMS = [
"All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.",
"The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. It includes access to core CRM features and the platform features made available under the applicable license terms. It does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.",
"SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges billed separately based on actual usage.",
"Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, and premium automation, may require additional fees, setup costs, subscriptions, or usage-based billing.",
"Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.",
];
var FIELDS = [
{ name: "firstName", label: "First name", type: "text", autocomplete: "given-name", placeholder: "Jane" },
{ name: "lastName", label: "Last name", type: "text", autocomplete: "family-name", placeholder: "Smith" },
{ name: "companyName", label: "Company name", type: "text", autocomplete: "organization", placeholder: "Smith Roofing LLC" },
{ name: "email", label: "Email address", type: "email", autocomplete: "email", placeholder: "jane@smithroofing.com" },
{ name: "phone", label: "Phone number", type: "tel", autocomplete: "tel", placeholder: "(555) 010-9999" },
{ name: "salesReps", label: "Number of sales reps", type: "select", autocomplete: "off", options: SALES_REP_BUCKETS, empty: "Select…" },
{ name: "licenses", label: "Number of licenses", type: "select", autocomplete: "off", options: licenseOptions(), value: "1" },
// Street is full-width with autocomplete; selecting a suggestion fills the
// city / state / zip fields below (which sit in their own 3-column row).
{ name: "streetAddress", label: "Street address", type: "text", autocomplete: "off", full: true, placeholder: "Start typing your address…" },
{ name: "city", label: "City", type: "text", autocomplete: "off", col3: true, placeholder: "Plano" },
{ name: "state", label: "State", type: "text", autocomplete: "off", col3: true, placeholder: "TX" },
{ name: "zip", label: "ZIP code", type: "text", autocomplete: "off", col3: true, placeholder: "75074" },
];
function licenseOptions() {
var a = [];
for (var i = 1; i <= 10; i++) a.push(String(i));
return a;
}
var modal = null; // overlay element (built once)
var body = null; // the step container inside the modal
var activeBtn = null; // the .deal-btn that opened the modal
var lastFocused = null; // element to restore focus to on close
// plan: null = normal paid ($2,000/license). "demo" = fixed $1 Stripe test.
// "free" = $0 no-card demo (page/19.html): runs the whole flow but skips Stripe
// entirely and lands on /thanks, so the team can demo it without a card.
var state = { step: "form", values: null, plan: null };
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function money(n) {
return "$" + Number(n).toLocaleString("en-US");
}
function digitCount(v) {
return (String(v).match(/\d/g) || []).length;
}
// ---------------------------------------------------------------------------
// Validation. Mirrors api/_lead.js (server is authoritative; this is for UX).
// ---------------------------------------------------------------------------
function validate(values) {
var errors = {};
if (values.firstName.length < 1) errors.firstName = "Please enter your first name.";
if (values.lastName.length < 1) errors.lastName = "Please enter your last name.";
if (values.companyName.length < 2) errors.companyName = "Please enter your company name.";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(values.email)) errors.email = "Please enter a valid email address.";
var d = digitCount(values.phone);
if (d < 10 || d > 15) errors.phone = "Please enter a valid phone number.";
if (SALES_REP_BUCKETS.indexOf(values.salesReps) === -1) errors.salesReps = "Please select how many sales reps you have.";
var lic = Number(values.licenses);
if (!Number.isInteger(lic) || lic < 1 || lic > MAX_LICENSES) errors.licenses = "Please choose 1 to " + MAX_LICENSES + " licenses.";
if (values.streetAddress.length < 3) errors.streetAddress = "Please enter your street address.";
if (values.city.length < 2) errors.city = "Please enter your city.";
if (values.state.length < 2) errors.state = "Please enter your state.";
if (!/^\d{5}(-\d{4})?$/.test(values.zip)) errors.zip = "Please enter a valid ZIP code.";
if (!values.smsConsent) errors.smsConsent = "Please agree to receive SMS updates to continue.";
return errors;
}
function readFormValues() {
function val(n) {
var el = body.querySelector('[name="' + n + '"]');
return el ? String(el.value || "").trim() : "";
}
return {
firstName: val("firstName"),
lastName: val("lastName"),
companyName: val("companyName"),
email: val("email").toLowerCase(),
phone: val("phone"),
salesReps: val("salesReps"),
licenses: parseInt(val("licenses"), 10) || 1,
streetAddress: val("streetAddress"),
city: val("city"),
state: val("state"),
zip: val("zip"),
smsConsent: body.querySelector('[name="smsConsent"]').checked,
};
}
function showErrors(errors) {
var names = FIELDS.map(function (f) { return f.name; }).concat(["smsConsent"]);
var first = null;
names.forEach(function (name) {
var msgEl = body.querySelector('[data-err="' + name + '"]');
var input = body.querySelector('[name="' + name + '"]');
var msg = errors[name];
if (msgEl) msgEl.textContent = msg || "";
if (input) {
input.setAttribute("aria-invalid", msg ? "true" : "false");
if (msg && !first) first = input;
}
});
if (first && first.focus) first.focus();
}
// ---------------------------------------------------------------------------
// Address autocomplete. Provider: Photon (photon.komoot.io) — OpenStreetMap,
// FREE, no API key, CORS-enabled. Chosen because no provider key was supplied.
// TO SWAP FOR GOOGLE PLACES: replace geocode()'s body so it returns [{label}].
// ---------------------------------------------------------------------------
function geocode(query) {
var url = "https://photon.komoot.io/api/?limit=5&lang=en&q=" + encodeURIComponent(query);
return fetch(url)
.then(function (r) { return r.ok ? r.json() : { features: [] }; })
.then(function (data) {
return (data.features || []).map(function (f) {
var p = f.properties || {};
var street = [p.housenumber, p.street].filter(Boolean).join(" ") ||
(p.name && p.name !== (p.city || p.town) ? p.name : "");
var city = p.city || p.town || p.village || p.county || "";
var state = p.state || "";
var zip = p.postcode || "";
// Structured components used to fill the separate fields on select.
var comp = { street: street, city: city, state: state, zip: zip };
var label = [street, city, [state, zip].filter(Boolean).join(" "), p.country]
.filter(Boolean).join(", ");
comp.label = label;
return comp;
}).filter(function (s) { return s.label; });
})
.catch(function () { return []; });
}
// Fill the street field plus city/state/zip from a chosen suggestion, and
// clear their error messages.
function fillAddress(comp) {
var map = { streetAddress: comp.street, city: comp.city, state: comp.state, zip: comp.zip };
Object.keys(map).forEach(function (name) {
var el = body.querySelector('[name="' + name + '"]');
if (el && map[name]) {
el.value = map[name];
el.setAttribute("aria-invalid", "false");
}
var msg = body.querySelector('[data-err="' + name + '"]');
if (msg && map[name]) msg.textContent = "";
});
}
function attachAddressAutocomplete(input) {
var box = document.createElement("ul");
box.className = "lu-ac";
box.setAttribute("hidden", "");
input.parentNode.appendChild(box);
var timer = null, lastQuery = "";
function hide() { box.setAttribute("hidden", ""); box.innerHTML = ""; }
function render(items) {
box.innerHTML = "";
if (!items.length) { hide(); return; }
items.forEach(function (it) {
var li = document.createElement("li");
li.className = "lu-ac-item";
li.textContent = it.label;
li.addEventListener("mousedown", function (e) {
e.preventDefault();
// Prefer the parsed street; fall back to the full label if none.
input.value = it.street || it.label;
fillAddress(it);
hide();
});
box.appendChild(li);
});
box.removeAttribute("hidden");
}
input.addEventListener("input", function () {
var q = input.value.trim();
if (timer) clearTimeout(timer);
if (q.length < 4) { hide(); return; }
timer = setTimeout(function () {
if (q === lastQuery) return;
lastQuery = q;
geocode(q).then(function (items) { if (input.value.trim() === q) render(items); });
}, 250);
});
input.addEventListener("blur", function () { setTimeout(hide, 120); });
input.addEventListener("keydown", function (e) { if (e.key === "Escape") hide(); });
}
// ---------------------------------------------------------------------------
// Markup helpers
// ---------------------------------------------------------------------------
function fieldHtml(f) {
var id = "lu-f-" + f.name;
var common = 'id="' + id + '" name="' + f.name + '" autocomplete="' + f.autocomplete +
'" aria-describedby="lu-e-' + f.name + '" class="lu-input"';
var control;
if (f.type === "select") {
var opts = (f.empty ? '<option value="">' + esc(f.empty) + "</option>" : "") +
f.options.map(function (o) {
var sel = f.value === o ? " selected" : "";
return '<option value="' + esc(o) + '"' + sel + ">" + esc(o) + "</option>";
}).join("");
control = "<select " + common + ">" + opts + "</select>";
} else {
control = "<input " + common + ' type="' + f.type + '" placeholder="' + esc(f.placeholder || "") + '" />';
}
return (
'<div class="lu-field' + (f.full ? " lu-field-full" : "") + '">' +
'<label class="lu-label" for="' + id + '">' + esc(f.label) + ' <span class="lu-req">*</span></label>' +
control +
'<div class="lu-err" id="lu-e-' + f.name + '" data-err="' + f.name + '" aria-live="polite"></div>' +
"</div>"
);
}
// ---------------------------------------------------------------------------
// Step 1 — registration form
// ---------------------------------------------------------------------------
function renderForm() {
state.step = "form";
body.innerHTML =
'<h2 class="lu-title">Claim your Founders License</h2>' +
'<p class="lu-sub">Tell us a bit about your business so we can set up your account and licenses.</p>' +
'<form class="lu-form" novalidate>' +
'<div class="lu-grid">' + FIELDS.filter(function (f) { return !f.col3; }).map(fieldHtml).join("") + "</div>" +
'<div class="lu-grid3">' + FIELDS.filter(function (f) { return f.col3; }).map(fieldHtml).join("") + "</div>" +
'<div class="lu-subtotal" data-subtotal></div>' +
'<div class="lu-consent">' +
'<label class="lu-check"><input type="checkbox" name="smsConsent" aria-describedby="lu-e-smsConsent" />' +
'<span class="lu-consent-text">' + esc(SMS_CONSENT_TEXT) + "</span></label>" +
'<div class="lu-err" data-err="smsConsent" id="lu-e-smsConsent" aria-live="polite"></div>' +
"</div>" +
'<div class="lu-form-err" data-form-err aria-live="polite"></div>' +
'<button type="submit" class="lu-submit">Continue</button>' +
'<p class="lu-fine">All fields are required. Next you\'ll review your order.</p>' +
"</form>";
// Restore previously entered values if the buyer stepped back.
if (state.values) {
Object.keys(state.values).forEach(function (k) {
var el = body.querySelector('[name="' + k + '"]');
if (!el) return;
if (el.type === "checkbox") el.checked = !!state.values[k];
else el.value = state.values[k];
});
}
body.querySelector("form").addEventListener("submit", onFormSubmit);
var addr = body.querySelector('[name="streetAddress"]');
if (addr) attachAddressAutocomplete(addr);
var lic = body.querySelector('[name="licenses"]');
if (lic) { lic.addEventListener("change", updateSubtotal); }
updateSubtotal();
focusFirst('[name="firstName"]');
}
function updateSubtotal() {
var el = body.querySelector("[data-subtotal]");
var lic = body.querySelector('[name="licenses"]');
if (!el || !lic) return;
var n = parseInt(lic.value, 10) || 1;
if (n > SELF_SERVE_MAX) {
el.innerHTML = '<span class="lu-subtotal-note">' + n + " licenses — you may qualify for special founder pricing. We'll take your details next.</span>";
} else {
el.innerHTML = "<span>" + n + " × " + money(UNIT_PRICE) + "</span><strong>" + money(UNIT_PRICE * n) + "</strong>";
}
}
function onFormSubmit(e) {
e.preventDefault();
var values = readFormValues();
var errors = validate(values);
body.querySelector("[data-form-err]").textContent = "";
showErrors(errors);
if (Object.keys(errors).length) return;
state.values = values;
setSubmitting(true, "Saving your details…");
// Capture the record server-side FIRST (both branches), then route.
fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
.then(function (res) {
setSubmitting(false);
if (!res.ok) {
if (res.data && res.data.fields) { showErrors(res.data.fields); return; }
throw new Error((res.data && res.data.error) || "Could not save your details.");
}
var path = (res.data && res.data.path) || (values.licenses > SELF_SERVE_MAX ? "sales" : "checkout");
if (path === "sales") renderSales();
else renderCart();
})
.catch(function (err) {
setSubmitting(false);
console.error("Register error:", err);
var fe = body.querySelector("[data-form-err]");
if (fe) fe.textContent = "Sorry, something went wrong. Please try again in a moment.";
});
}
// ---------------------------------------------------------------------------
// Step 2 — cart + terms (<= SELF_SERVE_MAX licenses)
// ---------------------------------------------------------------------------
function renderCart() {
state.step = "cart";
var n = state.values.licenses;
var free = state.plan === "free";
var unit = free ? 0 : UNIT_PRICE;
var total = unit * n;
body.innerHTML =
'<button type="button" class="lu-back" data-back>← Back</button>' +
'<h2 class="lu-title">' + (free ? "Review your demo" : "Review your order") + "</h2>" +
'<p class="lu-sub">' +
(free
? "Demo access — no payment and no card required."
: "Founders Lifetime License — one-time payment, use forever.") +
"</p>" +
(free ? '<div class="lu-demo-flag">🎬 Demo mode · $0 · no card</div>' : "") +
'<div class="lu-cart">' +
'<div class="lu-cart-row"><span>Founders Lifetime License' + (free ? " (demo)" : "") + "</span><span>" + money(unit) + " × " + n + "</span></div>" +
'<div class="lu-cart-line"></div>' +
'<div class="lu-cart-row lu-cart-total"><span>Total today</span><span>' + money(total) + "</span></div>" +
'<div class="lu-cart-sub">' + (free ? "Demo · no charge" : "One-time payment") + " · " + n + (n > 1 ? " licenses" : " license") + "</div>" +
"</div>" +
'<div class="lu-terms-label">Deal Terms &amp; Conditions</div>' +
'<div class="lu-terms">' + TERMS.map(function (p) { return "<p>" + esc(p) + "</p>"; }).join("") + "</div>" +
'<div class="lu-consent">' +
'<label class="lu-check"><input type="checkbox" name="termsAgree" />' +
'<span class="lu-consent-text">I have read and agree to the Deal Terms &amp; Conditions above.</span></label>' +
'<div class="lu-err" data-err="termsAgree" aria-live="polite"></div>' +
"</div>" +
'<div class="lu-form-err" data-form-err aria-live="polite"></div>' +
'<button type="button" class="lu-submit" data-pay>' +
(free ? "Complete demo" : "Continue to Secure Payment") + "</button>" +
'<p class="lu-fine">' +
(free
? "No card needed — this is a demo of the full flow."
: "You'll be redirected to Stripe to complete payment securely.") +
"</p>";
body.querySelector("[data-back]").addEventListener("click", renderForm);
body.querySelector("[data-pay]").addEventListener("click", onPay);
focusFirst("[data-pay]");
}
function onPay() {
var agree = body.querySelector('[name="termsAgree"]');
var errEl = body.querySelector('[data-err="termsAgree"]');
if (!agree.checked) {
if (errEl) errEl.textContent = "Please agree to the terms to continue.";
agree.focus();
return;
}
if (errEl) errEl.textContent = "";
// $0 no-card demo (page/19.html): the record was already captured at Step 1,
// so just land on the Thanks page — no Stripe, no card. Full flow, zero cost.
if (state.plan === "free") {
setSubmitting(true, "Finishing demo…", "[data-pay]");
window.location.href = "/thanks?demo=1";
return;
}
setSubmitting(true, "Loading checkout…", "[data-pay]");
var plan = activeBtn ? activeBtn.getAttribute("data-plan") : null;
var promoEl = document.querySelector("[data-promo-input]");
var promo = promoEl && promoEl.value ? promoEl.value.trim() : "";
var qs = [];
if (plan) qs.push("plan=" + encodeURIComponent(plan));
if (promo) qs.push("promo=" + encodeURIComponent(promo));
var endpoint = "/api/checkout" + (qs.length ? "?" + qs.join("&") : "");
var payload = Object.assign({}, state.values, { termsAgreed: true });
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, data: d }; }); })
.then(function (res) {
if (res.ok && res.data && res.data.url) { window.location.href = res.data.url; return; }
// Server guard: bulk orders route to sales even if we reach here.
if (res.data && res.data.path === "sales") { renderSales(); return; }
setSubmitting(false, null, "[data-pay]");
var fe = body.querySelector("[data-form-err]");
if (fe) fe.textContent = (res.data && res.data.error) || "We couldn't start checkout. Please try again.";
})
.catch(function (err) {
setSubmitting(false, null, "[data-pay]");
console.error("Checkout error:", err);
var fe = body.querySelector("[data-form-err]");
if (fe) fe.textContent = "Sorry, we couldn't start the checkout. Please try again in a moment.";
});
}
// ---------------------------------------------------------------------------
// Sales screen (> SELF_SERVE_MAX licenses)
// ---------------------------------------------------------------------------
function buildMailto() {
var v = state.values || {};
var name = [v.firstName, v.lastName].filter(Boolean).join(" ");
var address = [v.streetAddress, v.city, [v.state, v.zip].filter(Boolean).join(" ")]
.filter(Boolean).join(", ");
var subject = "Founders License — bulk order (" + (v.licenses || "") + " licenses)";
var lines = [
"Hi LynkedUp Pro team,",
"",
"I'd like to discuss a bulk Founders License order.",
"",
"Name: " + name,
"Company: " + (v.companyName || ""),
"Email: " + (v.email || ""),
"Phone: " + (v.phone || ""),
"Sales reps: " + (v.salesReps || ""),
"Licenses wanted: " + (v.licenses || ""),
"Business address: " + address,
];
return "mailto:" + SALES_EMAIL + "?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(lines.join("\n"));
}
function renderSales() {
state.step = "sales";
var n = state.values.licenses;
body.innerHTML =
'<button type="button" class="lu-back" data-back>← Back</button>' +
'<div class="lu-celebrate">🎉</div>' +
'<h2 class="lu-title lu-center">Congratulations — you may be eligible for a special discount</h2>' +
'<p class="lu-sub lu-center">You\'re looking at <strong>' + n + " licenses</strong>. Orders above " + SELF_SERVE_MAX +
" licenses qualify for special founder pricing that isn't available online. Your details are saved — our team will reach out, or you can start the conversation now." +
"</p>" +
'<a class="lu-submit lu-link-btn" href="' + buildMailto() + '">Talk to Sales</a>' +
'<p class="lu-fine">Prefer fewer licenses? Go back and choose ' + SELF_SERVE_MAX + " or fewer to check out instantly.</p>";
body.querySelector("[data-back]").addEventListener("click", renderForm);
focusFirst(".lu-link-btn");
}
// ---------------------------------------------------------------------------
// Shared modal chrome
// ---------------------------------------------------------------------------
function focusFirst(sel) {
var el = body.querySelector(sel);
if (el && el.focus) el.focus();
}
function setSubmitting(on, label, sel) {
var b = body.querySelector(sel || ".lu-submit");
if (!b) return;
b.disabled = !!on;
if (on) { b.dataset.orig = b.dataset.orig || b.textContent; b.textContent = label || "Loading…"; }
else if (b.dataset.orig) b.textContent = b.dataset.orig;
}
var CSS =
".lu-overlay{position:fixed;inset:0;z-index:2147483000;background:rgba(6,6,10,.78);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding:24px 16px;overflow-y:auto;}" +
".lu-overlay[hidden]{display:none;}" +
".lu-modal{position:relative;width:100%;max-width:560px;margin:auto;background:#121216;border:1px solid rgba(255,255,255,.10);border-radius:18px;padding:28px;box-shadow:0 30px 80px rgba(0,0,0,.6);font-family:inherit;}" +
".lu-close{position:absolute;top:14px;right:16px;width:32px;height:32px;border:0;border-radius:8px;background:rgba(255,255,255,.06);color:#fff;font-size:20px;line-height:1;cursor:pointer;z-index:2;}" +
".lu-close:hover{background:rgba(255,255,255,.12);}" +
".lu-back{background:none;border:0;color:#9a9aa2;font-size:13px;font-family:inherit;cursor:pointer;padding:0;margin:0 0 12px;}" +
".lu-back:hover{color:#fff;}" +
".lu-title{margin:0 0 6px;color:#fff;font-size:22px;font-weight:800;letter-spacing:-.01em;line-height:1.25;}" +
".lu-center{text-align:center;}" +
".lu-sub{margin:0 0 20px;color:#9a9aa2;font-size:13.5px;line-height:1.55;}" +
".lu-sub strong{color:#fff;}" +
".lu-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}" +
".lu-field-full{grid-column:1/-1;position:relative;}" +
".lu-grid3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:14px;margin-top:14px;}" +
"@media(max-width:560px){.lu-grid3{grid-template-columns:1fr 1fr;}}" +
".lu-label{display:block;margin-bottom:6px;color:#e6e6ea;font-size:12.5px;font-weight:600;}" +
".lu-req{color:#ff9321;}" +
".lu-input{width:100%;box-sizing:border-box;padding:11px 13px;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:#fff;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s;}" +
".lu-input::placeholder{color:#6f6f78;}" +
".lu-input:focus{border-color:#4dc5ff;box-shadow:0 0 0 3px rgba(77,197,255,.15);}" +
'.lu-input[aria-invalid="true"]{border-color:#ff5f56;}' +
"select.lu-input{appearance:none;cursor:pointer;}select.lu-input option{background:#121216;color:#fff;}" +
".lu-ac{position:absolute;left:0;right:0;top:100%;margin:4px 0 0;padding:4px;list-style:none;z-index:5;background:#1b1b21;border:1px solid rgba(255,255,255,.14);border-radius:10px;box-shadow:0 18px 40px rgba(0,0,0,.5);max-height:220px;overflow-y:auto;}" +
".lu-ac[hidden]{display:none;}" +
".lu-ac-item{padding:9px 11px;border-radius:7px;color:#e6e6ea;font-size:13px;cursor:pointer;}" +
".lu-ac-item:hover{background:rgba(77,197,255,.14);}" +
".lu-subtotal{display:flex;align-items:center;justify-content:space-between;margin:14px 0 2px;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#c9c9d2;font-size:13px;}" +
".lu-subtotal strong{color:#fff;font-size:16px;}" +
".lu-subtotal-note{color:#ffb066;font-size:12.5px;line-height:1.4;}" +
".lu-cart{margin:0 0 18px;padding:18px;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);}" +
".lu-cart-row{display:flex;align-items:center;justify-content:space-between;color:#e6e6ea;font-size:14px;}" +
".lu-cart-line{height:1px;background:rgba(255,255,255,.10);margin:14px 0;}" +
".lu-cart-total{font-weight:800;}" +
".lu-cart-total span:last-child{font-size:24px;color:#fff;}" +
".lu-cart-sub{color:#8a8a93;font-size:11.5px;margin-top:6px;}" +
".lu-terms-label{color:#e6e6ea;font-size:12.5px;font-weight:700;margin:0 0 6px;}" +
".lu-terms{max-height:150px;overflow-y:auto;padding:12px 14px;border-radius:10px;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.08);color:#9a9aa2;font-size:11.5px;line-height:1.6;}" +
".lu-terms p{margin:0 0 9px;}.lu-terms p:last-child{margin:0;}" +
".lu-consent{margin:16px 0 4px;}" +
".lu-check{display:flex;gap:10px;align-items:flex-start;cursor:pointer;}" +
".lu-check input{margin-top:2px;width:16px;height:16px;flex:0 0 auto;accent-color:#4dc5ff;cursor:pointer;}" +
".lu-consent-text{color:#9a9aa2;font-size:11.5px;line-height:1.5;}" +
".lu-form-err{color:#ff6b61;font-size:12.5px;min-height:16px;margin:6px 0;}" +
".lu-err{color:#ff6b61;font-size:11.5px;margin-top:4px;min-height:14px;}" +
".lu-submit{display:block;width:100%;margin-top:8px;padding:14px 24px;border:0;border-radius:12px;color:#fff;font-size:15px;font-weight:700;font-family:inherit;cursor:pointer;text-align:center;text-decoration:none;box-sizing:border-box;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,.3);}" +
".lu-submit:disabled{opacity:.6;cursor:default;}" +
".lu-link-btn{margin-top:14px;}" +
".lu-demo-flag{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;background:rgba(77,197,255,.14);border:1px solid rgba(77,197,255,.35);color:#8fd6ff;font-size:11.5px;font-weight:700;}" +
".lu-celebrate{font-size:44px;text-align:center;margin:4px 0 10px;}" +
".lu-fine{margin:12px 0 0;text-align:center;color:#6f6f78;font-size:11px;}" +
"@media(max-width:560px){.lu-modal{padding:22px 18px;}.lu-grid{grid-template-columns:1fr;}}";
function injectCss() {
if (document.getElementById("lu-checkout-css")) return;
var s = document.createElement("style");
s.id = "lu-checkout-css";
s.textContent = CSS;
document.head.appendChild(s);
}
function build() {
var el = document.createElement("div");
el.className = "lu-overlay";
el.setAttribute("hidden", "");
el.innerHTML =
'<div class="lu-modal" role="dialog" aria-modal="true">' +
'<button type="button" class="lu-close" aria-label="Close">&times;</button>' +
'<div class="lu-body"></div>' +
"</div>";
el.addEventListener("click", function (e) { if (e.target === el) close(); });
el.querySelector(".lu-close").addEventListener("click", close);
document.body.appendChild(el);
body = el.querySelector(".lu-body");
return el;
}
function open(btn) {
injectCss();
if (!modal) modal = build();
activeBtn = btn;
lastFocused = document.activeElement;
// keep prior entries if reopened; read the plan off the clicked button
state = { step: "form", values: state.values, plan: btn.getAttribute("data-plan") };
modal.removeAttribute("hidden");
document.body.style.overflow = "hidden";
renderForm();
document.addEventListener("keydown", onKeydown, true);
}
function close() {
if (!modal || modal.hasAttribute("hidden")) return;
modal.setAttribute("hidden", "");
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown, true);
if (lastFocused && lastFocused.focus) lastFocused.focus();
activeBtn = null;
}
function onKeydown(e) {
if (e.key === "Escape") { close(); return; }
if (e.key !== "Tab") return;
var f = modal.querySelectorAll("button, input, select, textarea, a[href]");
var list = [];
for (var i = 0; i < f.length; i++) if (!f[i].disabled && f[i].offsetParent !== null) list.push(f[i]);
if (!list.length) return;
var firstEl = list[0], lastEl = list[list.length - 1];
if (e.shiftKey && document.activeElement === firstEl) { e.preventDefault(); lastEl.focus(); }
else if (!e.shiftKey && document.activeElement === lastEl) { e.preventDefault(); firstEl.focus(); }
}
// Re-enable a stuck button after a bfcache restore (e.g. back from Stripe).
window.addEventListener("pageshow", function (e) {
if (e.persisted && body) { var b = body.querySelector(".lu-submit"); if (b) { b.disabled = false; if (b.dataset.orig) b.textContent = b.dataset.orig; } }
});
document.addEventListener(
"click",
function (e) {
var target = e.target;
var btn = target && target.closest ? target.closest(".deal-btn") : null;
if (!btn) return;
e.preventDefault();
open(btn);
},
true
);
})();