Skip to main content
TonPayButton is a ready-to-use React component that handles wallet connection and payment flow with configurable styling for TON payments.

Install packages

1

Install necessary libraries

npm install @ton-pay/ui-react @ton-pay/api @tonconnect/ui-react
2

Create a TON Connect manifest

Create tonconnect-manifest.json in the app public directory:
{
  "url": "<APP_URL>",
  "name": "<APP_NAME>",
  "iconUrl": "<APP_ICON_URL>"
}
3

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 requiredTo 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.
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: "<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.
4

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

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

Render the button

  return <TonPayButton handlePay={handlePay} isLoading={isLoading} />;
}

TonPayButton props

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

Button variants

Use useTonPay for a complete example.
<TonPayButton
  variant="long"
  handlePay={handlePay}
/>
// Displays: "Pay with [TON icon] Pay"

Presets

Use useTonPay for a complete example.
<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%)".

Button states

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

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

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