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

@solopress/payments-react

v2.1.0

Published

React helpers for the Solopress Payments API: handles the next_action union once

Readme

@solopress/payments-react

React helpers for the Solopress Payments API. Handles the next_action union once, so every consumer does not reimplement it.

npm install @solopress/payments-react @solopress/payments-client

Usage

Create the intent and the client session on the server, pass the session to the browser, and let the hooks do the rest.

Cards and PayPal are separate: usePayment drives the card form, PayPalButton renders PayPal. Both take the same session and act on the same intent, so a checkout offering the two is just both on the page.

Cards

"use client";

import { usePayment } from "@solopress/payments-react";
import type { ClientSession } from "@solopress/payments-client";

export function PaymentForm({ session }: { session: ClientSession }) {
  const { phase, error, cardErrors, payWithCard } = usePayment({
    baseUrl: process.env.NEXT_PUBLIC_PAYMENTS_API_URL!,
    session,
    onSuccess: (intent) => console.log("paid", intent.id),
  });

  if (phase === "redirecting") return <p>Redirecting you to your bank…</p>;
  if (phase === "succeeded") return <p>Payment complete.</p>;

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const data = new FormData(e.currentTarget);
        payWithCard({
          cardholderName: String(data.get("name")),
          cardNumber: String(data.get("number")),
          expiryDate: String(data.get("expiry")), // MMYY
          securityCode: String(data.get("cvv")),
        });
      }}
    >
      <input name="name" autoComplete="cc-name" />
      <input name="number" autoComplete="cc-number" inputMode="numeric" />
      {cardErrors.cardNumber && <span>{cardErrors.cardNumber}</span>}
      <input name="expiry" autoComplete="cc-exp" placeholder="MMYY" />
      <input name="cvv" autoComplete="cc-csc" inputMode="numeric" />

      <button disabled={phase === "processing"}>Pay</button>
      {error && <p role="alert">{error.message}</p>}
    </form>
  );
}

PayPal

"use client";

import { PayPalButton } from "@solopress/payments-react";
import type { ClientSession } from "@solopress/payments-client";

export function PayWithPayPal({ session }: { session: ClientSession }) {
  return (
    <PayPalButton
      baseUrl={process.env.NEXT_PUBLIC_PAYMENTS_API_URL!}
      session={session}
      onSuccess={(intent) => console.log("paid", intent.id)}
      fallback={<div style={{ height: 48 }} />}
    />
  );
}

That is the whole integration. The button is PayPal's own web component, so it looks the way PayPal's branding rules require without you styling it.

| Prop | Default | Notes | |---|---|---| | type | pay | Also checkout, buynow, subscribe — changes the wording | | color | paypal-gold | Also paypal-blue, paypal-white | | presentationMode | auto | popup, modal or redirect to force one | | fallback | nothing | Rendered while the SDK loads; reserve the height or the page will jump | | whenIneligible | nothing | Rendered when PayPal declines to serve this buyer | | children | — | A function given the hook state, for a status line of your own |

For a button you render yourself, usePayPal returns the same state plus a start() to call from your own click handler. You are then responsible for PayPal's branding requirements.

What happens

  1. The buyer presses the button. The order is opened through this service, which returns an order id.
  2. PayPal takes over in a popup, a modal or a redirect.
  3. On approval the order is captured, again through this service.

The capture also happens server-side when PayPal reports the approval, so a buyer who approves and then closes the tab is still charged. The two cannot double charge. It does mean a payment can succeed after the browser has given up on it, which is one more reason fulfilment belongs on the payment.succeeded webhook rather than on anything the browser saw.

Apple Pay

import { ApplePayButton } from "@solopress/payments-react";

<ApplePayButton
  baseUrl={process.env.NEXT_PUBLIC_PAYMENTS_API_URL!}
  session={session}
  label={`Order ${intent.merchant_reference}`}
  onSuccess={(intent) => router.push(`/thanks/${intent.id}`)}
  fallback={<div style={{ height: 48 }} />}
