npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@solvapay/react

v1.2.0

Published

Headless React components and hooks for SolvaPay payment integration with Stripe.

Readme

@solvapay/react

Headless React components and hooks for SolvaPay payment integration with Stripe.

Install

pnpm add @solvapay/react

Peer Dependencies

  • react ^18.2.0 || ^19.0.0
  • react-dom ^18.2.0 || ^19.0.0

Quick Start

Zero-Config Usage (Recommended)

import { SolvaPayProvider, PaymentForm, usePurchase } from '@solvapay/react'

export default function App() {
  return (
    <SolvaPayProvider>
      <CheckoutPage />
    </SolvaPayProvider>
  )
}

By default, SolvaPayProvider uses:

  • /api/check-purchase for purchase checks
  • /api/create-payment-intent for payment creation
  • /api/process-payment for payment processing
  • /api/merchant for merchant identity (useMerchant, MandateText)
  • /api/get-product for single-product lookup (useProduct, CheckoutSummary)

Golden path — <CheckoutLayout> one-liner

<CheckoutLayout> handles the full flow: plan selection, payment (paid/free), and usage-based activation. For 90% of integrations this is the only component you need.

import { SolvaPayProvider, CheckoutLayout } from '@solvapay/react'

export function BuyNow({ email }: { email: string }) {
  return (
    <SolvaPayProvider>
      <CheckoutLayout
        productRef="prd_myapi"
        prefillCustomer={{ email }}
        requireTermsAcceptance
        onResult={result => {
          // result.kind === 'paid' | 'activated'
        }}
      />
    </SolvaPayProvider>
  )
}

What happens based on what <CheckoutLayout> resolves:

| Product shape | Flow | |---|---| | One active plan | Auto-skip selection → pay or activate | | Multiple plans | Styled <PlanSelector> → pay or activate | | Free plan (requiresPayment: false) | Skip Stripe entirely → useActivation | | Usage-based plan | <ActivationFlow> (summary → top-up → retry → activated) |

Pass planRef explicitly to skip the selector and keep today's payment-only behavior byte-for-byte (backwards compatible with pre-selector integrations).

Skipping plan selection

<CheckoutLayout
  planRef="pln_premium"
  productRef="prd_myapi"
  prefillCustomer={{ email }}
  requireTermsAcceptance
  size="auto"
  onSuccess={() => console.log('paid')}
/>

The section below ("Drop-in checkout with <CheckoutLayout>") is the original single-plan form — still supported, still works the same.

Drop-in checkout with <CheckoutLayout>

For a fixed plan checkout (summary, SCA mandate, Stripe PaymentElement, prefilled customer echo, optional terms checkbox):

import { SolvaPayProvider, CheckoutLayout } from '@solvapay/react'

export function BuyNow({ email }: { email: string }) {
  return (
    <SolvaPayProvider>
      <CheckoutLayout
        planRef="pln_premium"
        productRef="prd_myapi"
        prefillCustomer={{ email }}
        requireTermsAcceptance
        size="auto"
        onSuccess={() => console.log('paid')}
      />
    </SolvaPayProvider>
  )
}

size="auto" uses a ResizeObserver, so the same component reflows cleanly in chat bubbles, phone viewports, and desktop iframes.

Composition: slot subcomponents

When you need custom layout, compose <PaymentForm> with slot children:

<PaymentForm planRef="pln_premium" productRef="prd_myapi" prefillCustomer={{ email }}>
  <PaymentForm.Summary />
  <PaymentForm.CustomerFields />
  <PaymentForm.PaymentElement />
  <PaymentForm.MandateText />
  <PaymentForm.TermsCheckbox />
  <PaymentForm.SubmitButton />
</PaymentForm>

Passing no children keeps the current default tree for backwards compatibility.

Localization

English ships by default. Swap the locale (which also flows through to Stripe Elements) and override any strings you like:

import { SolvaPayProvider, CheckoutLayout, type PartialSolvaPayCopy } from '@solvapay/react'

const svSECopy: PartialSolvaPayCopy = {
  cta: { subscribe: 'Prenumerera', processing: 'Bearbetar...' },
  terms: { checkboxLabel: 'Jag godkänner villkoren och integritetspolicyn' },
}

<SolvaPayProvider config={{ locale: 'sv-SE', copy: svSECopy }}>
  <CheckoutLayout planRef="..." productRef="..." />
</SolvaPayProvider>

Currency formatting is already locale-correct — formatPrice uses Intl.NumberFormat with the provider locale and the plan's currency field.

Custom API Routes

import { SolvaPayProvider, PaymentForm } from '@solvapay/react'

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

