NextReady

Payments

NextReady integrates with Stripe to provide a complete payment system for your SaaS application, supporting subscriptions, one-time payments, and usage-based billing.

Overview

NextReady uses Stripe for payment processing, providing a secure and reliable way to handle payments in your SaaS application. The payment system supports various features including subscriptions, one-time payments, and usage-based billing.

Key Features

  • Stripe integration for secure payment processing
  • Subscription management with different pricing tiers
  • One-time payments for specific products or services
  • Automatic invoicing and receipt generation
  • Customer portal for subscription management
  • Webhook handling for payment events

Stripe Setup

To use the payment system in NextReady, you need to set up a Stripe account and configure the necessary environment variables.

Creating a Stripe Account

  1. Sign up for a Stripe account at stripe.com
  2. Complete the onboarding process to activate your account
  3. Get your API keys from the Stripe Dashboard

Environment Variables

Set up the following environment variables in your .env.local file:

# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
STRIPE_CUSTOMER_PORTAL_URL=https://yourdomain.com/account/billing

Important Note

Always use test keys during development. Only switch to live keys when you're ready to deploy to production. Never commit your Stripe API keys to version control.

Stripe Products and Prices

Before using the payment system, you need to set up your products and prices in the Stripe Dashboard:

  1. Create products for your different offerings
  2. Set up prices for each product (one-time or recurring)
  3. Note the price IDs for use in your application

Subscriptions

NextReady includes a complete subscription management system built on Stripe Subscriptions.

Subscription Model

The subscription data is stored in your MongoDB database and linked to the user or organization:

// Example subscription data in User or Organization model
{
  subscription: {
    id: "sub_1234567890",
    status: "active", // active, canceled, past_due, etc.
    plan: "pro",
    currentPeriodEnd: "2023-12-31T00:00:00.000Z",
    cancelAtPeriodEnd: false,
    stripeCustomerId: "cus_1234567890"
  }
}

Creating a Subscription

To create a new subscription, you redirect the user to the Stripe Checkout page:

// Client component
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function SubscribeButton({ plan, userId }) {
  const [loading, setLoading] = useState(false)
  const router = useRouter()

  const handleSubscribe = async () => {
    try {
      setLoading(true)
      
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          plan,
          userId,
        }),
      })
      
      const data = await response.json()
      
      if (data.url) {
        router.push(data.url)
      }
    } catch (error) {
      console.error('Error creating checkout session:', error)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <button
      onClick={handleSubscribe}
      disabled={loading}
      className="btn btn-primary"
    >
      {loading ? 'Loading...' : `Subscribe to ${plan}`}
    </button>
  )
}

Subscription Status

You can check the subscription status to determine user access to features:

// Helper function to check subscription
export function hasActiveSubscription(user) {
  if (!user?.subscription) {
    return false
  }
  
  const { status, currentPeriodEnd } = user.subscription
  
  // Check if subscription is active
  if (status !== 'active' && status !== 'trialing') {
    return false
  }
  
  // Check if subscription is expired
  if (new Date(currentPeriodEnd) < new Date()) {
    return false
  }
  
  return true
}

// Check if user has access to a specific feature
export function hasFeatureAccess(user, featureName) {
  if (!hasActiveSubscription(user)) {
    return false
  }
  
  const planFeatures = getPlanFeatures(user.subscription.plan)
  return planFeatures.includes(featureName)
}

Checkout Process

NextReady uses Stripe Checkout to handle the payment process. This provides a secure, pre-built payment page that you can customize.

Creating a Checkout Session

The checkout process starts by creating a Stripe Checkout session:

// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import Stripe from 'stripe'

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

export async function POST(req: NextRequest) {
  try {
    const session = await getServerSession(authOptions)
    
    if (!session?.user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    
    const { plan } = await req.json()
    
    // Get price ID based on plan
    const priceId = getPriceIdForPlan(plan)
    
    if (!priceId) {
      return NextResponse.json(
        { error: 'Invalid plan selected' },
        { status: 400 }
      )
    }
    
    // Create checkout session
    const checkoutSession = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing?success=true`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing?canceled=true`,
      customer_email: session.user.email,
      metadata: {
        userId: session.user.id,
        plan,
      },
    })
    
    return NextResponse.json({ url: checkoutSession.url })
  } catch (error) {
    console.error('Error creating checkout session:', error)
    return NextResponse.json(
      { error: 'Failed to create checkout session' },
      { status: 500 }
    )
  }
}

