Stripe Integration

Complete Stripe integration for payments, subscriptions, and billing.

Dependencies#

npm install stripe @stripe/stripe-js @stripe/react-stripe-js

Environment Variables#

# .env.local STRIPE_SECRET_KEY=sk_test_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_WEBHOOK_SECRET=whsec_...

Server Client#

1// lib/stripe.ts 2import Stripe from 'stripe'; 3 4export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 5 apiVersion: '2024-11-20.acacia', 6 typescript: true, 7});

Client Setup#

1// lib/stripe-client.ts 2import { loadStripe } from '@stripe/stripe-js'; 3 4export const stripePromise = loadStripe( 5 process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! 6);

Customer Management#

1// lib/stripe/customers.ts 2import { stripe } from '@/lib/stripe'; 3import { prisma } from '@/lib/prisma'; 4 5export async function getOrCreateCustomer(userId: string) { 6 const user = await prisma.user.findUnique({ 7 where: { clerkId: userId }, 8 }); 9 10 if (!user) throw new Error('User not found'); 11 12 if (user.stripeCustomerId) { 13 return user.stripeCustomerId; 14 } 15 16 const customer = await stripe.customers.create({ 17 email: user.email, 18 name: user.name || undefined, 19 metadata: { userId: user.id }, 20 }); 21 22 await prisma.user.update({ 23 where: { id: user.id }, 24 data: { stripeCustomerId: customer.id }, 25 }); 26 27 return customer.id; 28} 29 30export async function getCustomerPortalUrl(customerId: string) { 31 const session = await stripe.billingPortal.sessions.create({ 32 customer: customerId, 33 return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings`, 34 }); 35 36 return session.url; 37}

Payment Methods#

1// lib/stripe/payment-methods.ts 2import { stripe } from '@/lib/stripe'; 3 4export async function getPaymentMethods(customerId: string) { 5 const paymentMethods = await stripe.paymentMethods.list({ 6 customer: customerId, 7 type: 'card', 8 }); 9 10 return paymentMethods.data; 11} 12 13export async function attachPaymentMethod( 14 customerId: string, 15 paymentMethodId: string 16) { 17 await stripe.paymentMethods.attach(paymentMethodId, { 18 customer: customerId, 19 }); 20 21 // Set as default 22 await stripe.customers.update(customerId, { 23 invoice_settings: { 24 default_payment_method: paymentMethodId, 25 }, 26 }); 27} 28 29export async function detachPaymentMethod(paymentMethodId: string) { 30 await stripe.paymentMethods.detach(paymentMethodId); 31}

Payment Intent#

1// app/api/payment-intent/route.ts 2import { auth } from '@clerk/nextjs/server'; 3import { NextRequest, NextResponse } from 'next/server'; 4import { stripe } from '@/lib/stripe'; 5import { getOrCreateCustomer } from '@/lib/stripe/customers'; 6 7export async function POST(request: NextRequest) { 8 const { userId } = await auth(); 9 if (!userId) { 10 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 11 } 12 13 const { amount, currency = 'usd' } = await request.json(); 14 15 const customerId = await getOrCreateCustomer(userId); 16 17 const paymentIntent = await stripe.paymentIntents.create({ 18 amount, 19 currency, 20 customer: customerId, 21 automatic_payment_methods: { 22 enabled: true, 23 }, 24 metadata: { userId }, 25 }); 26 27 return NextResponse.json({ 28 clientSecret: paymentIntent.client_secret, 29 }); 30}

Payment Form Component#

1// components/PaymentForm.tsx 2'use client'; 3 4import { useState } from 'react'; 5import { 6 PaymentElement, 7 Elements, 8 useStripe, 9 useElements, 10} from '@stripe/react-stripe-js'; 11import { stripePromise } from '@/lib/stripe-client'; 12 13function CheckoutForm({ onSuccess }: { onSuccess: () => void }) { 14 const stripe = useStripe(); 15 const elements = useElements(); 16 const [error, setError] = useState<string | null>(null); 17 const [processing, setProcessing] = useState(false); 18 19 const handleSubmit = async (e: React.FormEvent) => { 20 e.preventDefault(); 21 if (!stripe || !elements) return; 22 23 setProcessing(true); 24 setError(null); 25 26 const { error: submitError } = await elements.submit(); 27 if (submitError) { 28 setError(submitError.message || 'An error occurred'); 29 setProcessing(false); 30 return; 31 } 32 33 const { error: confirmError } = await stripe.confirmPayment({ 34 elements, 35 confirmParams: { 36 return_url: `${window.location.origin}/checkout/success`, 37 }, 38 }); 39 40 if (confirmError) { 41 setError(confirmError.message || 'Payment failed'); 42 setProcessing(false); 43 } else { 44 onSuccess(); 45 } 46 }; 47 48 return ( 49 <form onSubmit={handleSubmit} className="space-y-6"> 50 <PaymentElement /> 51 52 {error && ( 53 <div className="p-3 bg-red-50 text-red-600 rounded-lg">{error}</div> 54 )} 55 56 <button 57 type="submit" 58 disabled={!stripe || processing} 59 className="w-full py-3 bg-brand-600 text-white rounded-lg disabled:opacity-50" 60 > 61 {processing ? 'Processing...' : 'Pay Now'} 62 </button> 63 </form> 64 ); 65} 66 67export function PaymentForm({ 68 clientSecret, 69 onSuccess, 70}: { 71 clientSecret: string; 72 onSuccess: () => void; 73}) { 74 return ( 75 <Elements 76 stripe={stripePromise} 77 options={{ 78 clientSecret, 79 appearance: { 80 theme: 'stripe', 81 variables: { 82 colorPrimary: '#6366f1', 83 }, 84 }, 85 }} 86 > 87 <CheckoutForm onSuccess={onSuccess} /> 88 </Elements> 89 ); 90}

