Build a Stripe Subscription Dashboard with Claude Code
Build a Stripe Subscription Dashboard with Claude Code
What you'll build
A subscription management card with plan details, payment failure warnings, trial countdowns, and one-click plan switching
The Problem
You launched your SaaS. People are paying. But when a subscriber logs into their dashboard, they see nothing about their subscription. No plan name. No renewal date. No billing amount. When a credit card expires and payment fails — silence. The user just loses access with zero explanation. And trial users? They wake up on day 4 to a paywall with no warning. Your payment system works, but your payment UX doesn't exist.
What You're Building
A subscription info card that lives on the dashboard. It shows your plan, what you're paying, when it renews, and your status — with smart warnings that change based on what's happening. Failed payment? Red banner across the top of every page with a fix-it button. Trial ending tomorrow? Amber countdown. Canceling? A notice showing when access ends. Plus a one-click button to switch between monthly and annual.
Milestone 1: The Subscription Info Card
Your subscribers are paying but flying blind. Let's give them a card that shows everything at a glance.
Prompt:
Create a new component at components/subscription-info.tsx. It's a server component that receives a Subscription object as a prop (with fields: status, plan, current_period_end, cancel_at_period_end). Display it as a shadcn Card with a 4-column grid showing: Plan name (Pro Monthly or Pro Annual with a crown icon), Billing amount ($39/mo or $299/yr), Renewal date (formatted as "Feb 21, 2026"), and Status badge. Color-code the badge: green for active, blue for trialing, red for past_due, amber for cancel_at_period_end, gray for canceled. Use lucide-react icons (Crown, CreditCard, Calendar, ArrowUpRight).
What Claude Code does: It generates a server component with helper functions for formatting dates, computing status badges, and mapping plan names to prices. Server components render on the server with zero client-side JavaScript — perfect for displaying subscription data that doesn't need interactivity.
Try it: Import SubscriptionInfo into any page and pass it a mock subscription object with status: "active" and plan: "monthly". You should see a clean card with four columns and a green "Active" badge.
Milestone 2: Wire It Into the Dashboard
The component exists but the dashboard doesn't use it. Time to feed it real subscription data from Supabase.
Prompt:
Update the dashboard page to fetch the full subscription row from Supabase for the current user. Query for active or trialing subscriptions first. If none found, also check for past_due subscriptions so we can still show the card when payment fails. Pass the subscription object to the SubscriptionInfo component and render it between the stats grid and the tutorial list. Only render the card when a subscription exists.
What Claude Code does: It adds a fallback query pattern — try the happy path first (active/trialing), then check for problem states (past_due). This way the card always appears when a subscription exists, even a broken one. No subscription at all? The card simply doesn't render.
Try it: Log into your dashboard. If you have a subscription, the info card appears below your stats. The plan, billing, renewal date, and status should all match your Stripe subscription.
Milestone 3: Payment Failure Banner
A card isn't enough when money is involved. If payment fails, users need a red banner they can't miss — on every dashboard page, not just the home.
Prompt:
Create components/payment-failure-banner.tsx as a client component. Red background, white text, full-width banner with an AlertTriangle icon, the message "Your last payment failed. Update your payment method to keep Pro access", an "Update payment" button that calls POST /api/stripe/portal and redirects to the returned Stripe billing portal URL, and a dismiss X button with useState. Then update the dashboard layout (not the page — the layout) to check if the user has a past_due subscription and conditionally render this banner above the children.
What Claude Code does: Two things happen here. The banner is a client component (needs useState for dismiss, needs fetch for the portal redirect). But the layout check is a server component that queries Supabase. This is the layout-level pattern for critical UX — by putting the check in the layout instead of the page, the banner automatically appears on every dashboard route.
Try it: In Supabase, update a subscription's status to past_due. Refresh any dashboard page — a red banner should appear at the top. Click "Update payment" — it opens the Stripe billing portal. Click X — it dismisses until page reload.
Milestone 4: Trial Expiration Warning
Trial users are your most at-risk subscribers. They need to know it's ending before it ends.
Prompt:
Add a trial expiration warning to the SubscriptionInfo component. When status is "trialing", show a banner inside the card that says "Trial ends in X days" with the billing amount that kicks in after. Calculate days remaining from current_period_end. If more than 1 day remaining, use blue styling (bg-blue-50, border-blue-200). If 1 day or less, switch to amber urgent styling (bg-amber-100, border-amber-300) with "Trial ends today" and a warning about upcoming billing. Also add a similar amber warning when cancel_at_period_end is true showing the access end date.
What Claude Code does: This adds urgency escalation — the same warning changes appearance based on how close the deadline is. Blue means "heads up." Amber means "act now." The key pattern is conditional styling driven by time calculations — one component, multiple visual states, all controlled by a simple getDaysUntil() function.
Try it: Set a subscription to trialing with current_period_end set to tomorrow. You should see an amber warning saying "Trial ends today." Change the end date to 3 days out — it flips to calm blue with "Trial ends in 3 days."
Milestone 5: Plan Switching
Monthly users should be able to upgrade to annual and save money. One click, no Stripe portal required.
Prompt:
Create two things: 1) An API route at app/api/stripe/change-plan/route.ts that accepts { newPlan: "monthly" | "annual" }, validates with zod, checks the user has an active subscription in Supabase, retrieves the Stripe subscription, swaps the price item using stripe.subscriptions.update with proration_behavior "create_prorations", and updates the local Supabase record immediately. 2) A client component at components/switch-plan-button.tsx that shows "Switch to Annual — Save 36%" for monthly users or "Switch to Monthly" for annual users, calls the API on click, shows a loading state, handles errors, and reloads the page on success. Add this button to SubscriptionInfo below the plan details grid, only visible when status is active and not canceling.
What Claude Code does: The API route uses Stripe proration — when you swap a subscription's price item mid-cycle, Stripe automatically calculates the credit for unused time on the old plan and applies it to the new one. You don't write any billing math. The proration_behavior: "create_prorations" flag tells Stripe to handle everything.
Try it: Your dashboard card should now show "Switch to Annual — Save 36%" below the plan details. Click it. Check your Stripe dashboard — the subscription should show the annual price with a prorated credit applied. Your page reloads with the updated plan info.
What You Built
Remember that empty dashboard where subscribers were paying but flying blind? You just replaced it with a complete subscription management experience. Users see their plan, know when it renews, get warned when something goes wrong, and can switch plans in one click. Five components, one API route, zero confusion.
What you picked up along the way: server vs client component boundaries, the layout-level pattern for global warnings, urgency escalation with time-based styling, and Stripe's proration system that handles billing math so you don't have to.
Take It Further
- Add tax rates — create a Stripe tax rate for your country's VAT and attach it to checkout line items with
lineItem.tax_rates = [taxRateId] - In-app cancellation — build a cancel page with a feedback form and a winback offer instead of sending users to Stripe's portal
- Failed payment emails — fire an email from your webhook when
invoice.payment_failedhits, with a direct link to update their card
Ready to build your first AI agent?
Live Zoom workshop + 1 month WhatsApp follow-up with Yuval Keshtcher (Hebrew)
Learn about the Workshop