Skip to main content
Openfront provides a flexible payment adapter system that supports multiple payment providers including Stripe, PayPal, and manual payment processing. The payment system is built with a provider-agnostic architecture that makes it easy to integrate additional payment gateways.

Payment Provider Architecture

The payment system in Openfront uses an adapter pattern that abstracts payment provider implementation details. Each payment provider implements a standard set of functions:
  • createPaymentFunction - Initialize a payment intent
  • capturePaymentFunction - Capture an authorized payment
  • refundPaymentFunction - Process refunds
  • getPaymentStatusFunction - Check payment status
  • generatePaymentLinkFunction - Generate provider-specific payment links
  • handleWebhookFunction - Process webhook events

Payment Providers

Stripe Integration

Openfront includes full Stripe integration with support for automatic payment methods, payment intents, and webhook processing.

Configuration

Add your Stripe credentials to your environment variables:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Implementation

The Stripe adapter (features/integrations/payment/stripe.ts) implements all required payment functions:
import Stripe from "stripe";

const getStripeClient = () => {
  const stripeKey = process.env.STRIPE_SECRET_KEY;
  if (!stripeKey) {
    throw new Error("Stripe secret key not configured");
  }
  return new Stripe(stripeKey, {
    apiVersion: "2023-10-16",
  });
};

export async function createPaymentFunction({ cart, amount, currency }) {
  const stripe = getStripeClient();

  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: currency.toLowerCase(),
    automatic_payment_methods: {
      enabled: true,
    },
  });

  return {
    clientSecret: paymentIntent.client_secret,
    paymentIntentId: paymentIntent.id,
  };
}

Capturing Payments

export async function capturePaymentFunction({ paymentId, amount }) {
  const stripe = getStripeClient();

  const paymentIntent = await stripe.paymentIntents.capture(paymentId, {
    amount_to_capture: amount,
  });

  return {
    status: paymentIntent.status,
    amount: paymentIntent.amount_captured,
    data: paymentIntent,
  };
}

Processing Refunds

export async function refundPaymentFunction({ paymentId, amount }) {
  const stripe = getStripeClient();

  const refund = await stripe.refunds.create({
    payment_intent: paymentId,
    amount,
  });

  return {
    status: refund.status,
    amount: refund.amount,
    data: refund,
  };
}

Webhook Handling

Stripe webhooks are verified using signature validation:
export async function handleWebhookFunction({ event, headers }) {
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
  const stripe = getStripeClient();

  try {
    const stripeEvent = stripe.webhooks.constructEvent(
      JSON.stringify(event),
      headers['stripe-signature'],
      webhookSecret
    );

    return {
      isValid: true,
      event: stripeEvent,
      type: stripeEvent.type,
      resource: stripeEvent.data.object,
    };
  } catch (err) {
    throw new Error(`Webhook signature verification failed: ${err.message}`);
  }
}

PayPal Integration

PayPal integration supports both sandbox and production environments with OAuth2 authentication.

Configuration

NEXT_PUBLIC_PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_WEBHOOK_ID=...
NEXT_PUBLIC_PAYPAL_SANDBOX=true  # Set to false for production

Currency Handling

PayPal has special handling for no-division currencies (JPY, KRW, etc.):
const NO_DIVISION_CURRENCIES = [
  "JPY", "KRW", "VND", "CLP", "PYG", "XAF", "XOF",
  "BIF", "DJF", "GNF", "KMF", "MGA", "RWF", "XPF",
  "HTG", "VUV", "XAG", "XDR", "XAU"
];

const formatPayPalAmount = (amount: number, currency: string): string => {
  const upperCurrency = currency.toUpperCase();
  const isNoDivision = NO_DIVISION_CURRENCIES.includes(upperCurrency);

  if (isNoDivision) {
    return amount.toString();
  }
  return (amount / 100).toFixed(2);
};

Creating PayPal Orders