/>

The element is Apple's own, so it meets Apple's branding rules without styling. It renders nothing where Apple Pay cannot work — any non-Apple browser, a Safari with no card in Wallet, or a vendor account without Apple Pay enabled — so whenIneligible is usually left unset and the customer simply sees your other payment methods.

Before this works, the domain showing the button must be registered. Apple validates every merchant session against the top-level page's domain, so it needs registering against the Opayo vendor account in MyOpayo and adding to the service's PAYMENTS_APPLE_PAY_DOMAINS. Inside a cross-origin iframe, register both domains, pass the parent's as domain, and put allow="payment" on the iframe.

To avoid registering anything, redirect to the payments service's hosted checkout at /v1/apple-pay/checkout instead. It runs on an already-registered domain and needs no browser code at all.

useApplePay returns the same state plus a start() for a button you render yourself. Call it synchronously from the click or Safari will refuse to open the sheet.

Google Pay

<GooglePayButton
  baseUrl="https://payments-api.example.com"
  session={session}
  label={`Order ${intent.merchant_reference}`}
  onSuccess={(intent) => router.push(`/thanks/${intent.id}`)}
/>

There is no domain registration step and no merchant validation endpoint. The client session must include providers.opayo.google_pay, which requires Elavon to enable the wallet, a gateway merchant id on the Opayo row, PAYMENTS_GOOGLE_PAY_MERCHANT_ID on the API, and the admin TUI toggle (g).

What it does for you

  • Card tokenisation. The card number goes straight from the browser to Opayo and is exchanged for a token. It never reaches your servers or the payments service, which is what holds PCI scope at SAQ-A/A-EP.
  • The 3-D Secure form post. A real form submission to the issuer, not a fetch, because the customer has to see the bank's page. Defaults to a top-level navigation; the customer returns via the intent's return_url.
  • The PayPal handoff. Loads the SDK once, checks the buyer is eligible, opens the order on click and captures it on approval — including after a redirect, which it resumes on the way back.
  • Device profiling. collectBrowserInfo() gathers the 3-D Secure 2 profile the issuer uses for risk-based authentication. A complete profile makes a silent approval more likely, so fewer customers face a challenge.

Content-Security-Policy

Tokenisation posts to Opayo from the browser, and the PayPal SDK loads from PayPal:

connect-src 'self' https://sandbox.opayo.eu.elavon.com https://live.opayo.eu.elavon.com
            https://www.paypal.com https://www.sandbox.paypal.com;
script-src  'self' https://www.paypal.com https://www.sandbox.paypal.com
            https://applepay.cdn-apple.com https://pay.google.com;
frame-src   https://www.paypal.com https://www.sandbox.paypal.com;

Apple's host is only needed for the <apple-pay-button> element. ApplePaySession itself is built into Safari, and the sheet is drawn by the operating system rather than the page, so nothing else has to be allowed for it.

Drop the sandbox hosts in production. The Opayo host is in session.providers.opayo.api_base, so it can be read rather than hard-coded.

Phases

| Phase | Meaning | |---|---| | ready | Waiting for the customer | | processing | In flight | | redirecting | The page is being handed to the issuer; render a holding message | | succeeded | Authorised | | failed | Declined or errored; error explains |

usePayPal reports its own, because the PayPal flow has a step cards do not:

| Status | Meaning | |---|---| | loading | Fetching the SDK and checking eligibility | | ready | Waiting for the customer to press the button | | paying | PayPal's window is open, or the order is being captured | | succeeded | Captured | | failed | error explains | | ineligible | PayPal will not serve this buyer; show something else |

Reading the result page

import { readPaymentResult } from "@solopress/payments-react";

const { paymentIntentId, status } = readPaymentResult();

Treat this as a hint for what to render. It comes from a query string and can be edited by anyone. Fulfilment belongs to the payment.succeeded webhook on your server, which is verified and cannot be forged.