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

Executive Summary

During a recent handoff review of the Adam Cherry Comics storefront, we discovered a critical mismatch between documented checkout behavior and production reality. The Lambda function was already configured for Stripe's embedded checkout (`ui_mode="embedded"`), but the frontend modal was missing the required Stripe.js library initialization, causing checkout to fail silently in production. This post documents the diagnostic process, the fixes applied, and the architectural decisions that ensure compliance with our platform's modal-first checkout standard.

What Was Done

We performed a multi-layer verification of the adamcherrycomics checkout flow across production, staging, and EC2 development environments:

  • Verified page health: All 12 product pages returned HTTP 200 from CloudFront distribution E2Q4UU71SRNTMB
  • Diagnosed checkout failure: Lambda adam-cherry-checkout was correctly minting Stripe PaymentIntent objects with ui_mode="embedded", but the frontend modal JS had no Stripe.js initialization
  • Patched frontend: Added missing <script src="https://js.stripe.com/v3/"></script> tag and modal initialization logic to index.html
  • Fixed Lambda CORS: Updated checkout Lambda to derive RETURN_URL from the request's Origin header, supporting both staging and production domains
  • Validated multi-origin support: Patched API Gateway n0nh1zscq4 CORS configuration to accept adamcherrycomics.dangerouscentaur.com and staging origins
  • Smoke-tested: Deployed fixes to staging, validated with Playwright, promoted to production, and re-verified

Technical Details: The Diagnostic Journey

Layer 1: Handoff vs. Reality

The handoff documentation claimed the site had been migrated from embedded modal to "hosted redirect" due to `stripe-python 15.1.0` rejecting the embedded mode. However, inspecting the live Lambda code revealed it was already using `ui_mode="embedded"` when creating PaymentIntent objects. The actual problem wasn't the backend—it was the frontend.

We captured the live S3 object from s3://dc-sites/adamcherrycomics.dangerouscentaur.com/index.html and compared it against the EC2 development copy. The missing piece: no Stripe.js library load and no modal initialization in the checkout button handler.

Layer 2: Lambda Configuration Audit

The checkout Lambda at adam-cherry-checkout was correctly configured:


# Lambda handler (simplified)
def lambda_handler(event, context):
    publishable_key = os.environ['STRIPE_PUBLISHABLE_KEY']
    secret_key = os.environ['STRIPE_SECRET_KEY']
    
    intent = stripe.PaymentIntent.create(
        amount=int(float(product_price) * 100),
        currency='usd',
        ui_mode='embedded',
        return_url=derive_return_url(event)  # ORIGIN-aware
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'client_secret': intent.client_secret
        })
    }

The Lambda was returning a valid PaymentIntent client_secret, but the frontend had no code to receive it or mount the Stripe Elements form.

Layer 3: Frontend Modal Gap

Inspecting the live modal HTML (extracted from CloudFront), we found:


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

<!-- BUTTON HANDLER -->
document.getElementById('checkout-btn').addEventListener('click', async () => {
  // Called Lambda to get client_secret
  // BUT: no Stripe object, no Elements initialization, no form mount
});

The JavaScript was making the API call to Lambda but couldn't do anything with the response because the Stripe library was never loaded.

Infrastructure & Deployment Changes

S3 & CloudFront

Files modified and deployed to s3://dc-sites/adamcherrycomics.dangerouscentaur.com/:

  • index.html — Added Stripe.js library tag in <head> and embedded checkout initialization in modal handler
  • CloudFront invalidation — Pattern /index.html on distribution E2Q4UU71SRNTMB to clear cached versions

Lambda Environment & CORS

Updated adam-cherry-checkout Lambda:

  • RETURN_URL derivation: Changed from hardcoded production URL to extracting from event['headers']['Origin'], enabling the same Lambda to serve staging and production
  • CORS headers: Added Access-Control-Allow-Origin: * (permissive for now; restrict to known origins in production hardening)

Patched API Gateway n0nh1zscq4 CORS configuration:

  • Added staging origin (e.g., https://adamcherrycomics-staging.dangerouscentaur.com)
  • Ensured OPTIONS preflight requests route correctly before hitting the Lambda
  • Verified response headers include Access-Control-Allow-Credentials: true for session handling

Monitoring & Validation

During testing, we:

  • Tailed Lambda CloudWatch logs: /aws/lambda/adam-cherry-checkout
  • Ran direct Lambda invocations with test payloads to isolate backend errors from frontend issues
  • Executed Playwright smoke tests against staging before promoting to production
  • Verified each product page (12 total, prices $10–$40) could reach the modal and receive a valid PaymentIntent

Key Decisions & Rationale

Why Embedded Modal (Not Redirect)

We maintain a platform-wide rule: all checkout flows use embedded modal. This ensures:

  • User experience: Customers stay on the merchant site instead of being redirected away
  • Brand consistency: All partner sites follow the same UX pattern
  • Conversion: Embedded flows typically outperform redirects in A/B studies

The handoff's claim that `stripe-python 15.1.0` couldn't support embedded mode was incorrect or outdated. The current Lambda code proves embedded mode works fine with the library version in use.