// Helper function to get price ID for a plan
function getPriceIdForPlan(plan: string): string | null {
  const prices = {
    basic: process.env.STRIPE_PRICE_BASIC,
    pro: process.env.STRIPE_PRICE_PRO,
    enterprise: process.env.STRIPE_PRICE_ENTERPRISE,
  }
  
  return prices[plan] || null
}

Handling Checkout Success

After a successful checkout, Stripe will redirect the user to your success URL. You can display a confirmation message and update the UI:

// Client component for success page
'use client'

import { useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'

export default function BillingPage() {
  const searchParams = useSearchParams()
  const [status, setStatus] = useState<'success' | 'canceled' | null>(null)
  
  useEffect(() => {
    if (searchParams.get('success')) {
      setStatus('success')
    } else if (searchParams.get('canceled')) {
      setStatus('canceled')
    }
  }, [searchParams])
  
  return (
    <div>
      {status === 'success' && (
        <div className="p-4 bg-green-50 text-green-700 rounded-lg mb-4">
          Your subscription has been successfully activated! You now have access to all features.
        </div>
      )}
      
      {status === 'canceled' && (
        <div className="p-4 bg-yellow-50 text-yellow-700 rounded-lg mb-4">
          Your checkout was canceled. No charges were made.
        </div>
      )}
      
      {/* Rest of billing page content */}
    </div>
  )
}

Stripe Webhooks

Webhooks are essential for handling asynchronous events from Stripe, such as successful payments, subscription updates, and failed payments.

Setting Up Webhooks

  1. Go to the Stripe Dashboard and navigate to Developers > Webhooks
  2. Add an endpoint with your webhook URL (e.g., https://yourdomain.com/api/webhooks/stripe)
  3. Select the events you want to listen for (e.g., checkout.session.completed, invoice.paid, etc.)
  4. Copy the webhook signing secret and add it to your environment variables

Webhook Handler

NextReady includes a webhook handler to process Stripe events:

// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { buffer } from 'micro'
import dbConnect from '@/lib/mongodb'
import User from '@/models/User'

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

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET

export async function POST(req: NextRequest) {
  try {
    const buf = await buffer(req)
    const sig = req.headers.get('stripe-signature')
    
    if (!sig) {
      return NextResponse.json(
        { error: 'Missing stripe-signature header' },
        { status: 400 }
      )
    }
    
    // Verify webhook signature
    let event: Stripe.Event
    
    try {
      event = stripe.webhooks.constructEvent(
        buf,
        sig,
        webhookSecret
      )
    } catch (err) {
      console.error('Webhook signature verification failed:', err)
      return NextResponse.json(
        { error: 'Webhook signature verification failed' },
        { status: 400 }
      )
    }
    
    // Handle the event
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session
        
        // Update user subscription
        await handleCheckoutSessionCompleted(session)
        break
      }
      
      case 'invoice.paid': {
        const invoice = event.data.object as Stripe.Invoice
        
        // Update subscription period
        await handleInvoicePaid(invoice)
        break
      }
      
      case 'customer.subscription.updated': {
        const subscription = event.data.object as Stripe.Subscription
        
        // Update subscription status
        await handleSubscriptionUpdated(subscription)
        break
      }
      
      case 'customer.subscription.deleted': {
        const subscription = event.data.object as Stripe.Subscription
        
        // Cancel subscription
        await handleSubscriptionDeleted(subscription)
        break
      }
      
      default:
        console.log(`Unhandled event type: ${event.type}`)
    }
    
    return NextResponse.json({ received: true })
  } catch (error) {
    console.error('Webhook error:', error)
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 500 }
    )
  }
}

