Skip to content

Instantly share code, notes, and snippets.

@melanyss
Created January 25, 2026 17:19
Show Gist options
  • Select an option

  • Save melanyss/e8264619ae4a8fbca5f5e7fc3e69c98c to your computer and use it in GitHub Desktop.

Select an option

Save melanyss/e8264619ae4a8fbca5f5e7fc3e69c98c to your computer and use it in GitHub Desktop.
<!--
================================================================================
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