Skip to content

Replace Stripe Customer Portal with in-app payment methods page (ZUP-6010)#2537

Open
max-zuplo wants to merge 6 commits into
mainfrom
claude/nifty-fermat-M776d
Open

Replace Stripe Customer Portal with in-app payment methods page (ZUP-6010)#2537
max-zuplo wants to merge 6 commits into
mainfrom
claude/nifty-fermat-M776d

Conversation

@max-zuplo

Copy link
Copy Markdown
Contributor

What

The Manage payment details profile menu item used to open the Stripe-hosted Customer Portal in a new tab — where a user could delete their only/default card and break billing (ZUP-6010). This replaces /manage-payment with a custom in-app page that only manages payment methods (no subscription cancel / plan change), in @zuplo/zudoku-plugin-monetization.

UX

  • List: {Brand} •••• {last4}, Expires mm/yy, a Default badge on the default card.
  • Add: "Add payment method" → POST .../setup-intent → modal with Stripe PaymentElementconfirmSetup({ redirect: "if_required" }) → refresh on success (the new card may briefly show as non-default until Stripe's webhook syncs the default — expected).
  • Set as default per non-default card.
  • Remove with a confirm dialog. Disabled on the default and the last remaining card, with an explanation to set another default first. If the backend guard (zuplo/openmeter#122) still fires (e.g. stale list), its 409 message is surfaced inline.
  • Empty state: "No payment methods yet. Add one to get started." Stripe PaymentElement errors are shown inline.

Implementation

  • New pages/PaymentMethodsPage.tsx + lazy pages/AddPaymentMethodDialog.tsx (Stripe Elements is code-split — confirmed it lands in its own chunk, not the plugin base bundle).
  • /manage-payment moved into the default-layout route group and repointed to the new page; menu item no longer opens a new tab. Old ManagePaymentPage.tsx (the redirect) removed.
  • All calls go through the gateway (/v3/zudoku-metering/:deployment/stripe/*) using the existing TanStack Query + signRequest pattern; the browser never talks to OpenMeter directly. Response types are hand-typed (no OpenAPI codegen against the gateway in this repo).
  • Adds @stripe/stripe-js and @stripe/react-stripe-js.

Verification

Automated (all ✓): pnpm --filter @zuplo/zudoku-plugin-monetization typecheck, build (tsdown), vitest (26 files / 276 tests), biome lint, oxfmt.

Manual E2E (run against a staging deployment with a Stripe-test app installed): this sandbox has no staging creds / Stripe test keys / running gateway, so end-to-end wasn't run here — script to validate:

  1. Sign in to the developer portal → Manage payment details (/manage-payment). Empty state shows if no cards.
  2. Add → card 4242 4242 4242 4242, any future expiry / CVC / ZIP → Save. Card appears (becomes default once the webhook syncs).
  3. Add a second card 5555 5555 5555 4444. Two cards listed; one Default.
  4. Set as default on the non-default card → badge moves.
  5. Remove the now-non-default card → confirm → it disappears.
  6. Confirm Remove is disabled on the default card and when only one card remains; forcing the call returns 409 with the guard message inline.
  7. Reload → list reflects final state.

Screenshots to be attached from the staging run.

Depends on

zuplo/gateway-service#666 (gateway endpoints) and zuplo/openmeter#120 + #122 (OpenMeter portal API + delete guard).

https://claude.ai/code/session_01BUZGtV2rjrDwWU9pphd1YW


Generated by Claude Code

…6010)

The "Manage payment details" menu item used to redirect users to the Stripe-
hosted Customer Portal, where they could remove their only/default card and
break billing. Replace /manage-payment with a custom in-app page where users
list cards, add a card via Stripe Elements, set a default, and remove a
non-default card — but not their default or last card.

- PaymentMethodsPage: lists methods (brand •••• last4, expiry, Default badge)
  with set-default and remove per row. Remove is disabled on the default/last
  card with an explanation; the OpenMeter 409 guard is surfaced inline too.
- AddPaymentMethodDialog: lazy-loaded Stripe Elements (PaymentElement +
  confirmSetup with redirect "if_required"), keeping Stripe out of the base
  bundle.
- All calls go through the gateway (/v3/zudoku-metering/:deployment/stripe/*)
  using the existing TanStack Query + signRequest pattern; the browser never
  talks to OpenMeter directly.
- Adds @stripe/stripe-js and @stripe/react-stripe-js.

https://claude.ai/code/session_01BUZGtV2rjrDwWU9pphd1YW
Copilot AI review requested due to automatic review settings June 1, 2026 19:02
@linear

linear Bot commented Jun 1, 2026

Copy link
Copy Markdown

ZUP-6010

@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zudoku-cosmo-cargo Ready Ready Preview, Comment Jun 10, 2026 12:34pm
zudoku-dev Ready Ready Preview, Comment Jun 10, 2026 12:34pm

Request Review

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​stripe/​react-stripe-js@​6.4.09410010099100
Added@​stripe/​stripe-js@​9.7.0100100100100100

View full report

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.6%
⬇️ -0.27%
4675 / 6915
🔵 Statements 66.93%
⬇️ -0.26%
5038 / 7527
🔵 Functions 58.35%
⬇️ -0.43%
1117 / 1914
🔵 Branches 57.17%
⬇️ -0.44%
3364 / 5884
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/plugin-zuplo-monetization/src/ZuploMonetizationPlugin.tsx 56.25%
🟰 ±0%
27.27%
🟰 ±0%
50%
🟰 ±0%
56.25%
🟰 ±0%
20-28, 40-56
packages/plugin-zuplo-monetization/src/pages/PaymentMethodsPage.tsx 9.75% 0% 0% 10.25% 22-23, 46, 49, 52-267
Generated in workflow #5785 for commit b423517 by the Vitest Coverage Report Action

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.

2 participants