With Supabase Authentication

import { SolvaPayProvider } from '@solvapay/react'
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase'
import { supabase } from './lib/supabase'

export default function App() {
  const adapter = createSupabaseAuthAdapter({ client: supabase })

  return (
    <SolvaPayProvider config={{ auth: { adapter } }}>
      <CheckoutPage />
    </SolvaPayProvider>
  )
}

Fully Custom Implementation

Every data-access call flows through config.transport. Build a custom one by spreading createHttpTransport(config) and overriding the methods you need:

import { SolvaPayProvider, createHttpTransport } from '@solvapay/react'

export default function App() {
  const transport = {
    ...createHttpTransport(undefined),
    createPayment: async ({ planRef, productRef }) => {
      const res = await fetch('/api/custom/payment', {
        method: 'POST',
        body: JSON.stringify({ planRef, productRef }),
      })
      if (!res.ok) throw new Error('Failed to create payment')
      return res.json()
    },
    checkPurchase: async () => {
      const res = await fetch('/api/custom/purchase')
      if (!res.ok) throw new Error('Failed to check purchase')
      return res.json()
    },
  }

  return (
    <SolvaPayProvider config={{ transport }}>
      <CheckoutPage />
    </SolvaPayProvider>
  )
}

MCP App

For React trees hosted inside an MCP App (where Stripe.js and direct HTTP to your backend are both blocked by the host sandbox), use the MCP subpath:

import { App } from '@modelcontextprotocol/ext-apps'
import { SolvaPayProvider } from '@solvapay/react'
import { createMcpAppAdapter } from '@solvapay/react/mcp'

const app = new App({ name: 'solvapay', version: '1.0.0' })
const transport = createMcpAppAdapter(app)

export default function Root() {
  return (
    <SolvaPayProvider config={{ transport }}>
      <CheckoutPage />
    </SolvaPayProvider>
  )
}

The MCP server is expected to expose tools whose names match MCP_TOOL_NAMES from @solvapay/react/mcp — each transport method maps 1:1 to a tool call.

Managing plans in an MCP App

Once a customer is paid, drop <CurrentPlanCard /> into the UI and the SDK does the rest — plan name, price, next-billing / expiry line, payment-method summary (via get_payment_method), Update-card and Cancel-plan actions. The card returns null when there's no active purchase, so you can render it unconditionally:

import { CurrentPlanCard } from '@solvapay/react'

function Account() {
  return <CurrentPlanCard />
}

