Skip to content

feat(ui-react): add billing & subscription management#6232

Open
luizhf42 wants to merge 2 commits intomasterfrom
feat/ui-react/billing
Open

feat(ui-react): add billing & subscription management#6232
luizhf42 wants to merge 2 commits intomasterfrom
feat/ui-react/billing

Conversation

@luizhf42
Copy link
Copy Markdown
Member

@luizhf42 luizhf42 commented Apr 23, 2026

What

Adds the full billing and subscription management surface to the React console for Cloud tenants. Cloud users can now subscribe, manage payment methods, access the Stripe Billing Portal, and see billing-status warnings — all from the Settings page.

Why

The React UI had zero billing functionality (Team #71: 0 of 7 billing tasks done). The backend (9 billing endpoints) and design system were already in place; this PR wires the UI to them.

Fixes: shellhub-io/team#71

Changes

  • hooks/useBilling.ts: React Query wrappers for all billing mutations and queries (useSubscription, useCustomer, useCreateCustomer, useCreateSubscription, useAttachPaymentMethod, useDetachPaymentMethod, useSetDefaultPaymentMethod). Includes useOpenBillingPortal as a manual axios wrapper for /api/billing/portal, which is not in the OpenAPI spec (registered dynamically in the cloud module).

  • types/billing.ts: BillingStatus union literal, typed Customer, Subscription, PaymentMethod, NamespaceBilling with safe narrowing functions. cvc is intentionally absent — the backend always sends the masked sentinel "xxx" and the field is never displayed.

  • BillingSection (Settings.tsx): Status badge, per-status warning banners, Subscribe button (shown only for inactive / canceled / incomplete_expired — statuses where no active Stripe subscription exists), and Open Portal button. Includes hash-scroll on mount for the /settings/billing return URL and a pageshow listener to refetch subscription state after navigating back from the Portal.

  • BillingDialog: 4-step wizard (Overview → Payment method → Review → Success), lazy-loaded so Stripe never lands in the Settings initial chunk. Step 3 gates success on active or trialing status only — incomplete and other non-terminal states surface specific error messages rather than false positives.

  • BillingPayment: Stripe Elements card form with payment-method list (set-default / detach). Bootstraps the Stripe customer on mount with a cancellation-safe sequential flow: refresh namespace → create customer if absent → refresh namespace again.

  • BillingWarning: Device-limit dialog for HTTP 402 responses, wired into DeviceActionDialog and ContainerActionDialog. Non-owners see a passive message; owners get a "Go to Billing" shortcut.

  • env.ts: Added stripePublishableKey to ClientConfig; fixed a concurrency bug where simultaneous loadConfig() callers could each fire a separate fetch("/config.json").

  • App.tsx: Added /settings/billing redirect route so the Stripe Portal return_url (hardcoded to that path in the backend) lands correctly.

  • gen-config.sh / docker-compose.yml: Emit SHELLHUB_STRIPE_PUBLISHABLE_KEY into config.json.

Testing

End-to-end requires SHELLHUB_CLOUD=true, SHELLHUB_BILLING=stripe, and valid Stripe keys. Key paths to verify:

  • Fresh namespace: Subscribe → auto-create customer → test card 4242 4242 4242 4242 → Confirm → status flips to active.
  • Declined cards: 4000 0000 0000 0002 and 4000 0000 0000 9995 should surface mapped error messages.
  • 402 path: open unpaid invoice in Stripe → Subscribe → "unpaid invoices" alert with role="alert".
  • Portal return: navigate to Portal → browser back → subscription status refetches (verify via React Query Devtools).
  • /settings/billing direct navigation → Settings opens with Billing section in view.
  • Device-limit 402: accept device over free-plan limit → BillingWarning → "Go to Billing" → /settings#billing.
  • Non-owner: billing card shows owner-only notice; no Subscribe or Portal buttons.
  • Community / Enterprise (SHELLHUB_CLOUD=false): billing section not rendered.

@luizhf42 luizhf42 self-assigned this Apr 23, 2026
@luizhf42 luizhf42 marked this pull request as ready for review April 23, 2026 21:26
@luizhf42 luizhf42 requested review from a team as code owners April 23, 2026 21:26
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

Claude finished @gustavosbarreto's task in 3m 8s —— View job


Code Review Complete

Reviewed 31 files across code quality, security, testing, TypeScript/React patterns, and architecture. 2 inline issues posted (2 medium).

Additional notes

  • Security: No vulnerabilities found. Stripe publishable key handling, noopener,noreferrer on window.open, and permission gating via useHasPermission are all correctly implemented.
  • Architecture: Stripe dependencies are properly code-split via React.lazy() and only loaded when getConfig().cloud is true. Interface changes (onBillingWarning prop) are additive and optional — no breaking changes.
  • Testing: Good coverage for BillingDialog, BillingSection, BillingWarning, useBilling hooks, and type narrowing functions. BillingPayment.tsx (419 lines, complex bootstrap + Stripe integration) has no direct tests — it's mocked in all test suites. Consider adding tests for the bootstrap flow and error paths.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/components/billing/BillingPayment.tsx
Comment thread ui-react/apps/console/src/components/billing/BillingDialog.tsx
@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch 2 times, most recently from 7a8fe14 to a50cf8c Compare April 24, 2026 11:32
@otavio
Copy link
Copy Markdown
Member

otavio commented Apr 24, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 24, 2026

Claude finished @otavio's task in 7m 6s —— View job


Code Review Complete

Reviewed 32 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

Summary

  • Security: Stripe publishable key handling is correct (public by design). window.open uses noopener,noreferrer. Permission gating via useHasPermission("billing:subscribe") is consistently applied across BillingSection, BillingWarning, and action dialogs. No XSS vectors — all dynamic data rendered as React text nodes.
  • Architecture: Stripe dependencies properly code-split via React.lazy(). The onBillingWarning prop on DeviceActionDialog and ContainerActionDialog is additive and optional — no breaking changes. Config changes (gen-config.sh, env.ts, docker-compose.yml) default to empty strings, maintaining backward compatibility with non-Cloud deployments.
  • Cross-repo: Billing portal response shape ({url: string}) matches the cloud backend's StripePortal struct. All BillingStatus values align with the cloud backend's SubscriptionStatus constants.
  • Testing: Good coverage across BillingDialog, BillingSection, BillingWarning, useBilling hooks, type narrowing functions, and Stripe error mapping. Mock setups correctly satisfy transitive dependencies from React.lazy loading.
  • Patterns: Fetch error interceptor enrichment pattern (isSdkError → cast → extract message) correctly extracts server-provided error messages from the response body. The loadConfig() concurrency fix properly deduplicates in-flight fetches.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 added the status/under-testing Tests broadened label Apr 24, 2026
@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch 2 times, most recently from 550a8b5 to 10d6db6 Compare April 24, 2026 20:10
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 24, 2026

Claude finished @gustavosbarreto's task in 2m 28s —— View job


Code Review Complete

Reviewed 32 files across code quality, security, testing, TypeScript/React patterns, and architecture. 1 inline issue posted (1 medium).

Additional notes

  • Security: No vulnerabilities found. Stripe publishable key handling, noopener,noreferrer on window.open, and permission gating via useHasPermission are all correctly implemented. No XSS vectors — all dynamic data rendered as React text nodes.
  • Architecture: All frontend billing types (BillingStatus, Customer, Subscription, Invoice, PaymentMethod) match the cloud backend contracts. The onBillingWarning prop additions are optional and backward compatible. Config changes default to empty strings, maintaining compatibility with non-Cloud deployments.
  • Testing: Good coverage across BillingDialog, BillingSection, BillingWarning, useBilling hooks, type narrowing functions, and Stripe error mapping. BillingPayment.tsx is mocked in all consumer tests (deliberate — deeply coupled to Stripe Elements runtime).
  • Cross-repo: Billing portal response shape ({url: string}) matches the cloud backend's StripePortal struct. All 10 BillingStatus values align with the cloud backend's SubscriptionStatus constants.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/components/billing/BillingSection.tsx Outdated
@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch from 10d6db6 to 96bff2c Compare April 24, 2026 21:10
@otavio
Copy link
Copy Markdown
Member

otavio commented Apr 24, 2026

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 24, 2026

Claude finished @otavio's task in 2m 47s —— View job


Code Review Complete

Reviewed 32 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

Summary

  • Security: No vulnerabilities found. Stripe publishable key handling is correct (public by design). window.open uses noopener,noreferrer. Permission gating via useHasPermission("billing:subscribe") is consistently applied across BillingSection, BillingWarning, and action dialogs. No XSS vectors — all dynamic data rendered as React text nodes. The fetch error interceptor enrichment pattern (isSdkError → cast → extract message) correctly extracts server-provided error messages from the response body.
  • Architecture: All 10 BillingStatus values match the cloud backend's SubscriptionStatus constants exactly. Billing portal response shape ({url: string}) matches the cloud backend's StripePortal struct. The onBillingWarning prop additions are optional and backward compatible. Config changes default to empty strings, maintaining compatibility with non-Cloud deployments.
  • Testing: Good coverage across BillingDialog, BillingSection, BillingWarning, useBilling hooks, type narrowing functions, and Stripe error mapping (75 test cases). Mock setups correctly satisfy transitive dependencies from React.lazy loading.
  • React patterns: Bootstrap effect cleanup correctly resets bootstrapRef.current = false for StrictMode double-mount compatibility (fixed from prior review). The invalidateRef pattern in BillingSection avoids unnecessary pageshow listener re-registration while remaining functionally correct (queryClient is a stable singleton). Stripe dependencies properly code-split via React.lazy().
  • Prior review findings: All 3 issues from previous reviews have been addressed — StrictMode bootstrap fix (aba1810), React Compiler rationale for skipping useCallback, and useRef for invalidate stability (f15b534).

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 added product/cloud Issue/PR related to Cloud Edition and removed status/work-in-progress status/under-testing Tests broadened do-not-merge labels Apr 27, 2026
@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch from 96bff2c to 1e8d224 Compare April 27, 2026 13:50
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Claude finished @gustavosbarreto's task in 5m 20s —— View job


Code Review Complete

Reviewed 32 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

Summary

  • Security: No vulnerabilities found. Stripe publishable key handling is correct (public by design). window.open uses noopener,noreferrer. Permission gating via useHasPermission("billing:subscribe") is consistently applied across BillingSection, BillingWarning, and action dialogs. No XSS vectors — all dynamic data rendered as React text nodes.
  • Architecture: All 10 BillingStatus values match the cloud backend's SubscriptionStatus constants exactly. Billing portal response shape ({url: string}) matches the cloud backend's StripePortal struct. The onBillingWarning prop additions are optional and backward compatible. Config changes default to empty strings, maintaining compatibility with non-Cloud deployments.
  • Testing: Good coverage across BillingDialog, BillingSection, BillingWarning, useBilling hooks, type narrowing functions, and Stripe error mapping (75 test cases). Mocks are correctly structured with mutable { value: ... } objects for per-test configuration.
  • React patterns: Bootstrap effect cleanup correctly resets bootstrapRef.current = false for StrictMode double-mount compatibility (fixed in aba1810). The invalidateRef pattern in BillingSection avoids unnecessary pageshow listener re-registration while remaining functionally correct (queryClient is a stable singleton). Stripe dependencies properly code-split via React.lazy(). The 402 handling in DeviceActionDialog and ContainerActionDialog correctly falls through to setError when onBillingWarning is not provided.
  • Prior review findings: All 3 issues from previous reviews have been addressed — StrictMode bootstrap fix, React Compiler rationale for skipping useCallback, and useRef for invalidate stability.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch 2 times, most recently from f0f3578 to 26d6783 Compare April 28, 2026 17:25
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Claude finished @gustavosbarreto's task in 2m 51s —— View job


Code Review Complete

Reviewed 32 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

Summary

  • Security: No vulnerabilities found. Stripe publishable key handling is correct (public by design). window.open uses noopener,noreferrer. Permission gating via useHasPermission("billing:subscribe") is consistently applied across BillingSection, BillingWarning, and action dialogs. No XSS vectors — all dynamic data rendered as React text nodes.
  • Architecture: All 10 BillingStatus values match the cloud backend's SubscriptionStatus constants exactly. Billing portal response shape ({url: string}) matches the cloud backend's StripePortal struct. The onBillingWarning prop additions are optional and backward compatible. Config changes default to empty strings, maintaining compatibility with non-Cloud deployments.
  • Testing: Good coverage across BillingDialog, BillingSection, BillingWarning, useBilling hooks, type narrowing functions, and Stripe error mapping (75 test cases). BillingPayment.tsx is mocked in consumer tests (deliberate — deeply coupled to Stripe Elements runtime).
  • React patterns: Bootstrap effect cleanup correctly resets bootstrapRef.current = false for StrictMode double-mount compatibility (fixed in aba1810). The invalidateRef pattern in BillingSection avoids unnecessary pageshow listener re-registration while remaining functionally correct (queryClient is a stable singleton). Stripe dependencies properly code-split via React.lazy(). The fetch error interceptor enrichment pattern (isSdkError → cast → extract error) correctly extracts server-provided error messages from the response body.
  • Prior review findings: All 3 issues from previous reviews have been addressed — StrictMode bootstrap fix, React Compiler rationale for skipping useCallback, and useRef for invalidate stability.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Implements the full billing workstream for Cloud tenants:

- New hooks/useBilling.ts: useSubscription, useCustomer,
  useCreateCustomer, useCreateSubscription, useAttachPaymentMethod,
  useDetachPaymentMethod, useSetDefaultPaymentMethod,
  useOpenBillingPortal (axios wrapper for /api/billing/portal)
- New types/billing.ts: BillingStatus union, typed Customer,
  Subscription, PaymentMethod, NamespaceBilling with safe narrowing
- New utils/stripeErrors.ts: Stripe error-code → user message map
- New BillingSection in Settings.tsx: status badge, warning banners
  (past_due / unpaid / incomplete / incomplete_expired / to_cancel),
  Subscribe button (inactive/canceled/incomplete_expired only),
  Open Portal button, hash-scroll + pageshow refetch for portal return
- New BillingDialog wizard (4 steps): Overview → Payment method →
  Review → Success; lazy-loaded to exclude Stripe from initial chunk
- New BillingPayment: Stripe Elements card form, payment-method list
  with set-default / detach, auto-creates Stripe customer on mount
- New BillingWarning: 402 device-limit dialog wired to
  DeviceActionDialog + ContainerActionDialog
- env.ts: add stripePublishableKey to ClientConfig + in-flight
  promise dedup for concurrent loadConfig callers
- App.tsx: /settings/billing redirect route for Stripe portal return
- gen-config.sh: emit SHELLHUB_STRIPE_PUBLISHABLE_KEY into config.json
- docker-compose.yml: add SHELLHUB_STRIPE_PUBLISHABLE_KEY env var

Fixes: shellhub-io/team#71
Covers the billing workstream with Vitest + React Testing Library:

- useBilling.test.ts: mutations (create customer/subscription,
  attach/detach/default PM), query enable/disable behavior,
  useOpenBillingPortal URL open + missing-URL error
- billing.test.ts (types): toCustomer and toSubscription transforms,
  readNamespaceBilling safe narrowing, getSubscriptionStatus sentinel
- stripeErrors.test.ts: known-code mapping + unknown-code fallback
- BillingDialog.test.tsx: all 4 wizard steps, Next/Back nav, error
  paths (402, generic, incomplete status), pending state, live region
  announcements, close buttons
- BillingSection.test.tsx: Subscribe/portal button visibility per
  status, non-owner view, warning banners, dialog open/close
- BillingWarning.test.tsx: renders, confirm navigates, cancel closes
- Update CreateNamespaceDialog + InvitationsMenu + Login mock getConfig
  to include new stripePublishableKey field in ClientConfig
@luizhf42 luizhf42 force-pushed the feat/ui-react/billing branch from 26d6783 to 0aa82a8 Compare April 28, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ui kind/feature New feature or request product/cloud Issue/PR related to Cloud Edition

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants