> ## 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.

# React Integration Guide

> This guide shows you how to integrate SolvaPay React components and hooks into your React application to build payment flows and purchase management UIs.

## Table of Contents

* [Installation](#installation)
* [Basic Setup](#basic-setup)
* [Provider Configuration](#provider-configuration)
* [Components](#components)
* [Hooks](#hooks)
* [Payment Flow](#payment-flow)
* [Payment Confirmation](#payment-confirmation)
* [Multi-Currency Plans & Top-Ups](#multi-currency-plans--top-ups)
* [Credit Top-Ups & Auto-Recharge](#credit-top-ups--auto-recharge)
* [Purchase Management](#purchase-management)
* [Complete Example](#complete-example)

## Installation

Install the required packages:

```bash theme={null}
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:

```tsx theme={null}
// 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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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.

```tsx theme={null}
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:

```tsx theme={null}
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>
  )
}
```

### TopupForm

A compound component for one-off credit top-ups. It wraps `useTopup` and
Stripe Elements, so you compose the parts you need instead of accepting a
fixed layout:

```tsx theme={null}
import { TopupForm } from '@solvapay/react'

function TopUpCredits() {
  return (
    <TopupForm.Root
      amount={1000} // minor units — 1000 = $10.00
      currency="USD"
      onSuccess={(paymentIntent, extras) => {
        // extras?.creditsAdded is the wallet delta, when the backend reports it
        console.log('Topped up!', extras?.creditsAdded)
      }}
      onError={error => console.error('Top-up failed:', error)}
    >
      <TopupForm.Loading>Preparing payment…</TopupForm.Loading>
      <TopupForm.PaymentElement />
      <TopupForm.Error />
      <TopupForm.SubmitButton>Add credits</TopupForm.SubmitButton>
      <TopupForm.LegalFooter />
    </TopupForm.Root>
  )
}
```

#### Compound parts

* `TopupForm.Root` — provides context and drives the top-up. Accepts the props below.
* `TopupForm.PaymentElement` — renders the Stripe Payment Element.
* `TopupForm.SubmitButton` — confirms the payment; disabled until the card input is complete.
* `TopupForm.Loading` — shown while the payment intent and Stripe are initializing.
* `TopupForm.Error` — renders the current error, if any.
* `TopupForm.AmountPicker` — optional in-place amount picker (re-exported from `AmountPicker`).
* `TopupForm.LegalFooter` — mandate / legal copy footer.

#### Root props

* `amount` (required) — top-up amount in **minor units** (e.g. `1000` = `$10.00`).
* `currency` — ISO 4217 currency code (default: `USD`).
* `autoRecharge` — optional `AutoRechargeInput` to enable auto-recharge in the **same** payment (see [Credit Top-Ups & Auto-Recharge](#credit-top-ups--auto-recharge)).
* `onSuccess(paymentIntent, extras?)` — fires once the customer is fully credited. `extras.creditsAdded` carries the wallet delta when the backend reports it.
* `onError(error)` — called when the payment fails.
* `returnUrl` — return URL used for redirect-based payment methods (defaults to the current URL).
* `className` / `buttonClassName` — styling hooks.

Credits mint when the charge succeeds. The webhook is an idempotent backstop, not
the primary booking path. When your backend implements the
`/api/process-topup-payment` route, `TopupForm` waits for that round-trip so
`onSuccess` only fires once the credit has actually landed. Transports without
that route fall back to firing on Stripe confirmation.

### AutoRecharge

A drop-in component that lets a customer turn on auto-recharge. It
renders a summary card with a trigger that opens a modal for configuring a
balance **threshold**, a **fixed top-up amount**, and an optional **monthly spend
cap** (under **Advanced**). When enabled, SolvaPay charges the saved card
off-session and mints credits whenever the balance falls below the threshold — no
manual checkout.

```tsx theme={null}
import { AutoRecharge } from '@solvapay/react'

function AutoRechargeSettings() {
  return (
    <AutoRecharge
      currency="USD"
      onSaved={() => console.log('Auto-recharge saved')}
      onDisabled={() => console.log('Auto-recharge disabled')}
    />
  )
}
```

#### AutoRecharge Props

* `currency` — ISO 4217 currency code (default: `USD`).
* `defaultThresholdAmountMajor` — pre-fills the threshold field (form default: `5`).
* `defaultTopupAmountMajor` — pre-fills the top-up field (form default: `10`).
* `deferCardSetup` — when `true`, saving stages the config **without** creating a Stripe SetupIntent and fires `onPendingConfig` instead. Use this to arm auto-recharge inside a top-up payment (see below).
* `onPendingConfig(payload)` — receives the `AutoRechargeInput` to forward into `TopupForm` when `deferCardSetup` is set.
* `onSetupRequired(result)` — called when a separate card-setup step is required (non-deferred flow).
* `onSaved(result)` / `onDisabled()` — lifecycle callbacks.
* `className` — styling hook.

The form is opt-in (`enabled: false` by default). Amounts can be entered in
either display currency or credits; the component handles the Stripe SetupIntent
and the 3DS redirect return internally. Set an optional `maxMonthlySpendMajor` cap
(blank = unlimited) in the form. The summary card shows current-period
spend when a cap is configured, and a **Monthly spend limit reached** status when
the next recharge would exceed the cap. If a series of off-session charges keeps
declining, the card shows a text-only "payment failed" status — see the
[failed-recharge guidance](#credit-top-ups--auto-recharge) for building a card-update prompt.

For full layout control, compose the primitive from `@solvapay/react/primitives`:
`AutoRecharge.MaxMonthlySpendField`,
`AutoRecharge.MonthlySpend`, and `AutoRecharge.Status` (cap-reached and failed badges).

## Hooks

### usePurchase

Check purchase status and access purchase data:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
import { useSolvaPay } from '@solvapay/react'

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

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

### useTopup

Manage a credit top-up flow programmatically. Handles payment-intent creation
and Stripe initialization — the top-up analogue of `useCheckout`:

```tsx theme={null}
import { useTopup } from '@solvapay/react'

function CustomTopup() {
  const { startTopup, loading, error, stripePromise, clientSecret, reset } = useTopup({
    amount: 1000, // minor units
    currency: 'USD',
  })

  return (
    <button onClick={() => startTopup()} disabled={loading}>
      {loading ? 'Preparing…' : 'Add $10.00'}
    </button>
  )
}
```

#### useTopup Options

* `amount` (required) — top-up amount in **minor units** (e.g. `1000` = `$10.00`).
* `currency` — ISO 4217 currency code (default: `USD`).
* `autoRecharge` — optional `AutoRechargeInput` to enable auto-recharge in the same payment.

#### useTopup Return Values

* `loading` — `true` while the payment intent is being created.
* `error` — the last error, or `null`.
* `stripePromise` — the resolved Stripe instance for mounting Elements.
* `clientSecret` — the payment-intent client secret.
* `startTopup()` — creates the payment intent and initializes Stripe.
* `reset()` — clears state to start over.

### useAutoRecharge

Read and manage the customer's auto-recharge configuration. Backed by a
short-lived module cache so multiple components share one request:

```tsx theme={null}
import { useAutoRecharge } from '@solvapay/react'

function AutoRechargeStatus() {
  const { config, loading, save, disable, refresh } = useAutoRecharge()

  if (loading) return <div>Loading…</div>
  if (!config?.enabled) return <div>Auto-recharge is off</div>

  return (
    <div>
      <p>Status: {config.status}</p>
      {config.status === 'failed' && <p>Recent recharges failed — please update your card.</p>}
      <button onClick={() => disable()}>Turn off</button>
    </div>
  )
}
```

#### useAutoRecharge Return Values

* `config` — the current `AutoRechargeConfig`, or `null` if none exists.
* `loading` / `saving` / `disabling` — in-flight state flags.
* `error` — the last error, or `null`.
* `refresh(force?)` — re-fetch the config.
* `save(input)` — set or update the config. Accepts `SaveAutoRechargeInput` (see the [top-up section](#credit-top-ups--auto-recharge) for the `maxMonthlySpendMajor` cap and `deferSetupIntent`).
* `disable()` — turn auto-recharge off.

`config.status` is `active`, `disabled`, `pending_setup`, or `failed`. Read
`config.failureCount` alongside `status === 'failed'` to decide when to prompt
the customer to update their card.

## Payment Flow

### Simple Payment Flow

Use `PaymentForm` for a complete payment flow:

```tsx theme={null}
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:

```tsx theme={null}
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>
  )
}
```

## Payment Confirmation

Payment success is gated on real confirmation from Stripe — a form only reports success once the
payment intent actually succeeded (and, where the backend route is wired, once SolvaPay has
processed it). Two pieces make this work: the `confirmPayment` utility and the built-in
return-path resume.

### Payment Element is the default

`PaymentForm` and `TopupForm` render the Stripe **Payment Element** by default, which supports
cards plus redirect- and async-based payment methods (SEPA, iDEAL, and others).

<Warning>
  The Card Element surface is **deprecated**: `PaymentForm.CardElement`,
  `ConfirmPaymentMode: 'card-element'`, and `StripePaymentFormWrapper` remain as
  backwards-compatible shims (since `@solvapay/react` 1.5.0) and will be removed in the next major
  version. Migrate to the default Payment Element rendering.
</Warning>

### confirmPayment

If you build your own submit handler, use `confirmPayment` instead of calling Stripe directly. It
wraps Elements submission and confirmation, and returns a discriminated result:

```tsx theme={null}
import { confirmPayment } from '@solvapay/react'

const result = await confirmPayment({
  stripe,
  elements,
  clientSecret,
  returnUrl: window.location.href,
  copy,
})

switch (result.status) {
  case 'succeeded':
    // result.paymentIntent — safe to unlock / redirect
    break
  case 'pending':
    // Async payment method still processing (e.g. SEPA) — show
    // result.message and wait; do NOT treat as success
    break
  case 'requires_action':
  case 'other':
  case 'error':
    // Show result.message
    break
}
```

`pending` maps to Stripe's `processing` state. The built-in forms surface it as a message and keep
the customer on the form — `onSuccess` never fires from a pending payment.

### Return-path resume

Redirect-based payment methods send the customer away and back. `PaymentForm` and `TopupForm`
handle the return automatically: on mount they read the `payment_intent_client_secret` query
parameters Stripe appends to the `returnUrl`, retrieve the intent, strip the parameters from the
URL, and resume — running `handleNextAction` if 3DS is still required, showing the pending message
while processing, and firing `onSuccess` once the intent succeeded and the payment was processed.
You only need to make sure `returnUrl` points at the page that renders the form.

## Multi-Currency Plans & Top-Ups

Plans price multiple currencies by repeating money options (`charge` or `tier`) that differ only
by `currency`. Amounts are set per currency, not FX-converted. See [Plans](/plans/overview).

### Plan checkout

`PlanSelector` shows a currency switcher when the product's plans span more than one currency, and
each plan card can render its per-currency price. The selected currency flows through checkout
automatically — `useCheckout` passes it when creating the payment. For custom UIs, resolve prices
with the helpers:

```tsx theme={null}
import { getPlanPricingOptions, resolvePlanPricingOption } from '@solvapay/react'

const options = getPlanPricingOptions(plan)          // all currency options
const eur = resolvePlanPricingOption(plan, 'EUR')    // one currency, falls back to default
```

If a customer selects a currency the plan has no `pricingOptions` entry for, checkout rejects with
an explicit "currency not supported" error listing the available currencies.

### Top-up currencies

Credit top-ups use a **separate** currency list: the provider's `defaultCurrency` plus any
additional `supportedTopupCurrencies` configured in the SolvaPay Console. Plan pricing currencies
are never used for top-ups. `useCheckoutFlow` exposes `topupCurrencies`, `topupCurrency`, and
`setTopupCurrency` for building a currency picker on the amount step; pass the chosen currency to
`TopupForm.Root` / `useTopup`.

## Credit Top-Ups & Auto-Recharge

Credit-based products let customers buy a balance and spend it as they use your
API. SolvaPay supports both one-off **top-ups** and **auto-recharge**, where the
balance is refilled automatically once it drops below a threshold. All three
flows below are powered by `@solvapay/next` route helpers.

### Backend routes

Add these API routes once — the React components and hooks call them through the
provider. All are one-liners over `@solvapay/next`:

```typescript theme={null}
// app/api/create-topup-payment-intent/route.ts
import { NextRequest } from 'next/server'
import { createTopupPaymentIntent } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { amount, currency, autoRecharge } = await request.json()
  return createTopupPaymentIntent(request, { amount, currency, autoRecharge })
}
```

```typescript theme={null}
// app/api/process-topup-payment/route.ts
import { NextRequest } from 'next/server'
import { processTopupPaymentIntent } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { paymentIntentId } = await request.json()
  return processTopupPaymentIntent(request, { paymentIntentId })
}
```

```typescript theme={null}
// app/api/auto-recharge/route.ts
import { NextRequest } from 'next/server'
import { disableAutoRecharge, getAutoRecharge, saveAutoRecharge } from '@solvapay/next'

export const GET = (request: NextRequest) => getAutoRecharge(request)
export const PUT = (request: NextRequest) => saveAutoRecharge(request)
export const DELETE = (request: NextRequest) => disableAutoRecharge(request)
```

The `/api/process-topup-payment` route is what lets `TopupForm` confirm that the
credit actually landed before firing `onSuccess`. The three auto-recharge verbs
share a single route and a single provider key, `api.autoRecharge` (default
`/api/auto-recharge`).

### Standalone top-up

Render `TopupForm` with the amount in minor units. Credits are booked by the
webhook handler, and — because the `/api/process-topup-payment` route is wired —
`onSuccess` fires only once the credit has landed:

```tsx theme={null}
import { TopupForm } from '@solvapay/react'

function TopUpCredits() {
  return (
    <TopupForm.Root
      amount={2000} // $20.00
      currency="USD"
      onSuccess={(_paymentIntent, extras) => {
        console.log('Credits added:', extras?.creditsAdded)
      }}
      onError={error => console.error(error)}
    >
      <TopupForm.Loading>Preparing payment…</TopupForm.Loading>
      <TopupForm.PaymentElement />
      <TopupForm.Error />
      <TopupForm.SubmitButton>Add credits</TopupForm.SubmitButton>
    </TopupForm.Root>
  )
}
```

### Enable auto-recharge in the same payment

You can arm auto-recharge as part of the **initial** top-up charge — there's no
separate card-setup step. Render `<AutoRecharge deferCardSetup>` on the amount
step, capture the pending config, and pass it into `TopupForm.Root`:

```tsx theme={null}
import { useMemo, useState } from 'react'
import type { AutoRechargeInput } from '@solvapay/server'
import { AutoRecharge, TopupForm, configToAutoRechargeInput, useAutoRecharge } from '@solvapay/react'

function TopupWithAutoRecharge({ amount, currency = 'USD' }: { amount: number; currency?: string }) {
  const { config: savedConfig } = useAutoRecharge()
  const [pending, setPending] = useState<AutoRechargeInput | null>(null)

  // Prefer the just-edited config; otherwise reuse a saved+enabled one.
  const autoRecharge = useMemo((): AutoRechargeInput | undefined => {
    if (pending) return pending
    if (!savedConfig?.enabled) return undefined
    return configToAutoRechargeInput(savedConfig, { currency }) ?? undefined
  }, [pending, savedConfig, currency])

  return (
    <>
      <AutoRecharge currency={currency} deferCardSetup onPendingConfig={setPending} />

      <TopupForm.Root amount={amount} currency={currency} autoRecharge={autoRecharge}>
        <TopupForm.Loading>Preparing payment…</TopupForm.Loading>
        <TopupForm.PaymentElement />
        <TopupForm.Error />
        <TopupForm.SubmitButton>Add credits</TopupForm.SubmitButton>
      </TopupForm.Root>
    </>
  )
}
```

With `deferCardSetup`, saving the `AutoRecharge` form sends `deferSetupIntent: true`
(no inline SetupIntent) and fires `onPendingConfig`. The backend creates the
payment intent with `setup_future_usage: 'off_session'`, saves the card on that
same charge, and activates the config (`pending_setup` → `active`) when the
webhook lands — so the customer configures and funds auto-recharge in one step.

### Managing auto-recharge on its own

Outside checkout, drop `<AutoRecharge>` into an account or settings page. It
reads and writes through the `/api/auto-recharge` route:

```tsx theme={null}
import { AutoRecharge } from '@solvapay/react'

function BillingSettings() {
  return (
    <section>
      <h2>Automatic top-ups</h2>
      <AutoRecharge currency="USD" />
    </section>
  )
}
```

### Reacting to auto-recharge on usage

When you call `trackUsage` (via `/api/track-usage`), the response's
`creditDebit.autoRecharge.triggered` flag signals that an off-session charge was
**initiated** — it does not mean the credits are in that same response. Call
`balance.reconcileAfterUsageDebit({ expectIncrease: true })` **only** when
`triggered` is `true`; it polls the balance (\~32s grace) and counts back-to-back
recharges so the badge converges once the credit lands:

```tsx theme={null}
import { useBalance } from '@solvapay/react'

function UsageAction() {
  const { adjustBalance, reconcileAfterUsageDebit, refetch } = useBalance()

  async function runQuery() {
    const res = await fetch('/api/track-usage', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ actionType: 'api_call', units: 1, productRef: 'prd_myapi' }),
    })
    const data = await res.json()

    if (data.creditDebit?.debited) {
      adjustBalance(-data.creditDebit.amount)
      reconcileAfterUsageDebit({ expectIncrease: data.creditDebit.autoRecharge?.triggered === true })
      await refetch()
    }
  }

  return <button onClick={runQuery}>Run query</button>
}
```

<Note>
  **Configuration, limits, and pricing.** Auto-recharge uses a balance
  **threshold** plus a **fixed top-up amount**. When enabled, both must be greater
  than zero, the top-up must be at least the threshold, and both must be at most
  `10,000` major units. The top-up amount must also clear Stripe's per-currency
  minimum (for example `$0.50` for USD/EUR/CHF/CAD/AUD, `£0.30` for GBP,
  `30 kr` for SEK/NOK, `2.50 kr` for DKK), and the currency must be one of the
  provider's supported top-up currencies.

  An optional `maxMonthlySpendMajor` cap limits how much auto-recharge spend is
  allowed per UTC calendar month. Set it in the `<AutoRecharge>` form under
  **Advanced** (or pass it through `useAutoRecharge().save({ ..., maxMonthlySpendMajor })`
  and the `/api/auto-recharge` route). When the cap is hit mid-month, the config
  stays `active` and charges resume automatically next UTC month. The drop-in
  component shows a spend line (`$45 / $100 this month`) and a **Monthly spend limit
  reached** status when applicable; read `config.monthlySpendMinor` and
  `config.monthlySpendPeriod` if you build a custom UI.

  Thresholds and amounts are stored in **display-currency minor units** and
  re-resolved to credits against **live FX** at trigger time (no fixed credit
  threshold is stored). When a config has a `display` block, render its
  `display.formatted` values verbatim rather than re-deriving them from minor
  units.
</Note>

<Warning>
  **Off-session declines are surfaced explicitly.** If auto-recharge charges keep
  failing, `useAutoRecharge().config.status` flips to `failed` after repeated
  declines (and `failureCount` increments). Declines are **not** returned in the
  `trackUsage` / usage-debit response — they arrive via the
  `customer.credit.auto_topup_failed` webhook (see [Webhooks](./webhooks)). The
  drop-in `<AutoRecharge>` renders a text-only "payment failed" status; to prompt
  a fix, build your own banner off `config.status === 'failed'` and pair it with
  `<UpdatePaymentMethodButton>` so the customer can update their card and resume.
</Warning>

<Note>
  **Migrating early-adopter route keys.** The separate `api.getAutoRecharge`,
  `api.saveAutoRecharge`, and `api.disableAutoRecharge` provider keys were
  collapsed into a single `api.autoRecharge` key (default `/api/auto-recharge`).
  If you set any of the old keys, rename them to `api.autoRecharge`.
</Note>

## Purchase Management

### Check Purchase Status

```tsx theme={null}
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

```tsx theme={null}
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

```tsx theme={null}
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:

```tsx theme={null}
// 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:

```tsx theme={null}
<PaymentForm
  planRef="pln_premium"
  productRef="prd_myapi"
  className="my-custom-form"
  buttonClassName="my-custom-button"
/>
```

```css theme={null}
.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

* [Next.js Integration Guide](./nextjs) - Learn Next.js-specific integration
* [Purchase management](./purchase-management) - Cancel, reactivate, switch plans, and wire up top-ups
* [Auto-recharge](./auto-recharge) - Full auto-recharge stack: routes, server helpers, monthly spend cap
* [Business checkout (B2B)](./business-checkout) - Collect business details and tax IDs at checkout
* [Custom Authentication Adapters](./custom-auth) - Build custom auth adapters
* [Usage Events](./usage-events) - Track purchase and usage activity
* [API Reference](/sdks/typescript/intro) - Full API documentation