Behind the scenes:

  • Plan metadata comes from usePurchase (provider state, no extra fetch)
  • Payment-method line comes from usePaymentMethodtransport.getPaymentMethod()
  • <UpdatePaymentMethodButton> and <LaunchCustomerPortalButton> render enabled from the first paint. They share a single in-flight transport.createCustomerSession() fetch across every instance under the same provider (de-duped via useCustomerSessionUrl), so two buttons on the same surface only round-trip once. When the URL has resolved, click is a synchronous <a target="_blank"> navigation — MCP host sandboxes permit direct anchor clicks. If the user clicks before the URL resolves, the handler awaits the in-flight promise and falls back to window.open (works on hosts that don't sandbox scripted opens, e.g. ChatGPT).
  • <CancelPlanButton> reuses Phase 1 behaviour — no new plumbing.

If you want the bare portal-launch button on its own (e.g. in a top nav), use <LaunchCustomerPortalButton /> directly.

<PlanSwitcher> (plan upgrade/downgrade with proration) and <PaymentMethodForm> (inline Stripe Elements card update) are deferred to follow-up PRs — see the plan in the SDK repo for scope. For plan changes today, MCP apps use <CancelPlanButton> + <CurrentPlanCard>

  • a fresh hosted checkout flow.

Building MCP Apps

@solvapay/react/mcp ships a turnkey compound plus four composable primitives for SolvaPay MCP Apps built on @modelcontextprotocol/ext-apps.

Quick start — <McpApp>

import { createRoot } from 'react-dom/client'
import { App } from '@modelcontextprotocol/ext-apps'
import { McpApp } from '@solvapay/react/mcp'
import '@solvapay/react/styles.css'
import '@solvapay/react/mcp/styles.css'

const app = new App({ name: 'my-mcp-app', version: '1.0.0' })
createRoot(document.getElementById('root')!).render(<McpApp app={app} />)

<McpApp> handles app.connect(), calls the open_* tool matching the host's invocation context, mounts <SolvaPayProvider> with createMcpAppAdapter(app) + createMcpFetch(transport), and routes to the correct per-view primitive.

Pass applyContext to wire host theme / fonts / safe-area insets from the ext-apps helpers, override individual screens via views, and tweak per-slot styling via classNames:

<McpApp
  app={app}
  applyContext={ctx => {
    if (ctx?.theme) applyDocumentTheme(ctx.theme)
  }}
  views={{
    account: MyCustomAccountScreen, // optional — one-off override
  }}
  classNames={{ card: 'my-card', button: 'my-btn' }}
  onInitError={err => console.error('[mcp]', err)}
/>

See examples/mcp-checkout-app for the full host integration including the server-side open_* tool + UI resource registration.

Per-view primitives

When you need a custom shell — your own provider mount, additional routes, a bespoke layout — compose the pieces directly:

import {
  AppHeader,
  createMcpAppAdapter,
  createMcpFetch,
  fetchMcpBootstrap,
  McpCheckoutView,
  McpAccountView,
  McpTopupView,
  McpViewRouter,
  useStripeProbe,
} from '@solvapay/react/mcp'

Every view accepts a classNames?: McpViewClassNames partial. Props are typed per-view (McpCheckoutViewProps, McpAccountViewProps, McpTopupViewProps). The previous McpPaywallView / McpNudgeView / McpUpsellStrip surfaces were removed with the text-only paywall refactor — merchant paywall / nudge responses narrate in content[0].text and don't open the widget iframe.

<McpAccountView> is opinionated about the host iframe: it renders <CurrentPlanCard hideUpdatePaymentButton hideCancelButton /> and collapses card updates and cancellation into a single Manage account <LaunchCustomerPortalButton>, paired with a one-line hint pointing users at it. The portal's confirm/cancel flow is reliable inside MCP host sandboxes; the inline <CancelPlanButton> in the card is not (see issue tracker for the planned fix).

<AppHeader> — host-aware merchant strip

Every built-in view renders <AppHeader /> at its surface root: a compact [icon] Merchant-Name row at the top of the card. When you compose your own view (via <McpViewRouter> or <SolvaPayProvider> directly) drop it in as the first child to keep branding consistent.

import { AppHeader } from '@solvapay/react/mcp'

function MyView() {
  return (
    <div className="solvapay-mcp-card">
      <AppHeader />
      <h2>Your custom step</h2>
      {/* ... */}
    </div>
  )
}

<AppHeader> is host-aware. In mode="auto" (the default) it suppresses itself on hosts that already paint a merchant mark in their chrome:

  • ChatGPT, whose Apps SDK UI guidelines explicitly prohibit in-widget logos ("ChatGPT will always append your logo and app name before the widget is rendered.").
  • Claude Desktop, which paints its own MCP app chrome strip (app icon + app name + active tool name) above every widget iframe — a second in-widget merchant row stacks on top of it.

On MCP Jam, VS Code, and other hosts that leave in-widget branding to the app, <AppHeader> paints the strip so the user always sees who they're dealing with.

<AppHeader mode="auto" />    // default — host-aware
<AppHeader mode="always" />  // force render (e.g. testing)
<AppHeader mode="never" />   // force hide (e.g. custom chrome)

Use classNames={{ appHeader, appHeaderIcon, appHeaderInitials, appHeaderName }} to restyle any slot, or pass children for inline content on the right side (e.g. a close affordance or status chip). The merchant is read from useMerchant() automatically.

Integrators building entirely custom shells can also read the raw host name via useHostName():

import { useHostName, HOSTS_WITH_MERCHANT_CHROME } from '@solvapay/react/mcp'

const hostName = useHostName() // 'ChatGPT' | 'Claude Desktop' | 'MCP Jam' | null
const showMark = !hostName || !HOSTS_WITH_MERCHANT_CHROME.test(hostName)

Architecture ADR

See docs/mcp-app-architecture.md for the hybrid-API decision record and the three primitive seam fixes (AmountPicker emit="minor" / selector={…}, LaunchCustomerPortalButton asChild) that landed with this lift.

Components

SolvaPayProvider

Headless context provider that manages purchase state, payment methods, and customer references.

Features:

  • Zero-config with sensible defaults
  • Auto-fetches purchases on mount
  • Built-in localStorage caching with user validation
  • Supports auth adapters for extracting user IDs and tokens
  • Customizable API routes via config

Props:

  • config?: SolvaPayConfig - Configuration object (optional)
    • config.api? - Custom API route paths
    • config.auth? - Auth adapter configuration
  • createPayment?: (params: { planRef: string; productRef?: string }) => Promise<PaymentIntentResult> - Custom payment creation function (optional, overrides config)
  • checkPurchase?: (customerRef: string) => Promise<CustomerPurchaseData> - Custom purchase check function (optional, overrides config)
  • processPayment?: (params: { paymentIntentId: string; productRef: string; planRef?: string }) => Promise<ProcessPaymentResult> - Custom payment processing function (optional)
  • children: React.ReactNode - Child components

Config Options:

interface SolvaPayConfig {
  api?: {
    checkPurchase?: string // Default: '/api/check-purchase'
    createPayment?: string // Default: '/api/create-payment-intent'
    processPayment?: string // Default: '/api/process-payment'
  }
  auth?: {
    adapter?: AuthAdapter // Auth adapter for extracting user ID/token
    getToken?: () => Promise<string | null> // Deprecated: use adapter
    getUserId?: () => Promise<string | null> // Deprecated: use adapter
  }
}

PricingSelector

Component for selecting and displaying available pricing options.

Props:

  • productRef?: string - Product reference to filter pricing options
  • fetcher?: (productRef: string) => Promise<Plan[]> - Custom fetcher function
  • onPlanSelect?: (plan: Plan) => void - Callback when option is selected
  • renderPlan?: (plan: Plan) => React.ReactNode - Custom option renderer
  • className?: string - Container className

Example:

import { PricingSelector, usePlans } from '@solvapay/react'

function PricingPage() {
  const { plans, loading } = usePlans({ productRef: 'my-product' })

  return (
    <div>
      {loading ? 'Loading...' : plans.map(plan => <div key={plan.reference}>{plan.price}/{plan.interval}</div>)}
    </div>
  )
}

PaymentForm

Payment form component using Stripe PaymentElement. Automatically handles Stripe Elements provider setup.

Props:

  • planRef: string - Plan reference for the payment
  • productRef?: string - Optional product reference
  • onSuccess?: (paymentIntent: PaymentIntent) => void - Callback on successful payment
  • onError?: (error: Error) => void - Callback on payment error
  • returnUrl?: string - Return URL after payment
  • submitButtonText?: string - Submit button text (default: "Pay Now")
  • formClassName?: string - Form element className
  • messageClassName?: string - Message container className
  • buttonClassName?: string - Submit button className

Example:

import { PaymentForm } from '@solvapay/react'

function CheckoutPage() {
  return (
    <PaymentForm
      planRef="pln_YOUR_PLAN"
      productRef="prd_YOUR_PRODUCT"
      onSuccess={() => console.log('Payment successful!')}
    />
  )
}

ProductBadge

Displays current product subscription with render props or className pattern.

Props:

  • children?: (props) => React.ReactNode - Render prop function
  • as?: React.ElementType - Component to render (default: "div")
  • className?: string | ((props) => string) - ClassName or function

Example:

<ProductBadge className="badge badge-primary" />

PurchaseGate

Controls access to content based on purchase status.

Props:

  • requireProduct?: string - Optional product name to check for an active purchase
  • children: (props) => React.ReactNode - Render prop function

Example:

<PurchaseGate requireProduct="Pro Plan">
  {({ hasAccess, loading, purchases }) => {
    if (loading) return <Loading />
    if (!hasAccess) return <Paywall />
    return <PremiumContent />
  }}
</PurchaseGate>

Hooks

usePurchase

Access purchase status, active purchases, and helper functions.

const {
  purchases, // Array of all purchases (raw — plans + balance transactions)
  balanceTransactions, // Credit top-ups and other non-plan rows
  loading, // Loading state
  hasPaidPurchase, // Boolean: has any paid plan purchase
  activePurchase, // Most recent active plan purchase
  refetch, // Function to refetch purchases
} = usePurchase()

Plans vs balance

Plans and balance credits are orthogonal. A customer can hold at most one active plan and any number of balance transactions (credit top-ups today; gift credits, referral bonuses, refunds tomorrow).

Every plan-shaped accessor — activePurchase, activePaidPurchase, hasPaidPurchase, hasProduct, plus cancelledPurchase / shouldShowCancelledNotice on usePurchaseStatus — filters out balance transactions. Top-ups surface on balanceTransactions, and the aggregate credit balance on useBalance().

Classification is structural (isPlanPurchase / isTopupPurchase): a purchase with no planSnapshot was never a plan. metadata.purpose === 'credit_topup' acts as a defense-in-depth guard.

import { isPlanPurchase, isTopupPurchase } from '@solvapay/react'

const plans = purchases.filter(isPlanPurchase)
const topups = purchases.filter(isTopupPurchase)

If you previously relied on top-ups surfacing on activePurchase (e.g. "show the most recent transaction"), read from purchases or balanceTransactions directly.

usePlans

Fetch and manage available plans.

const {
  plans, // Array of available plans
  loading, // Loading state
  error, // Error object if fetch failed
  refetch, // Function to refetch plans
} = usePlans({
  productRef: 'my-product', // Optional product reference
  fetcher: customFetcher, // Optional custom fetcher function
})

usePurchaseStatus

Advanced purchase status helpers.

const {
  cancelledPurchase, // Most recent cancelled purchase
  shouldShowCancelledNotice, // Boolean: should show cancellation notice
  formatDate, // Helper to format dates
  getDaysUntilExpiration, // Helper to get days until expiration
} = usePurchaseStatus()

useCheckout

Manage checkout flow for a specific plan.

const { loading, error, startCheckout, reset } = useCheckout('plan_ref')

useSolvaPay

Access SolvaPay context directly.

const {
  purchaseData, // Full purchase data
  loading, // Loading state
  createPayment, // Payment creation function
  processPayment, // Payment processing function
  customerRef, // Current customer reference
  updateCustomerRef, // Function to update customer reference
} = useSolvaPay()

TypeScript

All components and hooks are fully typed. Import types as needed:

import type {
  PaymentFormProps,
  PurchaseStatus,
  PaymentIntentResult,
  CheckoutResult,
  PaymentResult,
  ActivationResult,
} from '@solvapay/react'

Plan lifecycle

Beyond checkout itself, three thin styled-default components cover the post-purchase experience without requiring custom UI.

<CancelPlanButton>

Wraps usePurchaseActions.cancelRenewal with a built-in confirm dialog, loading state, and plan-type-aware copy (subscription vs usage-based). Auto-reads the active purchase from usePurchase().

import { CancelPlanButton } from '@solvapay/react'

<CancelPlanButton onCancelled={() => router.push('/')} />

Use confirm={false} for a single-click cancel, or pass a string to override the default copy. The render-prop form exposes { cancel, isCancelling, disabled, purchase } for fully custom UI.

<CancelledPlanNotice>

Surfaces automatically when the customer has a cancelled-but-still-active purchase. Renders the expiration date, days remaining, cancellation reason, and a reactivate CTA. Renders nothing when there's nothing to show.

import { CancelledPlanNotice } from '@solvapay/react'

<CancelledPlanNotice onReactivated={() => refetch()} />

<CreditGate>

Companion to <PurchaseGate> for usage-based flows. Blocks access when the customer's credit balance falls below a threshold; renders an embedded <TopupForm> by default.

import { CreditGate } from '@solvapay/react'

<CreditGate minCredits={10}>
  <ExpensiveFeature />
</CreditGate>

Customize via the fallback prop, or use the render-prop form (children: ({ balance, hasCredits, topup }) => …) for fully custom UI.

Server-side usage tracking

There is intentionally no useTrackUsage() client hook. Client-reported usage is trivially gamed — a user can simply block the fetch to conserve credits. Instead, record usage from your server when the expensive work actually runs:

// app/api/do-thing/route.ts
import { trackUsage } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const result = await doTheExpensiveThing()
  await trackUsage(request, { units: 1 })
  return NextResponse.json(result)
}

