Skip to content

[Slice 1] Anonymous guest checkout — full happy path #321

@field123

Description

@field123

Parent PRD

#317

What to build

An anonymous shopper completes a guest checkout end-to-end, paying with a non-3DS test card, and ends with an EP order marked paid and an empty cart.

The full vertical user journey:

  1. Anonymous shopper has an item in their cart.
  2. They navigate to the checkout page in the demo Plasmic project.
  3. The page renders an EPCheckoutSessionProvider that auto-creates a checkout session (no cartId argument needed — server resolves it from the better-auth session).
  4. They fill customer info, shipping address, and billing address using the existing form components driven by Plasmic refActions on the session.
  5. They request shipping rates (calculateShipping); rates appear in $ctx.checkoutSession.session.availableShippingRates and the designer's repeated UI shows them.
  6. They select a shipping rate.
  7. They enter card details into EPStripePayment (Stripe <PaymentElement> with mode: 'payment').
  8. They click "Place Order".
  9. Browser captures a Stripe confirmation_token via stripe.createConfirmationToken, calls placeOrder with it, and the server runs the single-shot flow: createCartPaymentIntent({ confirm: true, confirmation_token })checkoutApiconfirmOrder → cart cleanup.
  10. Response carries the completed session; the designer's success UI renders from session.status === "complete" and session.order.id.
  11. The cart is empty when the user returns to the storefront.

This slice deliberately covers only: anonymous (no account), no subscription items, non-3DS cards. The further customer types (account, subscription gate, 3DS) ship in subsequent slices.

Cuts through every layer:

  • Package — auth. Better-auth EP plugin gains a client_credentials grant path. SessionHandlerContext gains shopperAccessToken and getClientCredentialsToken: () => Promise<string> (request-scoped, memoized via closure, never cached across requests). The unused clientSecret field on EPCredentials is removed.
  • Package — adapter. stripe-adapter is rewritten to call EP's createCartPaymentIntent with gateway: "elastic_path_payments_stripe", method: "purchase", confirm: true, and the supplied confirmation_token. The host-side stripe npm package dependency is dropped. Result mapped to existing PaymentAdapterResult union.
  • Package — handler. handlePay restructured to single-shot flow: cart-hash check → createCartPaymentIntent({confirm:true,...}) → on succeeded, checkoutApiconfirmOrder → cart cleanup → return complete session. On failed, session stays open for retry. (requires_action and the subscription/account branches arrive in slices 2–4.) handleUpdateSession and handleCalculateShipping thread shopperAccessToken for their EP SDK calls.
  • Package — components. EPStripePayment rewritten: <Elements> with mode: 'payment', deferred PaymentIntent, <PaymentElement> + <AddressElement>, on submit calls stripe.createConfirmationToken({ elements }) and hands the token to placeOrder. No client-side stripe.confirmPayment. New StripeProvider Plasmic global context exposes publishableKey to all EPStripePayment instances. EPCheckoutSessionProvider auto-creates the session on mount when none exists (configurable via autoCreate prop, default true). useCheckoutSession.createSession() no longer requires a cartId argument.
  • Host — context. examples/ep-commerce-app-router/lib/checkout-context.ts is a per-request factory: resolves shopper access token from epAuth.api.getSession(), builds a closure-memoized getClientCredentialsToken callable, conditionally registers the Stripe adapter when EP_CLIENT_SECRET is present.
  • Host — routes. Five route files mount the existing handlers via thin NextRequestSessionRequest adapters: POST /api/checkout/sessions, GET /api/checkout/sessions/current, PATCH /api/checkout/sessions/current, POST /api/checkout/sessions/current/shipping, POST /api/checkout/sessions/current/pay. Each is ~6 lines of glue.
  • Host — env. .env.local.example adds EP_CLIENT_SECRET (server-only, with comment forbidding NEXT_PUBLIC_ prefix), adds NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, removes STRIPE_SECRET_KEY. StripeProvider is registered in plasmic-register-components.ts with publishableKey from process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
  • Tests. Unit tests for the deep modules: Client Credentials Token Resolver, Cart Payment Intent Adapter (success + failed paths), Cart Cleanup Operation, Checkout Body Builder (guest path), Session State Transition (open → complete). Integration tests for each of the five routes against a mocked EP SDK; /pay integration test exercises the full happy path.

See parent PRD §Solution and §Implementation Decisions for full detail.

Acceptance criteria

  • Better-auth EP plugin can mint a client_credentials token via the EP /oauth/access_token endpoint.
  • SessionHandlerContext carries shopperAccessToken: string and getClientCredentialsToken: () => Promise<string>. The token resolver mints once per request lifecycle and returns the same token on subsequent calls within that request. There is no shared cache across requests.
  • stripe-adapter makes no direct network calls to Stripe; all Stripe interaction goes through EP's createCartPaymentIntent. The stripe npm package is no longer a runtime dependency of the package.
  • handlePay happy path produces an EP order with payment.status === "paid" (or EP equivalent) and a session with status === "complete" and payment.status === "succeeded".
  • On Stripe-side failure, no EP order is created; the session stays open and payment.status === "failed"; the customer can retry.
  • After successful checkout, the EP cart is deleted and epCartId is cleared from the better-auth session. The next add-to-cart action lazily creates a fresh cart.
  • EPCheckoutSessionProvider automatically calls createSession() on first render when no session exists and autoCreate !== false.
  • 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; EPStripePayment falls back to $ctx.stripe.publishableKey when its publishableKey prop is unset.
  • .env.local.example documents EP_CLIENT_SECRET with explicit server-only comment; no STRIPE_SECRET_KEY is referenced.
  • Unit tests pass for all five deep modules listed above (happy paths only at this slice).
  • Integration tests pass for all five routes; /pay integration test covers the full guest happy path.
  • Manual end-to-end against EP sandbox + Stripe test mode: anonymous shopper completes a checkout with 4242 4242 4242 4242, sees an EP order created and marked paid, returns to the storefront and finds the cart empty.

Blocked by

None - can start immediately.

User stories addressed

Reference by number from the parent PRD:

  • User story 1
  • User story 2
  • User story 3
  • User story 4
  • User story 5
  • User story 6
  • User story 7
  • User story 12
  • User story 13
  • User story 14
  • User story 15
  • User story 16
  • User story 17
  • User story 19
  • 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