vantezzen/pay
Components

PurchaseButton and PurchaseDialog

Show available prices and start popup or redirect checkout.

PurchaseDialog loads your vantezzen/pay 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/pay/purchase-dialog";

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

Each offer is presented as a complete purchase card with clear billing cadence, included credits, unlocked features, unit pricing, and one obvious action. The dialog keeps the order decision separate from the provider-hosted payment step. 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/pay-purchase.json

PurchaseButton

import { PurchaseButton } from "@/components/pay/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>

vantezzen/pay allows entering provider promotion or discount codes at checkout. If you want to disable this, pass allowPromotionCodes={false}:

<PurchaseButton allowPromotionCodes={false}>Buy credits</PurchaseButton>

If you want to start the checkout with a discount code already applied:

<PurchaseButton discountCode="LAUNCH10">Buy credits for 10% off</PurchaseButton>

PurchaseDialog

Use the dialog directly when you want custom trigger control.

import { PurchaseDialog } from "@/components/pay/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.
titlestringFilter-aware titleDialog title.
descriptionstringContext-awareDialog description. Defaults to a filter-aware line about what happens after payment (e.g. credits landing instantly).
emptyMessagestringPlease check back soon.Body of the empty state shown when filters match no prices.
flow"auto" | "popup" | "redirect""auto"Checkout window behavior.
allowPromotionCodesbooleantrueLets customers enter a provider promotion or discount code at checkout.
discountCodestring-Applies or pre-fills a specific provider promotion or discount code.
customerEmailstring-Pre-fills the receipt and wallet-recovery email.
collectCustomerEmailbooleanMode-awareShows the optional receipt and wallet-recovery email field. Defaults to true in credit-codes mode and false in external-auth mode.
recommendedPriceIdstring | (options) => string | null-Highlights one price. Pass an id, or a selector that receives the filtered options — e.g. the exported recommendMiddleOption.
recommendedLabelstringPopularLabel shown on the recommended price.
showBrandingbooleantrueShows the vantezzen/pay attribution link.
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.
titlestringFilter-aware titleDialog title.
descriptionstringContext-awareDialog description. Defaults to a filter-aware line about what happens after payment (e.g. credits landing instantly).
emptyMessagestringPlease check back soon.Body of the empty state shown when filters match no prices.
flow"auto" | "popup" | "redirect""auto"Checkout window behavior.
allowPromotionCodesbooleantrueLets customers enter a provider promotion or discount code at checkout.
discountCodestring-Applies or pre-fills a specific provider promotion or discount code.
customerEmailstring-Pre-fills the receipt and wallet-recovery email.
collectCustomerEmailbooleanMode-awareShows the optional receipt and wallet-recovery email field. Defaults to true in credit-codes mode and false in external-auth mode.
recommendedPriceIdstring | (options) => string | null-Highlights one price. Pass an id, or a selector that receives the filtered options — e.g. the exported recommendMiddleOption.
recommendedLabelstringPopularLabel shown on the recommended price.
showBrandingbooleantrueShows the vantezzen/pay attribution link.
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] }}.

Custom option renderers receive PurchaseOptionControls with busy, disabled, current, recurring, recommended, and a buy() callback.

Recommend an option

Pass a price id, or let the dialog pick from what it is actually showing:

import { PurchaseButton, recommendMiddleOption } from "@/components/pay/purchase-dialog";

<PurchaseButton
  filters={{ grantsCredits: true }}
  recommendedPriceId={recommendMiddleOption}
/>

The selector receives the filtered, price-sorted options and returns a price id — write your own to pin a tier by label, currency, or experiment arm.

Checkout return behavior

The dialog calls client.startCheckout(priceId, { flow, allowPromotionCodes, discountCode, customerEmail }), which stores the order id in localStorage. The optional email is used only for provider checkout and wallet-recovery email; disable the field with collectCustomerEmail={false} when your app already collects receipts elsewhere.

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, PayProvider sees the pending order when the user returns, polls the order, saves the issued code for credit-code wallets, and refreshes balances. External-auth wallets refresh through your same-origin bridge and do not use browser recovery codes.

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 { usePay } from "@/components/pay/provider";

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