// Helper functions for handling different events
async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
  if (!session.metadata?.userId || !session.customer) {
    return
  }
  
  await dbConnect()
  
  const user = await User.findById(session.metadata.userId)
  
  if (!user) {
    return
  }
  
  // Get subscription details from Stripe
  const subscriptions = await stripe.subscriptions.list({
    customer: session.customer as string,
    limit: 1,
  })
  
  if (subscriptions.data.length === 0) {
    return
  }
  
  const subscription = subscriptions.data[0]
  
  // Update user subscription
  user.subscription = {
    id: subscription.id,
    status: subscription.status,
    plan: session.metadata.plan,
    currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    cancelAtPeriodEnd: subscription.cancel_at_period_end,
    stripeCustomerId: session.customer,
  }
  
  await user.save()
}

Customer Portal

Stripe Customer Portal allows users to manage their subscriptions, update payment methods, and view invoices.

Setting Up Customer Portal

  1. Go to the Stripe Dashboard and navigate to Settings > Customer Portal
  2. Configure the portal settings, including branding, features, and products
  3. Save your changes

Creating a Customer Portal Session

// src/app/api/customer-portal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import Stripe from 'stripe'
import User from '@/models/User'
import dbConnect from '@/lib/mongodb'

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

export async function POST(req: NextRequest) {
  try {
    const session = await getServerSession(authOptions)
    
    if (!session?.user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    
    await dbConnect()
    
    const user = await User.findById(session.user.id)
    
    if (!user?.subscription?.stripeCustomerId) {
      return NextResponse.json(
        { error: 'No active subscription found' },
        { status: 400 }
      )
    }
    
    // Create customer portal session
    const portalSession = await stripe.billingPortal.sessions.create({
      customer: user.subscription.stripeCustomerId,
      return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing`,
    })
    
    return NextResponse.json({ url: portalSession.url })
  } catch (error) {
    console.error('Error creating customer portal session:', error)
    return NextResponse.json(
      { error: 'Failed to create customer portal session' },
      { status: 500 }
    )
  }
}

Customer Portal Button

// Client component
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function ManageSubscriptionButton() {
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  
  const handleManageSubscription = async () => {
    try {
      setLoading(true)
      
      const response = await fetch('/api/customer-portal', {
        method: 'POST',
      })
      
      const data = await response.json()
      
      if (data.url) {
        router.push(data.url)
      }
    } catch (error) {
      console.error('Error opening customer portal:', error)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <button
      onClick={handleManageSubscription}
      disabled={loading}
      className="btn btn-outline"
    >
      {loading ? 'Loading...' : 'Manage Subscription'}
    </button>
  )
}

Payment API

NextReady provides a set of API endpoints for managing payments and subscriptions.

API Endpoints

EndpointMethodDescription
/api/checkoutPOSTCreate a Stripe Checkout session for subscription or one-time payment
/api/customer-portalPOSTCreate a Stripe Customer Portal session for managing subscriptions
/api/webhooks/stripePOSTHandle Stripe webhook events
/api/subscriptionsGETGet current user's subscription details
/api/subscriptions/cancelPOSTCancel current subscription at period end

Subscription Details API

// src/app/api/subscriptions/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import dbConnect from '@/lib/mongodb'
import User from '@/models/User'

export async function GET(req: NextRequest) {
  try {
    const session = await getServerSession(authOptions)
    
    if (!session?.user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    
    await dbConnect()
    
    const user = await User.findById(session.user.id)
    
    if (!user) {
      return NextResponse.json({ error: 'User not found' }, { status: 404 })
    }
    
    // Return subscription details
    return NextResponse.json({
      subscription: user.subscription || null,
    })
  } catch (error) {
    console.error('Error fetching subscription:', error)
    return NextResponse.json(
      { error: 'Failed to fetch subscription details' },
      { status: 500 }
    )
  }
}

Next Steps

Now that you understand how payments work in NextReady, you can:

  • Set up your Stripe account and create products and prices
  • Implement subscription-based access control for your features
  • Customize the checkout experience with Stripe Checkout
  • Set up webhook handlers for payment events
  • Create a billing dashboard for users to manage their subscriptions