Created
January 25, 2026 17:19
-
-
Save melanyss/e8264619ae4a8fbca5f5e7fc3e69c98c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| ================================================================================ | |
| STRIPE EXPRESS CHECKOUT TEMPLATE | |
| Google Pay & Apple Pay Implementation for Experimentation Platforms | |
| ================================================================================ | |
| Author: Melanys Figueredo | |
| Version: 1.0.0 | |
| Last Updated: 25/01/2026 | |
| WHAT THIS TEMPLATE DOES: | |
| - Renders Google Pay and Apple Pay buttons using Stripe Express Checkout | |
| - Handles the complete payment flow | |
| - Fires tracking events to GTM/GA4 | |
| - Includes fallback for when wallets aren't available | |
| - Supports multi-variant pages (e.g., language switchers) | |
| - Handles both redirect and no-redirect post-payment flows | |
| HOW TO USE: | |
| 1. Search for "CLIENT_TODO" to find all placeholders that need client values | |
| 2. Copy the HTML section where you want the buttons to appear | |
| 3. Copy the JavaScript into your Optimizely/VWO custom code | |
| 4. Test in preview mode before going live | |
| REQUIREMENTS FROM CLIENT: | |
| - Stripe Publishable Key (starts with pk_test_ or pk_live_) | |
| - Server endpoint URL that creates PaymentIntents | |
| - Product/checkout information (amount, currency, product ID) | |
| - (Optional) Return URL for successful payments if redirect is enabled | |
| ================================================================================ | |
| --> | |
| <!-- ============================================ --> | |
| <!-- SECTION 1: HTML CONTAINER --> | |
| <!-- Copy this where you want the buttons --> | |
| <!-- ============================================ --> | |
| <div id="express-checkout-wrapper" style="margin: 20px 0;"> | |
| <!-- Loading State --> | |
| <div id="checkout-loading" style="text-align: center; padding: 20px;"> | |
| <p style="color: #666; font-size: 14px;">Loading payment options...</p> | |
| </div> | |
| <!-- Express Checkout Buttons (Stripe renders here) --> | |
| <div id="express-checkout-element"></div> | |
| <!-- Fallback Card Form (shown when wallets not available) --> | |
| <div id="card-fallback" style="display: none;"> | |
| <p style="text-align: center; color: #666; font-size: 14px; margin-bottom: 15px;"> | |
| Pay with card | |
| </p> | |
| <div id="payment-element"></div> | |
| <button | |
| id="pay-button" | |
| style=" | |
| width: 100%; | |
| margin-top: 15px; | |
| padding: 12px 20px; | |
| background: #0066cc; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 16px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| " | |
| > | |
| Pay Now | |
| </button> | |
| </div> | |
| <!-- Error Message Container --> | |
| <div id="error-message" style="display: none; margin-top: 15px; padding: 12px; background: #fee2e2; border: 1px solid #fecaca; border-radius: 6px;"> | |
| <p style="color: #dc2626; font-size: 14px; margin: 0;"></p> | |
| </div> | |
| <!-- Success Message Container --> | |
| <div id="success-message" style="display: none; margin-top: 15px; padding: 12px; background: #dcfce7; border: 1px solid #bbf7d0; border-radius: 6px;"> | |
| <p style="color: #16a34a; font-size: 14px; margin: 0;"> | |
| ✓ Payment successful! | |
| </p> | |
| </div> | |
| <!-- Security Badge --> | |
| <div id="security-badge" style="text-align: center; margin-top: 10px; display: none;"> | |
| <span style="color: #666; font-size: 12px;">🔒 Secured by Stripe</span> | |
| </div> | |
| </div> | |
| <!-- ============================================ --> | |
| <!-- SECTION 2: JAVASCRIPT --> | |
| <!-- Copy this into your custom code section --> | |
| <!-- ============================================ --> | |
| <script> | |
| (function() { | |
| 'use strict'; | |
| // ============================================ | |
| // CONFIGURATION - CLIENT_TODO: Update these values | |
| // ============================================ | |
| const CONFIG = { | |
| // ---------------------------------------- | |
| // STRIPE PUBLISHABLE KEY | |
| // ---------------------------------------- | |
| // CLIENT_TODO: Replace with client's Stripe Publishable Key | |
| // | |
| // WHAT IS THIS? | |
| // This is a public key that identifies the client's Stripe account. | |
| // It's safe to use in frontend code (unlike the Secret Key which must stay on the server). | |
| // | |
| // WHERE TO GET IT: | |
| // 1. Ask the client to log into their Stripe Dashboard: https://dashboard.stripe.com | |
| // 2. Go to: Developers → API Keys | |
| // 3. Copy the "Publishable key" (NOT the Secret key!) | |
| // | |
| // FORMAT: | |
| // - Test mode: starts with pk_test_xxxxxxxxxxxx | |
| // - Live mode: starts with pk_live_xxxxxxxxxxxx | |
| // | |
| // IMPORTANT: Use test key for development, live key for production | |
| stripePublishableKey: 'pk_test_REPLACE_WITH_CLIENT_KEY', | |
| // ---------------------------------------- | |
| // PAYMENT INTENT ENDPOINT | |
| // ---------------------------------------- | |
| // CLIENT_TODO: Replace with client's PaymentIntent endpoint | |
| // | |
| // WHAT IS THIS? | |
| // This is a URL on the CLIENT'S SERVER that creates a "PaymentIntent" in Stripe. | |
| // A PaymentIntent represents a payment attempt and contains the payment details. | |
| // | |
| // WHY IS THIS NEEDED? | |
| // For security reasons, payments must be initiated from a server using Stripe's | |
| // Secret Key. The frontend (our code) cannot and should not have the Secret Key. | |
| // So we call this endpoint, which uses the Secret Key to create the PaymentIntent, | |
| // then returns a "clientSecret" that we use to complete the payment. | |
| // | |
| // WHERE TO GET IT: | |
| // The client's development team must CREATE this endpoint. It doesn't exist by default. | |
| // | |
| // WHAT TO ASK THE CLIENT: | |
| // 1. "Do you have an endpoint that creates Stripe PaymentIntents?" | |
| // 2. "What is the full URL of this endpoint?" | |
| // 3. "What parameters does it expect in the request body?" | |
| // 4. "What does it return in the response?" (should include clientSecret) | |
| // | |
| // EXAMPLE ENDPOINT RESPONSE (what we expect back): | |
| // { | |
| // "clientSecret": "pi_xxxxx_secret_xxxxx", | |
| // "paymentIntentId": "pi_xxxxx" | |
| // } | |
| // | |
| // IF THEY DON'T HAVE ONE: | |
| // They need their developers to create it. This is NOT something we can do | |
| // through Optimizely - it requires server-side code with Stripe Secret Key. | |
| paymentIntentEndpoint: 'https://client-website.com/api/create-payment-intent', | |
| // ---------------------------------------- | |
| // POST-PAYMENT BEHAVIOR | |
| // ---------------------------------------- | |
| // CLIENT_TODO: Configure what happens after successful payment | |
| // | |
| // Set to TRUE if you want to redirect users to a thank-you page | |
| // Set to FALSE if you want to show success message without redirecting | |
| redirectAfterPayment: false, | |
| // Only used if redirectAfterPayment is TRUE | |
| // Include any query parameters the client needs (e.g., order ID, payment reference) | |
| successUrl: 'https://client-website.com/thank-you', | |
| // ---------------------------------------- | |
| // CHECKOUT/PRODUCT DETAILS | |
| // ---------------------------------------- | |
| // CLIENT_TODO: Update with actual product/checkout details | |
| // | |
| // FOR SINGLE PRODUCT/VARIANT PAGES: | |
| // Just update the values below directly. | |
| // | |
| // FOR MULTI-VARIANT PAGES (e.g., language switcher, different products): | |
| // See the CHECKOUT_VARIANTS object below and set useVariants to TRUE | |
| useVariants: false, // Set to TRUE if using multiple variants | |
| // Default checkout info (used when useVariants is FALSE) | |
| checkout: { | |
| amount: 77.00, // Amount in dollars (will be converted to cents) | |
| currency: 'usd', // Currency code (usd, eur, gbp, etc.) | |
| productId: 'product-123', // Product identifier for tracking | |
| productName: 'Product Name', | |
| }, | |
| // ---------------------------------------- | |
| // EXPERIMENT TRACKING | |
| // ---------------------------------------- | |
| // Optional: For tracking which experiment variant is running | |
| experiment: { | |
| experimentId: 'exp_express_checkout_001', | |
| variantId: 'variant_google_apple_pay', | |
| variantName: 'Express Checkout (Google Pay + Apple Pay)', | |
| }, | |
| }; | |
| // ============================================ | |
| // MULTI-VARIANT CHECKOUT CONFIGURATION | |
| // ============================================ | |
| // CLIENT_TODO: If the page has multiple variants (language, region, products), | |
| // configure each variant here and set CONFIG.useVariants = true above. | |
| // | |
| // HOW IT WORKS: | |
| // The code will look at the current page URL or a data attribute to determine | |
| // which variant to use. You can customize the detection logic in getActiveCheckout(). | |
| const CHECKOUT_VARIANTS = { | |
| // English version | |
| 'en': { | |
| amount: 77.00, | |
| currency: 'usd', | |
| productId: 'product-123-en', | |
| productName: 'Product Name (English)', | |
| // Optional: override success URL per variant | |
| successUrl: 'https://client-website.com/en/thank-you', | |
| }, | |
| // Spanish version | |
| 'es': { | |
| amount: 65.00, // Different price for different region | |
| currency: 'eur', | |
| productId: 'product-123-es', | |
| productName: 'Nombre del Producto (Español)', | |
| successUrl: 'https://client-website.com/es/gracias', | |
| }, | |
| // German version | |
| 'de': { | |
| amount: 69.00, | |
| currency: 'eur', | |
| productId: 'product-123-de', | |
| productName: 'Produktname (Deutsch)', | |
| successUrl: 'https://client-website.com/de/danke', | |
| }, | |
| // Add more variants as needed... | |
| }; | |
| /** | |
| * Determine which checkout variant to use based on page context. | |
| * CLIENT_TODO: Customize this function based on how the client's site handles variants. | |
| * | |
| * Common approaches: | |
| * - URL path: /en/product, /es/product | |
| * - URL parameter: ?lang=en | |
| * - Data attribute: <html lang="en"> | |
| * - Cookie or localStorage | |
| */ | |
| function getActiveCheckout() { | |
| // If not using variants, return the default | |
| if (!CONFIG.useVariants) { | |
| return CONFIG.checkout; | |
| } | |
| // OPTION 1: Detect from URL path (e.g., /en/product or /es/producto) | |
| const pathMatch = window.location.pathname.match(/^\/(en|es|de|fr|it|pt)\//); | |
| if (pathMatch && CHECKOUT_VARIANTS[pathMatch[1]]) { | |
| console.log('[Express Checkout] Detected variant from URL path:', pathMatch[1]); | |
| return CHECKOUT_VARIANTS[pathMatch[1]]; | |
| } | |
| // OPTION 2: Detect from URL parameter (e.g., ?lang=es) | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const langParam = urlParams.get('lang') || urlParams.get('locale'); | |
| if (langParam && CHECKOUT_VARIANTS[langParam]) { | |
| console.log('[Express Checkout] Detected variant from URL param:', langParam); | |
| return CHECKOUT_VARIANTS[langParam]; | |
| } | |
| // OPTION 3: Detect from HTML lang attribute | |
| const htmlLang = document.documentElement.lang?.substring(0, 2).toLowerCase(); | |
| if (htmlLang && CHECKOUT_VARIANTS[htmlLang]) { | |
| console.log('[Express Checkout] Detected variant from HTML lang:', htmlLang); | |
| return CHECKOUT_VARIANTS[htmlLang]; | |
| } | |
| // OPTION 4: Detect from data attribute on a specific element | |
| // Example: <div id="product-container" data-variant="es"> | |
| const productEl = document.querySelector('[data-checkout-variant]'); | |
| if (productEl) { | |
| const variant = productEl.getAttribute('data-checkout-variant'); | |
| if (CHECKOUT_VARIANTS[variant]) { | |
| console.log('[Express Checkout] Detected variant from data attribute:', variant); | |
| return CHECKOUT_VARIANTS[variant]; | |
| } | |
| } | |
| // Fallback to default | |
| console.log('[Express Checkout] No variant detected, using default'); | |
| return CONFIG.checkout; | |
| } | |
| // Get the active checkout configuration | |
| const ACTIVE_CHECKOUT = getActiveCheckout(); | |
| // ============================================ | |
| // TRACKING HELPERS | |
| // ============================================ | |
| /** | |
| * Push event to dataLayer (for GTM) | |
| * Customize this based on your GTM setup | |
| */ | |
| function pushToDataLayer(eventData) { | |
| window.dataLayer = window.dataLayer || []; | |
| window.dataLayer.push(eventData); | |
| // Debug logging (remove in production) | |
| console.log('[Express Checkout] DataLayer push:', eventData); | |
| } | |
| /** | |
| * OPTIONAL: Fire GA4 event directly (if not using GTM) | |
| * Uncomment if you want to send events directly to GA4 | |
| */ | |
| /* | |
| function fireGA4Event(eventName, eventParams) { | |
| if (typeof gtag === 'function') { | |
| gtag('event', eventName, eventParams); | |
| console.log('[Express Checkout] GA4 event:', eventName, eventParams); | |
| } | |
| } | |
| */ | |
| // ============================================ | |
| // TRACKING EVENTS | |
| // ============================================ | |
| const TRACKING = { | |
| /** | |
| * Track when Express Checkout buttons become available | |
| */ | |
| expressCheckoutAvailable: function(availableMethods) { | |
| pushToDataLayer({ | |
| event: 'express_checkout_available', | |
| event_category: 'Ecommerce', | |
| event_action: 'Payment Methods Available', | |
| // Available payment methods | |
| apple_pay_available: availableMethods.applePay || false, | |
| google_pay_available: availableMethods.googlePay || false, | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| // Product data (using active checkout variant) | |
| product_id: ACTIVE_CHECKOUT.productId, | |
| product_name: ACTIVE_CHECKOUT.productName, | |
| value: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| }); | |
| /* OPTIONAL: GA4 Direct Event | |
| fireGA4Event('express_checkout_available', { | |
| apple_pay: availableMethods.applePay, | |
| google_pay: availableMethods.googlePay, | |
| }); | |
| */ | |
| }, | |
| /** | |
| * Track when user clicks a payment button (begin_checkout) | |
| * GA4 Recommended Event: https://developers.google.com/analytics/devguides/collection/ga4/reference/events#begin_checkout | |
| */ | |
| beginCheckout: function(paymentType) { | |
| pushToDataLayer({ | |
| event: 'begin_checkout', | |
| event_category: 'Ecommerce', | |
| event_action: 'Begin Checkout', | |
| event_label: paymentType, | |
| // GA4 Ecommerce parameters | |
| ecommerce: { | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| value: ACTIVE_CHECKOUT.amount, | |
| payment_type: paymentType, // 'apple_pay', 'google_pay', or 'card' | |
| items: [{ | |
| item_id: ACTIVE_CHECKOUT.productId, | |
| item_name: ACTIVE_CHECKOUT.productName, | |
| price: ACTIVE_CHECKOUT.amount, | |
| quantity: 1, | |
| }], | |
| }, | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| /* OPTIONAL: Enhanced Ecommerce (Universal Analytics style) | |
| pushToDataLayer({ | |
| event: 'checkout', | |
| ecommerce: { | |
| checkout: { | |
| actionField: { step: 1, option: paymentType }, | |
| products: [{ | |
| id: ACTIVE_CHECKOUT.productId, | |
| name: ACTIVE_CHECKOUT.productName, | |
| price: ACTIVE_CHECKOUT.amount, | |
| quantity: 1, | |
| }] | |
| } | |
| } | |
| }); | |
| */ | |
| }, | |
| /** | |
| * Track when payment info is added | |
| * GA4 Recommended Event: https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_payment_info | |
| */ | |
| addPaymentInfo: function(paymentType) { | |
| pushToDataLayer({ | |
| event: 'add_payment_info', | |
| event_category: 'Ecommerce', | |
| event_action: 'Add Payment Info', | |
| event_label: paymentType, | |
| // GA4 Ecommerce parameters | |
| ecommerce: { | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| value: ACTIVE_CHECKOUT.amount, | |
| payment_type: paymentType, | |
| items: [{ | |
| item_id: ACTIVE_CHECKOUT.productId, | |
| item_name: ACTIVE_CHECKOUT.productName, | |
| price: ACTIVE_CHECKOUT.amount, | |
| quantity: 1, | |
| }], | |
| }, | |
| // Stripe-specific data (available after payment sheet opens) | |
| // These can be enriched with actual Stripe response data | |
| stripe_payment_method_type: paymentType, | |
| }); | |
| }, | |
| /** | |
| * Track successful purchase | |
| * GA4 Recommended Event: https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase | |
| */ | |
| purchase: function(transactionId, paymentType, stripeData) { | |
| pushToDataLayer({ | |
| event: 'purchase', | |
| event_category: 'Ecommerce', | |
| event_action: 'Purchase', | |
| event_label: paymentType, | |
| // GA4 Ecommerce parameters (REQUIRED for revenue tracking) | |
| ecommerce: { | |
| transaction_id: transactionId, | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| value: ACTIVE_CHECKOUT.amount, | |
| payment_type: paymentType, | |
| items: [{ | |
| item_id: ACTIVE_CHECKOUT.productId, | |
| item_name: ACTIVE_CHECKOUT.productName, | |
| price: ACTIVE_CHECKOUT.amount, | |
| quantity: 1, | |
| }], | |
| }, | |
| // Stripe-specific data for enrichment | |
| stripe_payment_intent_id: transactionId, | |
| stripe_payment_method_type: paymentType, | |
| /* OPTIONAL: Additional Stripe data (if available from your endpoint) | |
| stripe_customer_id: stripeData.customerId, | |
| stripe_payment_method_id: stripeData.paymentMethodId, | |
| stripe_charge_id: stripeData.chargeId, | |
| */ | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| /* OPTIONAL: Facebook Pixel Purchase Event | |
| if (typeof fbq === 'function') { | |
| fbq('track', 'Purchase', { | |
| value: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| content_ids: [ACTIVE_CHECKOUT.productId], | |
| content_type: 'product', | |
| }); | |
| } | |
| */ | |
| /* OPTIONAL: Google Ads Conversion | |
| if (typeof gtag === 'function') { | |
| gtag('event', 'conversion', { | |
| send_to: 'AW-XXXXXXXXX/YYYYYYYYYYY', // CLIENT_TODO: Replace with conversion ID | |
| value: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| transaction_id: transactionId, | |
| }); | |
| } | |
| */ | |
| }, | |
| /** | |
| * Track checkout errors | |
| */ | |
| checkoutError: function(errorMessage, paymentType, errorCode) { | |
| pushToDataLayer({ | |
| event: 'checkout_error', | |
| event_category: 'Ecommerce', | |
| event_action: 'Checkout Error', | |
| event_label: errorMessage, | |
| // Error details | |
| error_message: errorMessage, | |
| error_code: errorCode || 'unknown', | |
| payment_type: paymentType, | |
| // Context | |
| product_id: ACTIVE_CHECKOUT.productId, | |
| value: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency.toUpperCase(), | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| }, | |
| /** | |
| * Track when user cancels checkout | |
| */ | |
| checkoutCancelled: function(paymentType) { | |
| pushToDataLayer({ | |
| event: 'checkout_cancelled', | |
| event_category: 'Ecommerce', | |
| event_action: 'Checkout Cancelled', | |
| event_label: paymentType, | |
| payment_type: paymentType, | |
| product_id: ACTIVE_CHECKOUT.productId, | |
| value: ACTIVE_CHECKOUT.amount, | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| }, | |
| /** | |
| * Track fallback to card form (wallets not available) | |
| */ | |
| fallbackToCard: function() { | |
| pushToDataLayer({ | |
| event: 'express_checkout_fallback', | |
| event_category: 'Ecommerce', | |
| event_action: 'Fallback to Card Form', | |
| event_label: 'Wallets Not Available', | |
| // Context | |
| product_id: ACTIVE_CHECKOUT.productId, | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| }, | |
| /** | |
| * Track successful payment (no redirect scenario) | |
| * This fires when payment succeeds and we show the success UI instead of redirecting | |
| */ | |
| paymentCompleteNoRedirect: function(transactionId, paymentType) { | |
| pushToDataLayer({ | |
| event: 'payment_complete_displayed', | |
| event_category: 'Ecommerce', | |
| event_action: 'Payment Complete UI Shown', | |
| event_label: paymentType, | |
| transaction_id: transactionId, | |
| payment_type: paymentType, | |
| redirect_enabled: false, | |
| // Experiment data | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| }); | |
| }, | |
| }; | |
| // ============================================ | |
| // UI HELPERS | |
| // ============================================ | |
| function showElement(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.style.display = 'block'; | |
| } | |
| function hideElement(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.style.display = 'none'; | |
| } | |
| function showError(message) { | |
| const errorEl = document.getElementById('error-message'); | |
| if (errorEl) { | |
| errorEl.querySelector('p').textContent = message; | |
| errorEl.style.display = 'block'; | |
| } | |
| } | |
| function hideError() { | |
| hideElement('error-message'); | |
| } | |
| function showSuccess(message) { | |
| const successEl = document.getElementById('success-message'); | |
| if (successEl) { | |
| // Update message if provided | |
| if (message) { | |
| successEl.querySelector('p').textContent = '✓ ' + message; | |
| } | |
| successEl.style.display = 'block'; | |
| } | |
| } | |
| function setButtonLoading(isLoading) { | |
| const button = document.getElementById('pay-button'); | |
| if (button) { | |
| button.disabled = isLoading; | |
| button.textContent = isLoading ? 'Processing...' : `Pay $${ACTIVE_CHECKOUT.amount.toFixed(2)}`; | |
| } | |
| } | |
| // ============================================ | |
| // STRIPE INITIALIZATION | |
| // ============================================ | |
| let stripe = null; | |
| let elements = null; | |
| let expressCheckoutElement = null; | |
| let paymentElement = null; | |
| function initializeStripe() { | |
| // Validate configuration | |
| if (CONFIG.stripePublishableKey.includes('REPLACE')) { | |
| console.error('[Express Checkout] ERROR: Stripe Publishable Key not configured!'); | |
| showError('Payment system not configured. Please contact support.'); | |
| return; | |
| } | |
| // Initialize Stripe | |
| stripe = Stripe(CONFIG.stripePublishableKey); | |
| // Create Elements instance (using active checkout variant for amount/currency) | |
| elements = stripe.elements({ | |
| mode: 'payment', | |
| amount: Math.round(ACTIVE_CHECKOUT.amount * 100), // Convert to cents | |
| currency: ACTIVE_CHECKOUT.currency.toLowerCase(), | |
| appearance: { | |
| theme: 'stripe', | |
| variables: { | |
| colorPrimary: '#0066cc', | |
| borderRadius: '6px', | |
| }, | |
| }, | |
| }); | |
| // Create Express Checkout Element | |
| expressCheckoutElement = elements.create('expressCheckout', { | |
| buttonType: { | |
| applePay: 'buy', | |
| googlePay: 'buy', | |
| }, | |
| buttonTheme: { | |
| applePay: 'black', | |
| googlePay: 'black', | |
| }, | |
| layout: { | |
| maxColumns: 1, | |
| maxRows: 2, | |
| }, | |
| paymentMethods: { | |
| applePay: 'always', | |
| googlePay: 'always', | |
| link: 'never', | |
| }, | |
| }); | |
| // Mount Express Checkout | |
| expressCheckoutElement.mount('#express-checkout-element'); | |
| // Set up event handlers | |
| setupExpressCheckoutEvents(); | |
| console.log('[Express Checkout] Stripe initialized successfully'); | |
| console.log('[Express Checkout] Active checkout variant:', ACTIVE_CHECKOUT); | |
| } | |
| function setupExpressCheckoutEvents() { | |
| // Ready event - called when element determines what's available | |
| expressCheckoutElement.on('ready', function(event) { | |
| const methods = event.availablePaymentMethods || {}; | |
| const hasWallet = methods.applePay || methods.googlePay; | |
| // Hide loading | |
| hideElement('checkout-loading'); | |
| if (hasWallet) { | |
| // Show security badge | |
| showElement('security-badge'); | |
| // Track availability | |
| TRACKING.expressCheckoutAvailable(methods); | |
| console.log('[Express Checkout] Wallets available:', methods); | |
| } else { | |
| // No wallets - show card fallback | |
| console.log('[Express Checkout] No wallets available, showing card form'); | |
| showCardFallback(); | |
| } | |
| }); | |
| // Click event - user clicked a payment button | |
| expressCheckoutElement.on('click', function(event) { | |
| // Track begin checkout | |
| TRACKING.beginCheckout(event.expressPaymentType); | |
| // IMPORTANT: Must call resolve() to show the payment sheet | |
| event.resolve(); | |
| }); | |
| // Confirm event - user confirmed in the payment sheet | |
| expressCheckoutElement.on('confirm', async function(event) { | |
| await processPayment(event.expressPaymentType); | |
| }); | |
| // Cancel event - user closed the payment sheet | |
| expressCheckoutElement.on('cancel', function() { | |
| TRACKING.checkoutCancelled('express_checkout'); | |
| console.log('[Express Checkout] User cancelled payment'); | |
| }); | |
| // Load error | |
| expressCheckoutElement.on('loaderror', function(event) { | |
| console.error('[Express Checkout] Load error:', event.error); | |
| TRACKING.checkoutError(event.error?.message || 'Failed to load', 'express_checkout', 'load_error'); | |
| showCardFallback(); | |
| }); | |
| } | |
| function showCardFallback() { | |
| // Track fallback | |
| TRACKING.fallbackToCard(); | |
| // Hide express checkout, show card form | |
| hideElement('express-checkout-element'); | |
| showElement('card-fallback'); | |
| showElement('security-badge'); | |
| // Create and mount Payment Element for card | |
| if (!paymentElement) { | |
| paymentElement = elements.create('payment', { | |
| layout: 'tabs', | |
| paymentMethodOrder: ['card'], | |
| }); | |
| paymentElement.mount('#payment-element'); | |
| } | |
| // Set up pay button (using active checkout variant for amount) | |
| const payButton = document.getElementById('pay-button'); | |
| if (payButton) { | |
| payButton.textContent = `Pay $${ACTIVE_CHECKOUT.amount.toFixed(2)}`; | |
| payButton.onclick = function() { | |
| TRACKING.beginCheckout('card'); | |
| processCardPayment(); | |
| }; | |
| } | |
| } | |
| // ============================================ | |
| // PAYMENT PROCESSING | |
| // ============================================ | |
| /** | |
| * Get the success URL (supports per-variant URLs) | |
| */ | |
| function getSuccessUrl(paymentIntentId) { | |
| // Check if the active checkout variant has its own success URL | |
| const baseUrl = ACTIVE_CHECKOUT.successUrl || CONFIG.successUrl; | |
| return baseUrl + '?payment_intent=' + paymentIntentId; | |
| } | |
| /** | |
| * Handle successful payment - either redirect or show success UI | |
| */ | |
| function handlePaymentSuccess(paymentIntentId, paymentType) { | |
| // Track the purchase | |
| TRACKING.purchase(paymentIntentId, paymentType, {}); | |
| if (CONFIG.redirectAfterPayment) { | |
| // REDIRECT FLOW: Show brief success then redirect | |
| showSuccess('Payment successful! Redirecting...'); | |
| setTimeout(function() { | |
| window.location.href = getSuccessUrl(paymentIntentId); | |
| }, 1500); | |
| } else { | |
| // NO REDIRECT FLOW: Show success UI and stay on page | |
| showSuccess('Payment successful! Thank you for your purchase.'); | |
| // Track that we showed the success UI (useful for measuring engagement) | |
| TRACKING.paymentCompleteNoRedirect(paymentIntentId, paymentType); | |
| // Hide the checkout buttons since payment is complete | |
| hideElement('express-checkout-element'); | |
| hideElement('card-fallback'); | |
| // Optional: Show additional content like download links, next steps, etc. | |
| // You can customize this based on the client's requirements | |
| showPostPurchaseContent(paymentIntentId, paymentType); | |
| } | |
| } | |
| /** | |
| * Show post-purchase content (customize based on client needs) | |
| * This is only used when redirectAfterPayment is FALSE | |
| */ | |
| function showPostPurchaseContent(paymentIntentId, paymentType) { | |
| // CLIENT_TODO: Customize this based on what should happen after purchase | |
| // Examples: | |
| // - Show download links | |
| // - Display order confirmation details | |
| // - Show next steps | |
| // - Reveal hidden content | |
| console.log('[Express Checkout] Payment complete, showing post-purchase content'); | |
| console.log('[Express Checkout] Transaction ID:', paymentIntentId); | |
| console.log('[Express Checkout] Payment type:', paymentType); | |
| // Example: Update a hidden element to show download links | |
| // const downloadSection = document.getElementById('download-section'); | |
| // if (downloadSection) { | |
| // downloadSection.style.display = 'block'; | |
| // } | |
| } | |
| async function processPayment(paymentType) { | |
| hideError(); | |
| try { | |
| // Track payment info | |
| TRACKING.addPaymentInfo(paymentType); | |
| // Create PaymentIntent on server (using active checkout variant) | |
| const response = await fetch(CONFIG.paymentIntentEndpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| amount: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency, | |
| productId: ACTIVE_CHECKOUT.productId, | |
| productName: ACTIVE_CHECKOUT.productName, | |
| // Add any other data your endpoint expects | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to create payment'); | |
| } | |
| const { clientSecret, paymentIntentId } = await response.json(); | |
| // Determine redirect behavior | |
| const redirectOption = CONFIG.redirectAfterPayment ? 'if_required' : 'if_required'; | |
| // Confirm the payment | |
| const { error } = await stripe.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: getSuccessUrl(paymentIntentId), | |
| }, | |
| redirect: redirectOption, | |
| }); | |
| if (error) { | |
| throw error; | |
| } | |
| // Payment succeeded! | |
| handlePaymentSuccess(paymentIntentId, paymentType); | |
| } catch (error) { | |
| console.error('[Express Checkout] Payment error:', error); | |
| TRACKING.checkoutError(error.message, paymentType, error.code); | |
| showError(error.message || 'Payment failed. Please try again.'); | |
| } | |
| } | |
| async function processCardPayment() { | |
| hideError(); | |
| setButtonLoading(true); | |
| try { | |
| // Submit the form first | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| throw submitError; | |
| } | |
| // Track payment info | |
| TRACKING.addPaymentInfo('card'); | |
| // Create PaymentIntent on server (using active checkout variant) | |
| const response = await fetch(CONFIG.paymentIntentEndpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| amount: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency, | |
| productId: ACTIVE_CHECKOUT.productId, | |
| productName: ACTIVE_CHECKOUT.productName, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to create payment'); | |
| } | |
| const { clientSecret, paymentIntentId } = await response.json(); | |
| // Confirm the payment | |
| const { error } = await stripe.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: getSuccessUrl(paymentIntentId), | |
| }, | |
| redirect: 'if_required', | |
| }); | |
| if (error) { | |
| throw error; | |
| } | |
| // Payment succeeded! | |
| handlePaymentSuccess(paymentIntentId, 'card'); | |
| } catch (error) { | |
| console.error('[Express Checkout] Card payment error:', error); | |
| TRACKING.checkoutError(error.message, 'card', error.code); | |
| showError(error.message || 'Payment failed. Please try again.'); | |
| } finally { | |
| setButtonLoading(false); | |
| } | |
| } | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| function loadStripeAndInit() { | |
| // Check if Stripe.js is already loaded | |
| if (typeof Stripe !== 'undefined') { | |
| initializeStripe(); | |
| return; | |
| } | |
| // Load Stripe.js | |
| const script = document.createElement('script'); | |
| script.src = 'https://js.stripe.com/v3/'; | |
| script.onload = function() { | |
| console.log('[Express Checkout] Stripe.js loaded'); | |
| initializeStripe(); | |
| }; | |
| script.onerror = function() { | |
| console.error('[Express Checkout] Failed to load Stripe.js'); | |
| showError('Failed to load payment system. Please refresh the page.'); | |
| }; | |
| document.head.appendChild(script); | |
| } | |
| // Initialize when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', loadStripeAndInit); | |
| } else { | |
| loadStripeAndInit(); | |
| } | |
| // Track that the experiment variant was loaded | |
| pushToDataLayer({ | |
| event: 'experiment_variant_loaded', | |
| experiment_id: CONFIG.experiment.experimentId, | |
| variant_id: CONFIG.experiment.variantId, | |
| variant_name: CONFIG.experiment.variantName, | |
| // Include checkout variant info for multi-variant pages | |
| checkout_variant: CONFIG.useVariants ? 'dynamic' : 'static', | |
| product_id: ACTIVE_CHECKOUT.productId, | |
| amount: ACTIVE_CHECKOUT.amount, | |
| currency: ACTIVE_CHECKOUT.currency, | |
| }); | |
| })(); | |
| </script> | |
| <!-- | |
| ================================================================================ | |
| QUICK REFERENCE: EVENTS PUSHED TO DATALAYER | |
| ================================================================================ | |
| 1. experiment_variant_loaded | |
| - Fires when the template loads | |
| - Use to track experiment impressions | |
| - Contains: experiment_id, variant_id, product_id, amount, currency | |
| 2. express_checkout_available | |
| - Fires when Stripe determines what payment methods are available | |
| - Contains: apple_pay_available, google_pay_available, product_id | |
| 3. begin_checkout (GA4 Recommended Event) | |
| - Fires when user clicks Google Pay, Apple Pay, or Pay button | |
| - Contains: ecommerce object with items, payment_type | |
| 4. add_payment_info (GA4 Recommended Event) | |
| - Fires when payment sheet opens | |
| - Contains: payment_type, ecommerce object | |
| 5. purchase (GA4 Recommended Event) | |
| - Fires on successful payment | |
| - Contains: transaction_id, ecommerce object with revenue, stripe_payment_intent_id | |
| 6. checkout_error | |
| - Fires when payment fails | |
| - Contains: error_message, error_code, payment_type | |
| 7. checkout_cancelled | |
| - Fires when user cancels payment | |
| - Contains: payment_type | |
| 8. express_checkout_fallback | |
| - Fires when wallets not available, showing card form | |
| - Use to understand device/browser compatibility | |
| 9. payment_complete_displayed (NEW - for no-redirect flow) | |
| - Fires when payment succeeds and success UI is shown (no redirect) | |
| - Contains: transaction_id, payment_type, redirect_enabled: false | |
| ================================================================================ | |
| CONFIGURATION OPTIONS SUMMARY | |
| ================================================================================ | |
| CONFIG.stripePublishableKey - Client's Stripe public key (pk_test_xxx or pk_live_xxx) | |
| CONFIG.paymentIntentEndpoint - Client's server URL to create PaymentIntents | |
| CONFIG.redirectAfterPayment - TRUE = redirect to success URL, FALSE = show success UI | |
| CONFIG.successUrl - Where to redirect (only if redirectAfterPayment is TRUE) | |
| CONFIG.useVariants - TRUE = use CHECKOUT_VARIANTS for multi-language/product pages | |
| CONFIG.checkout - Default product info (amount, currency, productId, productName) | |
| CONFIG.experiment - Experiment tracking IDs | |
| CHECKOUT_VARIANTS - Define multiple checkout configs for multi-variant pages | |
| getActiveCheckout() - Customize how the variant is detected (URL, data attribute, etc.) | |
| ================================================================================ | |
| GTM SETUP TIPS | |
| ================================================================================ | |
| For each event, create a GTM trigger with: | |
| - Trigger Type: Custom Event | |
| - Event Name: [event name from above] | |
| Then create GA4 Event tags using these triggers. | |
| For the 'purchase' event, make sure to: | |
| 1. Map 'ecommerce.transaction_id' to the GA4 transaction_id parameter | |
| 2. Map 'ecommerce.value' to the value parameter | |
| 3. Map 'ecommerce.currency' to the currency parameter | |
| 4. Map 'ecommerce.items' to the items parameter | |
| For multi-variant pages, the 'experiment_variant_loaded' event includes: | |
| - checkout_variant: 'dynamic' or 'static' | |
| - product_id, amount, currency: values from the active variant | |
| ================================================================================ | |
| --> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment