```html

Debugging a Broken Stripe Embedded Checkout on adamcherrycomics.dangerouscentaur.com

Overview

During a routine handoff verification of the Adam Cherry Comics e-commerce site, we discovered that the Stripe embedded checkout flow was returning a PaymentIntent client secret instead of a proper Checkout Session URL. This mismatch between frontend expectations and Lambda backend behavior rendered the checkout modal non-functional in production. This post walks through the diagnosis, the root cause, and the remediation strategy we deployed.

What We Found

The handoff documentation indicated that the site had been migrated from a Stripe hosted-redirect flow to an embedded modal checkout. However, when we:

  • Verified all pages returned HTTP 200 ✅
  • Smoke-tested the checkout Lambda (adam-cherry-checkout)
  • Inspected the live frontend modal JavaScript in the deployed index.html
  • Probed the Lambda's actual response payload

we found a critical mismatch: the Lambda at /tmp/checkout_connect.py was correctly configured with ui_mode="embedded" and returning a PaymentIntent client secret, but the frontend expected a Checkout Session ID for the Stripe.js redirectToCheckout() flow.

Infrastructure Context

Live deployment:

  • Frontend: S3 bucket dc-sites → CloudFront distribution E2Q4UU71SRNTMB → adamcherrycomics.dangerouscentaur.com
  • API layer: API Gateway n0nh1zscq4 → Lambda adam-cherry-checkout
  • AWS Profile: finalconstructclean
  • DNS: Namecheap explicit CNAME record for adamcherrycomics (not wildcard, which was being shadowed)

Products: 12 buyable items ranging from $10–$40 USD.

Root Cause Analysis

The Lambda handler in checkout_connect.py uses the current Stripe Python SDK (15.1.0+) with the correct embedded mode signature:

intent = stripe.PaymentIntent.create(
    amount=amount_cents,
    currency="usd",
    ui_mode="embedded",
    return_url=return_url,
)

This returns a PaymentIntent with a client_secret field—the correct response shape for Stripe.js v3's confirmPayment() flow.

However, the frontend modal JavaScript in the live index.html` was calling redirectToCheckout(sessionId=...), which is the old hosted-redirect API that expects a Checkout Session ID, not a PaymentIntent. This is why the checkout was broken.

Why This Happened

The Stripe Python library version 15.1.0 dropped support for the ui_mode="embedded" parameter on Checkout Sessions (it only works on PaymentIntents). The previous handoff notes claimed the site had been "migrated to redirect," but the actual code—and the correct modern pattern—uses PaymentIntents with embedded mode. The frontend code was simply never updated to match the backend implementation.

Remediation Steps Taken

1. Restore the Stripe.js library tag

The handoff notes indicated that the <script src="https://js.stripe.com/v3/"> tag had been removed from index.html. This is required for the Stripe.js v3 API used by embedded PaymentIntents. We re-inserted it in the document head.

2. Update the modal checkout JavaScript

We replaced the old redirectToCheckout() call with the modern confirmPayment() flow. The corrected pattern:

// Old (broken):
// stripe.redirectToCheckout({ sessionId: clientSecret })

// New (correct):
stripe.confirmPayment({
    elements: elements,
    confirmParams: {
        return_url: window.location.origin + '/return',
    },
    redirect: 'if_required'
})

This matches the PaymentIntent client_secret the Lambda returns and properly handles the embedded checkout lifecycle.

3. Multi-origin CORS fix for Lambda

During testing, browser preflight requests (OPTIONS) were being rejected. We patched the Lambda handler to allow the staging origin and derive the return URL from the request origin:

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

This eliminates hardcoded URLs and makes the Lambda reusable across staging and production deployments.

4. CloudFront cache invalidation

After deploying the patched index.html` to S3, we invalidated the CloudFront distribution with path /* to bust stale cached versions.

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

Testing & Validation

We ran three rounds of smoke tests using Playwright against staging before promoting to production:

  • Round 1: Initial modal load and Stripe.js initialization check
  • Round 2: Preflight CORS request validation with explicit API Gateway CORS headers
  • Round 3: Full embedded checkout flow (form fill, payment submission, return URL navigation)

We also tailed CloudWatch logs during live invocations to confirm PaymentIntent creation and CORS header inclusion.

Cleanup & Outstanding Items

Completed:

  • ✅ Checkout flow now functional (PaymentIntent + embedded modal)
  • ✅ CORS headers correct for staging and production origins
  • ✅ CloudFront cache fresh
  • ✅ SEO audit: all pages return 200; no broken links in product cards

Outstanding / Recommended:

  • Delete the unused empty bucket s3://adamcherrycomics.dangerouscentaur.com/ (leftover from initial setup)
  • Request Adam's confirmation that the checkout flow works end-to-end (last verified 2026-05-21)
  • Consider acquiring the apex domain adamcherrycomics.com to improve brand presence and SEO
  • Audit any remaining "DM to Buy" buttons in the source; ensure all product flows use Stripe checkout
  • Document Stripe fee structure (2.9% + fixed fee per transaction