neonFin
Components

PurchaseButton and PurchaseDialog

Show available prices and start popup or redirect checkout.

PurchaseDialog loads your neonFin catalog, lists purchasable offers - credit packs, subscription tiers, and one-time unlocks - and starts provider checkout. PurchaseButton is a convenience wrapper that opens the dialog.

By default checkout uses flow="auto": desktop users pay in a popup while your page stays mounted, and touch/mobile users use a normal redirect. Pass flow="redirect" if your app does not want popup checkout.

Stripe test card: 4242 4242 4242 4242

import { PurchaseButton } from "@/components/neonfin/purchase-dialog";

export function BuyCredits() {
  return <PurchaseButton>Buy credits</PurchaseButton>;
}

Each offer shows its tier label, included credits, and the features it unlocks. If the wallet already subscribes to a product shown here, its tiers are marked Current plan and a Manage subscription button opens the billing portal - because a fresh checkout can't switch an existing subscription (the API returns 409 already_subscribed, which the dialog handles for you).

Use filters to scope the dialog to the relevant offers: credit packs for one product, plans that unlock a feature, or your own predicate.

Install

npx shadcn@latest add https://pay.vantezzen.io/r/neonfin-purchase.json

PurchaseButton

import { PurchaseButton } from "@/components/neonfin/purchase-dialog";

export function EmptyState() {
  return <PurchaseButton>Buy credits</PurchaseButton>;
}

PurchaseButton accepts regular shadcn Button props:

<PurchaseButton variant="outline" size="sm" productId="prod_images">
  Add image credits
</PurchaseButton>

Filter to prices that unlock a feature:

<PurchaseButton filters={{ features: ["analytics"] }}>
  Unlock analytics
</PurchaseButton>

Filter to credit-granting prices for a product:

<PurchaseButton
  filters={{ productId: "prod_processing", grantsCredits: true }}
>
  Buy processing minutes
</PurchaseButton>

PurchaseDialog

Use the dialog directly when you want custom trigger control.

import { PurchaseDialog } from "@/components/neonfin/purchase-dialog";
import { Button } from "@/components/ui/button";

<PurchaseDialog title="Choose a package" description="Pick what you need.">
  <Button>Upgrade</Button>
</PurchaseDialog>

Controlled usage:

<PurchaseDialog
  open={open}
  onOpenChange={setOpen}
  filters={{ features: ["export"] }}
/>

Props

PurchaseButton

PropTypeDefaultDescription
productIdstringAll productsLimit purchase options to one product.
filtersPurchaseFiltersAll pricesFilter products/prices shown in the dialog.
childrenReact.ReactNodeFilter-aware labelButton label.
titlestringGet accessDialog title.
descriptionstringChoose an option to continue.Dialog description.
emptyMessagestringBuilt-in empty textText shown when filters match no prices.
flow"auto" | "popup" | "redirect""auto"Checkout window behavior.
renderOption(option, controls) => ReactNodeBuilt-in rowCustom option renderer.
classNamestring-Extra classes.
Button propsReact.ComponentProps<typeof Button>-Forwarded to the local shadcn button.

PurchaseDialog

PropTypeDefaultDescription
productIdstringAll productsLimit options to one product.
filtersPurchaseFiltersAll pricesFilter products/prices shown in the dialog.
openbooleanInternal stateControlled open state.
onOpenChange(open: boolean) => voidInternal setterControlled open callback.
childrenReact.ReactNode-Optional trigger element.
titlestringGet accessDialog title.
descriptionstringChoose an option to continue.Dialog description.
emptyMessagestringBuilt-in empty textText shown when filters match no prices.
flow"auto" | "popup" | "redirect""auto"Checkout window behavior.
renderOption(option, controls) => ReactNodeBuilt-in rowCustom option renderer.

PurchaseFilters

FieldTypeDescription
productIdstringMatch one product.
productIdsstring[]Match any listed product.
featuresstring[]Match prices that unlock every listed feature.
grantsCreditsbooleanMatch prices with included credits.
match(option) => booleanFinal custom predicate.

The default CreditGate fallback uses filters={{ productId, grantsCredits: true }}. The default FeatureGate fallback uses filters={{ features: [feature] }}.

Checkout return behavior

The dialog calls client.startCheckout(priceId, { flow }), which stores the order id in localStorage.

In popup checkout, the original page keeps polling the order and refreshes as soon as the webhook marks it paid. If the customer closes or cancels the popup, the dialog returns to the normal selectable state with a clear message.

In redirect checkout, NeonfinProvider sees the pending order when the user returns, polls the order, saves the issued code, and refreshes balances.

That is why a simple current-page successUrl works for most apps.

Fully custom purchase UI

Don't want the dialog? Call the client directly from your own components and you keep the same auto-resume behavior:

import { useNeonfin } from "@/components/neonfin/provider";

const client = useNeonfin();
await client.startCheckout("price_...", { flow: "auto" });