Subscription Management#

1// lib/stripe/subscriptions.ts 2import { stripe } from '@/lib/stripe'; 3 4export async function createSubscription( 5 customerId: string, 6 priceId: string, 7 options?: { 8 trialDays?: number; 9 couponId?: string; 10 } 11) { 12 return stripe.subscriptions.create({ 13 customer: customerId, 14 items: [{ price: priceId }], 15 trial_period_days: options?.trialDays, 16 coupon: options?.couponId, 17 payment_behavior: 'default_incomplete', 18 expand: ['latest_invoice.payment_intent'], 19 }); 20} 21 22export async function cancelSubscription( 23 subscriptionId: string, 24 options?: { 25 immediately?: boolean; 26 } 27) { 28 if (options?.immediately) { 29 return stripe.subscriptions.cancel(subscriptionId); 30 } 31 32 return stripe.subscriptions.update(subscriptionId, { 33 cancel_at_period_end: true, 34 }); 35} 36 37export async function resumeSubscription(subscriptionId: string) { 38 return stripe.subscriptions.update(subscriptionId, { 39 cancel_at_period_end: false, 40 }); 41} 42 43export async function updateSubscription( 44 subscriptionId: string, 45 newPriceId: string 46) { 47 const subscription = await stripe.subscriptions.retrieve(subscriptionId); 48 49 return stripe.subscriptions.update(subscriptionId, { 50 items: [ 51 { 52 id: subscription.items.data[0].id, 53 price: newPriceId, 54 }, 55 ], 56 proration_behavior: 'create_prorations', 57 }); 58}

Webhook Handler#

1// app/api/webhooks/stripe/route.ts 2import { NextRequest, NextResponse } from 'next/server'; 3import { stripe } from '@/lib/stripe'; 4import { prisma } from '@/lib/prisma'; 5import Stripe from 'stripe'; 6 7const webhookHandlers: Record< 8 string, 9 (event: Stripe.Event) => Promise<void> 10> = { 11 'checkout.session.completed': async (event) => { 12 const session = event.data.object as Stripe.Checkout.Session; 13 // Handle successful checkout 14 }, 15 16 'customer.subscription.created': async (event) => { 17 const subscription = event.data.object as Stripe.Subscription; 18 await prisma.user.update({ 19 where: { stripeCustomerId: subscription.customer as string }, 20 data: { 21 subscriptionId: subscription.id, 22 subscriptionStatus: subscription.status, 23 priceId: subscription.items.data[0].price.id, 24 }, 25 }); 26 }, 27 28 'customer.subscription.updated': async (event) => { 29 const subscription = event.data.object as Stripe.Subscription; 30 await prisma.user.update({ 31 where: { stripeCustomerId: subscription.customer as string }, 32 data: { 33 subscriptionStatus: subscription.status, 34 priceId: subscription.items.data[0].price.id, 35 currentPeriodEnd: new Date(subscription.current_period_end * 1000), 36 }, 37 }); 38 }, 39 40 'customer.subscription.deleted': async (event) => { 41 const subscription = event.data.object as Stripe.Subscription; 42 await prisma.user.update({ 43 where: { stripeCustomerId: subscription.customer as string }, 44 data: { 45 subscriptionId: null, 46 subscriptionStatus: 'canceled', 47 }, 48 }); 49 }, 50 51 'invoice.payment_failed': async (event) => { 52 const invoice = event.data.object as Stripe.Invoice; 53 // Send payment failure notification 54 }, 55}; 56 57export async function POST(request: NextRequest) { 58 const body = await request.text(); 59 const signature = request.headers.get('stripe-signature')!; 60 61 let event: Stripe.Event; 62 63 try { 64 event = stripe.webhooks.constructEvent( 65 body, 66 signature, 67 process.env.STRIPE_WEBHOOK_SECRET! 68 ); 69 } catch (err) { 70 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 }); 71 } 72 73 const handler = webhookHandlers[event.type]; 74 if (handler) { 75 await handler(event); 76 } 77 78 return NextResponse.json({ received: true }); 79}

Invoice Generation#

1// lib/stripe/invoices.ts 2import { stripe } from '@/lib/stripe'; 3 4export async function getInvoices(customerId: string) { 5 return stripe.invoices.list({ 6 customer: customerId, 7 limit: 10, 8 }); 9} 10 11export async function getInvoice(invoiceId: string) { 12 return stripe.invoices.retrieve(invoiceId); 13} 14 15export async function createInvoice( 16 customerId: string, 17 items: Array<{ description: string; amount: number }> 18) { 19 // Create invoice items 20 for (const item of items) { 21 await stripe.invoiceItems.create({ 22 customer: customerId, 23 amount: item.amount, 24 currency: 'usd', 25 description: item.description, 26 }); 27 } 28 29 // Create and send invoice 30 const invoice = await stripe.invoices.create({ 31 customer: customerId, 32 auto_advance: true, 33 }); 34 35 return stripe.invoices.sendInvoice(invoice.id); 36}

Testing#

1# Install Stripe CLI 2brew install stripe/stripe-cli/stripe 3 4# Login 5stripe login 6 7# Forward webhooks to local 8stripe listen --forward-to localhost:3000/api/webhooks/stripe 9 10# Trigger test events 11stripe trigger checkout.session.completed 12stripe trigger customer.subscription.created