The same pattern works from a Supabase Edge Function — see below.

Using with Supabase Edge Functions

Lovable-style apps deploy React frontends against Supabase Edge Functions (Deno runtime). The SDK's api URL overrides let the exact same <CheckoutLayout> drop-in work against /functions/v1/* routes:

<SolvaPayProvider
  config={{
    api: {
      checkPurchase: `${SUPABASE_URL}/functions/v1/check-purchase`,
      createPayment: `${SUPABASE_URL}/functions/v1/create-payment-intent`,
      processPayment: `${SUPABASE_URL}/functions/v1/process-payment`,
      listPlans: `${SUPABASE_URL}/functions/v1/list-plans`,
      getMerchant: `${SUPABASE_URL}/functions/v1/get-merchant`,
      getProduct: `${SUPABASE_URL}/functions/v1/get-product`,
      // …same pattern for every other endpoint
    },
    auth: {
      adapter: createSupabaseAuthAdapter({ client: supabase }),
    },
  }}
>
  <CheckoutLayout
    productRef="prd_myapi"
    prefillCustomer={{ email }}
    requireTermsAcceptance
  />
</SolvaPayProvider>

The matching Deno edge functions ship on the @solvapay/server/fetch subpath — see examples/supabase-edge for one-liner Deno.serve(handler) files covering every handler including get-merchant and get-product.

More Information

See docs/contributing/architecture.md for contributor architecture documentation.