Documentation Index
Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
Use this file to discover all available pages before exploring further.
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'
import { supabase } from './lib/supabase'
function App() {
const supabaseAdapter = createSupabaseAuthAdapter({ client: supabase })
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
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)
}}
/>
)
}
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 compound
primitive. Match by productRef (any plan of that product) or planRef
(specific plan). Both together require they match on the same active
purchase.
import { PurchaseGate } from '@solvapay/react'
function PremiumContent() {
return (
<PurchaseGate.Root planRef="pln_premium">
<PurchaseGate.Loading>Loading...</PurchaseGate.Loading>
<PurchaseGate.Blocked>Please subscribe to access this content.</PurchaseGate.Blocked>
<PurchaseGate.Allowed>Premium content here!</PurchaseGate.Allowed>
</PurchaseGate.Root>
)
}
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 the first purchase check for this user is in progress. Stays false on background refetches — gate your initial skeleton on this.
isRefetching - Boolean indicating a background refetch is in progress (first fetch already completed). Use for subtle “refreshing” indicators that shouldn’t remount the UI.
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)
hasPurchase(criteria?) - Predicate with AND semantics. Pass { productRef }, { planRef }, or both (both must match the same active purchase). Call with no arguments to check for any active purchase. Mirrors <PurchaseGate.Root> prop shape.
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, isRefetching } = usePurchase()
return (
<div>
<p>Status: {activePurchase?.status || 'None'}</p>
<button onClick={() => refetch()} disabled={loading || isRefetching}>
{isRefetching ? 'Refreshing...' : '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 { supabase } from './lib/supabase'
import { Dashboard } from './Dashboard'
import { Checkout } from './Checkout'
function App() {
const supabaseAdapter = createSupabaseAuthAdapter({ client: supabase })
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
-
Provider Placement: Place
SolvaPayProvider at the root of your app, above all routes.
-
Error Handling: Always handle errors from hooks and components.
-
Loading States: Show loading states while purchase checks are in progress.
-
Refetch After Payment: Call
refetch() after successful payment to update purchase status.
-
Type Safety: Use TypeScript for better type safety and autocomplete.
-
Custom Styling: Style components to match your design system.
Next Steps