# Story 3.7: Billing & Subscription UX — Complete User Journey > **Epic:** 3 — The SaaS Commercial Engine > **Priority:** High > **Status:** ready-for-dev > **Depends on:** 3.1 (quota tracking), 3.6 (Stripe tiers) > **Blocks:** 4.3 (data portability export needs billing status) --- ## Context The backend billing infrastructure (Stripe checkout, webhook sync, quota tracking via Redis) is fully implemented. The base UI components exist: - `billing-plans.tsx` — Plan selection cards + Stripe embedded checkout - `usage-meter.tsx` — Sidebar quota bar with upgrade modal - `/settings/billing` page — Current plan + upgrade CTA - API routes — `/billing/status`, `/billing/create-checkout`, `/billing/portal`, `/billing/webhook`, `/usage/current` However, the user experience has significant gaps that block a production launch: 1. **No detailed usage breakdown** — Users see a tiny progress bar in the sidebar but have no dedicated view showing per-feature consumption 2. **No billing history** — Users cannot view past invoices or download receipts 3. **No inline paywall** — When a quota-exceeded error occurs during an AI action, the user gets a generic error toast instead of being guided to upgrade 4. **No billing cycle overview** — Missing period start/end, next billing date, tokens used this cycle 5. **Upgrade flow not triggered from error states** — Quota exhaustion in AI features shows a toast but doesn't offer a path to resolution --- ## User Stories ### US-1: Detailed Usage Dashboard **As a** user on any tier, **I want** to see a detailed breakdown of my AI feature usage for the current billing period, **So that** I understand exactly where my quota is being consumed. **Acceptance Criteria:** - **Given** I am on the Billing settings page - **When** I scroll below the current plan card - **Then** I see a "Usage this period" section with a per-feature breakdown: - Feature name (i18n key) - Progress bar: used / limit - Numeric count (e.g., "47 / 100") - Visual state: normal (violet), warning ≥75% (amber), exhausted 100% (rose) - **And** for unlimited features (Enterprise), the bar is full and shows "Unlimited" - **And** the data refreshes every 30s via the existing `useQuery` with `refetchInterval` - **And** the section shows the period dates (e.g., "May 1 – May 31, 2026") ### US-2: Billing History **As a** paying user, **I want** to see my past invoices and download them, **So that** I have records for accounting. **Acceptance Criteria:** - **Given** I have an active or past paid subscription - **When** I view the Billing settings page - **Then** I see a "Billing History" section below usage - **And** it lists invoices sorted by date (newest first) with: - Date - Amount (formatted in user currency) - Status (Paid, Pending, Failed) with color badge - Download PDF link (via Stripe hosted invoice URL) - **And** for BASIC/free users, the section is hidden entirely - **And** the "Open Billing Portal" button remains for payment method changes ### US-3: Inline Paywall (Quota Exceeded → Upgrade) **As a** free user who just exhausted my AI Discovery Pack, **I want** to see a clear upgrade prompt instead of a cryptic error, **So that** I can take action immediately. **Acceptance Criteria:** - **Given** I trigger an AI feature (e.g., auto-tag) and my quota is exhausted - **When** the API returns a `QuotaExceededError` - **Then** the UI shows an inline paywall panel (not just a toast) with: - Clear message: "You've used all your [feature] credits this month" - Two CTAs: 1. "Upgrade to Pro" → links to `/settings/billing` 2. "Use your own API key" → links to `/settings/ai#byok` - A "Maybe later" dismiss button - **And** the paywall replaces the AI action result area (not a full-page overlay) - **And** the paywall auto-dismisses if the user navigates away or after 10s ### US-4: Billing Cycle Overview **As a** paying user, **I want** to see my current billing period details, **So that** I know when I'll be charged next and how much I've used. **Acceptance Criteria:** - **Given** I have an active paid subscription - **When** I view the Billing page - **Then** the Current Plan card also shows: - Billing period: "May 1 – May 31, 2026" - Next billing date (or "Access until [date]" if canceled) - A small ring/donut chart showing total AI credits used vs total limit (aggregate across features) ### US-5: Upgrade Success Confirmation **As a** user who just completed checkout, **I want** to see a clear confirmation that my plan is now active, **So that** I have confidence the payment worked. **Acceptance Criteria:** - **Given** I return from Stripe checkout (via `session_id` URL param) - **When** the page loads - **Then** I see a success banner at the top: "Welcome to [Plan Name]! Your subscription is now active." - **And** the banner auto-dismisses after 5s - **And** the usage meter in the sidebar updates immediately - **And** the plan cards section is hidden (replaced with current plan + manage) --- ## Technical Design ### Architecture All changes are **frontend-only** — no new API routes needed. The existing APIs provide all necessary data: | Data Need | Existing API | Field | |---|---|---| | Per-feature quotas | `GET /api/usage/current` | `quotas[feature].{used,limit,remaining}` | | Billing status | `GET /api/billing/status` | `tier, status, currentPeriodEnd, cancelAtPeriodEnd` | | Invoices | Stripe Customer Portal | Via `/api/billing/portal` redirect | | Checkout | `POST /api/billing/create-checkout` | `{clientSecret, url}` | ### Component Structure ``` settings/billing/page.tsx ├── BillingPlans (existing — current plan card + plan selection) │ ├── CurrentPlanCard (enhanced with billing cycle dates + mini usage ring) │ ├── PlanCard[] (existing — unchanged for BASIC) │ └── ManageBilling (existing — portal button for paid users) ├── UsageBreakdown (NEW — per-feature usage dashboard) │ ├── PeriodHeader ("Usage for May 1–31, 2026") │ └── FeatureRow[] (one per feature: label + progress bar + count) └── BillingHistory (NEW — for paid users, via Stripe portal link) ``` ``` components/quota-paywall.tsx (NEW — inline paywall) ├── Message ("You've used all X credits") ├── CTAs (Upgrade link + BYOK link) └── Dismiss button ``` ### Files to Create | File | Description | |---|---| | `components/settings/usage-breakdown.tsx` | Detailed per-feature usage table with progress bars | | `components/settings/billing-history.tsx` | Invoice list with download links (via Stripe) | | `components/quota-paywall.tsx` | Inline paywall component for quota-exceeded errors | ### Files to Modify | File | Change | |---|---| | `app/(main)/settings/billing/page.tsx` | Add `UsageBreakdown` + `BillingHistory` sections | | `components/settings/billing-plans.tsx` | Enhance `CurrentPlanCard` with billing period dates + mini usage ring | | `components/usage-meter.tsx` | Add click handler to navigate to `/settings/billing` (not just on exhausted) | | Various AI feature components | Catch `QuotaExceededError` and show `QuotaPaywall` instead of toast | ### Design Tokens (Existing — No New Colors) | Element | Token/Class | |---|---| | Section header | `text-[11px] font-bold uppercase tracking-[0.15em] text-muted-foreground` | | Card container | `rounded-xl border border-border/40 bg-paper p-6 space-y-4` | | Progress bar track | `h-1.5 rounded-full bg-secondary/40` | | Progress bar fill (normal) | `bg-gradient-to-r from-violet-400 to-purple-400` | | Progress bar fill (warning ≥75%) | `bg-amber-400` | | Progress bar fill (exhausted) | `bg-rose-400` | | Feature count text | `text-[11px] text-muted-ink tabular-nums` | | Badge (active) | `bg-emerald-100 text-emerald-700` | | Badge (past due) | `bg-amber-100 text-amber-700` | | CTA primary | `bg-[#D4A373] text-white hover:bg-[#C49363]` (highlighted) | | CTA secondary | `border border-border hover:bg-foreground/5` | | Paywall panel | `rounded-xl border border-rose-200 bg-rose-50/50 dark:border-rose-800/40 dark:bg-rose-900/10 p-4` | ### i18n Keys to Add (15 locales) ```json { "billing": { "usageThisPeriod": "Usage this period", "periodRange": "{start} – {end}", "featureUsedLimit": "{used} / {limit}", "unlimited": "Unlimited", "noUsage": "No AI usage yet this period.", "billingHistory": "Billing History", "noInvoices": "No invoices yet.", "viewInvoices": "View all invoices in Stripe portal", "nextBillingDate": "Next billing date", "billingPeriod": "Billing period", "planSince": "Subscribed since {date}" }, "quotaPaywall": { "title": "Monthly limit reached", "description": "You've used all your {feature} credits for this month.", "upgrade": "Upgrade plan", "useOwnKey": "Use your own API key", "later": "Maybe later" } } ``` --- ## Out of Scope - **Stripe Customer Portal customization** — uses the default hosted portal (already configured) - **Downgrade flow** — handled via Stripe portal (out of scope for this story) - **Enterprise self-serve** — "Contact Sales" mailto link is sufficient - **Real-time WebSocket quota updates** — polling every 30s is sufficient - **Admin billing dashboard** — deferred to a later epic - **Currency switching** — EUR default, handled by Stripe locale --- ## Testing Notes - **Unit**: `billing-sync.test.ts`, `billing-price-map.test.ts` already exist and pass - **Integration**: Test checkout flow with Stripe test mode (`sk_test_` keys) - **Visual**: Verify in both light and dark mode - **i18n**: All 15 locale files must have the new keys - **Responsive**: Plan cards grid must collapse to single column on mobile (<768px) - **Edge cases**: - User with no subscription (BASIC) should see only usage + upgrade cards - User with canceled subscription should see "Access until [date]" + no upgrade cards - User with past_due subscription should see warning banner - Redis down → usage section shows "Unable to load usage" (fail-open on API, graceful on UI) --- ## Estimated Effort | Task | Hours | |---|---| | `usage-breakdown.tsx` component | 2h | | `billing-history.tsx` component | 1.5h | | `quota-paywall.tsx` component | 1.5h | | Enhance `billing-plans.tsx` with cycle info | 1h | | Wire paywall into AI feature error handlers | 2h | | i18n keys (15 locales) | 1h | | Visual testing + dark mode | 1h | | **Total** | **~10h** |