Payments

This guide explains how to implement and handle payments in our application using Stripe for secure payment processing and subscription management.

Setup

Prerequisites

  • Stripe account
  • Configured products and prices in Stripe Dashboard
  • Webhook endpoint setup

Installation

npm install stripe

Environment Variables

# Stripe Configuration
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_webhook_secret
STRIPE_STARTER_PRICE_ID=price_id_for_starter_plan
STRIPE_PRO_PRICE_ID=price_id_for_pro_plan

Implementation

Stripe Client Setup

Configure the Stripe client in lib/stripe.ts:

import Stripe from "stripe"

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", {
  apiVersion: "2024-06-20",
  typescript: true,
})

Payment Button Component

Reusable component for initiating Stripe checkout:

export function StripeButton({
  type,
  text = "Buy",
  variant = "default",
  from = "landing",
  size = "default",
  icon,
  className,
}: StripeButtonProps) {
  const [isLoading, setIsLoading] = useState<boolean>(false)

  const createBillingSession = async () => {
    try {
      setIsLoading(true)
      const data = await stripeRedirect({ type, from })
      window.location.href = data
    } catch (error: any) {
      toast.error(error?.message || "An error occurred")
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <Button
      variant={variant}
      className={cn("w-full", className)}
      onClick={createBillingSession}
      disabled={isLoading}
      size={size}
    >
      {text}
      {icon && <Icon className="ml-2 size-4 shrink-0" />}
    </Button>
  )
}

Stripe Redirect Handler

Server action to create Stripe checkout sessions:

export async function stripeRedirect({ type, from = "landing" }: stripeRedirectProps) {
  // Verify environment and user
  if (!process.env.NEXT_PUBLIC_APP_URL) {
    throw new Error("NEXT_PUBLIC_APP_URL is not defined")
  }

  try {
    const successUrl = absoluteUrl(`${createRoute("success").href}?from=${from}`)
    const cancelUrl = absoluteUrl(`${createRoute("cancel").href}?from=${from}`)

    const plan = PLANS.find((plan) => plan.type === type)
    if (!plan?.price.priceIds.production) {
      throw new Error("Plan not found or price not configured")
    }

    const stripeSession = await stripe.checkout.sessions.create({
      success_url: successUrl + "&session_id={CHECKOUT_SESSION_ID}",
      cancel_url: cancelUrl,
      payment_method_types: ["card"],
      mode: "payment",
      billing_address_collection: "auto",
      customer_email: from === "dashboard" ? user?.email : undefined,
      line_items: [
        {
          quantity: 1,
          price: plan.price.priceIds.production,
        },
      ],
      metadata: {
        userId: from === "dashboard" ? user!.id : null,
        type: type,
        planName: plan.name,
      },
    })

    return stripeSession.url || ""
  } catch (error: any) {
    throw new Error(error)
  }
}

Features

Multiple Plans

Support for different pricing tiers and subscription plans

Secure Checkout

PCI-compliant payment processing with Stripe Checkout

User Context

Different flows for authenticated and anonymous users

Metadata Tracking

Track plan types and user information in payments

Payment Flow

1

Initiate Payment

User clicks payment button, triggering stripeRedirect server action

2

Create Session

Server creates Stripe checkout session with plan details

3

Redirect to Checkout

User is redirected to Stripe's hosted checkout page

4

Process Payment

Stripe handles payment processing and security

5

Handle Result

User is redirected back with success or cancel status

Best Practices

Security

  • Never log sensitive payment data
  • Use webhook signatures for verification
  • Implement proper error handling
  • Validate payment status server-side

User Experience

  • Show clear pricing information
  • Handle loading states properly
  • Provide clear error messages
  • Implement proper success/cancel flows