Stripe Integration
Complete Stripe integration for payments, subscriptions, and billing.
Dependencies#
npm install stripe @stripe/stripe-js @stripe/react-stripe-jsEnvironment 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