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.jsonPurchaseButton
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
| Prop | Type | Default | Description |
|---|---|---|---|
productId | string | All products | Limit purchase options to one product. |
filters | PurchaseFilters | All prices | Filter products/prices shown in the dialog. |
children | React.ReactNode | Filter-aware label | Button label. |
title | string | Filter-aware title | Dialog title. |
description | string | Context-aware | Dialog description. Defaults to a filter-aware line about what happens after payment (e.g. credits landing instantly). |
emptyMessage | string | Please check back soon. | Body of the empty state shown when filters match no prices. |
flow | "auto" | "popup" | "redirect" | "auto" | Checkout window behavior. |
allowPromotionCodes | boolean | true | Lets customers enter a provider promotion or discount code at checkout. |
discountCode | string | - | Applies or pre-fills a specific provider promotion or discount code. |
customerEmail | string | - | Pre-fills the receipt and wallet-recovery email. |
collectCustomerEmail | boolean | Mode-aware | Shows the optional receipt and wallet-recovery email field. Defaults to true in credit-codes mode and false in external-auth mode. |
recommendedPriceId | string | (options) => string | null | - | Highlights one price. Pass an id, or a selector that receives the filtered options — e.g. the exported recommendMiddleOption. |
recommendedLabel | string | Popular | Label shown on the recommended price. |
showBranding | boolean | true | Shows the vantezzen/pay attribution link. |
renderOption | (option, controls) => ReactNode | Built-in row | Custom option renderer. |
className | string | - | Extra classes. |
| Button props | React.ComponentProps<typeof Button> | - | Forwarded to the local shadcn button. |
PurchaseDialog
| Prop | Type | Default | Description |
|---|---|---|---|
productId | string | All products | Limit options to one product. |
filters | PurchaseFilters | All prices | Filter products/prices shown in the dialog. |
open | boolean | Internal state | Controlled open state. |
onOpenChange | (open: boolean) => void | Internal setter | Controlled open callback. |
children | React.ReactNode | - | Optional trigger element. |
title | string | Filter-aware title | Dialog title. |
description | string | Context-aware | Dialog description. Defaults to a filter-aware line about what happens after payment (e.g. credits landing instantly). |
emptyMessage | string | Please check back soon. | Body of the empty state shown when filters match no prices. |
flow | "auto" | "popup" | "redirect" | "auto" | Checkout window behavior. |
allowPromotionCodes | boolean | true | Lets customers enter a provider promotion or discount code at checkout. |
discountCode | string | - | Applies or pre-fills a specific provider promotion or discount code. |
customerEmail | string | - | Pre-fills the receipt and wallet-recovery email. |
collectCustomerEmail | boolean | Mode-aware | Shows the optional receipt and wallet-recovery email field. Defaults to true in credit-codes mode and false in external-auth mode. |
recommendedPriceId | string | (options) => string | null | - | Highlights one price. Pass an id, or a selector that receives the filtered options — e.g. the exported recommendMiddleOption. |
recommendedLabel | string | Popular | Label shown on the recommended price. |
showBranding | boolean | true | Shows the vantezzen/pay attribution link. |
renderOption | (option, controls) => ReactNode | Built-in row | Custom option renderer. |
PurchaseFilters
| Field | Type | Description |
|---|---|---|
productId | string | Match one product. |
productIds | string[] | Match any listed product. |
features | string[] | Match prices that unlock every listed feature. |
grantsCredits | boolean | Match prices with included credits. |
match | (option) => boolean | Final 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" });