LogoNEXTDEVKIT Docs

How to Use Payments

Learn how to use payment APIs with both Stripe and Creem in NEXTDEVKIT

🔧 Payment Provider Architecture

NEXTDEVKIT uses a provider pattern to support multiple payment systems. Both Stripe and Creem implement the same interface, making it easy to switch between providers.

Provider Interface

src/payment/types.ts
export interface PaymentProvider {
  createCheckoutLink(params: CreateCheckoutLinkParams): Promise<string>;
  createCustomerPortalLink(params: CreatePortalLinkParams): Promise<string>;
  handleWebhook(payload: string, signature: string): Promise<void>;
}

Stripe Provider Implementation

src/payment/providers/stripe.ts
import Stripe from 'stripe';
import { PaymentProvider, CreateCheckoutLinkParams } from '@/payment/types';

export class StripeProvider implements PaymentProvider {
  private stripe: Stripe;

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

  async createCheckoutLink(params: CreateCheckoutLinkParams): Promise<string> {
    //...rest of the code
    
    return session.url!;
  }

  async createCustomerPortalLink(params: CreatePortalLinkParams): Promise<string> {
    //...rest of the code
    
    return session.url!;
  }

  async handleWebhook(payload: string, signature: string): Promise<void> {
    const event = this.stripe.webhooks.constructEvent(
      payload,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case 'checkout.session.completed':
        await this.handleCheckoutCompleted(event.data.object);
        break;
      case 'customer.subscription.created':
        await this.handleSubscriptionCreated(event.data.object);
        break;
      case 'customer.subscription.updated':
        await this.handleSubscriptionUpdated(event.data.object);
        break;
      case 'customer.subscription.deleted':
        await this.handleSubscriptionDeleted(event.data.object);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }
  }
}

Creem Provider Implementation

src/payment/providers/creem.ts
import { createHmac } from "node:crypto";
import { PaymentProvider, CreateCheckoutLinkParams } from '@/payment/types';

export class CreemProvider implements PaymentProvider {
  async createCheckoutLink(params: CreateCheckoutLinkParams): Promise<string> {
    //...rest of the code
    return checkout_url;
  }

  async createCustomerPortalLink(params: CreatePortalLinkParams): Promise<string> {
    //...rest of the code
    return customer_portal_link;
  }

  async handleWebhook(payload: string, signature: string): Promise<void> {
    // Verify webhook signature
    //...rest of the code

    const event = JSON.parse(payload);

    switch (event.eventType) {
      case "checkout.completed":
        await this.handleOneTimePayment(event);
        break;
      case "subscription.active":
        await this.handleSubscriptionActive(event);
        break;
      case "subscription.trialing":
        await this.handleSubscriptionTrialing(event);
        break;
      case "subscription.canceled":
      case "subscription.expired":
        await this.handleSubscriptionCanceled(event);
        break;
      default:
        console.log(`Unhandled Creem event: ${event.eventType}`);
    }
  }
}

🔄 Webhooks

Stripe Webhook Handler

src/app/api/webhooks/stripe/route.ts
import { getPaymentProvider } from '@/payment/providers';
import { NextRequest, NextResponse } from 'next/server';

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

  try {
    const paymentProvider = getPaymentProvider();
    await paymentProvider.handleWebhook(payload, signature);
    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Stripe webhook error:', error);
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 400 }
    );
  }
}

Creem Webhook Handler

src/app/api/webhooks/creem/route.ts
import { getPaymentProvider } from '@/payment/providers';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const payload = await request.text();
  const signature = request.headers.get('creem-signature')!;

  try {
    const paymentProvider = getPaymentProvider();
    await paymentProvider.handleWebhook(payload, signature);
    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Creem webhook error:', error);
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 400 }
    );
  }
}

💳 Frontend Usage

import { createCheckoutLink } from '@/payment/actions';
import { PaymentType } from '@/payment/types';

// In your React component
const handleSubscribe = async (priceId: string) => {
  const result = await createCheckoutLink({
    type: PaymentType.SUBSCRIPTION,
    priceId: priceId,
    redirectUrl: window.location.origin + "/app/dashboard",
  });
  
  if (result.data?.checkoutUrl) {
    window.location.href = result.data.checkoutUrl;
  }
};

// Usage with different providers
// The same code works for both Stripe and Creem!
<button onClick={() => handleSubscribe("price_monthly")}>
  Subscribe Monthly
</button>

Customer Portal Access

import { createCustomerPortal } from '@/payment/actions';

const handleManageBilling = async () => {
  const result = await createCustomerPortal({
    customerId: user.customerId,
    redirectUrl: window.location.href,
  });
  
  if (result.data?.portalUrl) {
    window.location.href = result.data.portalUrl;
  }
};

<button onClick={handleManageBilling}>
  Manage Billing
</button>

🎯 Next Steps

Now that you understand how to use the payment system: