Skip to main content

Payment Library

The template implements a multi-provider payment system using the Factory and Strategy patterns. It supports Stripe, LemonSqueezy, Solidgate, and Polar as payment providers, with a unified interface for payments, subscriptions, webhooks, and refunds.

Architecture Overview

Source Files

FilePurpose
lib/payment/index.tsPublic API exports
lib/payment/lib/payment-provider-factory.tsFactory for creating provider instances
lib/payment/lib/payment-service.tsUnified service facade
lib/payment/lib/payment-service-manager.tsSingleton manager for service lifecycle
lib/payment/types/payment-types.tsCore interfaces and enums
lib/payment/types/payment.tsPayment flow and submission types
lib/payment/config/Provider configuration and validation
lib/payment/lib/providers/Individual provider implementations
lib/payment/hooks/React hooks for client-side payment flows
lib/payment/ui/Payment form components

Core Interfaces

PaymentProviderInterface

Every provider implements this comprehensive interface:

export interface PaymentProviderInterface {
// Payment operations
createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent>;
confirmPayment(paymentId: string, paymentMethodId: string): Promise<PaymentIntent>;
verifyPayment(paymentId: string): Promise<PaymentVerificationResult>;
createSetupIntent(user: User | null): Promise<SetupIntent>;

// Subscription management
createCustomer(params: CreateCustomerParams): Promise<CustomerResult>;
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionInfo>;
cancelSubscription(subscriptionId: string, cancelAtPeriodEnd?: boolean): Promise<SubscriptionInfo>;
updateSubscription(params: UpdateSubscriptionParams): Promise<SubscriptionInfo>;
hasCustomerId(user: User | null): boolean;
getCustomerId(user: User | null): Promise<string | null>;

// Webhooks and refunds
handleWebhook(payload: any, signature: string, ...args: any[]): Promise<WebhookResult>;
refundPayment(paymentId: string, amount?: number): Promise<any>;

// Client configuration and UI
getClientConfig(): ClientConfig;
getUIComponents(): UIComponents;
}

PaymentProviderFactory

Creates provider instances based on configuration:

export type SupportedProvider = 'stripe' | 'solidgate' | 'lemonsqueezy' | 'polar';

export class PaymentProviderFactory {
static createProvider(
providerType: SupportedProvider,
config: PaymentProviderConfig
): PaymentProviderInterface {
switch (providerType) {
case 'stripe': return new StripeProvider(config);
case 'solidgate': return new SolidgateProvider(config);
case 'lemonsqueezy': return new LemonSqueezyProvider(config);
case 'polar': return new PolarProvider(config);
default: throw new Error(`Unsupported payment provider: ${providerType}`);
}
}
}

PaymentService

The PaymentService class provides a unified facade over all provider operations:

export class PaymentService {
private provider: PaymentProviderInterface;

constructor(config: PaymentServiceConfig) {
this.provider = PaymentProviderFactory.createProvider(config.provider, config.config);
}

// All methods delegate to the underlying provider
async createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent> {
return this.provider.createPaymentIntent(params);
}

async createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionInfo> {
return this.provider.createSubscription(params);
}

// ... additional delegated methods
}

Data Types

Payment Enums

export enum PaymentType {
ONE_TIME = 'one_time',
SUBSCRIPTION = 'subscription',
FREE = 'free',
}

export enum SubscriptionStatus {
INCOMPLETE = 'incomplete',
INCOMPLETE_EXPIRED = 'incomplete_expired',
TRIALING = 'trialing',
ACTIVE = 'active',
PAST_DUE = 'past_due',
CANCELED = 'canceled',
UNPAID = 'unpaid',
}

export enum PaymentFlow {
PAY_AT_START = "pay_at_start",
PAY_AT_END = "pay_at_end",
}

Webhook Events

export enum WebhookEventType {
PAYMENT_SUCCEEDED = 'payment_succeeded',
PAYMENT_FAILED = 'payment_failed',
SUBSCRIPTION_CREATED = 'subscription_created',
SUBSCRIPTION_UPDATED = 'subscription_updated',
SUBSCRIPTION_CANCELLED = 'subscription_cancelled',
INVOICE_PAID = 'invoice_paid',
REFUND_CREATED = 'refund_created',
// ... additional event types
}

Key Data Structures

TypePurpose
PaymentIntentPayment session with id, amount, currency, status, clientSecret
SubscriptionInfoSubscription details with status, period end, trial info
CustomerResultCreated customer with id, email, name
WebhookResultProcessed webhook with type, id, data
ClientConfigFrontend-safe config with publicKey and gateway type
UIComponentsReact components and visual assets for the provider

Currency Utilities

The library includes helper functions for currency formatting:

// Format cents to display currency
export function formatCentsToCurrency(
cents: number, currency: string = 'USD', locale: string = 'en-US'
): string {
const amount = cents / 100;
return new Intl.NumberFormat(locale, {
style: 'currency', currency,
minimumFractionDigits: 2, maximumFractionDigits: 2,
}).format(amount);
}

// Convert between cents and decimal
export function convertCentsToDecimal(cents: number): number;
export function convertDecimalToCents(decimal: number): number;

// Convert timestamps to Date objects
export function convertNumberToDate(timestamp?: number): Date | null;
export function safeTimestampToDate(timestamp: number | null | undefined): Date | undefined;

Payment Flow Types

The system supports two submission payment flows:

FlowEnumDescription
Pay at StartPAY_AT_STARTPayment required before submission review
Pay at EndPAY_AT_ENDPayment collected after admin approval

Submission Status Lifecycle

UI Components Interface

Each provider exposes UI components for frontend integration:

export interface UIComponents {
PaymentForm: React.ComponentType<PaymentFormProps>;
logo: string;
cardBrands: CardBrandIcon[];
supportedPaymentMethods: string[];
translations: Record<string, Record<string, string>>;
}

Client-Side Integration

The usePayment hook and PaymentProvider context provide React integration:

import { usePayment, PaymentProvider } from '@/lib/payment';

// Wrap your app with the payment provider
<PaymentProvider>
<PaymentForm
amount={2999}
currency="usd"
isSubscription={false}
onSuccess={(paymentId) => console.log('Paid:', paymentId)}
onError={(error) => console.error('Failed:', error)}
/>
</PaymentProvider>

Provider Configuration

export interface PaymentProviderConfig {
apiKey: string;
webhookSecret?: string;
secretKey?: string;
options?: Record<string, any>;
}

Each provider requires at minimum an apiKey. Stripe and Solidgate also use webhookSecret for webhook signature verification.