The TonPayButton is a ready-to-use React component that handles wallet connection, payment flow, and provides a beautiful, customizable UI. Itβs the fastest way to integrate TON payments into your React application.
Installation
Install required packages
Install the React UI package along with TonConnect: npm install @ton-pay/ui-react @tonconnect/ui-react
Create TonConnect manifest
Create a tonconnect-manifest.json file in your public directory: {
"url" : "https://yourdomain.com" ,
"name" : "Your App Name" ,
"iconUrl" : "https://yourdomain.com/icon.png"
}
Wrap your app with TonConnect provider
In your root component, wrap your app with TonConnectUIProvider: import { TonConnectUIProvider } from "@tonconnect/ui-react" ;
function App () {
return (
< TonConnectUIProvider manifestUrl = "/tonconnect-manifest.json" >
< YourApp />
</ TonConnectUIProvider >
);
}
Integration Guide
Choose one of two integration approaches based on your needs:
Option 1: Using useTonPay Hook (Recommended)
The useTonPay hook simplifies wallet connection and transaction handling. Itβs the easiest way to integrate payments.
Import required dependencies
import { TonPayButton } from "@ton-pay/ui-react" ;
import { useTonPay } from "@ton-pay/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
Set up the component
function PaymentComponent () {
const { pay } = useTonPay ();
const [ isLoading , setIsLoading ] = useState ( false );
Create the payment handler
const handlePayment = async () => {
setIsLoading ( true );
try {
const { txResult , reference , bodyBase64Hash } = await pay (
async ( senderAddr : string ) => {
// Build the payment message
const { message , reference , bodyBase64Hash } =
await createTonPayTransfer (
{
amount: 3.5 ,
asset: "TON" ,
recipientAddr: "EQC...RECIPIENT" ,
senderAddr ,
commentToSender: "Order #12345" ,
},
{ chain: "mainnet" }
);
return { message , reference , bodyBase64Hash };
}
);
console . log ( "Payment sent:" , txResult );
console . log ( "Tracking:" , reference , bodyBase64Hash );
} catch ( error ) {
console . error ( "Payment failed:" , error );
} finally {
setIsLoading ( false );
}
};
The useTonPay hook automatically checks wallet connection. If the user isnβt connected, it will open the TonConnect modal first.
Render the button
return (
< TonPayButton
handlePay = { handlePayment }
isLoading = { isLoading }
loadingText = "Processing payment..."
/>
);
}
Option 2: Using TonConnect Directly
For more control over the wallet connection flow, you can use TonConnectβs API directly.
Import TonConnect hooks
import { TonPayButton } from "@ton-pay/ui-react" ;
import {
useTonAddress ,
useTonConnectModal ,
useTonConnectUI ,
} from "@tonconnect/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
Set up hooks and state
function DirectPaymentComponent () {
const address = useTonAddress ( true );
const modal = useTonConnectModal ();
const [ tonConnectUI ] = useTonConnectUI ();
const [ isLoading , setIsLoading ] = useState ( false );
Create the payment handler
const handlePay = async () => {
// Check if wallet is connected
if ( ! address ) {
modal . open ();
return ;
}
setIsLoading ( true );
try {
// Create the payment message
const { message } = await createTonPayTransfer (
{
amount: 1.2 ,
asset: "TON" ,
recipientAddr: "EQC...RECIPIENT" ,
senderAddr: address ,
commentToSender: "Invoice #5012" ,
},
{ chain: "mainnet" }
);
// Send the transaction
await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 5 * 60 ,
from: address ,
});
console . log ( "Payment completed!" );
} catch ( error ) {
console . error ( "Payment failed:" , error );
} finally {
setIsLoading ( false );
}
};
Render the button
return < TonPayButton handlePay = { handlePay } isLoading = { isLoading } /> ;
}
π API Reference
All props are optional except handlePay.
Prop Type Default Description handlePay() => Promise<void>required Payment handler function called when user clicks the button isLoadingbooleanfalseShows loading spinner and disables the button variant"long" | "short""long"Button text variant: βPay with TON Payβ (long) or βTON Payβ (short) preset"default" | "gradient"- Predefined theme preset (overrides bgColor/textColor) onError(error: unknown) => void- Called when handlePay throws. A built-in error popup is also shown by default unless showErrorNotification is false. showErrorNotificationbooleantrueWhether to show the built-in error notification popup when an error occurs bgColorstring"#0098EA"Background color (hex) or CSS gradient textColorstring"#FFFFFF"Text and icon color (hex) borderRadiusnumber | string8Border radius in pixels or CSS value fontFamilystring"inherit"Font family for button text widthnumber | string300Button width in pixels or CSS value heightnumber | string44Button height in pixels or CSS value loadingTextstring"Processing..."Text shown during loading state showMenubooleantrueShow dropdown menu with wallet actions disabledbooleanfalseDisables the button styleRecord<string, any>- Additional inline styles classNamestring- Additional CSS class name
Features
Highly Customizable Full control over colors, gradients, border radius, and fonts
Built-in Presets Ready-to-use themes matching Figma designs
Wallet Integration Connect, disconnect, and copy address via dropdown menu
Loading States Built-in spinner and customizable loading text
Responsive Design Flexible sizing with pixel or percentage values
Zero Config Works out of the box with sensible defaults
Customization
The button comes in two text variants:
Long Variant (Default)
Short Variant
< TonPayButton
variant = "long"
handlePay = { handlePay }
/>
// Displays: "Pay with [TON icon] Pay"
Presets
Use built-in themes for quick styling:
Default Preset
Gradient Preset
< TonPayButton
preset = "default"
handlePay = { handlePay }
/>
// Blue theme: #0098EA
Custom Styling
Customize colors, sizes, and appearance:
< TonPayButton
bgColor = "#7C3AED"
textColor = "#FFFFFF"
borderRadius = { 12 }
width = { 400 }
height = { 56 }
fontFamily = "'Inter', sans-serif"
handlePay = { handlePay }
/>
You can use CSS gradients in bgColor: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
Control button behavior and appearance:
< TonPayButton
handlePay = { handlePay }
isLoading = { isLoading }
loadingText = "Processing payment..."
disabled = { cartTotal === 0 }
showMenu = { false }
/>
Advanced Patterns
Error Handling
Always handle payment errors gracefully:
function PaymentWithErrors () {
const { pay } = useTonPay ();
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState < string | null >( null );
const handlePayment = async () => {
setIsLoading ( true );
setError ( null );
try {
await pay ( async ( senderAddr : string ) => {
const { message , reference , bodyBase64Hash } =
await createTonPayTransfer (
{
amount: 5.0 ,
asset: "TON" ,
recipientAddr: "EQC...RECIPIENT" ,
senderAddr ,
},
{ chain: "mainnet" }
);
return { message , reference , bodyBase64Hash };
});
// Show success message...
} catch ( err : any ) {
setError ( err . message || "Payment failed. Please try again." );
} finally {
setIsLoading ( false );
}
};
return (
< div >
< TonPayButton handlePay = { handlePayment } isLoading = { isLoading } />
{ error && < div style = { { color: "red" } } > { error } </ div > }
</ div >
);
}
Built-in error notification
TonPayButton automatically catches errors thrown from handlePay and shows a notification popup with the error message. This works out of the box - no configuration needed.
If you also render your own error UI (like the example above), users will see both messages. To avoid double messaging, either rely on the built-in popup or fully handle errors yourself as shown below.
Add a custom error handler
Use the onError prop to run side effects (analytics, logging, custom toasts). By default, the built-in popup will also appear. Set showErrorNotification={false} to disable it:
function PaymentWithCustomHandler () {
const { pay } = useTonPay ();
const [ isLoading , setIsLoading ] = useState ( false );
const handlePayment = async () => {
setIsLoading ( true );
try {
await pay ( async ( senderAddr : string ) => {
const { message } = await createTonPayTransfer (
{
amount: 3 ,
asset: "TON" ,
recipientAddr: "EQC...RECIPIENT" ,
senderAddr ,
},
{ chain: "mainnet" }
);
return { message };
});
} finally {
setIsLoading ( false );
}
};
return (
< TonPayButton
handlePay = { handlePayment }
isLoading = { isLoading }
onError = { ( error ) => {
analytics . track ( "payment_error" , {
message: ( error as any )?. message ?? String ( error ),
});
// Your own toast/notification can go here
} }
showErrorNotification = { false }
/>
);
}
If you prefer to completely replace the built-in popup, catch errors inside handlePay and do not rethrow. When handlePay resolves successfully (even after handling an error internally), the button will not show the default popup:
function PaymentWithOwnUI () {
const { pay } = useTonPay ();
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState < string | null >( null );
const handlePayment = async () => {
setIsLoading ( true );
setError ( null );
try {
await pay ( async ( senderAddr : string ) => {
const { message } = await createTonPayTransfer (
{
amount: 2.5 ,
asset: "TON" ,
recipientAddr: "EQC...RECIPIENT" ,
senderAddr ,
},
{ chain: "mainnet" }
);
return { message };
});
} catch ( e : any ) {
// Handle the error here and DO NOT rethrow
setError ( e ?. message ?? "Payment failed. Please try again." );
} finally {
setIsLoading ( false );
}
};
return (
< div >
< TonPayButton handlePay = { handlePayment } isLoading = { isLoading } />
{ error && < div style = { { color: "red" } } > { error } </ div > }
</ div >
);
}
Server-Side Payment Creation
For better security and tracking, create payments on your backend:
Create backend endpoint
Create an API endpoint that builds the payment message: // /api/create-payment
app . post ( "/api/create-payment" , async ( req , res ) => {
const { amount , senderAddr , orderId } = req . body ;
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: "EQ....yourWalletAddress" ,
senderAddr ,
commentToSender: `Order ${ orderId } ` ,
},
{ chain: "mainnet" }
);
// Store reference and bodyBase64Hash in your database
await db . savePayment ({ orderId , reference , bodyBase64Hash });
res . json ({ message });
});
Call from frontend
function ServerSidePayment () {
const { pay } = useTonPay ();
const [ isLoading , setIsLoading ] = useState ( false );
const handlePayment = async () => {
setIsLoading ( true );
try {
await pay ( async ( senderAddr : string ) => {
const response = await fetch ( "/api/create-payment" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
amount: 10.5 ,
senderAddr ,
orderId: "ORDER-123" ,
}),
});
const data = await response . json ();
return { message: data . message };
});
} finally {
setIsLoading ( false );
}
};
return < TonPayButton handlePay = { handlePayment } isLoading = { isLoading } /> ;
}
Server-side payment creation allows you to securely store tracking identifiers and validate payment parameters before creating the transaction.
Testing Your Integration
Run the interactive button showcase to test all variants and styling options:
npm run test:button-react
# or
bun test:button-react
This starts a local dev server with a visual gallery of all button configurations, allowing you to:
Test different button variants and presets
Try custom colors and sizes
Verify loading and disabled states
Interact with the dropdown menu
See how the button behaves in different scenarios
Always test with chain: "testnet" during development to avoid spending real TON tokens.
Best Practices
1. Handle errors gracefully
Wrap payment calls in try-catch blocks and display user-friendly error messages. Network issues and user cancellations are common scenarios.
Always set isLoading={true} during payment processing to prevent double submissions and provide visual feedback to users.
3. Validate before payment
Verify cart totals, user input, and business rules before calling the payment handler.
4. Test with testnet first
Use chain: "testnet" during development. Only switch to "mainnet" after thorough testing.
5. Store tracking identifiers
Always save reference and bodyBase64Hash to track payment status via webhooks. See the Webhooks guide for details.
6. Provide success feedback
After successful payment, show a confirmation message, redirect to a success page, or update your UI to reflect the completed transaction.