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 distributionE2Q4UU71SRNTMB - Updated Lambda environment and handler logic to ensure multi-origin CORS headers and correct
RETURN_URLderivation - 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 aPaymentIntentclient 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:
- 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
Originrequest header and reflect it back in the response, allowing both the staging and production domains to make preflight requests. - RETURN_URL derivation: Changed the Lambda to construct the return URL from the request's
Originheader 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-Originresponse - Checkout POST: Clicked "Buy" button, captured network request to
/api/checkout, confirmed Lambda returnedclient_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