feat(billing): landing-style redesign, billing details, nav reorg#902
feat(billing): landing-style redesign, billing details, nav reorg#902
Conversation
- Move Billing from left nav to user menu dropdown (gated on isOwner + isBillingEnabled; routes to /billing) - Move Address Book from user menu to left nav (replaces Billing slot); opens overlay on click - Add Report an issue button to left nav bottom section (below Documentation); opens FeedbackOverlay - New user menu order: Wallet, Settings, Connections, API Keys, Billing, Projects and Tags - Combine separate Projects and Tags overlays into one tabbed ProjectsAndTagsOverlay (mirrors SettingsOverlay pattern; supports initialTab prop) - NavItem ACTION_ITEM_IDS allowlist so href=null items that open overlays are not marked "Coming Soon"
…olish
UI redesign (/billing)
- PricingTable mirrors landing /pricing: pill Monthly/Annual toggle with inline
"Save 20%" badge; centered 4-col grid at xl; HeroMetrics stat panel per card;
custom TierSelect dropdown with green highlight for selected tier
- Outline current plan with a thin keeperhub-green-dark border + "CURRENT" badge
instead of the old POPULAR highlight
- Enterprise card consistent with others ("Custom" price, "Talk to us" CTA,
mailto:human@keeperhub.com); no gradient background or special border
- Shared ComparisonTable below the grid: "Compare all features" toggle reveals
a 10-row striped matrix with Enterprise column accented in green
- CTA label normalized to "Change plan" for all paid plan changes
- Remove subheadings under price; remove redundant executions pill on Current
Plan card; move renewal message (e.g. "Your plan ends on ...") inline with
plan name + status pill
- Execution usage bar and gas credits bar restyled on keeperhub-green tokens
with subtle /15 track; overage tail in yellow
- FAQ link footer at bottom of /billing routes to https://keeperhub.com/pricing
Billing Details card
- New BillingDetails card rendered next to BillingHistory (lg:grid-cols-[2fr_1fr])
- Fetches GET /api/billing/billing-details; shows card brand + last4 + expiry
and invoice email; empty state when no card on file
- Edit pencil inline next to "Billing Details" title; opens Stripe portal
Auth gate
- billing-page.tsx mirrors analytics page pattern: useSession + local AuthGate,
early-return when anonymous (no more pricing table exposure to logged-out users)
Data plumbing
- Add BillingDetails type and getBillingDetails(customerId) to BillingProvider
- Stripe implementation cascades: customer.invoice_settings.default_payment_method
-> subscription.default_payment_method -> first customer payment method
(Stripe Checkout attaches card to subscription, not customer, by default)
- Add BILLING_DETAILS to BILLING_API constants
- New GET /api/billing/billing-details route (owner-auth)
- /billing bumps refreshKey 2s after ?checkout=success so BillingDetails
remounts once Stripe has attached the payment method
- BillingHistory view/PDF links recolored keeperhub green
- Confirm plan change dialog copy: remove leading "--" before the prorated
billing note
Tests
- billing-handle-event.test.ts mock provider stubs getBillingDetails
- New AppBanner client component mounted in app/layout.tsx: fixed 36px strip at the top of the app, keeperhub-green tint with border, centered info icon + body + "See plans" link, close (X) at right edge - Dismissal is permanent-per-browser via localStorage key kh-billing-announce-v1 so the banner never reappears for a user who closes it (version suffix lets us introduce a new banner later without wiping other prefs) - Banner height is exposed via --app-banner-height CSS var on <html> so fixed overlays shift down cleanly when visible and snap back on dismiss. Updated: - components/workflow/workflow-toolbar.tsx (persistent toolbar top) - components/navigation-sidebar.tsx (sidebar top-[60px] now includes banner) - components/flyout-panel.tsx (two fixed surfaces) - app/workflows/[workflowId]/page.tsx (side panel lg breakpoint) - components/billing/billing-page.tsx (pt-20 -> calc) - components/analytics/analytics-page.tsx (pt-20 -> calc) - components/earnings/earnings-page.tsx (pt-20 -> calc) - No hydration flash: component renders null until mounted to avoid SSR/client mismatch reading localStorage
Solid
Concerns1. Stripe cascade can silently surface a card that is not being charged
What would need to be true for this to be fine? Every customer has exactly one card on file. That is optimistic. 2. The 2 second setTimeout after
|
|
Looking into these now... |
|
Code looks good. Concerns 1 2 and 3 from #902 (comment) are addressed |
Summary
/billingPlans section to mirror the landing page/pricingaesthetic and add a new Billing Details card next to Billing History/api/billing/billing-detailsendpoint andBillingProvider.getBillingDetails()that cascades customer default → subscription default → first customer payment method (Stripe Checkout attaches cards to subscription, not customer)/billingexposure to anonymous users (AuthGate mirroring analytics page pattern)Changes
Nav & user menu
isOwner && isBillingEnabled(); navigates to/billing)FeedbackOverlayProjectsAndTagsOverlayreplaces the two separate overlays (mirrorsSettingsOverlaytabbed pattern)NavItemgains anACTION_ITEM_IDSallowlist sohref: nullitems that open overlays are not marked "Coming Soon"Billing page (
/billing)TierSelectdropdown with green highlight for the selected tierkeeperhub-green-darkborder +CURRENTbadge (removed the oldPOPULARPro highlight)mailto:human@keeperhub.com)ComparisonTablewith the Enterprise column accented in green/15track; overage tail in yellowhttps://keeperhub.com/pricingBilling Details card (new)
lg:grid-cols-[2fr_1fr]layout/api/billing/portal); empty state when no card on fileData plumbing
BillingDetailstype +getBillingDetails(customerId)onBillingProviderinterfaceBILLING_DETAILSadded toBILLING_APIconstantsGET /api/billing/billing-detailsroute (owner-auth, same pattern as existing billing endpoints)/billingbumpsrefreshKey2s after?checkout=successso BillingDetails remounts once Stripe has attached the payment methodFixes & copy
/billingnow see a "Sign in to view billing" AuthGate (was rendering the pricing table to unauthenticated users)--before the prorated billing noteTests
tests/unit/billing-handle-event.test.tsmock provider stubsgetBillingDetailsUAT — Phase 31 Stripe Billing (11/11 passed, 0 issues)
Driven interactively over a live dev environment (
localhost:3000, Docker Postgres, Stripe CLI listener, test-mode customer).127.0.0.1:5433), added AuthGate4242 …)stripe trigger invoice.paid, all 200s, zero duplicatebilling_eventsrows/api/billing/overagereturned{billed:true, overageCount:50, totalChargeCents:10}(Pro $2/1K), idempotent re-call returnedalready billed for this period/api/billing/debt-scancreated debt, workflow execution returned 429 "Executions suspended due to unpaid overage invoice",stripe invoices payfiredinvoice.paidwebhook,clearDebtForInvoiceflipped status tocleared, workflow ran againshouldUpgrade: false(Pro 100k + projected overage still cheaper than Business 250k); once pushed to 130k actual (projected 185k), blueUpgradeSuggestionBannerrendered at top of Current Plan cardNotes for reviewers
lib/billing/plans.ts. UI already renders the correct rates. Verified with whoever owns pricing; guide was stale.HUB_SERVICE_API_KEYfor/api/billing/overageand/api/billing/debt-scanendpoint auth. Don't forward that value — production/staging should have its own service key rotation.workflow_executions, 1overage_billing_records, 1 clearedexecution_debt). Harmless but clean up if you reuse that local DB:Test plan
pnpm checkandpnpm type-checkpass locally/billingrenders correctly for Free org (pricing table, no Billing Details / no Overage section)/billingrenders correctly for Pro org with active subscription (Billing Details shows card, renewal message inline)/billingvisit renders AuthGate, not pricing table/billingonly for ownerspnpm test billing)Addendum: announcement banner (added 2026-04-21)
Third commit adds a skinny (36px) dismissible banner at the top of the app to introduce paid plans to existing users.
components/app-banner.tsx— fixed 36px strip, keeperhub-green tint with border, centered info icon + body + "See plans" link, close (X) at right edgelocalStoragekeykh-billing-announce-v1(version suffix lets us introduce a new banner later without wiping other prefs)--app-banner-heightCSS var on<html>so fixed overlays shift down and snap back cleanly. Touches: workflow-toolbar, navigation-sidebar, flyout-panel, workflow page side-panel, and thept-20of/billing,/analytics,/earningsTo re-trigger the banner in dev (for QA):
localStorage.removeItem("kh-billing-announce-v1")+ refresh.