Payments & Checkout
Stripe Checkout integration and payment flow
Payments & Checkout
What's Included
- Stripe Checkout session creation
- Plan listing with pricing display
- Success and cancel payment pages
- Support for both authenticated and guest checkout
Checkout Composable (useCheckout)
const {
createCheckoutSession, // (planId, interval?) → redirects to Stripe
isProcessing, // Ref<boolean>
error // Ref<string | null>
} = useCheckout()
The composable creates a Stripe Checkout session via the backend API and redirects the user to Stripe's hosted checkout page.
Plans Composable (usePlans)
const {
plans, // Ref<Plan[]> — all plans from backend
oneTimePlans, // Computed — filtered for one-time payment plans
isLoading, // boolean
error // Error | null
} = usePlans()
Plans are fetched from /api/v1/plans with SSR caching. The pricing section on the landing page uses this composable.
End-to-End Checkout Flow
1. User views pricing on landing page (PricingSection)
│
2. usePlans() fetches plans from GET /api/v1/plans
│
3. User clicks "Get Started" on a paid plan
│
4. useCheckout().createCheckoutSession(planId)
│
5. POST /api/v1/subscription/checkout
│ { priceId, successUrl, cancelUrl }
│ (+ Authorization header if authenticated)
│
6. Backend creates Stripe Checkout session
│ ├── Authenticated: email pre-filled, user_id in metadata
│ └── Guest: Stripe collects email
│
7. Frontend redirects to Stripe Checkout URL
│
8. User completes payment on Stripe
│
9. Stripe redirects to /payment/success?session_id=...
│
10. Stripe sends webhook to backend
│
11. POST /api/v1/webhooks/stripe
│ Event: checkout.session.completed
│
12. Backend creates/upgrades subscription
Authenticated vs Guest Checkout
- Authenticated — email is pre-filled,
user_idis included in the session metadata - Guest — Stripe collects the email, user account is created via webhook
What Happens After Payment
Successful Payment
- User sees the success page (
/payment/success) - Backend receives
checkout.session.completedwebhook - Subscription is created with the paid plan
- User can check their subscription on the profile page
Failed Payment (Recurring)
- Backend receives
invoice.payment_failedwebhook - Subscription status is set to
past_due - Stripe retries the payment according to its retry schedule
- If payment eventually succeeds:
invoice.paidwebhook reactivates the subscription - If all retries fail:
customer.subscription.deletedwebhook cancels the subscription
Cancelled Checkout
- User clicks "Back" or closes the Stripe checkout
- Stripe redirects to
/payment/cancel - User sees a "Payment cancelled" message with a link back to pricing
Payment Pages
Success Page (/payment/success)
Displayed after a successful Stripe payment. Shows a confirmation message and extracts the session_id from the URL query parameters.
Cancel Page (/payment/cancel)
Displayed when the user cancels the Stripe checkout. Shows a message with a link back to the pricing section.
Subscription Status on Frontend
The profile page shows subscription details via useSubscription():
const { subscription } = useSubscription()
// subscription.value contains:
// - planType: 'free' | 'starter' | 'pro'
// - status: 'active' | 'canceled' | 'past_due' | 'expired'
// - paymentType: 'recurring' | 'one_time'
// - currentPeriodEnd: string (date)
The SubscriptionCard component displays this with a color-coded status badge:
- Active — green badge
- Canceled — red badge
- Past Due — yellow badge
- Trial — blue badge
Pricing Section
The PricingSection component on the landing page:
- Fetches plans via
usePlans() - Renders pricing cards with plan details
- Maps backend plans to Stripe price IDs
- Handles checkout button clicks
- Shows loading skeletons while plans are loading
Stripe Test Cards
Use these test card numbers during development:
| Card Number | Result |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 0002 | Card declined |
4000 0000 0000 3220 | 3D Secure authentication |
Use any future expiry date and any 3-digit CVC.