Chapter 7: Payment Integration System
In the previous chapter, we explored how ShipNowKit's Authentication System helps identify users and secure your application. Now, let's learn how to accept payments and manage subscriptions with the Payment Integration System.
Introduction
Imagine opening a store. You need a way to accept payments from customers - a cash register, credit card terminal, and a system to track who bought what. In the digital world, your application faces similar challenges.
ShipNowKit's Payment Integration System works as your digital cash register and payment processor. It helps you:
- Accept one-time payments for products
- Manage recurring subscriptions
- Handle payment provider communication
- Track payment status and customer information
Let's explore how this system works to add payment capabilities to your application!
The Problem: Payment Processing is Complex
Without an integrated payment system, accepting payments is challenging:
- Setting up payment providers requires complex API integrations
- Managing subscriptions involves tracking renewal dates and status
- Processing webhooks from payment providers is technically complex
- Handling different payment methods and currencies is difficult
- Ensuring payments are properly linked to user accounts is error-prone
ShipNowKit's Payment Integration System
ShipNowKit solves these challenges with a unified Payment Integration System that works with both Stripe and Paddle. It provides:
- Simple checkout functions for one-time payments
- Subscription management tools
- Webhook processing to keep payment status updated
- Customer management across payment providers
Let's see how to implement payments in your application!
Key Components of the Payment System
1. Payment Configuration
First, let's understand the payment configuration:
// From config/index.ts
export const paymentConfig: PaymentConfig = {
paymentProvider: "stripe", // or "paddle"
systemName: "shipnowkit",
successPage: "/payment/success",
pricePage: `${siteConfig.baseUrl}/#pricing`
}
This configuration:
- Sets the payment provider (Stripe or Paddle)
- Defines a system name to identify your application in payment records, allowing you to use the same merchant account across different websites, eliminating the need to go through the payment account application process each time
- Specifies the success page where users are redirected after payment
- References the pricing page URL
2. Price Button Component
The PriceBtn
component makes it easy to add checkout buttons to your application:
// Usage example
import { PriceBtn } from "@/components/price/PriceBtn";
export function PricingSection() {
return (
<div className="pricing-card">
<h3>Pro Plan</h3>
<p>$49/month</p>
<PriceBtn
btnText="Subscribe Now"
targetPlan={{
isSubscription: true,
name: "Pro Plan",
priceId: "price_123abc",
isYearly: false
}}
activePlans={[]} // User's active plans would go here
className="button primary"
/>
</div>
);
}
This component creates a button that handles:
- Redirecting unauthenticated users to sign in
- Processing checkout for new subscriptions
- Managing plan changes for existing subscribers
- Displaying error messages
3. Payment Hooks
ShipNowKit provides custom React hooks to handle payment operations:
// components/price/hooks/useCheckout.ts (simplified)
export const useCheckout = () => {
const currentPath = usePathname();
const [error, setError] = useState<string | null>(null);
const handleCheckout = async (priceId: string) => {
if (paymentConfig.paymentProvider === "stripe") {
await handleStripeCheckout(priceId)
} else if (paymentConfig.paymentProvider === "paddle") {
await handlePaddleCheckout(priceId)
} else {
setError("Payment provider not supported");
}
}
// Other implementation details...
return {
handleCheckout,
error,
clearError: () => setError(null)
}
}
This hook abstracts the payment provider differences, making checkout simple regardless of which provider you're using.
Implementing Payment Functionality
Let's walk through how to implement payments in your application:
Step 1: Set Up a Pricing Page
First, create a pricing page that displays your plans and pricing information:
// app/pricing/page.tsx
import { ShipNowPrice } from "@/components/price/ShipNowKit";
import { getUserActiveSubscriptions } from "@/lib/actions/subscription";
export default async function PricingPage() {
// Get user's active subscriptions (if any)
const { subscriptions } = await getUserActiveSubscriptions();
// Pricing configuration
const pricingConfig = {
title: "Simple, Transparent Pricing",
subTitle: "Start free, upgrade when you need to",
cards: [
{
name: "Basic",
description: "Perfect for getting started",
features: ["Feature 1", "Feature 2", "Feature 3"],
price: {
amount: "Free",
priceId: undefined, // Free plan has no price ID
isSubscription: false
}
},
{
name: "Pro",
description: "For growing businesses",
features: ["Everything in Basic", "Feature 4", "Feature 5"],
price: {
amount: "$49",
period: "/mo",
priceId: "price_abc123", // Your Stripe/Paddle price ID
isSubscription: true
}
}
]
};
return <ShipNowPrice {...pricingConfig} activePlans={subscriptions} />;
}
This code:
- Fetches the user's active subscriptions (if they're logged in)
- Defines pricing information for each plan
- Renders a pricing component with the plan information
Step 2: Process Checkout
When a user clicks a pricing button, the checkout process begins:
// Inside components/price/hooks/useCheckout.ts
const handleStripeCheckout = async (priceId: string) => {
try {
// Get a checkout session ID from the server
const checkoutSessionId = await checkoutWithStripe(priceId, currentPath)
// Initialize Stripe and redirect to checkout
const stripe = await getStripe()
stripe?.redirectToCheckout({
sessionId: checkoutSessionId
})
} catch (error) {
setError(error instanceof Error ?
error.message :
'Payment initialization failed, please try again later.')
}
}
This function:
- Makes a server request to create a checkout session
- Initializes the Stripe client library
- Redirects the user to Stripe's checkout page
The server function that creates the checkout session:
// lib/payment/stripe/server.ts (simplified)
export async function checkoutWithStripe(priceId: string, currentPath: string) {
// Get the current user from the session
const session = await auth();
if (!session?.user?.id) {
throw new Error('Could not get user session.');
}
// Get or create a customer record
const customer = await upsertCustomer(
session.user.id,
session.user.email
);
// Get the price details
const price = await getPriceById(priceId);
// Create a checkout session
const checkoutSession = await stripe.checkout.sessions.create({
customer,
line_items: [{ price: priceId, quantity: 1 }],
mode: price.type === 'recurring' ? 'subscription' : 'payment',
success_url: getURL(paymentConfig.successPage),
cancel_url: getURL(currentPath),
// Other configuration...
});
return checkoutSession.id;
}
This function:
- Gets the authenticated user's information
- Creates or retrieves a customer record
- Creates a checkout session with the payment provider
- Returns the session ID for the frontend to use
Step 3: Create a Success Page
After successful checkout, users are redirected to a success page:
// app/payment/success/page.tsx
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function PaymentSuccessPage() {
return (
<div className="container max-w-lg mx-auto p-8 text-center">
<div className="bg-green-100 dark:bg-green-900 p-6 rounded-lg mb-6">
<h1 className="text-2xl font-bold mb-4">Payment Successful!</h1>
<p>Thank you for your purchase. Your payment has been processed successfully.</p>
</div>
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
</div>
);
}
This simple success page confirms the payment was successful and provides a link to the dashboard.
Processing Webhooks
A critical part of any payment system is handling webhooks from payment providers. Webhooks inform your application about events like successful payments, subscription renewals, or cancellations.
// app/api/payment/stripe/webhooks/route.ts (simplified)
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature") as string;
try {
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
// Process the event
const processor = new ProcessWebhook();
await processor.processEvent(event);
return new Response(null, { status: 200 });
} catch (error) {
return new Response(`Webhook Error: ${error.message}`, { status: 400 });
}
}
This webhook handler:
- Receives the webhook data from Stripe
- Verifies the signature to ensure it's authentic
- Processes the event using the
ProcessWebhook
class - Returns a success response
Event System for Payments
ShipNowKit uses an event system to handle payment-related events in a consistent way:
// app/api/payment/eventEmitter.ts (simplified)
type EventMap = {
'subscription.succeeded': string;
'subscription.renewed': string;
'subscription.plan_updated': {
subscriptionId: string;
oldPriceId: string;
newPriceId: string;
};
'subscription.canceled': string;
'payment.succeeded': string;
}
class EventEmitter {
private eventHandlers: Map<EventKey, EventHandler<any>[]> = new Map();
// Register an event handler
on<T extends EventKey>(event: T, handler: EventHandler<T>) {
this.eventHandlers.set(event, [
...(this.eventHandlers.get(event) || []),
handler
]);
}
// Emit an event
async emit<T extends EventKey>(event: T, eventId: string, data: EventMap[T]) {
const handlers = this.eventHandlers.get(event) || [];
for (const handler of handlers) {
await handler(eventId, data);
}
}
}
export const eventEmitter = new EventEmitter();
The event emitter:
- Defines a set of payment-related events (subscription created, renewed, etc.)
- Allows registering handlers for these events
- Emits events when payment actions occur
- Calls all registered handlers for the event
How the Payment System Works Under the Hood
Let's visualize the checkout process:
When a user makes a payment:
- They click a payment button in your app
- Your app requests a checkout session from your server
- Your server creates a checkout session with the payment provider
- The user is redirected to the payment provider's checkout page
- After payment, they're redirected back to your success page
- The payment provider sends a webhook to your application
- Your application updates the database with the payment status
Webhook Processing
When a payment provider sends a webhook event, ShipNowKit processes it:
// app/api/payment/stripe/webhooks/webhook-processor.ts (simplified)
export class ProcessWebhook {
async processEvent(eventData: Stripe.Event) {
switch (eventData.type) {
case 'checkout.session.completed':
const checkoutSession = eventData.data.object as Stripe.Checkout.Session;
// Process subscription or one-time payment
if (checkoutSession.mode === 'subscription') {
const subscriptionId = checkoutSession.subscription as string;
await this.upsertSubscription(subscriptionId, true);
await eventEmitter.emit('subscription.succeeded', eventData.id, subscriptionId);
} else if (checkoutSession.mode === 'payment') {
await this.upsertOneTimePayment(checkoutSession.id);
await eventEmitter.emit('payment.succeeded', eventData.id, checkoutSession.payment_intent as string);
}
break;
// Other event types...
}
}
// Other methods...
}
This code:
- Examines the event type from the payment provider
- Processes the event accordingly (updating subscription records, etc.)
- Emits events that other parts of your application can respond to
Managing Subscriptions
ShipNowKit includes tools for managing subscriptions, such as upgrading or downgrading plans:
// components/price/hooks/useChangePlan.ts (simplified)
export const useChangeSubscriptionPlan = () => {
const [error, setError] = useState<string | null>(null);
const handleChangePlan = async () => {
if (paymentConfig.paymentProvider === 'stripe') {
// Open Stripe customer portal
const url = await createStripeCustomerPortal(currentPath);
window.location.href = url;
} else if (paymentConfig.paymentProvider === 'paddle') {
// Show confirmation dialog for Paddle
setShowPaddleConfirmDialog(true);
}
};
const confirmChangePaddlePlan = async (currentPriceId: string, targetPriceId: string) => {
try {
await changeSubscription(currentPriceId, targetPriceId);
setShowSuccessChangePlanDialog(true);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to change subscription');
} finally {
setShowPaddleConfirmDialog(false);
}
};
// Other methods...
return {
handleChangePlan,
confirmChangePaddlePlan,
// Other properties...
};
};
This hook provides methods for changing subscription plans, with different behaviors depending on the payment provider.
Usage with Database Services
The Payment System works with ShipNowKit's Database Services to store payment information:
// From app/api/payment/stripe/webhooks/webhook-processor.ts (simplified)
private async upsertSubscription(subscriptionId: string, needToUpdatePrice: boolean) {
// Retrieve subscription details from Stripe
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Find the customer in our database
const customer = await prisma.customer.findFirst({
where: {
provider: PaymentProvider.stripe,
id: subscription.customer as string
},
select: { user_id: true }
});
// Create the subscription data
const subscriptionData = {
id: subscription.id,
provider: PaymentProvider.stripe,
user_id: customer.user_id,
// Other subscription details...
};
// Use the database service to update or create the subscription
return await upsertSubscription(subscriptionData);
}
This code:
- Gets subscription details from the payment provider
- Finds the corresponding customer in your database
- Prepares subscription data
- Uses the
upsertSubscription
database service to save the data
Practical Tips for Using the Payment System
1. Setting Up Your Payment Provider
Before using the Payment System, you need to set up accounts with payment providers:
- Create a Stripe account at stripe.com
- Or create a Paddle account at paddle.com
- Get your API keys and add them to your environment variables:
# .env file example
STRIPE_API_KEY=sk_test_123...
STRIPE_WEBHOOK_SECRET=whsec_456...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_789...
2. Creating Products and Prices
You'll need to create products and prices in your payment provider dashboard:
- In Stripe Dashboard, go to Products → Add Product
- Set product name, description, and price information
- Copy the Price ID (starts with "price_")
- Use this ID in your pricing configuration
3. Testing Payments
For testing, use these test card numbers:
- Stripe: 4242 4242 4242 4242 (successful payment)
- Stripe: 4000 0000 0000 0002 (declined payment)
- Expiry date: Any future date
- CVC: Any 3 digits
- ZIP: Any 5 digits
4. Handling Subscription Status in Your App
You can check subscription status to enable/disable features:
// Simplified example
import { getUserActiveSubscriptions } from "@/lib/actions/subscription";
export default async function DashboardPage() {
const { subscriptions } = await getUserActiveSubscriptions();
const hasPremiumPlan = subscriptions.some(
sub => sub.price_id === 'price_premium'
);
return (
<div>
<h1>Dashboard</h1>
{hasPremiumPlan ? (
<PremiumFeatures />
) : (
<FreeTierFeatures />
)}
</div>
);
}
Conclusion
ShipNowKit's Payment Integration System provides a complete solution for accepting payments and managing subscriptions in your application. By abstracting away the complexity of payment providers, it lets you focus on building your application rather than wrestling with payment APIs.
The system handles:
- One-time payments and subscriptions
- Multiple payment providers (Stripe and Paddle)
- Webhook processing and event management
- Customer information management
With just a few components and server actions, you can add professional payment capabilities to your application and start generating revenue.
Throughout this tutorial series, we've explored many of ShipNowKit's powerful features, from the Configuration System to Database Services, Authentication, and now Payments System. By combining these systems, you can quickly build robust, full-featured web applications that are secure, maintainable, and ready for production.