Skip to content

Next.js Integration ​

Production-Ready Integration Pattern

This guide shows battle-tested integration patterns for Bridge Payments in Next.js applications, supporting both App Router and Pages Router.

Bridge Payments is a production-ready REST API that you integrate directly using fetch. Official Next.js SDK coming soon!

Installation ​

bash
# Install Stripe.js for payment UI
npm install @stripe/stripe-js @stripe/react-stripe-js
# or
yarn add @stripe/stripe-js @stripe/react-stripe-js

Direct API Integration

Bridge Payments is a REST API - no Bridge Payments-specific packages needed! Just use fetch to make HTTP requests to your Bridge Payments instance. Works seamlessly with both server and client components.

Configuration ​

Environment Variables ​

Create .env.local:

bash
# Bridge Payments
NEXT_PUBLIC_BRIDGE_PAYMENTS_URL=https://your-instance.pubflow.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

# Flowless (for authentication)
NEXT_PUBLIC_FLOWLESS_URL=https://your-flowless.pubflow.com

API Client ​

Create lib/bridge-payments.ts:

typescript
const BRIDGE_URL = process.env.NEXT_PUBLIC_BRIDGE_PAYMENTS_URL;

export async function createPaymentIntent(data: {
  total_cents: number;
  currency: string;
  provider_id?: string;
  sessionId: string;
}) {
  const response = await fetch(`${BRIDGE_URL}/bridge-payment/payments/intents`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Session-ID': data.sessionId
    },
    body: JSON.stringify({
      total_cents: data.total_cents,
      currency: data.currency,
      provider_id: data.provider_id || 'stripe'
    })
  });

  if (!response.ok) {
    throw new Error('Failed to create payment intent');
  }

  return response.json();
}

export async function syncPaymentIntent(
  paymentIntentId: string,
  sessionId: string
) {
  const response = await fetch(
    `${BRIDGE_URL}/bridge-payment/payments/intents/${paymentIntentId}/sync`,
    {
      method: 'POST',
      headers: {
        'X-Session-ID': sessionId
      }
    }
  );

  if (!response.ok) {
    throw new Error('Failed to sync payment intent');
  }

  return response.json();
}

export async function getPaymentMethods(sessionId: string) {
  const response = await fetch(
    `${BRIDGE_URL}/bridge-payment/payment-methods`,
    {
      headers: {
        'X-Session-ID': sessionId
      }
    }
  );

  if (!response.ok) {
    throw new Error('Failed to fetch payment methods');
  }

  return response.json();
}

App Router Implementation ​

Payment Page ​

Create app/checkout/page.tsx:

typescript
'use client';

import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from '@/components/CheckoutForm';
import { createPaymentIntent } from '@/lib/bridge-payments';

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState('');
  const [paymentIntentId, setPaymentIntentId] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Get session from your auth system
    const sessionId = localStorage.getItem('session_id');

    if (!sessionId) {
      window.location.href = '/login';
      return;
    }

    // Create payment intent
    createPaymentIntent({
      total_cents: 2000, // $20.00
      currency: 'USD',
      provider_id: 'stripe',
      sessionId
    })
      .then((data) => {
        setClientSecret(data.client_secret);
        setPaymentIntentId(data.id);
        setLoading(false);
      })
      .catch((error) => {
        console.error('Error:', error);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!clientSecret) {
    return <div>Error loading payment</div>;
  }

  return (
    <div className="max-w-md mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Checkout</h1>
      <Elements stripe={stripePromise} options={{ clientSecret }}>
        <CheckoutForm paymentIntentId={paymentIntentId} />
      </Elements>
    </div>
  );
}

Checkout Form Component ​

Create components/CheckoutForm.tsx:

typescript
'use client';

import { useState } from 'react';
import {
  useStripe,
  useElements,
  PaymentElement
} from '@stripe/react-stripe-js';
import { syncPaymentIntent } from '@/lib/bridge-payments';

