You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
End customer completes a guest checkout with a Stripe test card (e.g. 4242 4242 4242 4242). Server runs the single-shot EP-native Stripe flow: createCartPaymentIntent({ confirm: true, confirmation_token }) → checkoutApi (cart→order) → confirmOrder (sync PI status). On success, the cart is deleted, epCartId is cleared on the better-auth session, and a fresh cart is lazily created on the customer's next add-to-cart.
This is the meat of the work. It establishes the entire payment path end-to-end. It deliberately ships only the guest-checkout, no-3DS, no-subscription happy path; subscriptions, 3DS, and account-checkout are layered on in slices D and E.
Cuts through every layer:
Package auth: better-auth EP plugin gains a client_credentials grant path. SessionHandlerContext gains getClientCredentialsToken: () => Promise<string> — request-scoped, memoized once per request via closure, never cached across requests.
Package adapter: stripe-adapter is rewritten. The @stripe/stripe-js server import is removed. The adapter now formats a request body for createCartPaymentIntent with gateway: "elastic_path_payments_stripe", method: "purchase", confirm: true, and the client-supplied confirmation_token. Maps EP's response to the existing PaymentAdapterResult union.
Package handler: handlePay is restructured to the single-shot sequence. Order creation is deferred until after createCartPaymentIntent returns succeeded. Cart cleanup tail (deleteAccountCartAssociation if applicable, deleteACart, clear epCartId) runs on success. Cleanup failures are logged but do not fail the response.
Package component: EPStripePayment is rewritten. Uses <Elements> with mode: 'payment' (deferred PaymentIntent), renders <PaymentElement> and <AddressElement>, calls stripe.createConfirmationToken({ elements }) on submit, hands the token to the session's placeOrder refAction. No client-side stripe.confirmPayment call.
Package context: new StripeProvider Plasmic global context with publishableKey field. EPStripePayment reads from $ctx.stripe.publishableKey when its own prop is unset.
Host: lib/checkout-context.ts factory grows to mint client_credentials tokens per-request (closure-memoized) and to register the Stripe adapter conditionally on EP_CLIENT_SECRET presence. POST /api/checkout/sessions/current/pay route mounted. Plasmic registration registers the new StripeProvider global context with publishableKey from process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
Env: .env.local.example adds EP_CLIENT_SECRET (server-only, with comment forbidding NEXT_PUBLIC_ prefix), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. Removes STRIPE_SECRET_KEY (no longer needed).
Tests: unit tests for Cart Payment Intent Adapter, Client Credentials Token Resolver, Cart Cleanup Operation, Checkout Body Builder (guest path), and Session State Transition (open → complete). Integration test for /pay exercising guest happy path including cart cleanup.
See parent PRD §Solution and §Implementation Decisions → "Stripe integration", "Single-shot checkout flow", "Cart cleanup", "Adapter / gateway flexibility".
Acceptance criteria
EP better-auth plugin can mint a client_credentials token via createAnAccessToken using EP_CLIENT_ID + EP_CLIENT_SECRET.
SessionHandlerContext.getClientCredentialsToken mints once per request lifecycle and returns the same token on subsequent calls within that request.
Stripe SDK (stripe npm package) is no longer a direct dependency of the package; @stripe/stripe-js and @stripe/react-stripe-js remain (client only).
stripe-adapter makes no network calls to Stripe directly; all Stripe interaction goes through EP via the shopper SDK.
On requires_payment_method or other failed status from createCartPaymentIntent, no order is created, session stays open, payment.status: "failed" with error surface.
Cart cleanup deletes the EP cart, clears epCartId on the better-auth session, and does not pre-create a new cart.
EPStripePayment renders <PaymentElement> and <AddressElement>, captures a confirmation_token on submit, and calls placeOrder({ confirmation_token }) exactly once.
StripeProvider global context is registered in the package's registerCheckout. Designer can fill publishableKey in Plasmic Studio's global context config.
EPStripePayment falls back to $ctx.stripe.publishableKey when its publishableKey prop is unset.
Host app .env.local.example documents EP_CLIENT_SECRET as server-only (comment forbids NEXT_PUBLIC_ prefix) and adds NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
Unit test coverage on the four payment-flow deep modules listed above.
Integration test for /pay covering the guest happy path with mocked EP SDK responses.
Manual end-to-end test against an EP sandbox + Stripe test mode: anonymous shopper adds item, completes checkout with 4242 4242 4242 4242, EP order is created and marked paid, cart is empty after.
Parent PRD
#317
What to build
End customer completes a guest checkout with a Stripe test card (e.g.
4242 4242 4242 4242). Server runs the single-shot EP-native Stripe flow:createCartPaymentIntent({ confirm: true, confirmation_token })→checkoutApi(cart→order) →confirmOrder(sync PI status). On success, the cart is deleted,epCartIdis cleared on the better-auth session, and a fresh cart is lazily created on the customer's next add-to-cart.This is the meat of the work. It establishes the entire payment path end-to-end. It deliberately ships only the guest-checkout, no-3DS, no-subscription happy path; subscriptions, 3DS, and account-checkout are layered on in slices D and E.
Cuts through every layer:
client_credentialsgrant path.SessionHandlerContextgainsgetClientCredentialsToken: () => Promise<string>— request-scoped, memoized once per request via closure, never cached across requests.stripe-adapteris rewritten. The@stripe/stripe-jsserver import is removed. The adapter now formats a request body forcreateCartPaymentIntentwithgateway: "elastic_path_payments_stripe",method: "purchase",confirm: true, and the client-suppliedconfirmation_token. Maps EP's response to the existingPaymentAdapterResultunion.handlePayis restructured to the single-shot sequence. Order creation is deferred until aftercreateCartPaymentIntentreturnssucceeded. Cart cleanup tail (deleteAccountCartAssociationif applicable,deleteACart, clearepCartId) runs on success. Cleanup failures are logged but do not fail the response.EPStripePaymentis rewritten. Uses<Elements>withmode: 'payment'(deferred PaymentIntent), renders<PaymentElement>and<AddressElement>, callsstripe.createConfirmationToken({ elements })on submit, hands the token to the session'splaceOrderrefAction. No client-sidestripe.confirmPaymentcall.StripeProviderPlasmic global context withpublishableKeyfield.EPStripePaymentreads from$ctx.stripe.publishableKeywhen its own prop is unset.lib/checkout-context.tsfactory grows to mintclient_credentialstokens per-request (closure-memoized) and to register the Stripe adapter conditionally onEP_CLIENT_SECRETpresence.POST /api/checkout/sessions/current/payroute mounted. Plasmic registration registers the newStripeProviderglobal context withpublishableKeyfromprocess.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY..env.local.exampleaddsEP_CLIENT_SECRET(server-only, with comment forbiddingNEXT_PUBLIC_prefix),NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. RemovesSTRIPE_SECRET_KEY(no longer needed).Cart Payment Intent Adapter,Client Credentials Token Resolver,Cart Cleanup Operation,Checkout Body Builder(guest path), andSession State Transition(open → complete). Integration test for/payexercising guest happy path including cart cleanup.See parent PRD §Solution and §Implementation Decisions → "Stripe integration", "Single-shot checkout flow", "Cart cleanup", "Adapter / gateway flexibility".
Acceptance criteria
client_credentialstoken viacreateAnAccessTokenusingEP_CLIENT_ID+EP_CLIENT_SECRET.SessionHandlerContext.getClientCredentialsTokenmints once per request lifecycle and returns the same token on subsequent calls within that request.stripenpm package) is no longer a direct dependency of the package;@stripe/stripe-jsand@stripe/react-stripe-jsremain (client only).stripe-adaptermakes no network calls to Stripe directly; all Stripe interaction goes through EP via the shopper SDK.handlePayhappy path:createCartPaymentIntent({confirm:true, confirmation_token})→checkoutApi→confirmOrder→ cart cleanup → returns session withstatus: "complete"andpayment.status: "succeeded".requires_payment_methodor other failed status fromcreateCartPaymentIntent, no order is created, session staysopen,payment.status: "failed"with error surface.epCartIdon the better-auth session, and does not pre-create a new cart.EPStripePaymentrenders<PaymentElement>and<AddressElement>, captures aconfirmation_tokenon submit, and callsplaceOrder({ confirmation_token })exactly once.StripeProviderglobal context is registered in the package'sregisterCheckout. Designer can fillpublishableKeyin Plasmic Studio's global context config.EPStripePaymentfalls back to$ctx.stripe.publishableKeywhen itspublishableKeyprop is unset..env.local.exampledocumentsEP_CLIENT_SECRETas server-only (comment forbidsNEXT_PUBLIC_prefix) and addsNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY./paycovering the guest happy path with mocked EP SDK responses.4242 4242 4242 4242, EP order is created and marked paid, cart is empty after.Blocked by
User stories addressed
Reference by number from the parent PRD: