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

@intrpay/react

v0.6.0

Published

A package for integrating Intrpay payments into your React application

Readme

@intrpay/react

A lightweight React package for embedding Intrpay UI in your own app: the payment page (as an inline embed or in a drawer), plus token-authenticated components for a signed-in customer's payment methods and invoices.

Installation

npm install @intrpay/react

Usage

Inline embed

Use PaymentEmbed to render the payment form directly in your page:

import { PaymentEmbed } from '@intrpay/react';

function CheckoutPage() {
  return (
    <div>
      <h1>Complete your payment</h1>
      <PaymentEmbed
        paymentLinkId="your-payment-link-id"
        params={{
          amount: 99.99,
          firstName: 'John',
          lastName: 'Doe',
          email: '[email protected]',
        }}
        height={500}
        onSuccess={(data) => console.log('Payment successful!', data)}
        onFailure={(data) => console.log('Payment failed.', data)}
      />
    </div>
  );
}

Auto height

Pass height="auto" to size the iframe to the payment page's content. The page reports its height over postMessage (intrpay:resize), so the embed grows and shrinks with the form and never shows an internal scrollbar:

<PaymentEmbed paymentLinkId="your-payment-link-id" height="auto" />

Until the first height report arrives (or with an older deployed payment page that does not send one), the iframe falls back to the default 400px.

Drawer

Use PaymentDrawer to show the payment form in a slide-out drawer:

import { useState } from 'react';
import { PaymentDrawer } from '@intrpay/react';

function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Payment</button>

      <PaymentDrawer
        open={open}
        onClose={() => setOpen(false)}
        paymentLinkId="your-payment-link-id"
        params={{
          amount: 99.99,
          firstName: 'John',
          lastName: 'Doe',
          email: '[email protected]',
        }}
        onSuccess={(data) => console.log('Payment successful!', data)}
        onFailure={(data) => console.log('Payment failed.', data)}
      />
    </>
  );
}

Fixed (locked) amount

Pass lockAmount: true together with amount to prevent the payer from editing the amount:

<PaymentEmbed paymentLinkId="your-payment-link-id" params={{ amount: 250, lockAmount: true }} />

This disables the amount input on the payment page while the value is still submitted and validated. It is a UI lock; server-side rules (minimum amount, invoice totals) always apply on top.

Controlling payment methods

You can narrow which payment methods are shown using an allow-list, a hide-list, or both:

// Show only ACH (even if the link also enables card):
<PaymentEmbed paymentLinkId="..." params={{ paymentMethods: ["ach"] }} />

// Hide card, keep everything else the link enables:
<PaymentEmbed paymentLinkId="..." params={{ hidePaymentMethods: ["card"] }} />

Narrowing precedence: the payment link's enabled methods (backend) -> paymentMethods (allow-list) -> hidePaymentMethods (hide-list) -> optional paymentMethod (lock to one). The package can only narrow the set of methods the payment link enables - it can never enable a method the link does not support. If narrowing would remove every method, the filters are ignored and the link's enabled methods are shown.

Embedded components (checkout, payment methods & invoices)

Checkout, AddPaymentMethod, InvoiceList, and InvoiceDetail show data belonging to one of your contacts, so unlike PaymentEmbed they need an embed session token. Mint it from your backend with @intrpay/node (or POST /embed/session) and pass it to the component. The token is short-lived and scoped to a single contact; your API key stays on the server.

// Your backend
const { token } = await intrpay.embed.sessions.create({
  contactId,
  scope: ['payment_methods:write:contact', 'invoices:read:contact'],
  ttlSeconds: 900,
});

Checkout

One card, one action - either collecting a bill the customer already owes, or selling them a cart. They pay with a saved method, a new card, a linked bank account, or Apple/Google Pay, and there is no separate "save this method" step, because the method they pay with is kept on file so renewals and later invoices can reuse it.

Pass invoiceId to collect an invoice. This needs a token with invoices:pay:contact:

import { Checkout } from '@intrpay/react';

<Checkout
  token={token}
  invoiceId={invoiceId}
  onSuccess={(payment) => {
    // `pending` means an ACH debit is still clearing and can still be
    // returned. Wait for `approved` before handing over anything you cannot
    // take back.
    if (payment.status === 'approved') fulfil(payment.invoiceId);
  }}
/>;

Wallet buttons appear by themselves when your account is set up for them and the customer's device offers them; there is nothing to enable here.

Prefer Checkout over InvoiceDetail when the customer has already decided to pay. InvoiceDetail is a document viewer that can also take a payment - it shows line items, dates and your terms. Checkout is only the transaction.

Checkout without an invoice: selling a cart

Leave invoiceId out and Checkout pays the checkout session its token carries. Your backend builds the cart out of products in your catalog; the browser is handed a token and nothing else:

// Your backend
const checkout = await intrpay.checkout.sessions.create({
  contactId,
  title: 'Pro plan + onboarding',
  items: [
    { productId: 'prod_pro_plan' },
    // An add-on the customer can tick, and buy more than one of.
    { productId: 'prod_onboarding', optional: true },
  ],
});
res.json({ token: checkout.token });
<Checkout token={token} onSuccess={(sale) => provision(sale.checkoutSessionId)} />

There is no checkoutSessionId prop, and no scope to request: the token is the cart. That is what makes selling different from collecting - a token minted so a customer can settle a bill should not also be able to transact against your catalog, so that power only ever comes attached to one specific cart.

Prices, quantities and tax are frozen when the session is created, so nothing about the total is decided in the browser. The invoice is raised from the charge rather than before it, which is why an abandoned cart or a declined card leaves no unpaid invoice behind.

onSuccess is a union. A sale carries checkoutSessionId, invoiceNumber, and an invoiceId that is null in the rare case where the charge landed but the invoice could not be written (the money is still captured):

onSuccess={(payment) => {
  if (payment.checkoutSessionId) provision(payment.checkoutSessionId);
  else markInvoicePaid(payment.invoiceId);
}}

Treat that callback as a hint, not proof. Read the outcome back from your backend with intrpay.checkout.sessions.get(id) before you fulfil anything: it is the only account of the sale that a closed tab or a wallet payment finishing without a browser cannot lose.

One limitation worth knowing: no promo codes. The cart's total is fixed by your backend, so a discount the embed could not verify would put a figure on the Pay button that is not what gets charged. A checkout that needs codes belongs on a hosted Pay link.

AddPaymentMethod

Collects a card or bank account and saves it to the contact. Requires the payment_methods:write:contact scope.

import { AddPaymentMethod } from '@intrpay/react';

<AddPaymentMethod token={token} onSuccess={({ id }) => console.log('saved', id)} />;

InvoiceList and InvoiceDetail

InvoiceList renders the contact's invoices. Requires invoices:read:contact. Passing onSelect makes the rows clickable, which pairs with InvoiceDetail to build a master/detail view:

import { useState } from 'react';
import { InvoiceDetail, InvoiceList } from '@intrpay/react';

function Billing({ token }: { token: string }) {
  const [invoiceId, setInvoiceId] = useState<string | null>(null);

  if (invoiceId) {
    return <InvoiceDetail token={token} invoiceId={invoiceId} />;
  }
  return <InvoiceList token={token} onSelect={(s) => setInvoiceId(s.invoiceId)} />;
}

InvoiceDetail only renders invoices belonging to the token's contact; anything else returns not-found rather than leaking another customer's invoice.

Without onSelect, clicking an invoice number opens a built-in detail drawer with a Pay button (onPaid fires after a payment). That drawer is rendered on document.body as a second iframe, so host CSS (transform, sticky cards, contain) cannot confine it. Body scroll is locked while it is open. Nothing is required on your side.

Pass refreshKey (a string or number) and change it when something outside the list should refetch, for example after you create an invoice. The first value is ignored; later changes post intrpay:refresh so the iframe keeps its current rows while it reloads, instead of remounting.

Appearance

params.appearance is how you theme the iframe. It cannot inherit your CSS.

<Checkout
  token={token}
  params={{
    appearance: {
      primaryColor: '#000076',
      textColor: '#0b1226',
      backgroundColor: '#ffffff',
      mode: 'light',
      // `flat` (default): paper card, ink text, primary as the Pay button.
      // `gradient`: branded wash, same look as a hosted Pay link.
      background: 'flat',
      // `fill`: drop the card's padding, border, radius and shadow so the
      // form runs edge to edge inside your own drawer or modal.
      layout: 'fill',
    },
  }}
/>

Shared props

All of them accept params (including appearance: primaryColor, secondaryColor, textColor, backgroundColor, background, layout, mode), onError, height (default "auto"), width, className, and style.

| Component | Required props | Scope needed | | ------------------ | -------------------- | -------------------------------------------------------- | | Checkout (bill) | token, invoiceId | invoices:pay:contact | | Checkout (cart) | token | none - the token comes from the checkout session | | AddPaymentMethod | token | payment_methods:write:contact | | InvoiceList | token | invoices:read:contact | | InvoiceDetail | token, invoiceId | invoices:read:contact and invoices:pay:contact (Pay) |

