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-checkoutand 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.htmlto 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_URLto 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()toCheckoutSession.create(..., ui_mode="embedded") - Added dynamic
RETURN_URLfrom request origin header - Added
Access-Control-Allow-Origin: *header (or explicit origin list) to handle cross-origin requests - Set
Content-Type: application/jsonin all responses
- Changed
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
- Added
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