1// Embedded Stripe Payment Element checkout. 2// 3// Activates on any page that renders a `#cc-embedded-checkout` container (the 4// buy page, behind the Checkout:Embedded flag). When there are multiple 5// household members, the buyer picks one first (`[data-cc-member-step]`), then 6// clicks continue â only THEN do we create the PaymentIntent, so changing the 7// recipient never mints extra intents / discount reservations. With a single 8// member it starts immediately. It asks our own endpoint to create the 9// PaymentIntent (+ reserve any discount), mounts Stripe's Payment Element on 10// the provider's connected account, and confirms in-page â the buyer never 11// leaves CoachCentral. Fulfillment is entirely webhook-driven 12// (payment_intent.succeeded â grant); a client that lies about success grants 13// nothing. 14// 15// Follows the app's enhanced-navigation pattern (see forms.js / 16// courses-editor-init.js and CLAUDE.md): init is idempotent (a data-marker on 17// the container) and re-runs after each Blazor enhancedload content swap. 18(function () { 19 function initEmbeddedCheckout() { 20 var container = document.querySelector('#cc-embedded-checkout'); 21 if (!container || container.__ccStripeBound) return; 22 container.__ccStripeBound = true; 23 24 var memberStep = container.querySelector('[data-cc-member-step]'); 25 if (memberStep) { 26 // Multi-member: defer intent creation until the buyer commits to a 27 // recipient (avoids a PaymentIntent + discount reservation per 28 // radio click). 29 var continueBtn = memberStep.querySelector('[data-cc-continue]'); 30 if (!continueBtn) return; 31 continueBtn.addEventListener('click', function () { 32 var checked = memberStep.querySelector('input[name="cc-member"]:checked'); 33 var forClientId = checked ? checked.value : container.getAttribute('data-for-client-id'); 34 var planChecked = memberStep.querySelector('input[name="cc-payplan"]:checked'); 35 var payPlan = planChecked ? planChecked.value === 'true' : false; 36 var paymentStep = container.querySelector('[data-cc-payment-step]'); 37 memberStep.hidden = true; 38 if (paymentStep) paymentStep.hidden = false; 39 startPayment(container, forClientId, payPlan); 40 }); 41 } else { 42 // Single member, no plan choice: start straight away with the default 43 // contact, pay-in-full. (A plan offer renders the options step above, 44 // so we never auto-start when there's a choice to make.) 45 startPayment(container, container.getAttribute('data-for-client-id'), false); 46 } 47 } 48 49 // Blazor SSR bakes an antiforgery token into the page via 50 // <AntiforgeryToken />, but enhanced navigation (and the inline sign-in 51 // identity change on the buy page) can leave that DOM token stale relative 52 // to the antiforgery cookie â a stale token 400s in middleware before our 53 // endpoint runs. Fetch a fresh token (the endpoint also re-syncs the 54 // cookie) right before POSTing; fall back to the DOM token if unreachable. 55 async function getAntiforgeryToken() { 56 try { 57 var r = await fetch('/buy/antiforgery-token', { 58 headers: { 'Accept': 'application/json' }, 59 }); 60 if (r.ok) { 61 var j = await r.json(); 62 if (j && j.token) return j.token; 63 } 64 } catch (e) { /* fall through to the DOM token */ } 65 var el = document.querySelector('input[name="__RequestVerificationToken"]'); 66 return el ? el.value : ''; 67 } 68 69 async function startPayment(container, forClientId, payPlan) { 70 var intentUrl = container.getAttribute('data-intent-url'); 71 var promoCode = container.getAttribute('data-promo-code') || ''; 72 var returnUrl = container.getAttribute('data-return-url'); 73 74 var errorEl = container.querySelector('[data-cc-checkout-error]'); 75 var submitBtn = container.querySelector('[data-cc-checkout-submit]'); 76 var mountEl = container.querySelector('#payment-element'); 77 if (!intentUrl || !mountEl || typeof Stripe === 'undefined') return; 78 79 function showError(msg) { 80 if (!errorEl) return; 81 errorEl.textContent = msg; 82 errorEl.hidden = false;
83 } 84 85 // 1. Create the PaymentIntent server-side (also reserves the discount). 86 // Fetch a FRESH antiforgery token first: a token scraped from the DOM 87 // (rendered by <AntiforgeryToken />) can be stale after enhanced 88 // navigation or the inline sign-in identity change, which makes this 89 // POST fail antiforgery validation in middleware and 400 before our 90 // endpoint runs. The fresh token always matches the current cookie. 91 var token = await getAntiforgeryToken(); 92 var body = new FormData(); 93 body.append('forClientId', forClientId); 94 body.append('payPlan', payPlan ? 'true' : 'false'); 95 if (promoCode) body.append('promoCode', promoCode); 96 if (token) body.append('__RequestVerificationToken', token); 97 98 var resp, data; 99 try { 100 resp = await fetch(intentUrl, { 101 method: 'POST', 102 body: body, 103 headers: { 'Accept': 'application/json' }, 104 }); 105 data = await resp.json(); 106 } catch (e) { 107 showError('Could not start checkout. Please try again.'); 108 return; 109 } 110 111 // $0 order (free / 100%-off): already materialized server-side. 112 if (data && data.completed) { 113 window.location.assign(returnUrl); 114 return; 115 } 116 if (!resp.ok || !data || !data.clientSecret || !data.publishableKey) { 117 showError((data && data.error) || 'Could not start checkout.'); 118 return; 119 } 120 121 // 2. Mount the Payment Element against the provider's connected account. 122 var stripe = data.stripeAccountId 123 ? Stripe(data.publishableKey, { stripeAccount: data.stripeAccountId }) 124 : Stripe(data.publishableKey); 125 var elements = stripe.elements({ clientSecret: data.clientSecret }); 126 var paymentElement = elements.create('payment'); 127 // Clear the "Loadingâ¦" placeholder so Stripe mounts into an empty node 128 // (it warns about child nodes otherwise); the Element replaces it anyway. 129 mountEl.textContent = ''; 130 paymentElement.mount(mountEl); 131 if (submitBtn) submitBtn.disabled = false; 132 133 // 3. Confirm on submit. confirmPayment redirects to return_url on 134 // success (incl. after any 3DS challenge); it only resolves here on 135 // a validation / card error, which we surface without leaving. 136 var form = container.querySelector('form'); 137 if (!form) return; 138 form.addEventListener('submit', async function (e) { 139 e.preventDefault(); 140 if (submitBtn) submitBtn.disabled = true; 141 if (errorEl) errorEl.hidden = true; 142 143 var result = await stripe.confirmPayment({ 144 elements: elements, 145 confirmParams: { return_url: returnUrl }, 146 }); 147 if (result.error) { 148 showError(result.error.message || 'Payment could not be completed.'); 149 if (submitBtn) submitBtn.disabled = false; 150 } 151 }); 152 } 153 154 if (document.readyState === 'loading') { 155 document.addEventListener('DOMContentLoaded', initEmbeddedCheckout); 156 } else { 157 initEmbeddedCheckout(); 158 } 159 160 // window.Blazor may not exist yet when this script loads â retry briefly. 161 function tryHookBlazor(retries) { 162 if (typeof Blazor !== 'undefined' && Blazor.addEventListener) { 163 Blazor.addEventListener('enhancedload', initEmbeddedCheckout); 164 return; 165 } 166 if (retries <= 0) return; 167 setTimeout(function () { tryHookBlazor(retries - 1); }, 50); 168 } 169 tryHookBlazor(20); 170})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.