export async function createPaymentFunction({ cart, amount, currency }) {
  const accessToken = await getPayPalAccessToken();
  const baseUrl = getPayPalBaseUrl();

  const response = await fetch(
    `${baseUrl}/v2/checkout/orders`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({
        intent: "AUTHORIZE",
        purchase_units: [
          {
            amount: {
              currency_code: currency.toUpperCase(),
              value: formatPayPalAmount(amount, currency),
            },
          },
        ],
      }),
    }
  );

  const order = await response.json();
  return {
    orderId: order.id,
    status: order.status,
  };
}

Manual Payment

The manual payment provider allows processing payments outside the automated flow, useful for bank transfers, checks, or other offline payment methods.

Payment Flow

1
Initialize Cart
2
Create a shopping cart for the customer’s region:
3
mutation {
  createCart(data: {
    region: { connect: { id: "region_id" } }
  }) {
    id
    total
  }
}
4
Create Payment Sessions
5
Initialize payment sessions for all available providers in the region:
6
mutation {
  createPaymentSessions(cartId: "cart_id") {
    id
    paymentSessions {
      id
      providerId
      status
    }
  }
}
7
Select Payment Provider
8
Customer selects their preferred payment method:
9
mutation {
  setPaymentSession(cartId: "cart_id", providerId: "pp_stripe_***") {
    id
    selectedPaymentSession {
      providerId
      data
    }
  }
}
10
Complete Payment
11
Finalize the cart and capture payment:
12
mutation {
  completeCart(cartId: "cart_id") {
    order {
      id
      displayId
      paymentStatus
    }
  }
}

Multi-Currency Support

Openfront handles multiple currencies automatically:
  • Amounts are stored in the smallest currency unit (cents)
  • Currency conversion happens at the region level
  • No-division currencies (JPY, KRW) are handled appropriately
  • Each payment provider receives properly formatted amounts

Payment Sessions

Payment sessions track payment state for each provider:
PaymentSession {
  providerId: string
  amount: integer
  data: json           // Provider-specific data
  status: "pending" | "authorized" | "captured" | "canceled"
  cart: → Cart
  paymentProvider: → PaymentProvider
}

Creating Custom Payment Adapters

To add a new payment provider:
  1. Create a new file in features/integrations/payment/
  2. Implement all required functions:
// features/integrations/payment/custom-provider.ts

export async function createPaymentFunction({ cart, amount, currency }) {
  // Initialize payment with your provider
  return {
    paymentId: "...",
    clientData: { ... }
  };
}

export async function capturePaymentFunction({ paymentId, amount }) {
  // Capture the payment
  return {
    status: "captured",
    amount: capturedAmount,
  };
}

export async function refundPaymentFunction({ paymentId, amount }) {
  // Process refund
  return {
    status: "refunded",
    amount: refundedAmount,
  };
}

export async function getPaymentStatusFunction({ paymentId }) {
  // Check payment status
  return {
    status: "authorized",
    amount: paymentAmount,
  };
}

export async function generatePaymentLinkFunction({ paymentId }) {
  return `https://provider.com/payment/${paymentId}`;
}

export async function handleWebhookFunction({ event, headers }) {
  // Validate and process webhook
  return {
    isValid: true,
    event: processedEvent,
    type: event.type,
    resource: event.data,
  };
}
  1. Register the adapter in features/integrations/payment/index.ts:
export const paymentProviderAdapters = {
  stripe: () => import("./stripe"),
  paypal: () => import("./paypal"),
  manual: () => import("./manual"),
  custom: () => import("./custom-provider"),
};
  1. Create the provider in the admin dashboard under Payment Providers

Security Best Practices

  • Never expose secret keys in client-side code
  • Always validate webhook signatures
  • Use environment variables for credentials
  • Implement proper error handling
  • Log payment events for audit trails
  • Use HTTPS for all payment communications

Testing Payments

Use provider test credentials for development: Stripe Test Cards:
  • Success: 4242 4242 4242 4242
  • Decline: 4000 0000 0000 0002
  • Authentication Required: 4000 0025 0000 3155
PayPal Sandbox:
  • Use sandbox credentials from PayPal Developer Dashboard
  • Test with sandbox buyer accounts

Build docs developers (and LLMs) love