@payxor/react
v0.1.2
Published
React hooks and components for PayXor
Downloads
7
Maintainers
Readme
@payxor/react
React hooks and unstyled UI components for integrating PayXor payments into your application. All components are headless/unstyled—bring your own styling via className and slot-level class overrides.
Table of Contents
Installation
pnpm add @payxor/react @payxor/sdk @payxor/typesnpm install @payxor/react @payxor/sdk @payxor/typesyarn add @payxor/react @payxor/sdk @payxor/typesPeer dependencies: react, react-dom, viem
Quick Start
import { useTokenPayment, usePayment, DropdownBuyButton } from "@payxor/react";
import { useAccount, useChainId, usePublicClient, useWalletClient } from "wagmi";
function BuyButton({ appId, productId, apiUrl }: Props) {
const { address } = useAccount();
const chainId = useChainId();
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const {
stablecoins,
selectedToken,
setSelectedToken,
needsApproval,
approve,
approving,
} = useTokenPayment({
appId,
productId,
address,
chainId,
publicClient,
walletClient,
apiUrl,
});
const { executePayment, loading } = usePayment({
appId,
productId,
tokenAddress: selectedToken,
address,
chainId,
publicClient,
walletClient,
apiUrl,
});
return (
<DropdownBuyButton
stablecoins={stablecoins}
selectedToken={selectedToken}
onSelect={setSelectedToken}
needsApproval={needsApproval}
onApprove={approve}
onBuy={executePayment}
loading={loading}
approving={approving}
/>
);
}Hooks
usePayXorClient
Creates a memoized PayXor SDK client instance.
import { usePayXorClient } from "@payxor/react";
const client = usePayXorClient({
apiUrl: "https://api.payxor.xyz",
walletClient,
});Options:
| Option | Type | Description |
|--------|------|-------------|
| apiUrl | string | PayXor API URL |
| walletClient | WalletClient \| null | Viem wallet client |
Returns: PayXorClient | null
useTokenPayment
Manages token selection, balance checking, allowance checking, and approval flow for a product purchase.
import { useTokenPayment } from "@payxor/react";
const {
stablecoins,
selectedToken,
setSelectedToken,
productAmount,
allowance,
balance,
needsApproval,
hasSufficientBalance,
loading,
approving,
error,
approve,
} = useTokenPayment({
appId: "your-app-id",
productId: "product-id",
address,
chainId,
publicClient,
walletClient,
apiUrl: "https://api.payxor.xyz",
enabled: true,
});Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| appId | string | — | Your PayXor app ID |
| productId | string | — | Product ID to purchase |
| address | Address \| null | — | Connected wallet address |
| chainId | number \| null | — | Current chain ID |
| publicClient | PublicClient \| null | — | Viem public client |
| walletClient | WalletClient \| null | — | Viem wallet client |
| apiUrl | string | — | PayXor API URL |
| enabled | boolean | true | Enable/disable the hook |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| stablecoins | StablecoinConfig[] | Available stablecoins for the app |
| selectedToken | Address \| null | Currently selected token address |
| setSelectedToken | (address: Address) => void | Token selection handler |
| productAmount | bigint | Product price in token units |
| allowance | bigint \| null | Current token allowance |
| balance | bigint \| null | User's token balance |
| needsApproval | boolean | Whether approval is required |
| hasSufficientBalance | boolean | Whether user has enough balance |
| loading | boolean | Loading state |
| approving | boolean | Approval transaction in progress |
| error | Error \| null | Any error that occurred |
| approve | () => Promise<string> | Trigger token approval |
usePayment
Handles the complete payment flow: quote fetching, transaction execution, and confirmation.
import { usePayment } from "@payxor/react";
const {
executePayment,
loading,
status,
error,
txHash,
explorerUrl,
waitingForConfirmation,
reset,
} = usePayment({
appId: "your-app-id",
productId: "product-id",
tokenAddress: selectedToken,
address,
chainId,
publicClient,
walletClient,
apiUrl: "https://api.payxor.xyz",
onSuccess: (txHash) => console.log("Payment successful:", txHash),
onError: (error) => console.error("Payment failed:", error),
getExplorerUrl: (hash, chainId) => `https://etherscan.io/tx/${hash}`,
});Options:
| Option | Type | Description |
|--------|------|-------------|
| appId | string | Your PayXor app ID |
| productId | string | Product ID to purchase |
| tokenAddress | Address \| null | Selected payment token |
| address | Address \| null | Connected wallet address |
| chainId | number \| null | Current chain ID |
| publicClient | PublicClient \| null | Viem public client |
| walletClient | WalletClient \| null | Viem wallet client |
| apiUrl | string | PayXor API URL |
| onSuccess | (txHash: string) => void | Success callback |
| onError | (error: Error) => void | Error callback |
| getExplorerUrl | (txHash: string, chainId: number) => string \| null | Block explorer URL builder |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| executePayment | () => Promise<void> | Execute the payment |
| loading | boolean | Transaction in progress |
| status | string | Human-readable status message |
| error | Error \| null | Any error that occurred |
| txHash | string \| null | Transaction hash |
| explorerUrl | string \| null | Block explorer URL |
| waitingForConfirmation | boolean | Waiting for tx confirmation |
| reset | () => void | Reset hook state |
useTokenApproval
Standalone hook for managing token approval (useful when building custom flows).
import { useTokenApproval } from "@payxor/react";
const {
allowance,
balance,
needsApproval,
hasSufficientBalance,
loading,
approving,
error,
approve,
} = useTokenApproval({
tokenAddress: "0x...",
requiredAmount: 1000000n,
address,
chainId,
publicClient,
walletClient,
apiUrl,
enabled: true,
});Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| tokenAddress | Address \| null | — | Token to check/approve |
| requiredAmount | bigint | — | Amount required for the transaction |
| address | Address \| null | — | Connected wallet address |
| chainId | number \| null | — | Current chain ID |
| publicClient | PublicClient \| null | — | Viem public client |
| walletClient | WalletClient \| null | — | Viem wallet client |
| apiUrl | string | — | PayXor API URL |
| enabled | boolean | true | Enable/disable the hook |
useTokenStatuses
Computes token status entries for display in selectors.
import { useTokenStatuses } from "@payxor/react";
const tokenStatuses = useTokenStatuses({
stablecoins,
selectedToken,
balance,
allowance,
requiredAmount: productAmount,
});
// Result: Record<string, TokenStatusEntry>
// {
// "0x...": { balance: 1000000n, allowance: 500000n, requiredAmount: 100000n }
// }useFeatureStatus
Checks if a user has unlocked a permanent feature/entitlement. Automatically polls until the feature is unlocked.
import { useFeatureStatus } from "@payxor/react";
const { isUnlocked, loading, error } = useFeatureStatus({
appId: "your-app-id",
entitlementId: "premium-feature",
address,
chainId,
walletClient,
apiUrl,
enabled: true,
pollInterval: 5000, // ms
});
if (isUnlocked) {
return <PremiumContent />;
}Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| appId | string | — | Your PayXor app ID |
| entitlementId | string | — | Feature/entitlement ID |
| address | Address \| null | — | Connected wallet address |
| chainId | number \| null | — | Current chain ID |
| walletClient | WalletClient \| null | — | Viem wallet client |
| apiUrl | string | — | PayXor API URL |
| enabled | boolean | true | Enable/disable the hook |
| pollInterval | number | 5000 | Polling interval in ms |
useSessionStatus
Checks if a user has an active time-based session. Continuously polls to track session expiration.
import { useSessionStatus } from "@payxor/react";
const { isActive, loading, error } = useSessionStatus({
appId: "your-app-id",
productId: "hourly-access",
address,
chainId,
walletClient,
apiUrl,
enabled: true,
pollInterval: 5000,
});
if (isActive) {
return <SessionContent />;
}Components
All components are unstyled. Use className and slotClassNames props to apply your styles.
Button
Basic button with loading state support.
import { Button } from "@payxor/react";
<Button
onClick={handleClick}
loading={isLoading}
disabled={isDisabled}
loadingIndicator={<Spinner />}
loadingIndicatorClassName="animate-spin"
className="px-4 py-2 rounded bg-blue-600 text-white"
>
Pay Now
</Button>Props:
| Prop | Type | Description |
|------|------|-------------|
| loading | boolean | Shows loading state |
| loadingIndicator | ReactNode | Custom loading indicator |
| loadingIndicatorClassName | string | Class for loading indicator |
| className | string | Button class name |
| children | ReactNode | Button content |
| ...rest | ButtonHTMLAttributes | Standard button props |
StatusMessage
Displays status messages with optional icon, link, and dismiss button.
import { StatusMessage } from "@payxor/react";
<StatusMessage
type="success"
message="Payment completed!"
link={{ url: explorerUrl, text: "View transaction" }}
onDismiss={() => setMessage(null)}
className="flex items-center gap-2 p-4 rounded border"
icons={{
success: "✅",
error: "❌",
info: "ℹ️",
loading: "⏳",
}}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| type | "info" \| "success" \| "error" \| "loading" | Message type |
| message | string | Message text |
| icon | ReactNode | Override icon for current type |
| icons | Record<StatusType, ReactNode> | Icons for each type |
| link | { url: string, text: string } | Optional link |
| onDismiss | () => void | Dismiss handler |
| className | string | Container class |
| iconClassName | string | Icon class |
| messageClassName | string | Message class |
| linkClassName | string | Link class |
| dismissButtonClassName | string | Dismiss button class |
StatusBadge
Displays feature unlock or session status.
import { StatusBadge } from "@payxor/react";
// Feature status
<StatusBadge
status={isUnlocked}
type="feature"
className="inline-flex items-center gap-2 px-3 py-1 rounded-full"
icons={{
unlocked: "🔓",
locked: "🔒",
checking: "⏳",
}}
/>
// Session status with time remaining
<StatusBadge
status={isActive}
type="session"
timeRemaining={3600}
labels={{
active: "Session Active",
inactive: "Session Expired",
timeRemainingPrefix: "Expires in:",
}}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| status | boolean \| null | Current status (null = checking) |
| type | "feature" \| "session" | Badge type |
| timeRemaining | number \| null | Seconds remaining (sessions only) |
| labels | StatusBadgeLabels | Custom label text |
| icons | StatusBadgeIcons | Custom icons |
| className | string | Container class |
| iconClassName | string | Icon class |
| textClassName | string | Text class |
| subtextClassName | string | Subtext class |
StablecoinSelector
Token selection grid/list with balance and approval status indicators.
import { StablecoinSelector } from "@payxor/react";
<StablecoinSelector
stablecoins={stablecoins}
selectedToken={selectedToken}
onSelect={setSelectedToken}
tokenStatuses={tokenStatuses}
disabled={loading}
className="space-y-2"
slotClassNames={{
list: "grid gap-2",
listLabel: "text-sm font-medium",
tokenButton: "p-3 border rounded hover:border-blue-500",
tokenButtonSelected: "border-blue-500 bg-blue-50",
tokenButtonDisabled: "opacity-50 cursor-not-allowed",
tokenSymbol: "font-bold",
tokenName: "text-sm text-gray-500",
tokenBalance: "text-xs",
tokenBalanceSufficient: "text-green-600",
tokenBalanceInsufficient: "text-red-600",
tokenApproval: "text-yellow-600",
tokenSelectedBadge: "text-xs bg-blue-100 px-2 py-0.5 rounded",
}}
labels={{
selectToken: "Choose payment method:",
balance: "Balance:",
insufficient: "(Not enough)",
approvalNeeded: "(Needs approval)",
}}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| stablecoins | StablecoinConfig[] | Available tokens |
| selectedToken | string \| null | Selected token address |
| onSelect | (address: string) => void | Selection handler |
| tokenStatuses | Record<string, TokenStatus> | Token status data |
| disabled | boolean | Disable selection |
| className | string | Container class |
| label | ReactNode | Custom label |
| labels | StablecoinSelectorLabels | Custom label text |
| formatBalance | (balance: bigint, decimals: number) => string | Balance formatter |
| slotClassNames | StablecoinSelectorClassNames | Slot classes |
ProductCard
Displays a product with price and purchase button.
import { ProductCard } from "@payxor/react";
<ProductCard
name="Premium Access"
description="Unlock all premium features forever"
price="9.99"
purchased={isPurchased}
loading={isLoading}
onPurchase={handlePurchase}
className="p-6 border rounded-lg shadow"
slotClassNames={{
header: "flex items-center justify-between",
title: "text-xl font-bold",
purchasedBadge: "text-sm bg-green-100 text-green-800 px-2 py-1 rounded",
description: "mt-2 text-gray-600",
footer: "mt-4 flex items-center justify-between",
price: "text-2xl font-bold",
currency: "text-sm text-gray-500",
action: "px-4 py-2 bg-blue-600 text-white rounded",
}}
labels={{
purchase: "Buy Now",
purchased: "Owned",
processing: "Processing...",
currency: "USD",
}}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| name | string | Product name |
| description | string | Product description |
| price | string | Display price |
| purchased | boolean | Already purchased |
| loading | boolean | Purchase in progress |
| onPurchase | () => void | Purchase handler |
| className | string | Container class |
| labels | ProductCardLabels | Custom label text |
| slotClassNames | ProductCardClassNames | Slot classes |
| action | ReactNode | Custom action element |
| buttonProps | ButtonProps | Props for default button |
DropdownBuyButton
Compact token selector + buy/approve button combo. Automatically switches between "Approve" and "Buy" states based on allowance.
import { DropdownBuyButton } from "@payxor/react";
<DropdownBuyButton
stablecoins={stablecoins}
selectedToken={selectedToken}
onSelect={setSelectedToken}
needsApproval={needsApproval}
onApprove={approve}
onBuy={executePayment}
loading={loading}
approving={approving}
disabled={!address}
label="Pay with"
slotClassNames={{
container: "space-y-2",
label: "text-sm font-medium",
row: "flex",
selectWrapper: "relative",
select: "border rounded-l px-3 py-2 pr-8 appearance-none",
selectIcon: "absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none",
button: "px-4 py-2 bg-blue-600 text-white rounded-r",
}}
selectIcon={<ChevronDownIcon className="w-4 h-4" />}
labels={{
selectToken: "Select token",
buyWith: (token) => `Pay ${token?.symbol}`,
approveWith: (token) => `Approve ${token?.symbol}`,
optionLabel: (token) => `${token.symbol} - ${token.name}`,
}}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| stablecoins | StablecoinConfig[] | Available tokens |
| selectedToken | string \| null | Selected token address |
| onSelect | (address: string) => void | Selection handler |
| onBuy | () => void | Buy handler |
| onApprove | () => void | Approve handler |
| needsApproval | boolean | Force approval state |
| tokenStatuses | Record<string, TokenStatus> | Token status (for auto-detection) |
| requiredAmount | bigint | Required amount (for auto-detection) |
| disabled | boolean | Disable interactions |
| loading | boolean | Buy loading state |
| approving | boolean | Approve loading state |
| className | string | Container class |
| label | ReactNode \| null | Label (set null to hide) |
| labels | DropdownBuyButtonLabels | Custom label text |
| slotClassNames | DropdownBuyButtonClassNames | Slot classes |
| selectIcon | ReactNode | Custom dropdown icon |
| buttonProps | ButtonProps | Props for the button |
| selectProps | SelectHTMLAttributes | Props for the select |
Slot Classes:
container- Outer wrapperlabel- Label elementrow- Row containing select and buttonselectWrapper- Wrapper around select (for icon positioning)select- The select elementselectIcon- Icon inside select wrapperbutton- The action buttonempty- Message when no tokens available
Full Integration Example
Here's a complete example integrating all pieces with wagmi:
import { useAccount, useChainId, usePublicClient, useWalletClient, useConfig } from "wagmi";
import {
useTokenPayment,
usePayment,
useTokenStatuses,
useFeatureStatus,
DropdownBuyButton,
StatusMessage,
StatusBadge,
} from "@payxor/react";
const API_URL = "https://api.payxor.xyz";
function PremiumFeature({ appId, productId, entitlementId }: Props) {
const { address } = useAccount();
const chainId = useChainId();
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const config = useConfig();
// Check if already unlocked
const { isUnlocked, loading: checkingStatus } = useFeatureStatus({
appId,
entitlementId,
address,
chainId,
walletClient,
apiUrl: API_URL,
});
// Token and approval management
const {
stablecoins,
selectedToken,
setSelectedToken,
productAmount,
allowance,
balance,
needsApproval,
approving,
approve,
} = useTokenPayment({
appId,
productId,
address,
chainId,
publicClient,
walletClient,
apiUrl: API_URL,
enabled: !isUnlocked, // Only fetch if not already unlocked
});
// Token statuses for display
const tokenStatuses = useTokenStatuses({
stablecoins,
selectedToken,
balance,
allowance,
requiredAmount: productAmount,
});
// Payment execution
const { executePayment, loading, status, error, explorerUrl, reset } = usePayment({
appId,
productId,
tokenAddress: selectedToken,
address,
chainId,
publicClient,
walletClient,
apiUrl: API_URL,
getExplorerUrl: (hash, targetChainId) => {
const chain = config.chains.find((c) => c.id === targetChainId);
return chain?.blockExplorers?.default?.url
? `${chain.blockExplorers.default.url}/tx/${hash}`
: null;
},
});
// Already unlocked - show content
if (isUnlocked) {
return (
<div>
<StatusBadge status={true} type="feature" className="mb-4" />
<PremiumContent />
</div>
);
}
return (
<div className="space-y-4">
<StatusBadge status={isUnlocked} type="feature" />
{status && (
<StatusMessage
type={error ? "error" : "info"}
message={status}
link={explorerUrl ? { url: explorerUrl, text: "View transaction" } : undefined}
onDismiss={reset}
/>
)}
<DropdownBuyButton
stablecoins={stablecoins}
selectedToken={selectedToken}
onSelect={setSelectedToken}
tokenStatuses={tokenStatuses}
needsApproval={needsApproval}
onApprove={approve}
onBuy={executePayment}
loading={loading}
approving={approving}
disabled={!address || checkingStatus}
/>
</div>
);
}Notes
- All hooks require
apiUrlto be provided for API communication. - Hooks are wallet-agnostic—pass
address,chainId,publicClient, andwalletClientfrom your wallet setup (wagmi, RainbowKit, etc.). - Components are completely unstyled. Use Tailwind, CSS modules, or any styling solution.
- This package does not include analytics utilities (see
@payxor/sdkfor analytics).
