TonPayButton is a ready-to-use React component that handles wallet connection and payment flow with configurable styling for TON payments.
Install packages
Install necessary libraries
npm install @ton-pay/ui-react @ton-pay/api @tonconnect/ui-react
Create a TON Connect manifest
Create tonconnect-manifest.json in the app public directory: {
"url" : "<APP_URL>" ,
"name" : "<APP_NAME>" ,
"iconUrl" : "<APP_ICON_URL>"
}
Wrap the app with the provider
Wrap the root component with TonConnectUIProvider. import { TonConnectUIProvider } from "@tonconnect/ui-react" ;
function App () {
return (
< TonConnectUIProvider manifestUrl = "/tonconnect-manifest.json" >
< AppRoutes />
</ TonConnectUIProvider >
);
}
HTTPS required To receive webhooks in production, ensure corresponding endpoints use HTTPS. Regular HTTP endpoints would be ignored by TON Pay.
Option 1: use useTonPay hook
The useTonPay hook simplifies wallet connection and transaction handling. Use it for a direct integration path.
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: "<RECIPIENT_ADDRESS>" ,
senderAddr ,
commentToSender: "Order #12345" ,
},
{ chain: "testnet" } // change to "mainnet" only after full validation
);
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 is not connected, it opens the TON Connect modal first.
Render the button
return (
< TonPayButton
handlePay = { handlePayment }
isLoading = { isLoading }
loadingText = "Processing payment..."
/>
);
}
Option 2: use TON Connect directly
Use TON Connect hooks directly when full control over the connection flow is required.
Import TON Connect 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: "<RECIPIENT_ADDRESS>" ,
senderAddr: "<SENDER_ADDRESS>" ,
commentToSender: "Invoice #5012" ,
},
{ chain: "mainnet" } // can be changed to testnet
);
// 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 } /> ;
}
All props are optional except handlePay.
Prop Type Default Description handlePay() => Promise<void>required Payment handler called when the button is selected. isLoadingbooleanfalseShows a 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 and textColor. onError(error: unknown) => void- Called when handlePay throws. A built-in error popup is also shown unless showErrorNotification is false. showErrorNotificationbooleantrueShows the built-in error notification popup on error. 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. showMenubooleantrueShows the dropdown menu with wallet actions. disabledbooleanfalseDisables the button. styleRecord<string, any>- Additional inline styles. classNamestring- Additional CSS class name.
Customization
Use useTonPay for a complete example.
Long variant (default)
Short variant
< TonPayButton
variant = "long"
handlePay = { handlePay }
/>
// Displays: "Pay with [TON icon] Pay"
Presets
Use useTonPay for a complete example.
Default preset
Gradient preset
< TonPayButton
preset = "default"
handlePay = { handlePay }
/>
// Blue theme: #0098EA
Custom styling
Use useTonPay for a complete example.
< TonPayButton
bgColor = "#7C3AED"
textColor = "#FFFFFF"
borderRadius = { 12 }
width = { 400 }
height = { 56 }
fontFamily = "'Inter', sans-serif"
handlePay = { handlePay }
/>
CSS gradients can be used in bgColor. For example: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)".
Use useTonPay for a complete example.
< TonPayButton
handlePay = { handlePay }
isLoading = { isLoading }
loadingText = "Processing payment..."
disabled = { cartTotal === 0 }
showMenu = { false }
/>
Advanced patterns
Error handling
import { TonPayButton , useTonPay } from "@ton-pay/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
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: "<RECIPIENT_ADDR>" ,
senderAddr ,
},
{ chain: "testnet" }
);
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 catches errors thrown from handlePay and shows a notification pop-up with the error message.
If a custom error UI is also rendered, both messages appear. Use either the built-in pop-up or a custom UI, but not both.
Add a custom error handler
Use onError for logging or custom notifications. Set showErrorNotification={false} to disable the built-in pop-up.
import { TonPayButton , useTonPay } from "@ton-pay/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
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: "<RECIPIENT_ADDR>" ,
senderAddr ,
},
{ chain: "testnet" }
);
return { message };
});
} finally {
setIsLoading ( false );
}
};
return (
< TonPayButton
handlePay = { handlePayment }
isLoading = { isLoading }
onError = { ( error ) => {
analytics . track ( "payment_error" , {
message: ( error as any )?. message ?? String ( error ),
});
// The toast/notification can go here
} }
showErrorNotification = { false }
/>
);
}
Replace the pop-up with a custom UI. Catch errors inside handlePay and do not rethrow. When handlePay resolves, the button does not show the default pop-up.
import { TonPayButton , useTonPay } from "@ton-pay/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
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: "<RECIPIENT_ADDR>" ,
senderAddr ,
},
{ chain: "testnet" }
);
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
Create the payment message on a backend to store tracking identifiers and validate parameters before sending.
Create a backend endpoint
Build an endpoint that returns the message for TonPayButton. // /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: "<RECIPIENT_ADDR>" ,
senderAddr ,
commentToSender: `Order ${ orderId } ` ,
},
{ chain: "testnet" }
);
// Store reference and bodyBase64Hash in the database
await db . savePayment ({ orderId , reference , bodyBase64Hash });
res . json ({ message });
});
Call the endpoint from the frontend
import { TonPayButton , useTonPay } from "@ton-pay/ui-react" ;
import { useState } from "react" ;
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_ID>" ,
}),
});
const data = await response . json ();
return { message: data . message };
});
} finally {
setIsLoading ( false );
}
};
return < TonPayButton handlePay = { handlePayment } isLoading = { isLoading } /> ;
}
Creating payments server-side allows storing tracking identifiers and validating payment parameters before sending.
Test the integration
Run the interactive button showcase to test variants and styling.
npm run test:button-react
# or
bun test:button-react
Funds at risk Running tests on mainnet can result in irreversible loss of real TON. Always use chain: "testnet" and testnet wallet addresses during development. Verify the network before switching to mainnet.
Best practices
Wrap payment calls in try-catch blocks and display user-friendly error messages. Network issues and cancellations are common.
Set isLoading={true} during payment processing to prevent double submissions and provide visual feedback.
Verify cart totals, user input, and business rules before calling the payment handler.
Use chain: "testnet" during development. Switch to "mainnet" only after validation.
Save reference and bodyBase64Hash to track payment status using webhooks .
After successful payment, show a confirmation message, redirect to a success page, or update the UI.