Reserve Your Spot for the Winter Sprint Series!

First meeting is December 1st at 8PM EST

(function(){ var s=document.querySelector("script[data-checkout-runtime]"); var pageId=s.getAttribute("data-page-id"); // Baked into a converted migrated Classic pop-up (checkout-migration/convert-org-popup-checkouts.ts): // the pop-up is its own document, framed by its host page, so mount up front and hand the // post-payment redirect to the host, whose relay navigates the top window. var hpInPopup=s.hasAttribute("data-checkout-popup"); var slug=s.getAttribute("data-page-slug"); var apiBase=s.getAttribute("data-api-url"); var gateways=["stripe"]; var corrId=0; var pendingCallbacks={}; var inSandbox=false; try{inSandbox=window!==window.parent&&!window.__lpJsEmbed;}catch(e){inSandbox=true;} function getConfig(el){ // HP-5123 - capture the authored success block BEFORE any mount wipes the // element's innerHTML, plus the authorable failure/pending copy. The block is // real page content (hidden inline until purchase), so every editor tool works // on it; missing block/attrs fall back to the hardcoded output below verbatim. var sb=el.querySelector?el.querySelector("[data-checkout-success]"):null; return{ successHtml:sb?sb.outerHTML:null, failHeading:el.getAttribute("data-checkout-fail-heading")||null, failText:el.getAttribute("data-checkout-fail-text")||null, pendingText:el.getAttribute("data-checkout-pending-text")||null, priceId:el.getAttribute("data-price-id")||null, amount:el.getAttribute("data-amount")?parseInt(el.getAttribute("data-amount"),10):null, currency:el.getAttribute("data-currency")||"usd", productName:el.getAttribute("data-product-name")||"Purchase", successUrl:el.getAttribute("data-success-url")||null, cancelUrl:el.getAttribute("data-cancel-url")||null, mode:el.getAttribute("data-mode")||"payment", gateway:el.getAttribute("data-gateway")||null, shopifyVariant:el.getAttribute("data-shopify-variant")||null, lemonSqueezyVariant:el.getAttribute("data-lemonsqueezy-variant")||null, gumroadProduct:el.getAttribute("data-gumroad-product")||null, quantity:el.getAttribute("data-quantity")?parseInt(el.getAttribute("data-quantity"),10):1, hpPriceId:el.getAttribute("data-checkout-price")||null, hpCheckoutId:el.getAttribute("data-persist-checkout-id")||null, hpClassicFormIdentifier:el.getAttribute("data-checkout-form-identifier")||null, hpClassicFormUuid:el.getAttribute("data-persist-form-source-id")||null, accent:el.getAttribute("data-checkout-accent")||null, accentText:el.getAttribute("data-checkout-accent-text")||null, heading:el.getAttribute("data-checkout-heading")||null, redirect:el.getAttribute("data-checkout-redirect")||null, fields:el.getAttribute("data-checkout-fields")||"", theme:el.getAttribute("data-checkout-theme")||"brand", hideLink:el.getAttribute("data-checkout-hide-link")==="1", // Minimal-fields checkout (Classic parity). Both are OPT-OUTS: the attribute is // absent on every checkout published before this shipped, and absent means the // field keeps rendering, so no live page changes shape on deploy. // noName drops OUR name input. minAddress narrows what Stripe's Payment Element // asks for to the address fields the payment actually requires, which drops the // country selector and leaves the postal code on a US card account - the // Classic layout the ask is for. Stripe decides that minimum per method, so // this is a request for the shortest valid form, not a promise about the exact // rows: an account whose cards do not require a postal code loses that too. // Narrows the intent to cards. Unlike hideLink this MUST reach the server - // the method list lives on the PaymentIntent, not the Element. Safe to trust // from the client: it only ever narrows the offer, never touches the amount. cardOnly:el.getAttribute("data-checkout-card-only")==="1", // "" = Payment Element (number / expiry / CVC on their own rows, wallets, every // method). "single" = Stripe's Card Element, the one-line widget Classic used: // number, expiry, CVC and postal-where-applicable in a single input. Additive - // absent keeps the Payment Element, which is what every existing page has. cardField:el.getAttribute("data-checkout-card-field")||"", noName:el.getAttribute("data-checkout-no-name")==="1", minAddress:el.getAttribute("data-checkout-min-address")==="1", // HP-4692 order bump + discount code. The label/price text are display-only // (authored in the inspector); the server re-derives every amount from the // catalog, so a tampered price text changes what the buyer READS, never pays. bumpPriceId:el.getAttribute("data-checkout-bump")||null, bumpLabel:el.getAttribute("data-checkout-bump-label")||"", bumpDesc:el.getAttribute("data-checkout-bump-desc")||"", bumpPriceText:el.getAttribute("data-checkout-bump-price-text")||"", // Border treatment for the bump card. Deliberately NOT a colour: the card // derives its accent from data-checkout-accent, which is already paired with a // legible text colour. A free colour here is the one thing that can make a // payment form unreadable, which costs the merchant money directly. bumpStyle:el.getAttribute("data-checkout-bump-style")||"dashed", promo:el.getAttribute("data-checkout-promo")==="1", promoLabel:el.getAttribute("data-checkout-promo-label")||"", promoPlaceholder:el.getAttribute("data-checkout-promo-placeholder")||"", promoApplyLabel:el.getAttribute("data-checkout-promo-apply-label")||"", // Prompt treatment, same no-colour rule as the bump's emphasis. promoStyle:el.getAttribute("data-checkout-promo-style")||"link", // Skip the prompt entirely and show the field from the start, for funnels // where the code is the point rather than an afterthought. promoOpen:el.getAttribute("data-checkout-promo-open")==="1", // HP-5172 - authorable label/placeholder for the fields WE render. Empty or // absent falls back to today's literal in the field() calls below, so every // existing page renders identically. Stripe-owned rows (address, card) take // their labels from Stripe, not from these. nameLabel:el.getAttribute("data-checkout-name-label")||"", namePlaceholder:el.getAttribute("data-checkout-name-placeholder")||"", emailLabel:el.getAttribute("data-checkout-email-label")||"", emailPlaceholder:el.getAttribute("data-checkout-email-placeholder")||"", phoneLabel:el.getAttribute("data-checkout-phone-label")||"", phonePlaceholder:el.getAttribute("data-checkout-phone-placeholder")||"", // HP-5172 (sizing) - form-level field size, corner radius and Pay button. All // CLOSED ENUMS; mapped to safe literals below and never concatenated raw into // style.cssText (the same reason the accent is hex-validated). Empty = today's // values, so an unstyled checkout is byte-identical to before. fieldSize:el.getAttribute("data-checkout-field-size")||"", radius:el.getAttribute("data-checkout-radius")||"", btnSize:el.getAttribute("data-checkout-btn-size")||"", btnWidth:el.getAttribute("data-checkout-btn-width")||"", btnAlign:el.getAttribute("data-checkout-btn-align")||"", // Field labels: "" (above, as every checkout renders today) | "floating". // Closed enum, same as the sizing keys. // // Floating rather than hidden on purpose. Stripe's appearance.labels takes // auto|above|floating and offers no way to remove a label at all - a position // its own API takes for its own fields - because a placeholder is not a label: // it vanishes on the first keystroke, so a buyer who tabs back to fix a card // error is left with an unnamed box. Moving the label inside the field gets // the uncluttered form an author wants while the label stays visible, stays // permanent, and stays attached to its input. labels:el.getAttribute("data-checkout-labels")||"" }; } function getVisitorId(){ try{var m=document.cookie.match(/(?:^|; )_hp_vid=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;} } // Fire a checkout_started analytics beacon if the page's analytics runtime // exposed a push hook; harmless no-op otherwise. function fireCheckoutStarted(priceId){ try{if(window.__hpTrack){window.__hpTrack("checkout_started",{priceId:priceId});}}catch(e){} } var stripeJsPromise=null; function loadStripeJs(){ if(window.Stripe)return Promise.resolve(window.Stripe); if(stripeJsPromise)return stripeJsPromise; stripeJsPromise=new Promise(function(resolve,reject){ var sc=document.createElement("script"); sc.src="https://js.stripe.com/v3/"; sc.onload=function(){resolve(window.Stripe);}; sc.onerror=function(){reject(new Error("Stripe.js failed to load"));}; document.head.appendChild(sc); }); return stripeJsPromise; } // Resolve a configured post-purchase redirect to a safe absolute http(s) URL — // relative "/path" and "#anchor" resolve against this page; null if missing or a // non-http(s) scheme (javascript:/data: never navigate). function resolveRedirect(raw){ if(!raw)return null; try{var rr=new URL((""+raw).replace(/^\s+|\s+$/g,""),location.href);if(rr.protocol==="http:"||rr.protocol==="https:")return rr.href;}catch(e){} return null; } // Navigate to a post-purchase redirect. Returns true if it left the page; false // when the target is THIS page (same origin+path — an #anchor, or an absolute URL // to the current path) since that wouldn't reload, so the caller shows its own // success UI in place. Compares origin+pathname+search (NOT hash) so only a true // same-document #anchor stays on-page; a different path OR query string is a real // navigation. The Stripe return params are stripped before this runs on the return // path, so they don't make a same-page target look different. function navigateRedirect(url){ var a;try{a=new URL(url,location.href);}catch(e){a=null;} if(a&&a.origin===location.origin&&a.pathname===location.pathname&&a.search===location.search){ if(a.hash){try{location.hash=a.hash;}catch(e){}} return false; } if(hpInPopup&&window!==window.parent&&a){ // The host relay (worker-pages scripts/popups.ts handleRedirectRelay) acks and navigates the top // window; without an ack (no relay, e.g. an embed on another site) the frame navigates itself. var acked=false; var onAck=function(e){if(e&&e.data&&e.data.type==="hp-popup-redirect-ack")acked=true;}; window.addEventListener("message",onAck); try{parent.postMessage({type:"hp-popup-redirect",url:a.href},"*");}catch(e){} setTimeout(function(){window.removeEventListener("message",onAck);if(!acked)window.location.href=url;},400); return true; } window.location.href=url;return true; } // The "free" argument is the $0.00 checkout: nothing was charged, so "Payment successful" // would assert a payment that never happened - and the reader is most often the // author walking their own form, for whom "no payment was charged" is the fact // they are checking for. function showCheckoutSuccess(host,free,config){ // The authored block (when present) speaks for BOTH the paid and the $0.00 // outcome - the author owns the copy. The hardcoded fallback below keeps the // paid/free distinction for pages authored before HP-5123. if(config&&config.successHtml){ host.innerHTML=config.successHtml; var sb=host.firstElementChild;if(sb){sb.style.display="";} return; } host.innerHTML="
\u2705
"+(free?"You\u2019re all set":"Payment successful")+"
"+(free?"Nothing was charged.":"Thank you for your purchase.")+"
"; } // "ok-free" is the $0.00 checkout returning from a redirecting method. Without // it this overlay asserted "Payment successful / Thank you for your purchase" on // an order that charged nothing - the same claim the on-page panel avoids. function ckEscape(v){return String(v).replace(/&/g,"&").replace(//g,">");} function checkoutReturnHtml(kind,stash){ if(kind==="fail"&&stash&&(stash.failHeading||stash.failText))return "
"+ckEscape(stash.failHeading||"Payment not completed")+"
"+ckEscape(stash.failText||"Your payment was not completed. Please try again.")+"
"; if(kind==="pending"&&stash&&stash.pendingText)return "
"+ckEscape(stash.pendingText)+"
"; if(kind==="ok")return "
\u2705
Payment successful
Thank you for your purchase.
"; if(kind==="ok-free")return "
\u2705
You\u2019re all set
Nothing was charged.
"; if(kind==="fail")return "
Payment not completed
Your payment was not completed. Please try again.
"; return "
Your payment is being processed. You can safely close this page.
"; } // (Re)mount the live checkout into every inline element, replacing the static // facsimile. Used when a return overlay is dismissed so the buyer never lands on a // dead facsimile and can retry after a failed auth. function remountInline(){ try{var els=document.querySelectorAll("[data-checkout-inline]");for(var i=0;i