Skip to content

React Integration ​

Production-Ready Integration Pattern

This guide shows battle-tested integration patterns for Bridge Payments in React applications. Supports Create React App, Vite, and other React setups.

Bridge Payments is a production-ready REST API that you integrate directly using fetch. Official React 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 with any React setup.

Configuration ​

Environment Variables ​

Create .env:

bash
# Bridge Payments
REACT_APP_BRIDGE_PAYMENTS_URL=https://your-instance.pubflow.com
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...

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

For Vite, use VITE_ prefix:

bash
VITE_BRIDGE_PAYMENTS_URL=https://your-instance.pubflow.com
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...

API Client ​

Create src/lib/bridge-payments.js:

javascript
const BRIDGE_URL = process.env.REACT_APP_BRIDGE_PAYMENTS_URL ||
                   import.meta.env.VITE_BRIDGE_PAYMENTS_URL;

export async function createPaymentIntent(data) {
  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, sessionId) {
  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) {
  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();
}

export async function createPaymentMethod(data, sessionId) {
  const response = await fetch(
    `${BRIDGE_URL}/bridge-payment/payment-methods`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Session-ID': sessionId
      },
      body: JSON.stringify(data)
    }
  );

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

  return response.json();
}

Payment Component ​

Create src/components/CheckoutForm.jsx:

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

export default function CheckoutForm({ paymentIntentId, amount, onSuccess }) {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState(null);
  const [processing, setProcessing] = useState(false);

  const handleSubmit = async (e) => {
    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);
      }

      // Call success callback
      if (onSuccess) {
        onSuccess(paymentIntent);
      }
    }
  };

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

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={!stripe || processing}
        className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 transition"
      >
        {processing ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
      </button>
    </form>
  );
}

Payment Page ​

Create src/pages/CheckoutPage.jsx:

jsx
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.REACT_APP_STRIPE_PUBLISHABLE_KEY ||
  import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
);

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

  const amount = 2000; // $20.00

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

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

    createPaymentIntent({
      total_cents: amount,
      currency: 'USD',
      provider_id: 'stripe',
      sessionId
    })
      .then((data) => {
        setClientSecret(data.client_secret);
        setPaymentIntentId(data.id);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Error:', err);
        setError(err.message);
        setLoading(false);
      });
  }, []);

  const handleSuccess = (paymentIntent) => {
    console.log('Payment successful:', paymentIntent);
    window.location.href = '/success';
  };

  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-lg">Loading...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-red-600">Error: {error}</div>
      </div>
    );
  }

  return (
    <div className="max-w-md mx-auto p-6 mt-10">
      <h1 className="text-3xl font-bold mb-6">Checkout</h1>
      
      <div className="bg-gray-50 p-4 rounded-lg mb-6">
        <div className="flex justify-between items-center">
          <span className="text-gray-600">Total</span>
          <span className="text-2xl font-bold">
            ${(amount / 100).toFixed(2)}
          </span>
        </div>
      </div>

      {clientSecret && (
        <Elements stripe={stripePromise} options={{ clientSecret }}>
          <CheckoutForm
            paymentIntentId={paymentIntentId}
            amount={amount}
            onSuccess={handleSuccess}
          />
        </Elements>
      )}
    </div>
  );
}

Saved Payment Methods ​

Create src/components/SavedPaymentMethods.jsx:

jsx
import { useState, useEffect } from 'react';
import { getPaymentMethods } from '../lib/bridge-payments';

export default function SavedPaymentMethods({ onSelect }) {
  const [methods, setMethods] = useState([]);
  const [loading, setLoading] = useState(true);
  const [selected, setSelected] = useState(null);

  useEffect(() => {
    const sessionId = localStorage.getItem('session_id');
    
    getPaymentMethods(sessionId)
      .then((data) => {
        setMethods(data);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Error:', err);
        setLoading(false);
      });
  }, []);

  const handleSelect = (method) => {
    setSelected(method.id);
    if (onSelect) {
      onSelect(method);
    }
  };

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

  if (methods.length === 0) {
    return <div>No saved payment methods</div>;
  }

  return (
    <div className="space-y-3">
      <h3 className="font-semibold text-lg mb-3">Saved Payment Methods</h3>
      
      {methods.map((method) => (
        <div
          key={method.id}
          onClick={() => handleSelect(method)}
          className={`
            border rounded-lg p-4 cursor-pointer transition
            ${selected === method.id ? 'border-blue-600 bg-blue-50' : 'border-gray-300 hover:border-gray-400'}
          `}
        >
          <div className="flex items-center justify-between">
            <div className="flex items-center space-x-3">
              <div className="text-2xl">
                {method.card_brand === 'visa' && '💳'}
                {method.card_brand === 'mastercard' && '💳'}
                {method.card_brand === 'amex' && '💳'}
              </div>
              <div>
                <div className="font-medium capitalize">
                  {method.card_brand} •••• {method.card_last_four}
                </div>
                {method.alias && (
                  <div className="text-sm text-gray-600">{method.alias}</div>
                )}
              </div>
            </div>
            
            {method.is_default && (
              <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
                Default
              </span>
            )}
          </div>
        </div>
      ))}
    </div>
  );
}

Guest Checkout ​

jsx
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from '../components/CheckoutForm';

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);

export default function GuestCheckout() {
  const [clientSecret, setClientSecret] = useState('');
  const [paymentIntentId, setPaymentIntentId] = useState('');
  const [guestData, setGuestData] = useState({ email: '', name: '' });

  const handleCreatePayment = async (e) => {
    e.preventDefault();

    const response = await fetch(
      `${process.env.REACT_APP_BRIDGE_PAYMENTS_URL}/bridge-payment/payments/intents`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          total_cents: 2000,
          currency: 'USD',
          provider_id: 'stripe',
          guest_data: guestData
        })
      }
    );

    const data = await response.json();
    setClientSecret(data.client_secret);
    setPaymentIntentId(data.id);
  };

  if (!clientSecret) {
    return (
      <form onSubmit={handleCreatePayment} className="max-w-md mx-auto p-6">
        <h2 className="text-2xl font-bold mb-6">Guest Checkout</h2>
        
        <input
          type="email"
          placeholder="Email"
          value={guestData.email}
          onChange={(e) => setGuestData({ ...guestData, email: e.target.value })}
          required
          className="w-full p-3 border rounded mb-3"
        />
        
        <input
          type="text"
          placeholder="Full Name"
          value={guestData.name}
          onChange={(e) => setGuestData({ ...guestData, name: e.target.value })}
          required
          className="w-full p-3 border rounded mb-6"
        />
        
        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold"
        >
          Continue to Payment
        </button>
      </form>
    );
  }

  return (
    <div className="max-w-md mx-auto p-6">
      <Elements stripe={stripePromise} options={{ clientSecret }}>
        <CheckoutForm paymentIntentId={paymentIntentId} amount={2000} />
      </Elements>
    </div>
  );
}

Best Practices ​

  1. Error Handling - Show user-friendly error messages
  2. Loading States - Provide feedback during async operations
  3. Validation - Validate form inputs before submission
  4. Security - Never expose secret keys in frontend code
  5. Testing - Use test mode during development

Next Steps ​