Skip to content

[Slice C] Stripe guest checkout (single-shot, happy path) with cart cleanup #320

@field123

Description

@field123

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, 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.
  • handlePay happy path: createCartPaymentIntent({confirm:true, confirmation_token})checkoutApiconfirmOrder → cart cleanup → returns session with status: "complete" and payment.status: "succeeded".
  • 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.

Blocked by

User stories addressed

Reference by number from the parent PRD:

  • User story 5
  • User story 6
  • User story 7
  • User story 14
  • User story 15
  • User story 16
  • User story 20
  • User story 25
  • User story 26
  • User story 27

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions