Skip to main content

Table of Contents

Installation

Install the required packages:
npm install @solvapay/react @solvapay/react-supabase
# or
pnpm add @solvapay/react @solvapay/react-supabase
# or
yarn add @solvapay/react @solvapay/react-supabase

Peer Dependencies

SolvaPay React requires:
  • react ^18.2.0 || ^19.0.0
  • react-dom ^18.2.0 || ^19.0.0
  • @stripe/react-stripe-js (for payment forms)
  • @stripe/stripe-js (for Stripe integration)

Basic Setup

1. Wrap Your App with Provider

The SolvaPayProvider is required to use SolvaPay hooks and components:
// App.tsx or main entry point
import { SolvaPayProvider } from '@solvapay/react'

function App() {
  return (
    <SolvaPayProvider>
      <YourApp />
    </SolvaPayProvider>
  )
}

2. Zero-Config Usage

By default, SolvaPayProvider uses these API endpoints:
  • /api/check-purchase - Check purchase status
  • /api/create-payment-intent - Create payment intents
  • /api/process-payment - Process payments
If your backend uses these routes, no configuration is needed!

Provider Configuration

Custom API Routes

If your backend uses different API routes, configure them:
import { SolvaPayProvider } from '@solvapay/react'

function App() {
  return (
    <SolvaPayProvider
      config={{
        api: {
          checkPurchase: '/api/custom/purchase',
          createPayment: '/api/custom/payment',
          processPayment: '/api/custom/process',
        },
      }}
    >
      <YourApp />
    </SolvaPayProvider>
  )
}

With Supabase Authentication

Use the Supabase auth adapter for automatic user ID extraction:
import { SolvaPayProvider } from '@solvapay/react'
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase'

function App() {
  const supabaseAdapter = createSupabaseAuthAdapter({
    supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
    supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  })

  return (
    <SolvaPayProvider
      config={{
        auth: { adapter: supabaseAdapter },
      }}
    >
      <YourApp />
    </SolvaPayProvider>
  )
}

Custom Authentication Adapter

Create a custom auth adapter for other authentication systems:
import { SolvaPayProvider, AuthAdapter } from '@solvapay/react'

const customAuthAdapter: AuthAdapter = {
  getToken: async () => {
    // Return auth token from your auth system
    return localStorage.getItem('auth-token')
  },

  getUserId: async () => {
    // Extract user ID from your auth system
    const token = localStorage.getItem('auth-token')
    if (!token) return null

    const decoded = JSON.parse(atob(token.split('.')[1]))
    return decoded.userId
  },
}

function App() {
  return (
    <SolvaPayProvider
      config={{
        auth: { adapter: customAuthAdapter },
      }}
    >
      <YourApp />
    </SolvaPayProvider>
  )
}

Components

PaymentForm

A complete payment form component with Stripe integration:
import { PaymentForm } from '@solvapay/react'

function CheckoutPage() {
  return (
    <PaymentForm
      planRef="pln_premium"
      productRef="prd_myapi"
      onSuccess={() => {
        console.log('Payment successful!')
        // Redirect or show success message
      }}
      onError={error => {
        console.error('Payment failed:', error)
      }}
    />
  )
}

PaymentForm Props

  • planRef (required) - Plan reference to subscribe to
  • productRef (optional) - Product reference for usage tracking
  • onSuccess - Callback when payment succeeds
  • onError - Callback when payment fails
  • returnUrl - Optional return URL after payment
  • submitButtonText - Custom submit button text (default: “Pay Now”)
  • className - Custom CSS class for form container
  • buttonClassName - Custom CSS class for submit button

PricingSelector

Select a pricing option from available options using the render-prop API:
import { PricingSelector } from '@solvapay/react'

function PricingPage() {
  return (
    <PricingSelector
      fetcher={async (productRef) => {
        const res = await fetch(`/api/plans?productRef=${productRef}`)
        return res.json()
      }}
      productRef="prd_myapi"
    >
      {({ plans, loading, error, selectedPlan, selectPlan }) => {
        if (loading) return <div>Loading pricing...</div>
        if (error) return <div>Error: {error.message}</div>
        return (
          <div>
            {plans.map(plan => (
              <button
                key={plan.id}
                onClick={() => selectPlan(plan)}
                style={{ fontWeight: selectedPlan?.id === plan.id ? 'bold' : 'normal' }}
              >
                ${plan.price}/{plan.interval}
              </button>
            ))}
          </div>
        )
      }}
    </PricingSelector>
  )
}

PurchaseGate

Conditionally render content based on purchase status using the render-prop pattern:
import { PurchaseGate } from '@solvapay/react'

function PremiumContent() {
  return (
    <PurchaseGate requireProduct="pln_premium">
      {({ hasAccess, loading }) => {
        if (loading) return <div>Loading...</div>
        if (!hasAccess) return <div>Please subscribe to access this content.</div>
        return <div>Premium content here!</div>
      }}
    </PurchaseGate>
  )
}

ProductBadge

Display product subscription information using the render-prop pattern:
import { ProductBadge } from '@solvapay/react'

function UserProfile() {
  return (
    <div>
      <h1>Your Profile</h1>
      <ProductBadge>
        {({ displayPlan, shouldShow }) =>
          shouldShow ? <span className="badge">{displayPlan}</span> : null
        }
      </ProductBadge>
    </div>
  )
}

Hooks

usePurchase

Check purchase status and access purchase data:
import { usePurchase } from '@solvapay/react'

function Dashboard() {
  const { hasPaidPurchase, loading, activePurchase, refetch } = usePurchase()

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

  return (
    <div>
      {hasPaidPurchase ? (
        <div>
          <h2>Active Purchase</h2>
          <p>Product: {activePurchase?.productName}</p>
          <p>Status: {activePurchase?.status}</p>
        </div>
      ) : (
        <div>
          <p>No active purchase</p>
          <button onClick={() => refetch()}>Refresh</button>
        </div>
      )}
    </div>
  )
}

usePurchase Return Values

  • loading - Boolean indicating if purchase check is in progress
  • purchases - Array of purchase objects
  • activePurchase - The active purchase (or null)
  • hasPaidPurchase - Boolean indicating if user has paid purchase
  • activePaidPurchase - The active paid purchase (or null)
  • hasProduct(productName) - Function to check if user has a specific product
  • refetch - Function to manually refetch purchase status

useCheckout

Programmatic checkout flow:
import { useCheckout } from '@solvapay/react'

function CustomCheckout() {
  const { startCheckout, loading, error, stripePromise, clientSecret, reset } = useCheckout(
    'pln_premium',
    'prd_myapi',
  )

  const handleCheckout = async () => {
    try {
      await startCheckout()
      console.log('Checkout started!')
    } catch (error) {
      console.error('Checkout failed:', error)
    }
  }

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? 'Processing...' : 'Checkout'}
    </button>
  )
}

useCustomer

Access customer information:
import { useCustomer } from '@solvapay/react'

function CustomerInfo() {
  const { customerRef, email, name, loading } = useCustomer()

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

  return (
    <div>
      <h2>Customer Information</h2>
      <p>Customer ID: {customerRef}</p>
      <p>Email: {email}</p>
      <p>Name: {name}</p>
    </div>
  )
}

usePlans

Fetch available plans:
import { usePlans } from '@solvapay/react'

function PlansPage() {
  const { plans, loading, error } = usePlans({
    fetcher: async (productRef) => {
      const res = await fetch(`/api/plans?productRef=${productRef}`)
      return res.json()
    },
    productRef: 'prd_myapi',
  })

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

  if (error) {
    return <div>Error loading plans: {error.message}</div>
  }

  return (
    <div>
      <h1>Available Plans</h1>
      {plans?.map(plan => (
        <div key={plan.id}>
          <h3>${plan.price}/{plan.interval}</h3>
        </div>
      ))}
    </div>
  )
}

useSolvaPay

Access all SolvaPay functionality:
import { useSolvaPay } from '@solvapay/react'

function CustomComponent() {
  const { activePurchase, startCheckout, customerRef, refetchPurchase } =
    useSolvaPay()

  // Use any SolvaPay functionality
  return <div>...</div>
}

Payment Flow

Simple Payment Flow

Use PaymentForm for a complete payment flow:
import { PaymentForm } from '@solvapay/react'
import { useRouter } from 'next/navigation' // or your router

function CheckoutPage() {
  const router = useRouter()

  return (
    <div className="checkout-container">
      <h1>Subscribe to Premium</h1>
      <PaymentForm
        planRef="pln_premium"
        productRef="prd_myapi"
        onSuccess={() => {
          router.push('/dashboard')
        }}
        onError={error => {
          alert(`Payment failed: ${error.message}`)
        }}
      />
    </div>
  )
}

Custom Payment Flow

Build a custom payment flow with hooks:
import { useCheckout, usePurchase } from '@solvapay/react'
import { loadStripe } from '@stripe/stripe-js'