Leave height on "auto" for Checkout. It grows - when the customer switches to bank transfer, or ticks an add-on - and a fixed height will clip it.

How it works (communication)

The components render the Intrpay payment page (pay.intrpay.us) in an iframe and configure it over a postMessage handshake - configuration is not placed in the iframe URL, so it cannot be tampered with by editing query params:

  1. The iframe loads with a minimal URL (/pay/<paymentLinkId>).
  2. The payment page posts intrpay:ready to the host page.
  3. The package replies with intrpay:config containing your params, targeted at the pay origin only.
  4. Payment results come back as payment_success / payment_failure messages, delivered to your onSuccess / onFailure callbacks. Both sides validate the message origin and source window.

URL query params remain supported as a fallback for raw-URL embeds (e.g. linking directly to pay.intrpay.us/pay/<id>?amount=50), but postMessage values win when both are present.

What you can and cannot control

Package-controlled (display & prefill): title, description, primaryColor, secondaryColor, textColor, logo, buttonText, showHeader, showContactForm, contact prefill (firstName, lastName, email, phone, companyName), amount (+ lockAmount).

Backend-enforced (the package can only narrow, never expand): which payment methods are enabled, minimum amount, partial payments, processing fees, gateway settings, link type, invoice totals. For example, passing paymentMethods: ["ach"] on a link that does not enable ACH will not show ACH.

Props

PaymentEmbedProps

| Prop | Type | Default | Description | | --------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------- | | paymentLinkId | string | required | Payment link ID | | params | PaymentParams | - | Payment parameters (see below) | | onSuccess | (data: unknown) => void | - | Callback when payment succeeds; receives iframe message | | onFailure | (data: unknown) => void | - | Callback when payment fails; receives iframe message | | height | string \| number | 400 | Height of the iframe (e.g. 400, "100%", or "auto" to track the page's content height) | | width | string \| number | "100%" | Width of the iframe | | className | string | - | Optional CSS class for the wrapper div | | style | CSSProperties | - | Optional inline styles for the wrapper div |

PaymentDrawerProps

| Prop | Type | Default | Description | | --------------------- | ------------------------- | --------- | --------------------------------------------------------------------- | | open | boolean | required | Whether the drawer is open | | onClose | () => void | required | Callback when the drawer should close | | paymentLinkId | string | required | Payment link ID | | params | PaymentParams | - | Payment parameters (see below) | | width | string \| number | 550 | Width of the drawer | | position | 'left' \| 'right' | 'right' | Position of the drawer | | showOverlay | boolean | true | Whether to show overlay backdrop | | onSuccess | (data: unknown) => void | - | Callback when payment succeeds; receives message data from the iframe | | onFailure | (data: unknown) => void | - | Callback when payment fails; receives message data from the iframe | | closeOnOverlayClick | boolean | true | Whether clicking overlay closes the drawer | | zIndex | number | 2000 | Z-index for the drawer |

PaymentParams

| Param | Type | Description | | ---------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | amount | number | Payment amount (prefill; editable unless lockAmount is set) | | lockAmount | boolean | Lock the amount input so the payer cannot edit it (requires amount) | | maxAmount | number | Maximum payable amount (UI cap on the editable amount; server limits still apply) | | firstName | string | Customer's first name | | lastName | string | Customer's last name | | email | string | Customer's email | | phone | string | Customer's phone number | | companyName | string | Company name | | showContactForm | boolean | Whether to show the contact form | | checkoutItems | string[] | Checkout item IDs (filtered against the link's configured items) | | allowPartialPayments | boolean | Whether to allow partial payment (only honored when the payment link allows it) | | title | string | Page title shown above the payment form | | description | string | Payment description | | primaryColor | string | Primary theme color (hex, e.g. '#000076') | | secondaryColor | string | Secondary theme color (hex, e.g. '#4774c0') | | textColor | string | Text color (hex, e.g. '#f3fcff') | | logo | string | Logo URL | | buttonText | string | Custom button text | | showHeader | boolean | Whether to show the header (default false for embeds) | | paymentMethod | 'card' \| 'daf' \| 'ojc' \| 'pledger' \| 'donorsFund' \| 'matbia' | Lock to a specific payment method (must be enabled on the link) | | paymentMethods | ('card' \| 'ach' \| 'daf')[] | Allow-list: show only these methods (intersected with the link's enabled methods) | | hidePaymentMethods | ('card' \| 'ach' \| 'daf')[] | Hide-list: hide these methods (applied after paymentMethods) |

License

ISC