Diagnosing and Fixing Stripe Embedded Checkout on adamcherrycomics.dangerouscentaur.com

Executive Summary

During a maintenance session on the Adam Cherry Comics storefront, we discovered that the Stripe checkout flow—documented as a working embedded modal—was actually returning a PaymentIntent client_secret instead of properly initializing Stripe's embedded checkout. This post walks through the diagnosis, the root causes (missing script tags, ui_mode inconsistencies), and the fixes deployed to restore checkout functionality across staging and production.

What Was Done

  • Verified all site pages returned HTTP 200 across https://adamcherrycomics.dangerouscentaur.com/
  • Smoke-tested the Lambda function adam-cherry-checkout and discovered it was returning PaymentIntent secrets, not Checkout Session URLs
  • Audited the live frontend HTML and found the critical <script src="https://js.stripe.com/v3/"></script> tag was missing
  • Patched index.html to restore Stripe.js and properly initialize embedded checkout
  • Fixed Lambda's CORS headers to support multi-origin requests (EC2 staging vs. production S3)
  • Corrected Lambda's RETURN_URL to dynamically derive from the request origin instead of hardcoding
  • Deployed fixes to staging via EC2 sync, smoke-tested with Playwright, promoted to production via S3 invalidation

Technical Details: The Root Causes

1. Missing Stripe.js Script Tag

The handoff notes indicated the <script src="https://js.stripe.com/v3/"></script> tag had been removed during a previous iteration. Without this, the browser has no Stripe object in scope, so modal initialization code fails silently:

// In modal.js on live frontend — this fails because Stripe is undefined
const stripe = Stripe('pk_live_...');  // ReferenceError: Stripe is not defined
stripe.initEmbeddedCheckout({ clientSecret: '...' });

The fix was straightforward: restore the script tag in index.html before any code that references Stripe.

2. Lambda Returning PaymentIntent Instead of Checkout Session

The handoff stated the Lambda was configured for embedded checkout via ui_mode="embedded", but inspection of the actual checkout.py revealed inconsistency:

# Lambda handler was creating PaymentIntent instead of Checkout Session
session = stripe.PaymentIntent.create(
    amount=amount_cents,
    currency='usd',
    metadata={'product': product_id}
)
return {
    'client_secret': session.client_secret  # Wrong — embedded needs CheckoutSession
}

Stripe's embedded checkout (as of stripe-python 15.1.0+) requires CheckoutSession.create() with ui_mode="embedded", not PaymentIntent. The client_secret from a PaymentIntent cannot initialize embedded checkout.

3. CORS and Origin Mismatch

The Lambda's RETURN_URL was hardcoded to the production domain. When staging (running on EC2) made requests to the Lambda, the response included a redirect URL pointing back to adamcherrycomics.dangerouscentaur.com instead of the staging origin. Additionally, API Gateway's CORS configuration only listed the production origin:

# Before fix: hardcoded RETURN_URL
RETURN_URL = 'https://adamcherrycomics.dangerouscentaur.com/success'

# After fix: derive from request
origin = event.get('headers', {}).get('origin', RETURN_URL)
RETURN_URL = f'{origin}/success'

Infrastructure and Deployment

Lambda Function: adam-cherry-checkout

Located in the finalconstructclean AWS profile. The handler receives a POST request with product_id and creates a Stripe Checkout Session:

  • Environment Variables: STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, RETURN_URL (now dynamic)
  • Handler: checkout.lambda_handler
  • Patches applied:
    • Changed PaymentIntent.create() to CheckoutSession.create(..., ui_mode="embedded")
    • Added dynamic RETURN_URL from request origin header
    • Added Access-Control-Allow-Origin: * header (or explicit origin list) to handle cross-origin requests
    • Set Content-Type: application/json in all responses

API Gateway: n0nh1zscq4

The Lambda is exposed via API Gateway endpoint. CORS configuration required updates:

# API GW CORS settings — add staging origin
Access-Control-Allow-Origin: https://adamcherrycomics.dangerouscentaur.com, https://<staging-ip>
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Amz-Date, Authorization

Frontend: index.html (S3 + CloudFront)

  • S3 bucket: dc-sites (shared multi-tenant bucket)
  • CloudFront distribution: E2Q4UU71SRNTMB
  • Patches:
    • Added <script src="https://js.stripe.com/v3/"></script> before modal.js
    • Updated modal.js to call stripe.initEmbeddedCheckout({ clientSecret }) with the session secret from Lambda
    • Snapshot taken, tested on EC2, promoted to staging, cache-invalidated, smoke-tested, promoted to prod

Deployment Flow

# 1. Patch index.html on EC2 dev box
scp /tmp/patch_acc_checkout.py <ec2-user>@<ec2-ip>:/tmp/
ssh <ec2-user>@<ec2-ip> "python /tmp/patch_acc_checkout.py"

# 2. Snapshot new index.html and sync to S3 staging prefix
aws s3 sync /tmp/acc-repo/public/ s3://dc-sites/adamcherrycomics/ --exclude '*' --include 'index.html'

# 3. Invalidate CloudFront
aws cloudfront create-invalidation --distribution-id E2Q4UU71SRNTMB --paths "/index.html" "/adamcherrycomics/*"

# 4. Smoke test staging