Skip to content

feat(checkout): plumb off-session card saving for yearly renewals (draft — needs consent UI)#265

Draft
kilbot wants to merge 2 commits into
mainfrom
feat/stripe-save-card
Draft

feat(checkout): plumb off-session card saving for yearly renewals (draft — needs consent UI)#265
kilbot wants to merge 2 commits into
mainfrom
feat/stripe-save-card

Conversation

@kilbot

@kilbot kilbot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Draft. Depends on #264merge #264 first. Until then this PR's diff also shows #264's commit; it narrows to just the Phase 2 delta once #264 lands. Do not mark ready until the owner decisions below are made.

What this is

The engineering seam for Level 2 (save the card), building on #264 (attach a Stripe Customer). It lets a yearly buyer's card be saved as a reusable, off-session Stripe payment method — the prerequisite for a future "click to renew" charge.

How it works

  • createPaymentSession gains an optional setupFutureUsage, forwarded as the session data.
  • Medusa's store payment-sessions route passes data verbatim into the Stripe provider's initiatePayment, where normalizePaymentIntentParameters copies setup_future_usage onto the PaymentIntent (verified in @medusajs/payment-stripe source).
  • The route sets 'off_session' only when all three hold: the buyer consented (saveCard), the plan is yearly (lifetime never renews), and it's the Stripe card provider (PayPal/BTCPay have no off-session-card concept).

⚠️ Why this is a draft — DORMANT until you decide

No client sets saveCard yet, so on the wire this changes nothing today. I deliberately stopped at the engineering boundary and did not build the consent UI, because it's a legal/UX decision that's yours:

  1. Consent copy + mandate text — the exact wording next to the "save my card" checkbox (card-network rules require a clear mandate for off-session reuse).
  2. Default state — checkbox checked or unchecked by default.
  3. Sequencing — a saved card is only useful once the renewal-charge flow (Level 3) exists (off-session charge → extend Keygen license, with an SCA fallback for EU/PL buyers). Storing cards before that flow exists means holding PII you can't yet use — decide whether to merge this ahead of L3 or hold it.

To activate: add the consent checkbox, thread its value as saveCard through the complete-cart client wrapper → this route. The seam is ready and tested.

Validation

  • vitest run1161/1161 pass (added: setup_future_usage threading; yearly-only / Stripe-only / consent-absent / lifetime gating)
  • eslint, tsc --noEmit, next build — all clean

Not in scope

Level 3 renewal-charge flow; the consent UI (see above); the Q2 order↔customer-linkage question flagged in #264.

🤖 Generated with Claude Code

kilbot added 2 commits July 5, 2026 22:47
Forward the signed-in customer's Medusa JWT on the payment-collection and
payment-session calls so Medusa resolves `auth_context.actor_id` to the
customer and creates/links a Stripe account holder (cus_...) on the
PaymentIntent — instead of the throwaway "Guest" that Stripe invents for
publishable-key-only requests.

The store payment-sessions workflow only creates an account holder
`when("customer-id-exists")`, and `customer_id` is derived solely from the
request's auth context; today the storefront's medusaFetch sends only the
publishable key, so actor_id is empty and every sale shows as Guest.

getCustomer() in the route already validates the token against
/store/customers/me (which rejects an empty actor_id), so the forwarded
token is guaranteed to resolve to a real customer. A missing token degrades
to the previous guest behaviour rather than breaking checkout.

Scope: attach-customer only (bookkeeping/reconciliation). Saving the card
for off-session renewals is a separate follow-up.
Add the engineering seam that saves a buyer's card as a reusable,
off-session Stripe payment method — the prerequisite for a future
"click to renew" charge on the yearly plan.

createPaymentSession now accepts an optional setup_future_usage, forwarded
as the session `data`; Medusa's store payment-sessions route passes `data`
into the Stripe provider's initiatePayment, where it lands on the
PaymentIntent (verified against @medusajs/payment-stripe
normalizePaymentIntentParameters). The route sets 'off_session' only when
the buyer consented (saveCard), the plan is yearly (lifetime never renews),
and it's the Stripe card provider (PayPal/BTCPay have no off-session-card
concept). A saved card is only chargeable because the customer is attached
(previous commit).

DORMANT until a consent UI is added: no client sets saveCard yet, so today
this changes nothing on the wire. The consent checkbox + mandate copy is a
deliberate owner/legal decision and is intentionally NOT included here.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a6d7ca9e-ef0d-4a0a-a820-00b7d41a90ef

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stripe-save-card

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-m8t8i1ffr-wcpos.vercel.app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant