Skip to content

[Slice 4] 3DS-challenged checkout completes #324

@field123

Description

@field123

Parent PRD

#317

What to build

A customer paying with a 3D Secure-required card (e.g. Stripe test card 4000 0038 0000 0200) completes the checkout through Stripe's 3DS challenge and ends with a paid order, exactly as in Slice 1's happy path.

The vertical user journey:

  1. Customer reaches /checkout (anonymous or account, no subscription gate). They fill the form, select shipping, enter card details.
  2. They click "Place Order". EPStripePayment captures a confirmation token and calls placeOrder.
  3. Server runs createCartPaymentIntent({ confirm: true, confirmation_token }). Stripe responds with requires_action because of 3DS.
  4. Server's handlePay detects the requires_action status, stores the action data into session.payment.actionData, leaves session.status as open, leaves session.payment.status as requires_action. No order is created at this point. Returns the session.
  5. Client-side EPStripePayment inspects the response, calls stripe.handleNextAction({ clientSecret }). Stripe's 3DS modal/redirect flow handles the bank challenge.
  6. On success, client calls a new resumePayment refAction, which POSTs /api/checkout/sessions/current/resume-payment.
  7. Server's new handleResumePayment re-reads the PI from EP. If it's now succeeded, runs the same tail as Slice 1: checkoutApiconfirmOrder → cart cleanup. If still pending or failed, returns the appropriate error.
  8. Customer sees order confirmation; the cart is empty. Same end state as a non-3DS Slice 1 checkout.

This slice also retires handleConfirm (legacy from the old session model) by aliasing it to handleResumePayment for backwards compatibility, then deprecating it. useCheckoutSession.confirmPayment() is marked deprecated and forwards to resumePayment.

Cuts through every layer:

  • Package — handler. handlePay gains a requires_action branch: stores action data, returns early without creating an order. New handleResumePayment handler completes the post-3DS flow. handleConfirm aliased to handleResumePayment and marked deprecated.
  • Package — adapter. Cart Payment Intent Adapter extended to map the requires_action response into a PaymentAdapterResult with the action data passed through.
  • Package — component. EPStripePayment inspects the placeOrder response. On requires_action, runs stripe.handleNextAction({ clientSecret }) (where the client secret is in the action data). On Stripe success, calls resumePayment refAction. On Stripe failure or user cancel, surfaces the error and leaves the session retryable.
  • Package — session provider. EPCheckoutSessionProvider exposes a new resumePayment refAction. confirmPayment refAction kept and aliased to resumePayment for backwards compatibility.
  • Host — routes. New route file app/api/checkout/sessions/current/resume-payment/route.ts mounting handleResumePayment.
  • Tests. Unit tests for the requires_action mapping in the adapter. Handler tests for the requires_action branch and handleResumePayment (success, still-pending, failed sub-cases). Integration test for /resume-payment end-to-end. Component test for EPStripePayment's 3DS handling, including failure and cancel cases.

See parent PRD §Implementation Decisions → "Single-shot checkout flow" and the 3DS notes in §Further Notes.

Acceptance criteria

  • handlePay returns HTTP 200 with a session in payment.status === "requires_action" when EP responds with requires_action. No EP order is created at this point.
  • session.payment.actionData carries the data needed for stripe.handleNextAction (typically a client_secret).
  • handleResumePayment re-reads the PaymentIntent status from EP and proceeds with checkoutApiconfirmOrder → cart cleanup when the PI is succeeded.
  • handleResumePayment returns a typed error and leaves the session retryable when the PI is still requires_action, requires_payment_method, or otherwise not succeeded.
  • EPStripePayment automatically runs stripe.handleNextAction when the placeOrder response carries requires_action, then calls the resumePayment refAction.
  • EPStripePayment surfaces the error to the designer (via $ctx.stripePaymentData.error) when the user cancels the 3DS challenge or it fails.
  • New host route POST /api/checkout/sessions/current/resume-payment is mounted and routes through the standard request adapter.
  • useCheckoutSession.confirmPayment() continues to work (aliased to resumePayment) and emits a deprecation warning in development builds.
  • Manual end-to-end: customer completes a checkout with 4000 0038 0000 0200, sees the 3DS challenge modal, completes it successfully, and lands on the order confirmation; the resulting EP order is paid; the cart is empty.
  • Manual end-to-end: customer cancels the 3DS challenge mid-flow, returns to /checkout, sees a retryable error, can resubmit with a different card and complete.

Blocked by

User stories addressed

Reference by number from the parent PRD:

  • User story 9
  • User story 23
  • User story 24

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