logoPressFast

Payments

Accept payments with Stripe

Payments

Integrate Stripe to accept one-time payments and subscriptions.

Setup

1. Install Stripe

pnpm add stripe @stripe/stripe-js

2. Configure Environment Variables

STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

3. Initialize Stripe

// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

One-Time Payments

Create Payment Intent

// app/api/create-payment-intent/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { amount } = await request.json();

  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount * 100, // Convert to cents
      currency: 'usd',
      automatic_payment_methods: {
        enabled: true,
      },
    });

    return NextResponse.json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    console.error('Payment intent error:', error);
    return NextResponse.json(
      { error: 'Failed to create payment intent' },
      { status: 500 }
    );
  }
}

Payment Form

'use client';

import { useState } from 'react';
import {
  PaymentElement,
  Elements,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const [isProcessing, setIsProcessing] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements) return;

    setIsProcessing(true);

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/payment/success`,
      },
    });

    if (error) {
      console.error('Payment error:', error);
    }

    setIsProcessing(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!stripe || isProcessing}>
        {isProcessing ? 'Processing...' : 'Pay Now'}
      </button>
    </form>
  );
}

export function PaymentPage({ clientSecret }: { clientSecret: string }) {
  return (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutForm />
    </Elements>
  );
}

Subscriptions

See the Stripe Subscriptions Tutorial for detailed implementation.

Webhook Events

Handle Stripe webhook events:

// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    );
  }

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log('Payment succeeded:', paymentIntent.id);
      // Update database, send confirmation email, etc.
      break;

    case 'payment_intent.payment_failed':
      const failedPayment = event.data.object;
      console.log('Payment failed:', failedPayment.id);
      // Notify user, retry payment, etc.
      break;

    case 'invoice.payment_succeeded':
      const invoice = event.data.object;
      console.log('Invoice paid:', invoice.id);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return NextResponse.json({ received: true });
}

Products and Prices

Create Product

const product = await stripe.products.create({
  name: 'Premium Plan',
  description: 'All premium features',
  images: ['https://yoursite.com/product-image.jpg'],
  metadata: {
    category: 'subscription',
  },
});

Create Price

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2000, // $20.00
  currency: 'usd',
  recurring: {
    interval: 'month',
  },
});

Customer Portal

Allow customers to manage billing:

// app/api/create-portal-session/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
import { db } from '@/prisma';
import { NextResponse } from 'next/server';

export async function POST() {
  const session = await auth();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const user = await db.user.findUnique({
    where: { id: session.user.id },
  });

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user!.stripeCustomerId!,
    return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
  });

  return NextResponse.json({ url: portalSession.url });
}

Testing

Use Stripe test cards:

  • Success: 4242 4242 4242 4242
  • Decline: 4000 0000 0000 0002
  • 3D Secure: 4000 0025 0000 3155

Pricing Table

Display pricing options:

export function PricingTable({ prices }) {
  return (
    <div className="grid grid-cols-3 gap-8">
      {prices.map((price) => (
        <div key={price.id} className="border rounded-lg p-6">
          <h3 className="text-2xl font-bold">{price.product.name}</h3>
          <p className="text-4xl font-bold mt-4">
            ${price.unit_amount / 100}
            <span className="text-lg text-gray-500">/mo</span>
          </p>
          <ul className="mt-6 space-y-4">
            {price.product.features.map((feature) => (
              <li key={feature} className="flex items-center">
                <CheckIcon className="w-5 h-5 text-green-500 mr-2" />
                {feature}
              </li>
            ))}
          </ul>
          <button className="w-full mt-8 btn-primary">
            Subscribe
          </button>
        </div>
      ))}
    </div>
  );
}

Security Best Practices

  1. Never expose secret keys: Use environment variables
  2. Verify webhook signatures: Prevent fake events
  3. Use HTTPS: Required for production
  4. Implement idempotency: Handle duplicate events
  5. Store minimal data: Let Stripe handle sensitive info
  6. Use customer portal: Don't build custom billing UI

Next Steps

Payments