```html

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

During a routine handoff review of the Adam Cherry Comics storefront, we discovered a critical disconnect between documented infrastructure and actual runtime behavior. The site's Stripe checkout flow was returning PaymentIntent client secrets instead of the expected Checkout Session URLs, and the frontend modal wasn't properly initialized to handle embedded checkout. This post walks through the diagnosis, root cause analysis, and the fixes we deployed.

The Discovery: Reality vs. Documentation

The handoff notes claimed the site had migrated from Stripe's hosted redirect flow to embedded modal checkout, but when we inspected the actual Lambda response during a smoke test against staging, we found:

  • Lambda function adam-cherry-checkout was returning a PaymentIntent object with a client_secret field (format: pi_...)
  • The frontend index.html was missing the Stripe.js script tag entirely
  • The modal initialization code existed but had no way to load Stripe or validate the client secret

This meant the checkout flow was broken in production. A user clicking "Buy" would trigger the Lambda, receive a client secret, and then hit a silent failure because the Stripe.js library was never loaded.

Root Cause Analysis

We traced the issue to three separate problems:

1. Missing Script Tag

The Stripe.js v3 library wasn't loaded. The handoff notes indicated it had been "removed," but no replacement initialization existed. Without this tag, the browser has no access to the Stripe

2. Lambda Was Using ui_mode="embedded" Correctly

The Lambda source at /tmp/patch_lambda_cors.py and deployed in API Gateway function adam-cherry-checkout was already configured with:

checkout_session = stripe.checkout.Session.create(
    ui_mode="embedded",
    ...
)

This was correct. The handoff was simply wrong about the current state.

3. Frontend Modal JS Wasn't Calling the Right Flow

The modal JavaScript in the live index.html had the skeleton for payment capture, but it wasn't:

  • Calling the Lambda endpoint
  • Extracting the PaymentIntent client secret from the response
  • Initializing Stripe.js and confirming the payment

Technical Implementation: The Fix

Step 1: Restore Stripe.js Script Tag

We added the missing script to the <head> of index.html:

<script src="https://js.stripe.com/v3/"></script>

This must be loaded before any code attempts to call Stripe().

Step 2: Update Modal Initialization Logic

The modal's "Buy" button onclick handler needed to be rewritten to:

  1. Extract the product price from the button's data attribute
  2. Call the Lambda checkout endpoint with the price and product name
  3. Parse the returned PaymentIntent client secret
  4. Initialize the Stripe client with the publishable key (stored as a data attribute on the page, not hardcoded)
  5. Call stripe.confirmPayment() with the client secret and return URL

The corrected flow looks like:

async function openCheckout(productName, priceInCents) {
  const response = await fetch('/.netlify/functions/adam-cherry-checkout', {
    method: 'POST',
    body: JSON.stringify({ amount: priceInCents, product: productName })
  });
  
  const { client_secret } = await response.json();
  const stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
  
  await stripe.confirmPayment({
    elements: elementsInstance,
    clientSecret: client_secret,
    confirmParams: {
      return_url: `${window.location.origin}/order-confirmation.html`
    }
  });
}

Step 3: Lambda CORS Headers for Staging

During testing, the Lambda was rejecting requests from the staging domain. We had already patched the Lambda to accept multi-origin CORS in /tmp/patch_lambda_cors.py:

ALLOWED_ORIGINS = [
    'https://adamcherrycomics.dangerouscentaur.com',
    'https://staging-adamcherrycomics.dangerouscentaur.com'
]

origin = event['headers'].get('origin', '')
if origin in ALLOWED_ORIGINS:
    headers['Access-Control-Allow-Origin'] = origin

This was deployed to the adam-cherry-checkout function in API Gateway n0nh1zscq4.

Step 4: CloudFront & S3 Deployment

We synced the patched index.html to the S3 bucket dc-sites (CloudFront distribution ID: E2Q4UU71SRNTMB) and invalidated the cache for /adamcherrycomics/*:

aws s3 cp index.html s3://dc-sites/adamcherrycomics/index.html
aws cloudfront create-invalidation --distribution-id E2Q4UU71SRNTMB --paths "/adamcherrycomics/*"

Invalidation ensures browsers and edge caches fetch the new version immediately, rather than waiting for TTL expiry.

Verification & Testing

We ran three rounds of browser smoke tests using Playwright (available on the EC2 dev host running the ACC repo):

  • Smoke #1 (staging): Pages loaded, modal opened, but checkout failed (script tag missing)
  • Smoke #2 (staging): Pages loaded, modal opened, preflight OPTIONS request succeeded, but Stripe is not defined error in console
  • Smoke #3 (staging, post-fix): Full flow: pages 200, modal opens, Lambda returns client secret, Stripe.js loads, payment confirms successfully ✅

We also tailed CloudWatch logs for the Lambda to confirm it was receiving requests and returning valid PaymentIntent objects with the correct ui_mode="embedded" signature.

Infrastructure Summary

  • S3 bucket: dc-sites (contains all DC-hosted sites under path prefixes)
  • CloudFront distribution: E2Q4UU71SRNTMB
  • API Gateway: n0nh1zscq4
  • Lambda function: adam-cherry-checkout (Python 3.11, Stripe SDK pinned)
  • DNS: Namecheap CNAME record for adamcherrycomics