Skip to main content
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

1

Install required packages

Install the React UI package along with TonConnect:
npm install @ton-pay/ui-react @tonconnect/ui-react
2

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"
}
Learn more about the manifest format in the TonConnect documentation.
3

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: The useTonPay hook simplifies wallet connection and transaction handling. It’s the easiest way to integrate payments.
1

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";
2

Set up the component

function PaymentComponent() {
  const { pay } = useTonPay();
  const [isLoading, setIsLoading] = useState(false);
3

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.
4

Render the button

  return (
    <TonPayButton
      handlePay={handlePayment}
      isLoading={isLoading}
      loadingText="Processing payment..."
    />
  );
}
For more details on using the useTonPay hook, see the Send via useTonPay guide.

Option 2: Using TonConnect Directly

For more control over the wallet connection flow, you can use TonConnect’s API directly.
1

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";
2

Set up hooks and state

function DirectPaymentComponent() {
  const address = useTonAddress(true);
  const modal = useTonConnectModal();
  const [tonConnectUI] = useTonConnectUI();
  const [isLoading, setIsLoading] = useState(false);
3

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);
    }
  };
4

Render the button

  return <TonPayButton handlePay={handlePay} isLoading={isLoading} />;
}
For more details on using TonConnect directly, see the Send via TonConnect guide.

πŸ“‹ API Reference

Props

All props are optional except handlePay.
PropTypeDefaultDescription
handlePay() => Promise<void>requiredPayment 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

Button Variants

The button comes in two text variants:
<TonPayButton
  variant="long"
  handlePay={handlePay}
/>
// Displays: "Pay with [TON icon] Pay"

Presets

Use built-in themes for quick styling:
<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%)"

Button States

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}
    />
  );
}

Replace the popup with your own UI

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:
1

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 });
});
2

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

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.
Verify cart totals, user input, and business rules before calling the payment handler.
Use chain: "testnet" during development. Only switch to "mainnet" after thorough testing.
Always save reference and bodyBase64Hash to track payment status via webhooks. See the Webhooks guide for details.
After successful payment, show a confirmation message, redirect to a success page, or update your UI to reflect the completed transaction.