export default function CheckoutForm({
  paymentIntentId
}: {
  paymentIntentId: string;
}) {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState<string | null>(null);
  const [processing, setProcessing] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements) return;

    setProcessing(true);
    setError(null);

    const { error: submitError, paymentIntent } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/success`
      },
      redirect: 'if_required'
    });

    if (submitError) {
      setError(submitError.message || 'Payment failed');
      setProcessing(false);
    } else if (paymentIntent && paymentIntent.status === 'succeeded') {
      // Sync with backend
      const sessionId = localStorage.getItem('session_id');
      if (sessionId) {
        await syncPaymentIntent(paymentIntentId, sessionId);
      }

      // Redirect to success page
      window.location.href = '/success';
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <PaymentElement />

      {error && (
        <div className="text-red-600 text-sm">{error}</div>
      )}

      <button
        type="submit"
        disabled={!stripe || processing}
        className="w-full bg-blue-600 text-white py-3 rounded-lg disabled:opacity-50"
      >
        {processing ? 'Processing...' : 'Pay $20.00'}
      </button>
    </form>
  );
}

Pages Router Implementation ​

Payment Page ​

Create pages/checkout.tsx:

typescript
import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from '../components/CheckoutForm';
import { createPaymentIntent } from '../lib/bridge-payments';

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState('');
  const [paymentIntentId, setPaymentIntentId] = useState('');

  useEffect(() => {
    const sessionId = localStorage.getItem('session_id');

    createPaymentIntent({
      total_cents: 2000,
      currency: 'USD',
      provider_id: 'stripe',
      sessionId: sessionId!
    }).then((data) => {
      setClientSecret(data.client_secret);
      setPaymentIntentId(data.id);
    });
  }, []);

  return (
    <div className="max-w-md mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Checkout</h1>
      {clientSecret && (
        <Elements stripe={stripePromise} options={{ clientSecret }}>
          <CheckoutForm paymentIntentId={paymentIntentId} />
        </Elements>
      )}
    </div>
  );
}

API Routes ​

Create Payment Intent ​

Create app/api/payments/create/route.ts (App Router):

typescript
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { total_cents, currency } = await request.json();
  const sessionId = request.headers.get('x-session-id');

  if (!sessionId) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const response = await fetch(
    `${process.env.NEXT_PUBLIC_BRIDGE_PAYMENTS_URL}/bridge-payment/payments/intents`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Session-ID': sessionId
      },
      body: JSON.stringify({
        total_cents,
        currency,
        provider_id: 'stripe'
      })
    }
  );

  const data = await response.json();
  return NextResponse.json(data);
}

Or pages/api/payments/create.ts (Pages Router):

typescript
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { total_cents, currency } = req.body;
  const sessionId = req.headers['x-session-id'];

  if (!sessionId) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const response = await fetch(
    `${process.env.NEXT_PUBLIC_BRIDGE_PAYMENTS_URL}/bridge-payment/payments/intents`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Session-ID': sessionId as string
      },
      body: JSON.stringify({
        total_cents,
        currency,
        provider_id: 'stripe'
      })
    }
  );

  const data = await response.json();
  res.status(200).json(data);
}

Guest Checkout ​

typescript
export async function createGuestPayment(data: {
  total_cents: number;
  currency: string;
  guest_data: {
    email: string;
    name: string;
  };
}) {
  const response = await fetch(
    `${BRIDGE_URL}/bridge-payment/payments/intents`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        total_cents: data.total_cents,
        currency: data.currency,
        provider_id: 'stripe',
        guest_data: data.guest_data
      })
    }
  );

  return response.json();
}

TypeScript Types ​

Create types/bridge-payments.ts:

typescript
export interface PaymentIntent {
  id: string;
  client_secret: string;
  status: string;
  total_cents: number;
  currency: string;
  provider_id: string;
}

export interface PaymentMethod {
  id: string;
  type: string;
  card_brand?: string;
  card_last_four?: string;
  is_default: boolean;
  alias?: string;
}

export interface CreatePaymentIntentRequest {
  total_cents: number;
  currency: string;
  provider_id?: string;
  payment_method_id?: string;
  guest_data?: {
    email: string;
    name: string;
  };
}

Best Practices ​

  1. Use Server Components - Fetch data on server when possible
  2. Secure API Routes - Validate session tokens
  3. Error Handling - Show user-friendly error messages
  4. Loading States - Provide feedback during async operations
  5. Type Safety - Use TypeScript for better DX

Next Steps ​