function CustomCheckoutPage() {
  const { startCheckout, loading, stripePromise, clientSecret, reset } = useCheckout('pln_premium', 'prd_myapi')
  const { refetch } = usePurchase()

  const handleCheckout = async () => {
    try {
      await startCheckout()

      // After successful checkout, refresh purchase status
      await refetch()
      router.push('/dashboard')
    } catch (error) {
      console.error('Checkout failed:', error)
    }
  }

  return (
    <div>
      {/* Your custom UI */}
      <button onClick={handleCheckout} disabled={loading}>
        Pay Now
      </button>
    </div>
  )
}

Purchase Management

Check Purchase Status

import { usePurchase } from '@solvapay/react'

function ProtectedContent() {
  const { hasPaidPurchase, loading } = usePurchase()

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

  if (!hasPaidPurchase) {
    return <div>Please subscribe to access this content.</div>
  }

  return <div>Premium content here!</div>
}

Display Purchase Details

import { usePurchase } from '@solvapay/react'

function PurchaseDetails() {
  const { activePurchase, loading } = usePurchase()

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

  if (!activePurchase) {
    return <div>No active purchase</div>
  }

  return (
    <div>
      <h2>Your Purchase</h2>
      <p>Product: {activePurchase.productName}</p>
      <p>Status: {activePurchase.status}</p>
      <p>Current Period End: {activePurchase.currentPeriodEnd}</p>
    </div>
  )
}

Refresh Purchase Status

import { usePurchase } from '@solvapay/react'

function PurchaseStatus() {
  const { activePurchase, refetch, loading } = usePurchase()

  return (
    <div>
      <p>Status: {activePurchase?.status || 'None'}</p>
      <button onClick={() => refetch()} disabled={loading}>
        Refresh
      </button>
    </div>
  )
}

Complete Example

Here’s a complete React application with SolvaPay integration:
// App.tsx
import { SolvaPayProvider } from '@solvapay/react'
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase'
import { Dashboard } from './Dashboard'
import { Checkout } from './Checkout'

function App() {
  const supabaseAdapter = createSupabaseAuthAdapter({
    supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
    supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  })

  return (
    <SolvaPayProvider config={{ auth: { adapter: supabaseAdapter } }}>
      <Router>
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/checkout" element={<Checkout />} />
        </Routes>
      </Router>
    </SolvaPayProvider>
  )
}

// Dashboard.tsx
import { usePurchase } from '@solvapay/react'
import { Link } from 'react-router-dom'

export function Dashboard() {
  const { hasPaidPurchase, loading, activePurchase } = usePurchase()

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

  return (
    <div>
      <h1>Dashboard</h1>

      {hasPaidPurchase ? (
        <div>
          <h2>Welcome, Premium User!</h2>
          <p>Product: {activePurchase?.productName}</p>
          <p>Status: {activePurchase?.status}</p>
        </div>
      ) : (
        <div>
          <p>Please subscribe to access premium features.</p>
          <Link to="/checkout">Go to Checkout</Link>
        </div>
      )}
    </div>
  )
}

// Checkout.tsx
import { PaymentForm } from '@solvapay/react'
import { useNavigate } from 'react-router-dom'

export function Checkout() {
  const navigate = useNavigate()

  return (
    <div>
      <h1>Subscribe to Premium</h1>
      <PaymentForm
        planRef="pln_premium"
        productRef="prd_myapi"
        onSuccess={() => {
          navigate('/dashboard')
        }}
        onError={error => {
          console.error('Payment failed:', error)
        }}
      />
    </div>
  )
}

Styling

SolvaPay components are headless and don’t include default styles. Style them to match your design system:
<PaymentForm
  planRef="pln_premium"
  productRef="prd_myapi"
  className="my-custom-form"
  buttonClassName="my-custom-button"
/>
.my-custom-form {
  max-width: 500px;
  margin: 0 auto;
  padding: 2rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

.my-custom-button {
  background-color: #007bff;
  color: white;
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

Best Practices

  1. Provider Placement: Place SolvaPayProvider at the root of your app, above all routes.
  2. Error Handling: Always handle errors from hooks and components.
  3. Loading States: Show loading states while purchase checks are in progress.
  4. Refetch After Payment: Call refetch() after successful payment to update purchase status.
  5. Type Safety: Use TypeScript for better type safety and autocomplete.
  6. Custom Styling: Style components to match your design system.

Next Steps