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

Executive Summary

During a routine status check on the Adam Cherry Comics storefront, we discovered that the Stripe checkout flow was returning a PaymentIntent client secret instead of initiating the intended embedded checkout modal. The handoff documentation claimed the site was using hosted-redirect checkout, but runtime inspection revealed the Lambda was already configured for embedded mode. This post walks through the diagnostic process, the root cause (missing Stripe.js initialization script), and the fixes applied to restore the checkout flow to working order.

What Was Done

  • Verified all site pages returned HTTP 200 and Lambda endpoint was reachable
  • Inspected live frontend modal JavaScript to understand the checkout flow
  • SSH'd into the EC2 development host and compared production source against deployed index.html
  • Identified missing <script src="https://js.stripe.com/v3/"> tag in production index.html
  • Patched index.html on EC2, deployed to S3 (bucket: dc-sites), and invalidated CloudFront distribution E2Q4UU71SRNTMB
  • Updated Lambda environment and handler logic to ensure multi-origin CORS headers and correct RETURN_URL derivation
  • Ran Playwright smoke tests against staging to confirm modal appeared and payment flow initiated
  • Promoted fixes to production and verified end-to-end checkout initiation

Technical Details: Root Cause Analysis

The issue surfaced when comparing what the Lambda was actually doing versus what the handoff documentation claimed:

  • Claimed: Hosted-redirect checkout (user sent to Stripe-hosted page)
  • Actual Lambda code: Already using ui_mode="embedded", returning a PaymentIntent client secret
  • Frontend: Modal JavaScript expected to call stripe.confirmPayment() with the client secret
  • Missing: The <script src="https://js.stripe.com/v3/"> tag was not present in the deployed index.html

Without the Stripe.js library loaded in the browser, the modal initialization code const stripe = Stripe(publishableKey) would fail silently, and the payment flow would never begin. This is a classic case where the backend was correct, but the frontend was missing a critical dependency.

Infrastructure & Deployment Changes

S3 and CloudFront

The site is hosted on:

  • S3 bucket: dc-sites (shared across multiple DangerousCentaur properties)
  • Site prefix: adamcherrycomics.dangerouscentaur.com/
  • CloudFront distribution ID: E2Q4UU71SRNTMB

After patching index.html on the development EC2 host, we synced the updated file to S3:

aws s3 cp index.html s3://dc-sites/adamcherrycomics.dangerouscentaur.com/index.html \
  --profile finalconstructclean \
  --cache-control "max-age=300"

Then invalidated the CloudFront cache to force edge nodes to fetch the new version:

aws cloudfront create-invalidation \
  --distribution-id E2Q4UU71SRNTMB \
  --paths "/*" \
  --profile finalconstructclean

Lambda Configuration

The checkout Lambda function (adam-cherry-checkout) required two fixes:

  1. CORS headers for multi-origin support: The API Gateway and Lambda were returning hardcoded origin headers. Updated the Lambda to derive the origin from the incoming Origin request header and reflect it back in the response, allowing both the staging and production domains to make preflight requests.
  2. RETURN_URL derivation: Changed the Lambda to construct the return URL from the request's Origin header rather than a hardcoded domain, ensuring that after payment users are redirected to the correct domain (staging vs. production).

Example patch applied to checkout.py:

origin = event.get('headers', {}).get('origin') or 'https://adamcherrycomics.dangerouscentaur.com'
return_url = f"{origin}/?paid=true"

response_headers = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': origin,
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type'
}

Stripe Modal Configuration

The frontend modal now includes:

<script src="https://js.stripe.com/v3/"></script>
<script>
  const stripe = Stripe('pk_live_...');  // publishable key
  
  async function openCheckout(priceId) {
    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ price_id: priceId })
    });
    const { client_secret } = await response.json();
    
    const { error } = await stripe.confirmPayment({
      elements,
      clientSecret: client_secret,
      confirmParams: { return_url: window.location.href + '?paid=true' }
    });
    
    if (error) console.error(error.message);
  }
</script>

This flow is compliant with Stripe's embedded payment element pattern and matches the architecture used on other DangerousCentaur storefronts.

Testing & Validation

Smoke tests were run at each stage:

  • Page load: Verified all 12 product pages returned 200 and modal button appeared
  • Preflight (OPTIONS): Confirmed browser's CORS preflight received correct Access-Control-Allow-Origin response
  • Checkout POST: Clicked "Buy" button, captured network request to /api/checkout, confirmed Lambda returned client_secret
  • Modal appearance: Verified Stripe.js initialized and payment element appeared in modal without JavaScript errors
  • CloudWatch logs: Tailed Lambda logs to confirm the function executed, derived the correct origin, and returned the PaymentIntent

Key Decisions

  • Why embedded, not redirect? Embedded checkout keeps users on the site and provides better UX. The Lambda was already configured for embedded mode; we just needed to enable it on the frontend.
  • Why derive origin at runtime? Allows the same Lambda code to serve both staging