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 ​
# Install Stripe.js for payment UI
npm install @stripe/stripe-js @stripe/react-stripe-js
# or
yarn add @stripe/stripe-js @stripe/react-stripe-jsDirect 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:
# 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.comAPI Client ​
Create lib/bridge-payments.ts:
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:
'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:
'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:
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):
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):
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 ​
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:
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 ​
- Use Server Components - Fetch data on server when possible
- Secure API Routes - Validate session tokens
- Error Handling - Show user-friendly error messages
- Loading States - Provide feedback during async operations
- Type Safety - Use TypeScript for better DX
Next Steps ​
- Payments API - Complete API reference
- Guest Checkout Guide - Implement guest payments
- Stripe Integration - Stripe-specific features