var stripe = Stripe('pk_live_51PLw9aK6h8aOTojnlB7Hh0Ql2zbHY28BpBDCwRcoldLw0xZ7zK55gfIPFkckLhjobySJyzrDnKvHxgUQhcrjEpAF00SV0bBGPO', { stripeAccount: 'acct_1Nnk3LIJ0hV9od34' }); let elements; let subscriptionId = null; // Store subscription ID for PostHog tracking let connected_account_id = 'acct_1Nnk3LIJ0hV9od34'; let customerId = null; // PostHog: build a consistent, campaign-tagged property bag for donation // events. Self-contained (no dependency on sv-posthog.js) since checkout_js is // rendered across donation/event/sponsorship/membership surfaces. Previously // these success events carried only the intent id + account, so revenue could // not be broken down by campaign, amount, designation, or interval. function svDonationProps(extra) { var get = function (id) { var e = document.getElementById(id); return e ? e.value : null; }; var cents = parseInt(get('amount'), 10); var props = { source: 'web', campaign_id: '51221', connected_account_id: connected_account_id, amount: isNaN(cents) ? null : cents / 100, amount_cents: isNaN(cents) ? null : cents, interval: get('interval'), designation: get('designation'), currency: (window.currency || null) }; return Object.assign(props, extra || {}); } // The URL Meta should record as the conversion page (CAPI `event_source_url`). // Inside the embed iframe the Pixel runs on the parent page, and `document // .referrer` there is the embedding page — use it so the server event's URL // matches the browser event's. Falls back to this frame's own URL. function svEventSourceUrl() { try { if (window.top !== window.self && document.referrer) { return document.referrer; } } catch (e) {} try { return window.location.href; } catch (e) { return ''; } } function svCapture(event, extra) { try { if (window.posthog && typeof window.posthog.capture === 'function') { window.posthog.capture(event, svDonationProps(extra)); } } catch (e) {} } document .querySelector("#payment-form") .addEventListener("submit", handleSubmit); let emailAddress = ''; // Fetches a payment intent and captures the client secret async function initialize(campaignId, designation, amount, interval, email, coverFees, anonymous, startDate = null, endDate = null, checkoutItems = '', checkoutItemsData = '') { var result; emailAddress = email; if (!('acct_1Nnk3LIJ0hV9od34')) { document.querySelector("#payment-element").style.display = "none"; document.querySelector("#address-element").style.display = "none"; document.querySelector("#submit").style.display = "none"; document.querySelector("#payment-message").style.color = "red"; document.querySelector("#payment-message").style.fontSize = "1em"; window.posthog?.capture('invalid_payment_processor', { trigger: 'missing_connected_account', flow: 'donation_checkout', campaign_id: '51221', account_id: 'ec7Oh6qO', connected_account_id: 'acct_1Nnk3LIJ0hV9od34', environment: 'live', page_url: window.location.href, referrer: document.referrer, timestamp: new Date().toISOString(), }); showMessage("Unable to make a payment right now due to an invalid payment processor.", true); return; } amount = parseInt(amount); // Capture the fee-coverage choice AND the covered amount so both survive to // the server. The covered amount is folded into `amount` (that's what gets // charged), but we also send it explicitly so the webhook can persist // donations.fee_covered / covered_fee_amount instead of losing the fact that // the donor opted in. let coverFeeAmount = 0; if (coverFees) { coverFeeAmount = Math.round(amount * 0.02 + amount * 0.022 + 30); amount += coverFeeAmount; } // Collect custom field values var customFields = {}; $('[name^="custom_fields"]').each(function() { var $field = $(this); var fieldId = $field.attr('name').match(/\[(\d+)\]/)[1]; if ($field.attr('type') === 'checkbox') { customFields[fieldId] = $field.is(':checked') ? '1' : '0'; } else if ($field.attr('type') === 'radio') { if ($field.is(':checked')) { customFields[fieldId] = $field.val(); } } else { customFields[fieldId] = $field.val(); } }); // Save custom fields for recurring payments const customFieldsData = JSON.stringify(customFields); // If start date is set and is after today, we aren't processing a payment immediately so just collect payment method through setup intent const today = new Date(); today.setHours(0, 0, 0, 0); const startDateObj = startDate ? new Date(startDate.replace(/-/g, '/')) : null; if (startDate != null && startDate != '' && startDateObj > today) { result = await fetch('/create-setupintent.php?campaign_id=51221', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ campaignId: campaignId, designation: designation, email: email, donationAmount: amount, interval: interval, startDate: startDate, endDate: endDate, anonymous: anonymous, customFields: customFieldsData, feeCovered: coverFees ? 1 : 0, coverFeeAmount: coverFeeAmount, fundraiserId: '', teamId: '' }), }).then((r) => r.json()); } else { result = await fetch("/create.php?campaign_id=51221", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ campaignId: campaignId, designation: designation, interval: interval, donationAmount: amount, email: email, anonymous: anonymous, checkoutItems: checkoutItems, checkoutItemsData: checkoutItemsData, customFields: customFieldsData, feeCovered: coverFees ? 1 : 0, coverFeeAmount: coverFeeAmount, fundraiserId: '', teamId: '', // Meta Pixel identifiers for Conversions API match quality. Read // here (same frame as the pixel) so they survive iframe/cookie // contexts where the server can't see the _fbp/_fbc cookies. fbp: (document.cookie.match(/(?:^|;\s*)_fbp=([^;]+)/) || [])[1] || '', fbc: (document.cookie.match(/(?:^|;\s*)_fbc=([^;]+)/) || [])[1] || '', // Page the donor converted on → Meta CAPI `event_source_url`. Meta // requires it to match the URL the browser Pixel reported or the // two events won't dedupe (and Events Manager flags the server // event). When embedded, the Pixel fires on the PARENT page (see // js/swiftgive-embed.js), so prefer the referrer — which inside an // iframe is the embedding page — over this frame's own URL. eventSourceUrl: svEventSourceUrl() }), }).then((r) => r.json()); } if (result.connected_account_id) { connected_account_id = result.connected_account_id; } if (result.customer_id) { customerId = result.customer_id; } if (result.subscription_id) { subscriptionId = result.subscription_id; } if (result.error) { console.log(result.error); document.querySelector("#payment-element").style.display = "none"; document.querySelector("#address-element").style.display = "none"; document.querySelector("#submit").style.display = "none"; document.querySelector("#payment-message").style.color = "red"; document.querySelector("#payment-message").style.fontSize = "1em"; window.posthog?.capture('invalid_payment_processor', { trigger: 'create_intent_error', flow: 'donation_checkout', campaign_id: campaignId, account_id: 'ec7Oh6qO', connected_account_id: connected_account_id, server_error: result.error, server_response: JSON.stringify(result), environment: 'live', email: email, amount: amount, interval: interval, designation: designation, page_url: window.location.href, referrer: document.referrer, timestamp: new Date().toISOString(), }); showMessage("Unable to make a payment right now due to an invalid payment processor.", true); return; } const clientSecret = result.clientSecret; stripe = Stripe('pk_live_51PLw9aK6h8aOTojnlB7Hh0Ql2zbHY28BpBDCwRcoldLw0xZ7zK55gfIPFkckLhjobySJyzrDnKvHxgUQhcrjEpAF00SV0bBGPO', {stripeAccount: result.connected_account_id}); elements = stripe.elements({ clientSecret, appearance: { variables: { borderRadius: '8px' } } }); // Collect phone but make it optional const addressElement = elements.create('address', { mode: 'billing', fields: {phone: 'always'}, validation: {phone: {required: 'never'}} }); addressElement.mount("#address-element"); const paymentElementOptions = { // Wallets are handled exclusively by the Express Checkout Element above, // so suppress them here to keep them out of the card accordion. wallets: { googlePay: "never", applePay: "never", link: "never", }, layout: { type: "accordion", defaultCollapsed: false, radios: true, spacedAccordionItems: false, }, }; const paymentElement = elements.create("payment", paymentElementOptions); paymentElement.mount("#payment-element"); // Express Checkout (Apple Pay / Google Pay / Link) — reuses the same elements + clientSecret. // On confirm we delegate to handleSubmit, which already branches setup-intent vs payment-intent // and handles the recurring subscription creation step. const expressCheckoutElement = elements.create('expressCheckout', { buttonHeight: 48, paymentMethods: { applePay: 'always', googlePay: 'always', link: 'auto', paypal: 'auto' }, paymentMethodOrder: ['applePay', 'googlePay', 'paypal', 'link'], layout: { overflow: 'never', maxColumns: 1, maxRows: 0 }, }); expressCheckoutElement.on('ready', function (event) { if (event.availablePaymentMethods) { document.getElementById('express-checkout-divider').style.display = 'flex'; } }); expressCheckoutElement.on('confirm', async function () { await handleSubmit({ preventDefault: function () {} }); }); expressCheckoutElement.mount('#express-checkout-element'); document.querySelector("#submit").disabled = false; paymentElement.on("ready", function(event) { // Hide the loading spinner //document.querySelector(".payment-loading-spinner").style.display = "none"; // Enable the submit button once payment form is ready console.log("Payment form ready"); }); } async function handleSubmit(e) { e.preventDefault(); setLoading(true); var startDate = (document.getElementById("customStartDate") != null ? document.getElementById("customStartDate").value : ''); var endDate = (document.getElementById("customEndDate") != null ? document.getElementById("customEndDate").value : ''); // If start date is set and is after today, we aren't processing a payment immediately so just collect payment method through setup intent const today = new Date(); today.setHours(0, 0, 0, 0); const startDateObj = startDate ? new Date(startDate.replace(/-/g, '/')) : null; if (startDate != "" && startDateObj > today) { var designation = (document.getElementById("designation") != null ? document.getElementById("designation").value : ''); var donationAmount = document.getElementById("amount").value; var interval = document.getElementById("interval").value; var email = document.getElementById("email").value; // startDate = // endDate = const { error, setupIntent } = await stripe.confirmSetup({ elements, confirmParams: { //return_url: "https://donations.swiftgive.org/donations/success?campaign_id=51221&donationAmount=" + donationAmount + "&interval=" + interval + "&designation=" + designation + "&start_date=" + startDate + "&end_date=" + endDate }, redirect: "if_required", }); if (error) { console.error(error); // handleError(); } else if (setupIntent && setupIntent.status === "succeeded") { svCapture('setup_succeeded', { setup_intent: setupIntent.id }); var interval = document.getElementById("interval")?.value; // If interval is not one-time, we are creating a recurring plan if (interval != 'One-Time') { var designation = (document.getElementById("designation") != null ? document.getElementById("designation").value : ''); var donationAmount = document.getElementById("amount").value; var interval = document.getElementById("interval").value; var email = document.getElementById("email").value; var anonymous = (document.getElementById("anonymous") != null ? document.getElementById("anonymous").value : ''); var customFieldsData = (document.getElementById("customFieldsData") != null ? document.getElementById("customFieldsData").value : ''); result = await fetch('/create-sg-subscription.php?campaign_id=51221', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ customerId: customerId, campaignId: '51221', designation: designation, email: email, donationAmount: donationAmount, interval: interval, startDate: startDate, endDate: endDate, anonymous: anonymous, customFields: customFieldsData, setupIntent: setupIntent.id, paymentMethod: setupIntent.payment_method, subscriptionId: subscriptionId, fundraiserId: '', teamId: '' }), }).then((r) => r.json()); if (result.subscriptionId) { subscriptionId = result.subscriptionId; console.log("Subscription created"); console.log(subscriptionId); svCapture('subscription_created', { subscription_id: subscriptionId }); } } console.log("Setup succeeded"); // For recurring (setup intent) flows there is no payment intent ID; // expose the setup intent so DTD pending capture can still key off it. window.lastPaymentIntentId = setupIntent && setupIntent.id ? setupIntent.id : null; window.lastConnectedAccountId = connected_account_id || null; nextPage() // handleSuccess(); } else { console.log("Payment failed"); // handleOther(); } } else { const { error, paymentIntent } = await stripe.confirmPayment({ elements, confirmParams: { //return_url: "https://donations.swiftgive.org/donations/success", //receipt_email: emailAddress, }, redirect: "if_required", }); if (error) { console.error(error); // handleError(); } else if (paymentIntent && (paymentIntent.status === "succeeded" || paymentIntent.status === "processing")) { // status is included so settled ("succeeded") payments can be // separated from still-"processing" ones (e.g. ACH/bank debits), // which otherwise inflate the conversion count. svCapture('payment_succeeded', { payment_intent: paymentIntent.id, status: paymentIntent.status }); var interval = document.getElementById("interval")?.value; // If interval is not one-time, we are creating a recurring plan if (interval != 'One-Time') { var designation = (document.getElementById("designation") != null ? document.getElementById("designation").value : ''); var donationAmount = document.getElementById("amount").value; var interval = document.getElementById("interval").value; var email = document.getElementById("email").value; var anonymous = (document.getElementById("anonymous") != null ? document.getElementById("anonymous").value : ''); var customFieldsData = (document.getElementById("customFieldsData") != null ? document.getElementById("customFieldsData").value : ''); result = await fetch('/create-sg-subscription.php?campaign_id=51221', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ customerId: customerId, campaignId: '51221', designation: designation, email: email, donationAmount: donationAmount, interval: interval, startDate: startDate, endDate: endDate, anonymous: anonymous, customFields: customFieldsData, paymentIntent: paymentIntent.id, paymentMethod: paymentIntent.payment_method, subscriptionId: subscriptionId, fundraiserId: '', teamId: '' }), }).then((r) => r.json()); if (result.subscriptionId) { subscriptionId = result.subscriptionId; console.log("Subscription created"); console.log(subscriptionId); svCapture('subscription_created', { subscription_id: subscriptionId }); } } console.log("Payment succeeded"); // Expose IDs for the DTD step (donation row hasn't been webhooked yet, // so we key the pending employer capture by payment intent + account). window.lastPaymentIntentId = paymentIntent.id; window.lastConnectedAccountId = connected_account_id || null; nextPage() // handleSuccess(); } else { console.log("Payment failed"); // handleOther(); } } setLoading(false); } // Fetches the payment intent status after payment submission async function checkStatus() { const clientSecret = new URLSearchParams(window.location.search).get( "payment_intent_client_secret" ); if (!clientSecret) { return; } const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret); switch (paymentIntent.status) { case "succeeded": showMessage("Payment succeeded!"); break; case "processing": showMessage("Your payment is processing."); break; case "requires_payment_method": showMessage("Your payment was not successful, please try again."); break; default: showMessage("Something went wrong."); break; } } // ------- UI helpers ------- function showMessage(messageText, permanent = false) { const messageContainer = document.querySelector("#payment-message"); messageContainer.classList.remove("hidden"); messageContainer.textContent = messageText; if (!permanent) { setTimeout(function () { messageContainer.classList.add("hidden"); messageContainer.textContent = ""; }, 4000); } } // Show a spinner on payment submission function setLoading(isLoading) { if (isLoading) { // Disable the button and show a spinner document.querySelector("#submit").disabled = true; document.querySelector("#spinner").classList.remove("hidden"); document.querySelector("#button-text").classList.add("hidden"); } else { document.querySelector("#submit").disabled = false; document.querySelector("#spinner").classList.add("hidden"); document.querySelector("#button-text").classList.remove("hidden"); } }