diff --git a/docs.json b/docs.json index 132e21c8b..9c6a97429 100644 --- a/docs.json +++ b/docs.json @@ -232,10 +232,9 @@ { "group": "Payment integration", "pages": [ - "ecosystem/ton-pay/payment-integration/transfer", "ecosystem/ton-pay/payment-integration/payments-react", - "ecosystem/ton-pay/payment-integration/payments-tonconnect", - "ecosystem/ton-pay/payment-integration/status-info" + "ecosystem/ton-pay/payment-integration/payments-server-side", + "ecosystem/ton-pay/payment-integration/migration" ] }, { @@ -245,7 +244,6 @@ "ecosystem/ton-pay/ui-integration/button-js" ] }, - "ecosystem/ton-pay/webhooks", "ecosystem/ton-pay/api-reference" ] }, diff --git a/ecosystem/ton-pay/api-reference.mdx b/ecosystem/ton-pay/api-reference.mdx index e5b1370eb..10a536300 100644 --- a/ecosystem/ton-pay/api-reference.mdx +++ b/ecosystem/ton-pay/api-reference.mdx @@ -5,32 +5,150 @@ sidebarTitle: "API reference" import { Aside } from "/snippets/aside.jsx"; -TypeScript exports for the TON Pay SDK packages `@ton-pay/api` and `@ton-pay/ui-react`. To install the packages, use: +TypeScript exports for the TON Pay [`v0.4`](https://www.npmjs.com/package/@ton-pay/api?activeTab=versions) packages. + +## Install ```bash -npm install @ton-pay/api @ton-pay/ui-react +npm install @ton-pay/ui-react +``` + +Install `@ton-pay/api` separately only when server-side helpers are required. + +## `@ton-pay/ui-react` + +```ts +import { TonPayWidget } from "@ton-pay/ui-react"; +import type { + TonPayWidgetProps, + WidgetErrorPayload, + WidgetRouteChangedPayload, + WidgetSuccessPayload, +} from "@ton-pay/ui-react"; +``` + +### `TonPayWidget` + +Self-contained React component that: + +- resolves the TON Pay base URL from `chain`; +- creates and caches widget sessions; +- preloads the hosted iframe; +- maps merchant checkout props to widget intent; +- opens the checkout UI; +- forwards widget lifecycle callbacks. + +### `TonPayWidgetProps` + +```ts +type TonPayWidgetProps = { + amount: number | string; + recipientWalletAddress: string; + asset?: string; + bgColor?: string; + borderRadius?: number | string; + chain?: "mainnet" | "testnet"; + className?: string; + comment?: string; + currency?: string; + disabled?: boolean; + fontFamily?: string; + height?: number | string; + isLoading?: boolean; + itemTitle?: string; + onClose?: () => void; + onError?: (payload: WidgetErrorPayload) => void; + onReady?: () => void; + onRouteChange?: (payload: WidgetRouteChangedPayload) => void; + onSuccess?: (payload: WidgetSuccessPayload) => void; + referenceId?: string; + style?: React.CSSProperties; + text?: string; + textColor?: string; + variant?: "long" | "short"; + width?: number | string; +}; +``` + + + +### `WidgetErrorPayload` + +```ts +type WidgetErrorPayload = { + code: + | "unknown" + | "invalid_intent" + | "route_blocked" + | "wallet_disconnected" + | "storage_access_denied" + | "network_error" + | "setup_required" + | "setup_failed" + | "unlock_failed" + | "payment_failed" + | "insufficient_balance" + | "signless_stopped"; + message: string; + recoverable: boolean; + route?: string; +}; +``` + +### `WidgetRouteChangedPayload` + +```ts +type WidgetRouteChangedPayload = { + pathname: string; + search: string; +}; ``` -## Imports +### `WidgetSuccessPayload` + +```ts +type WidgetSuccessPayload = { + action: "setup" | "payment" | "topup" | "withdraw" | "reset"; + reference?: string; + referenceId?: string; + sessionId?: string; + txHash?: string; + walletAddress?: string; +}; +``` + +### Checkout prop notes + +- `comment` – payment comment added to the blockchain transaction. It maps to `commentToRecipient`. +- `itemTitle` – UI-only text displayed in the widget. + +## `@ton-pay/api` ```ts -// API helpers import { + TON, + USDT, createTonPayTransfer, getTonPayTransferByBodyHash, getTonPayTransferByReference, - type CompletedTonPayTransferInfo, - type CreateTonPayTransferParams, - type CreateTonPayTransferResponse, + verifySignature, + type TransferCompletedWebhookPayload, + type TransferRefundedWebhookPayload, + type WebhookPayload, } from "@ton-pay/api"; - -// React UI -import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; ``` -## Functions +Use `@ton-pay/api` for backend and non-widget flows such as: + +- transfer creation for TON Connect or custom payment flows; +- transfer status lookup by `reference` or `bodyBase64Hash`; +- MoonPay helper calls outside the hosted widget flow. + +### Functions -### `createTonPayTransfer(params, options?)` +#### `createTonPayTransfer(params, options?)` Build a canonical message and return tracking identifiers. @@ -38,7 +156,7 @@ Build a canonical message and return tracking identifiers. - `options`: `APIOptions`. - `options.chain`: `mainnet | testnet`. -### `getTonPayTransferByBodyHash(bodyHash, options?)` +#### `getTonPayTransferByBodyHash(bodyHash, options?)` Fetch a transfer by Base64 hash of the signed message body content (payload). @@ -46,7 +164,7 @@ Fetch a transfer by Base64 hash of the signed message body content (payload). - `options`: `APIOptions`. - Return `CompletedTonPayTransferInfo`. -### `getTonPayTransferByReference(reference, options?)` +#### `getTonPayTransferByReference(reference, options?)` Fetch a transfer by reference. @@ -54,17 +172,7 @@ Fetch a transfer by reference. - `options`: `APIOptions`. - Return `CompletedTonPayTransferInfo`. -### `useTonPay(options?)` - -A React hook. Connect a wallet through [TON Connect](/ecosystem/ton-connect/overview) and send a transaction. - -- `pay(getMessage)`: Receive `senderAddr`, request `{ message }` from the factory, and send through TON Connect. Resolve `{ txResult, ...factoryReturn }`. - -### `TonPayButton` - -Prebuilt button. Handle wallet connect or disconnect flow and call `handlePay`. - -## Types: `@ton-pay/api` +### Types: `@ton-pay/api` ### `CreateTonPayTransferParams` @@ -118,13 +226,13 @@ type CompletedTonPayTransferInfo = { ``` - `status`: `pending`, `success`, or `error`; -- `errorCode`: TON Pay error codes in [Check status and retrieve info](/ecosystem/ton-pay/payment-integration/status-info). +- `errorCode`: TON Pay error code returned for failed transfers, when available. ### `APIOptions` ```ts type APIOptions = { - chain: "mainnet" | "testnet"; + chain?: "mainnet" | "testnet"; }; ``` @@ -153,7 +261,6 @@ import { TON, USDT } from "@ton-pay/api"; All API helpers throw `Error` with an HTTP `cause` if the network call fails. For example, `createTonPayTransfer` may throw "Failed to create TON Pay transfer". -## Peer dependencies +## See also -- [`@tonconnect/ui-react`](https://www.npmjs.com/package/@tonconnect/ui-react) – React UI kit for TON Connect SDK. -- [React 18](https://react.dev/) or later and `react-dom` 18 or later. +- End-to-end [server-side integration](/ecosystem/ton-pay/payment-integration/payments-server-side) examples diff --git a/ecosystem/ton-pay/on-ramp.mdx b/ecosystem/ton-pay/on-ramp.mdx index 32f7e6876..9612d12ab 100644 --- a/ecosystem/ton-pay/on-ramp.mdx +++ b/ecosystem/ton-pay/on-ramp.mdx @@ -5,87 +5,36 @@ sidebarTitle: "On-ramp" import { Aside } from '/snippets/aside.jsx'; -The TON Pay SDK includes a built-in fiat-to-crypto on-ramp powered by [MoonPay](https://www.moonpay.com/). It allows users to top up their wallets with a bank card directly from the payment modal. +TON Pay includes a built-in fiat-to-crypto on-ramp powered by [MoonPay](https://www.moonpay.com/). Within `TonPayWidget`, MoonPay appears as an embedded top-up step when the user needs additional funds to complete checkout. -## How it works - -When a user opens the payment modal and the connected wallet balance is insufficient, the SDK offers a "Top up by card" option. The flow proceeds as follows: - -1. Balance check. The SDK verifies whether the connected wallet has funds to complete the payment. -1. Geographic and limit check. The SDK verifies that card purchases are supported in the user's region and retrieves the provider's minimum and maximum purchase limits. -1. Amount calculation. The SDK computes the required top-up amount, clamped to the provider's minimum. -1. Widget rendering. The MoonPay purchase widget loads in an iframe. The purchase transfers funds directly to the user's wallet. -1. Balance polling. After the provider reports transaction completion, the SDK polls the wallet balance every 5 seconds. -1. Auto-pay. Once the wallet balance becomes sufficient, the SDK displays a modal to complete the merchant payment. - -Funds are always credited to the user’s wallet before any transfer to the merchant occurs. - -## How to enable - -The [`TonPayButton` React component](/ecosystem/ton-pay/ui-integration/button-react) enables the on-ramp by default. To turn it off, set `isOnRampAvailable` to `false`: - -```tsx - -``` - -When `isOnRampAvailable` is `false`, the "Top up by card" option is hidden. Payments can only be completed using the current wallet balance. - -## Supported assets +It allows users to purchase crypto with a bank card and receive it directly in the wallet used for the payment. The merchant payment is completed after the funds arrive and the balance is sufficient. -The SDK supports the following assets for on-ramp top-ups: - -| Token | Master address | -| :------------- | :------------------------------------------------- | -| TON | Native | -| USDT | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` | -| DOGS | `EQCvxJy4eG8hyHBFsZ7eePxrRsUQSFE_jpptRAYBmcG_DOGS` | -| Notcoin | `EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT` | -| Hamster Kombat | `EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo` | -| Catizen | `EQD-cvR0Nz6XAyRBvbhz-abTrRC6sI5tvHvvpeQraV9UAAD7` | - -## Geographic restrictions - -Before displaying the card top-up option, the SDK checks the user's geographic eligibility with the MoonPay provider. If card purchases are not supported in the user's country, the option is not displayed. - -The SDK performs this check using the user's IP address. If the integration proxies requests or omits the original client IP address, provide the user's IP explicitly using the `userIp` prop. - -## Purchase limits - -The SDK retrieves minimum and maximum purchase limits from MoonPay for the target asset. These limits are enforced automatically: +## How it works -- If the calculated top-up amount is below the minimum, the SDK increases it to `minAmount * 1.05`. -- The provider determines the maximum purchase amount; this cannot be overridden. +When the user opens `TonPayWidget`, TON Pay evaluates the checkout state. If the wallet balance is insufficient, the hosted flow offers an on-ramp step. -Limits use the quote currency returned by the provider, e.g., USD, and vary by region and payment method. +1. Balance check. TON Pay verifies whether the connected wallet has enough funds for the requested payment. +1. MoonPay availability check. TON Pay checks whether the session is eligible for MoonPay, including regional availability, supported assets, and provider-side limits. +1. Amount calculation. TON Pay calculates the top-up amount required to continue checkout. +1. Widget rendering. If the session is eligible, the MoonPay purchase widget opens inside the hosted TON Pay flow. +1. Wallet funding. The purchased assets are sent directly to the user's wallet, not to the merchant. +1. Checkout resume. After the balance becomes sufficient, the user can continue and complete the merchant payment in the same flow. -## Props reference +## Regional availability -| Prop | Type | Default | Description | -| :------------------ | :-------- | :------ | :---------------------------------------------------- | -| `isOnRampAvailable` | `boolean` | `true` | Show or hide the card top-up option. | -| `userIp` | `string` | – | User's IP address for the geographic check; optional. | +TON Pay offers the on-ramp step only in regions where MoonPay supports purchases. If unavailable, the widget does not offer card-based top-ups, and checkout can proceed only with the existing balance or externally added funds. -## Iframe events +Availability also depends on provider rules for the selected asset, payment method, and transaction amount. -The on-ramp widget communicates through `postMessage`. The SDK handles the following events internally: +## Merchant integration -| Event | Description | -| :------------------------------------------------ | :-------------------------------------------------------------------------------------- | -| `TONPAY_IFRAME_LOADED` | The on-ramp widget loaded successfully. | -| `TONPAY_MOONPAY_EVENT` (`onTransactionCompleted`) | The MoonPay purchase completes successfully. The SDK starts polling the wallet balance. | -| `TONPAY_MOONPAY_EVENT` (`onTransactionFailed`) | The MoonPay purchase failed. The user may retry the transaction. | -| `TONPAY_PAYMENT_ERROR` | On-ramp widget initialization error; e.g., the link expired. | +No additional on-ramp setup is required for the default hosted flow. Render [`TonPayWidget`](/ecosystem/ton-pay/ui-integration/button-react) with the checkout parameters, and TON Pay determines whether to show the MoonPay top-up step. -Merchants using `TonPayButton` do not need to handle these events manually. +For [custom server-side flows](/ecosystem/ton-pay/payment-integration/payments-server-side) instead of the hosted widget, use `@ton-pay/api`, including MoonPay-related helper calls outside the widget flow. diff --git a/ecosystem/ton-pay/overview.mdx b/ecosystem/ton-pay/overview.mdx index ce772bb1e..4f1babf42 100644 --- a/ecosystem/ton-pay/overview.mdx +++ b/ecosystem/ton-pay/overview.mdx @@ -91,16 +91,21 @@ TON Pay SDK is compatible with TON wallets through the [TON Connect protocol](/e While the SDK provides full programmatic control over payments, built-in UI components reduce integration time. -The `TonPayButton` React component handles wallet connection, transaction signing, loading states, and error notifications out of the box without requiring manual integration with TON Connect hooks or UI state management. It also covers common edge cases such as duplicate submissions, wallet disconnects, and user-visible errors. +- UI components render a payment button and open the hosted TON Pay widget. +- The host application passes checkout parameters such as amount, recipient wallet address, asset, and comment. +- TON Pay handles session bootstrap, checkout routing, wallet connection, transaction signing, and result handling. -For most web applications, the UI layer provides a functional checkout flow. The guide on [how to add a TON Pay button using React](/ecosystem/ton-pay/ui-integration/button-react) provides step-by-step instructions. +UI components provide a complete checkout experience for web applications. Select an integration based on the application stack: + +- React – React component [`TonPayWidget`](/ecosystem/ton-pay/ui-integration/button-react). +- JavaScript – framework-agnostic SDK [`@ton-pay/ui`](/ecosystem/ton-pay/ui-integration/button-js). ## How TON Pay works 1. TON Pay backend generates a transaction message. -1. The user reviews and signs the transaction in the wallet. +1. The wallet presents the transaction for review and requires a signature. 1. The transaction is executed and confirmed on the TON blockchain. -1. TON Pay backend monitors the transaction status and sends webhook notifications to the application upon completion. +1. After submitting a payment for signing, the SDK allows the application to track its status using `reference` or `bodyBase64Hash`. ## Security diff --git a/ecosystem/ton-pay/payment-integration/migration.mdx b/ecosystem/ton-pay/payment-integration/migration.mdx new file mode 100644 index 000000000..2efa9b0f2 --- /dev/null +++ b/ecosystem/ton-pay/payment-integration/migration.mdx @@ -0,0 +1,122 @@ +--- +title: "How to migrate from version 0.3.2 and lower" +sidebarTitle: "Migrate from version 0.3.2" +--- + +import { Aside } from "/snippets/aside.jsx"; + +This guide covers migration from `@ton-pay/ui-react` `v0.3.2` and lower, which use `TonPayButton` and `useTonPay`, to the `v0.4` hosted widget integration using `TonPayWidget`. + + + +## Server-side unchanged + +These server-side components remain unchanged despite the update to the React UI package: + +- `createTonPayTransfer()`; +- `getTonPayTransferByReference()`; +- `getTonPayTransferByBodyHash()`; +- merchant-side order storage for `reference`, `bodyBase64Hash`, and `txHash`. + +## Migration mapping + +| Legacy React flow | Hosted widget flow | Notes | +| ----------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- | +| `TonConnectUIProvider` | Not required for the default widget flow | `TonPayWidget` owns wallet interaction inside the hosted UI. | +| `TonPayButton` | `TonPayWidget` | The visible button and checkout launcher are the same component. | +| `useTonPay().pay(getMessage)` | `TonPayWidget` props + callbacks | Pass checkout parameters directly as props and handle outcomes with `onSuccess` or `onError`. | +| Manual TON Connect transaction submission | Built into the hosted widget | No direct TON Connect send step is needed for the default hosted checkout. | +| Manual session or intent handling | Internal to `TonPayWidget` | The widget resolves the base URL, creates sessions, and routes the checkout flow. | +| `@ton-pay/api` for backend tracking | Unchanged | Keep using it for backend or custom non-widget flows. | + +## Migration checklist + +1. Update the frontend package `@ton-pay/ui-react` to version `0.4`. +1. Remove `TonConnectUIProvider` from the TON Pay entry path if it was used only for the legacy checkout. +1. Replace `TonPayButton` and `useTonPay` with `TonPayWidget`. +1. Pass checkout parameters through widget props: `amount`, `recipientWalletAddress`, `asset`, `comment`, and `itemTitle`. +1. Replace legacy client-side success or error handling with `onSuccess`, `onError`, and `onClose` callbacks. +1. Keep existing server-side lookups for backend reconciliation or non-widget flows. +1. Validate the hosted flow on testnet before switching to mainnet. + +## Compare legacy and hosted widget flows + +### Before: legacy React flow + +```tsx +import { TonConnectUIProvider } from "@tonconnect/ui-react"; +import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; + +function CheckoutButton() { + const { pay } = useTonPay(); + + async function handlePay() { + await pay(async (senderAddr: string) => { + const response = await fetch("/api/create-payment", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ senderAddr }), + }); + + return response.json(); + }); + } + + return ; +} + +export default function App() { + return ( + + + + ); +} +``` + +### After: hosted widget flow + +```tsx +import { TonPayWidget } from "@ton-pay/ui-react"; + +export default function Checkout() { + return ( + { + console.error(payload.code, payload.message); + }} + onSuccess={(payload) => { + console.log("Payment completed", payload); + }} + /> + ); +} +``` + +## Migration notes + +- Backend endpoints that existed only to build TON Connect messages for `useTonPay` are no longer required for the default hosted widget checkout. +- Keep endpoints that support custom flows, reconciliation jobs, or audit trails, continuing to use `@ton-pay/api`. +- In the widget flow, `comment` maps to `commentToRecipient`. +- `itemTitle` is UI-only metadata displayed inside the hosted checkout. + +## Rollout validation + +- Verify the hosted widget opens and closes correctly on testnet. +- Verify `onSuccess` and `onError` handlers receive the expected payloads. +- Verify no payment screen depends on `TonConnectUIProvider`, `useTonPay`, or direct TON Connect send logic. +- Verify transfer lookups still function for any remaining backend or custom flows. +- Verify the final production configuration uses the intended chain, asset, recipient address, and callback logging. + +## See also + +- [How to add a TON Pay widget using React](/ecosystem/ton-pay/ui-integration/button-react) +- [How to send payments using TON Pay React widget](/ecosystem/ton-pay/payment-integration/payments-react) diff --git a/ecosystem/ton-pay/payment-integration/payments-react.mdx b/ecosystem/ton-pay/payment-integration/payments-react.mdx index b6466022e..fe7b2535b 100644 --- a/ecosystem/ton-pay/payment-integration/payments-react.mdx +++ b/ecosystem/ton-pay/payment-integration/payments-react.mdx @@ -1,505 +1,87 @@ --- -title: "How to send payments using TON Pay React hook" -sidebarTitle: "Send payments React" +title: "How to send payments using TON Pay React widget" +sidebarTitle: "Send payments with React" --- -import { Aside } from '/snippets/aside.jsx'; +`TonPayWidget` is the React component for TON Pay [`v0.4`](https://www.npmjs.com/package/@ton-pay/api?activeTab=versions). The hosted widget manages checkout state internally and returns lifecycle callbacks to the host app. -The `useTonPay` hook provides a React interface for creating TON Pay transfers. It integrates with [TON Connect](/ecosystem/ton-connect/overview) to connect a wallet, sign transactions, and surface transfer errors through the hook state. - -## How `useTonPay` works - -The `useTonPay` hook manages the client-side flow for sending a TON Pay transfer. It performs the following steps: - -1. Checks whether a wallet is connected and, if not, opens the TON Connect modal and waits for a successful connection. -1. Calls an application-provided factory function that builds the transaction message, either in the client-side or by requesting it from a backend service. -1. Sends the transaction message to the connected wallet for user approval and signing. -1. Returns the transaction result along with tracking identifiers that can be used for reconciliation. - -## Integration approaches - -`useTonPay` can be integrated in two ways, depending on where the transaction message is created. - -### Client-side message building - -- Message construction happens in the browser. -- All transaction fields are provided by the client. - -### Server-side message building - -- Message construction happens on the backend. -- Tracking identifiers are stored on the server before the message is returned to the client for signing. - -## Client-side implementation - -### Prerequisites - -The application must be wrapped with the TON Connect UI provider: +## Recommended integration ```tsx -import { TonConnectUIProvider } from "@tonconnect/ui-react"; +import { TonPayWidget } from "@ton-pay/ui-react"; -export function App() { +export function Checkout() { return ( - - {/* Application components */} - + { + console.log("Payment completed", payload); + }} + onError={(payload) => { + console.error("Payment failed", payload); + }} + /> ); } ``` -[The TON Connect manifest](/ecosystem/ton-connect/manifest) file must be publicly accessible and include valid application metadata. Replace `` with the public HTTPS origin that serves `tonconnect-manifest.json`. - -### Basic implementation - -```tsx expandable -import { useTonPay } from "@ton-pay/ui-react"; -import { createTonPayTransfer } from "@ton-pay/api"; - -export function PaymentButton() { - const { pay } = useTonPay(); - - const handlePayment = async () => { - try { - const { txResult, message, reference, bodyBase64Hash } = await pay( - async (senderAddr: string) => { - const result = await createTonPayTransfer( - { - amount: 3.5, - asset: "TON", - recipientAddr: "", - senderAddr, - commentToSender: "Payment for Order #8451", - }, - { - chain: "testnet", - // Optional API key can be shown on the client-side, but the secret key never - apiKey: "", // optional - } - ); - - return { - message: result.message, - reference: result.reference, - bodyBase64Hash: result.bodyBase64Hash, - }; - } - ); - - console.log("Transaction completed:", txResult.boc); - console.log("Reference for tracking:", reference); - console.log("Body hash:", bodyBase64Hash); - - // Store tracking identifiers in the database - await savePaymentRecord({ - reference, - bodyBase64Hash, - amount: 3.5, - asset: "TON", - }); - } catch (error) { - console.error("Payment failed:", error); - // Handle error appropriately - } - }; - - return ; -} -``` - -### Response fields - -The `pay` function returns the following fields: +## Widget capabilities - - Transaction result returned by TON Connect. It contains the signed transaction [bag of cells](/foundations/serialization/boc) and additional transaction details. - +- Preload the widget session automatically. +- Open the iframe checkout on click. +- Manage wallet connection inside the widget. +- Handle checkout routing and result states. +- Handles payment status on the TON Pay side. +- Forwards success and error payloads to the host app. - - The transaction message that was sent, including the recipient address, amount, and payload. - +## Success handling - - Unique tracking identifier for this transaction. Use it to correlate webhook notifications with orders. - - - - - - Base64-encoded hash of the transaction body. Can be used for advanced transaction verification. - - -## Server-side implementation - -### Backend endpoint - -Create an API endpoint that builds the transaction message and stores tracking data: - -```typescript expandable -import { createTonPayTransfer } from "@ton-pay/api"; - -app.post("/api/create-payment", async (req, res) => { - const { amount, senderAddr, orderId } = req.body; - - try { - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr: "", - senderAddr, - commentToSender: `Payment for Order ${orderId}`, - commentToRecipient: `Order ${orderId}`, - }, - { - chain: "testnet", - apiKey: "yourTonPayApiKey", // optional - } - ); - - // Store tracking identifiers in your database - await db.createPayment({ - orderId, - reference, - bodyBase64Hash, - amount, - asset: "TON", - status: "pending", - senderAddr, - }); - - // Return only the message to the client - res.json({ message }); - } catch (error) { - console.error("Failed to create payment:", error); - res.status(500).json({ error: "Failed to create payment" }); - } -}); +```tsx + { + console.log("action:", payload.action); + console.log("reference:", payload.reference); + console.log("txHash:", payload.txHash); + }} +/> ``` - - -### Frontend implementation - -```tsx expandable -import { useTonPay } from "@ton-pay/ui-react"; - -export function ServerPaymentButton({ - orderId, - amount, -}: { - orderId: string; - amount: number; -}) { - const { pay } = useTonPay(); - - const handlePayment = async () => { - try { - const { txResult } = await pay(async (senderAddr: string) => { - const response = await fetch("/api/create-payment", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amount, senderAddr, orderId }), - }); - - if (!response.ok) { - throw new Error("Failed to create payment"); - } - - const { message } = await response.json(); - return { message }; - }); - - console.log("Transaction sent:", txResult.boc); - - // Optionally redirect or show success message - window.location.href = `/orders/${orderId}/success`; - } catch (error) { - console.error("Payment failed:", error); - alert("Payment failed. Please try again."); - } - }; - - return ; -} -``` +For direct wallet payments, `reference` is preserved in the success payload when available. ## Error handling -The `useTonPay` hook throws errors in the following scenarios: - -### Wallet connection errors - -```typescript -const { pay } = useTonPay(); - -try { - await pay(getMessage); -} catch (error) { - if (error.message === "Wallet connection modal closed") { - // User closed the connection modal without connecting - console.log("User cancelled wallet connection"); - } else if (error.message === "Wallet connection timeout") { - // Connection attempt exceeded 5-minute timeout - console.log("Connection timeout - please try again"); - } -} -``` - -### Transaction errors - -```typescript -try { - await pay(getMessage); -} catch (error) { - // User rejected the transaction in their wallet - if (error.message?.includes("rejected")) { - console.log("User rejected the transaction"); - } - - // Network or API errors - else if (error.message?.includes("Failed to create TON Pay transfer")) { - console.log("API error - check your configuration"); - } - - // Other unexpected errors - else { - console.error("Unexpected error:", error); - } -} +```tsx + { + console.error(payload.code, payload.message); + }} +/> ``` -## Best practices - -- Persist tracking identifiers immediately. - - ```typescript - // Good: Store before transaction - const { message, reference } = await createTonPayTransfer(...); - await db.createPayment({ reference, status: "pending" }); - return { message }; - - // Bad: Only storing after successful transaction - const { txResult, reference } = await pay(...); - await db.createPayment({ reference }); // Too late if network fails - ``` +Session bootstrap failures are normalized to `WidgetErrorPayload` and returned through `onError`. -- Always validate or generate amounts server-side to prevent manipulation. Never trust amount values sent from the client. - - ```typescript - // Server-side endpoint - app.post('/api/create-payment', async (req, res) => { - const { orderId, senderAddr } = req.body; - - // Fetch the actual amount from your database - const order = await db.getOrder(orderId); - - // Use the verified amount, not req.body.amount - const { message } = await createTonPayTransfer({ - amount: order.amount, - asset: order.currency, - recipientAddr: '', - senderAddr, - }); - - res.json({ message }); - }); - ``` - -- Wrap the payment components with React error boundaries to handle failures. - - ```tsx - class PaymentErrorBoundary extends React.Component { - componentDidCatch(error: Error) { - console.error('Payment component error:', error); - // Log to the error tracking service - } - - render() { - return this.props.children; - } - } - ``` - -- Payment processing can take several seconds. Provide clear feedback to users during wallet connection and transaction signing. - - ```tsx - const [loading, setLoading] = useState(false); - - const handlePayment = async () => { - setLoading(true); - try { - await pay(getMessage); - } finally { - setLoading(false); - } - }; - - return ( - - ); - ``` - -- Use environment variables for sensitive data and chain configuration. - - ```typescript - const { message } = await createTonPayTransfer(params, { - chain: process.env.TON_CHAIN as 'mainnet' | 'testnet', - apiKey: process.env.TONPAY_API_KEY, // optional - }); - ``` - -## Troubleshooting - - - Wrap the application with `TonConnectUIProvider`: - - ```tsx - import { TonConnectUIProvider } from "@tonconnect/ui-react"; - - function App() { - return ( - - - - ); - } - ``` - - - - 1. Verify that the TON Connect manifest URL is accessible and valid. - 1. Check the browser console for TON Connect initialization errors. - 1. Ensure the manifest file is served with correct [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers. - 1. Open the manifest URL directly in the browser to verify it loads. - - - - 1. Verify that the recipient address is [a valid TON address](/foundations/addresses/formats). - 1. Ensure the address format matches the selected chain; mainnet or testnet. - 1. Verify that the address includes the full base64 representation with workchain. - - - - 1. Check whether the optional API key is missing or invalid for endpoints that require authentication. - 1. Verify network connectivity. - 1. Validate parameter values. For example, non-negative amounts and valid addresses. - 1. Confirm the correct chain configuration; mainnet or testnet. - - To inspect the underlying API error: - - ```ts - try { - await createTonPayTransfer(...); - } catch (error) { - console.error("API Error:", error.cause); // HTTP status text - } - ``` - - - - 1. Note that TON Connect may not emit an explicit error on rejection. - 1. Add timeout handling: - - ```ts - const paymentPromise = pay(getMessage); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error("Transaction timeout")), 60000) - ); - - await Promise.race([paymentPromise, timeoutPromise]); - ``` - - -## Optional API key configuration - -When using `useTonPay` with server-side message building, the optional API key can be included in the backend endpoint to enable dashboard features and webhooks: - -```typescript -// Backend endpoint -app.post("/api/create-payment", async (req, res) => { - const { amount, senderAddr, orderId } = req.body; - - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr: process.env.MERCHANT_WALLET_ADDRESS!, - senderAddr, - }, - { - chain: "testnet", - apiKey: process.env.TONPAY_API_KEY, // Optional: enables dashboard features - } - ); - - await db.createPayment({ orderId, reference, bodyBase64Hash }); - res.json({ message }); -}); -``` +- `comment` – payment comment added to the blockchain transaction. +- `itemTitle` – UI-only text displayed in the widget. -## Testnet configuration +## When to use server-side integration - +Use [`@ton-pay/api`](/ecosystem/ton-pay/payment-integration/payments-server-side) only when the application requires flows outside the default widget checkout, such as: -A complete description of testnet configuration, including obtaining testnet TON, testing jetton transfers, verifying transactions, and preparing for mainnet deployment, is available in the [Testnet configuration](/ecosystem/ton-pay/payment-integration/transfer#testnet-configuration) section. +- backend transfer creation; +- explicit transfer status lookups by `reference` or `bodyBase64Hash`. -## Next steps +## Migration from version 0.3.2 or lower - - - Receive real-time notifications when payments complete. - +The current React integration is based on `TonPayWidget`, not the legacy `TonPayButton` + `useTonPay` integration. - - Query payment status using reference or body hash. - - +Use the [migration guide](/ecosystem/ton-pay/payment-integration/migration) for the upgrade checklist and before-and-after examples. diff --git a/ecosystem/ton-pay/payment-integration/payments-server-side.mdx b/ecosystem/ton-pay/payment-integration/payments-server-side.mdx new file mode 100644 index 000000000..bed5af1c5 --- /dev/null +++ b/ecosystem/ton-pay/payment-integration/payments-server-side.mdx @@ -0,0 +1,131 @@ +--- +title: "How to run a server-side TON Pay integration" +sidebarTitle: "Server-side integration" +--- + +import { Aside } from '/snippets/aside.jsx'; + +Use `@ton-pay/api` when the application requires server-side transfer creation, transfer status lookups, or MoonPay helper calls outside the hosted widget flow. + +## Install + +```bash +npm install @ton-pay/api +``` + +## API options + +```ts +type APIOptions = { + chain?: "mainnet" | "testnet"; +}; +``` + + + +## Create a transfer + +```ts +import { TON, createTonPayTransfer } from "@ton-pay/api"; + +const { message, bodyBase64Hash, reference } = await createTonPayTransfer( + { + amount: 10.5, + asset: TON, + recipientAddr: "", + senderAddr: "", + commentToSender: "Payment for Order #1234", + commentToRecipient: "Order #1234", + }, + { + chain: "testnet", + }, +); +``` + +Persist `reference` and `bodyBase64Hash` before sending the transaction to the client. These values are the canonical identifiers for status lookups. + +### `CreateTonPayTransferResponse` + +```ts +type CreateTonPayTransferResponse = { + message: { + address: string; + amount: string; + payload: string; + }; + bodyBase64Hash: string; + reference: string; +}; +``` + +## Check transfer status + +### By reference + +```ts +import { getTonPayTransferByReference } from "@ton-pay/api"; + +const info = await getTonPayTransferByReference(reference, { + chain: "testnet", +}); +``` + +### By body hash + +```ts +import { getTonPayTransferByBodyHash } from "@ton-pay/api"; + +const info = await getTonPayTransferByBodyHash(bodyBase64Hash, { + chain: "testnet", +}); +``` + +### `CompletedTonPayTransferInfo` + +```ts +type CompletedTonPayTransferInfo = { + amount: string; + rawAmount: string; + senderAddr: string; + recipientAddr: string; + asset: string; + assetTicker?: string; + status: string; + reference: string; + bodyBase64Hash: string; + txHash: string; + traceId: string; + commentToSender?: string; + commentToRecipient?: string; + date: string; + errorCode?: number; + errorMessage?: string; +}; +``` + +Use lookup helpers for backend reconciliation, TON Connect flows, or custom payment UIs. They are not required for `TonPayWidget` checkout. + +## Asset constants + +```ts +import { TON, USDT } from "@ton-pay/api"; +``` + +- `TON` – Toncoin asset identifier. +- `USDT` – mainnet USDT jetton master address. + +For testnet, pass the correct testnet jetton master address explicitly instead of using mainnet constants, such as `USDT`. + +## See also + +- [Quick start](/ecosystem/ton-pay/quick-start) +- [Add TON Pay widget React](/ecosystem/ton-pay/ui-integration/button-react) +- [API reference](/ecosystem/ton-pay/api-reference) diff --git a/ecosystem/ton-pay/payment-integration/payments-tonconnect.mdx b/ecosystem/ton-pay/payment-integration/payments-tonconnect.mdx deleted file mode 100644 index b396b90b6..000000000 --- a/ecosystem/ton-pay/payment-integration/payments-tonconnect.mdx +++ /dev/null @@ -1,728 +0,0 @@ ---- -title: "How to send payments using TON Connect" -sidebarTitle: "Send payments TON Connect" ---- - -import { Aside } from '/snippets/aside.jsx'; - -Use TON Connect native `sendTransaction` method when: - -- The application manages wallet connection UI and transaction flow directly. -- TON Connect is already used elsewhere in the application. -- Access to TON Connect-specific APIs is required, such as status change listeners or custom wallet adapters. - -## Integration overview - -Direct TON Connect integration follows these steps: - -1. Configure the TON Connect UI provider with the application manifest. -1. Manage wallet connection state and provide UI for users to connect the wallets. -1. Create a transaction message on the client or backend using `createTonPayTransfer`. -1. Send the transaction using TON Connect's `sendTransaction` method. -1. Observe connection state changes and transaction status updates. - -## React implementation - -### Set up application - -Wrap the application with the TON Connect UI provider: - -```tsx -import { TonConnectUIProvider } from "@tonconnect/ui-react"; - -export function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} -``` - -The manifest URL must be publicly accessible and served over HTTPS in production. Replace `` with the public HTTPS origin that serves `tonconnect-manifest.json`. [The manifest file](/ecosystem/ton-connect/manifest) provides application metadata required for wallet identification. - -### Payment component - -```tsx expandable -import { - useTonAddress, - useTonConnectModal, - useTonConnectUI, -} from "@tonconnect/ui-react"; -import { createTonPayTransfer } from "@ton-pay/api"; -import { useState } from "react"; - -export function PaymentComponent({ orderId, amount }: { orderId: string; amount: number }) { - const address = useTonAddress(true); // Get user-friendly address format - const { open } = useTonConnectModal(); - const [tonConnectUI] = useTonConnectUI(); - const [loading, setLoading] = useState(false); - - const handlePayment = async () => { - // Check if wallet is connected - if (!address) { - open(); - return; - } - - setLoading(true); - - try { - // Create the transaction message - // Note: For production, consider moving this to a server endpoint - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr: "", - senderAddr: address, - commentToSender: `Payment for Order ${orderId}`, - commentToRecipient: `Order ${orderId}`, - }, - { - chain: "testnet", - // Optional API key can be used client-side - apiKey: "", // optional - } - ); - - // Store tracking identifiers - await fetch("/api/store-payment", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ reference, bodyBase64Hash, orderId, amount }), - }); - - // Send transaction through TonConnect - const result = await tonConnectUI.sendTransaction({ - messages: [message], - validUntil: Math.floor(Date.now() / 1000) + 300, // 5 minutes - from: address, - }); - - console.log("Transaction sent:", result.boc); - - // Handle success - window.location.href = `/orders/${orderId}/success`; - } catch (error) { - console.error("Payment failed:", error); - alert("Payment failed. Please try again."); - } finally { - setLoading(false); - } - }; - - return ( - - ); -} -``` - -### Manage connection state - -Listen for wallet connection status changes using the TON Connect UI API: - -```tsx -import { useEffect } from "react"; -import { useTonConnectUI } from "@tonconnect/ui-react"; - -export function WalletStatus() { - const [tonConnectUI] = useTonConnectUI(); - const [walletInfo, setWalletInfo] = useState(null); - - useEffect(() => { - // Listen for connection status changes - const unsubscribe = tonConnectUI.onStatusChange((wallet) => { - if (wallet) { - console.log("Wallet connected:", wallet.account.address); - setWalletInfo({ - address: wallet.account.address, - chain: wallet.account.chain, - walletName: wallet.device.appName, - }); - } else { - console.log("Wallet disconnected"); - setWalletInfo(null); - } - }); - - return () => { - unsubscribe(); - }; - }, [tonConnectUI]); - - if (!walletInfo) { - return
No wallet connected
; - } - - return ( -
-

Connected: {walletInfo.walletName}

-

Address: {walletInfo.address}

-
- ); -} -``` - -## Server-side message building - -For production applications, build transaction messages on the server to centralize tracking and validation. - -### Backend endpoint - -```typescript expandable -import { createTonPayTransfer } from "@ton-pay/api"; -import { validateWalletAddress } from "./utils/validation"; - -app.post("/api/create-transaction", async (req, res) => { - const { orderId, senderAddr } = req.body; - - try { - // Validate inputs - if (!validateWalletAddress(senderAddr)) { - return res.status(400).json({ error: "Invalid wallet address" }); - } - - // Fetch order details from database - const order = await db.orders.findById(orderId); - if (!order) { - return res.status(404).json({ error: "Order not found" }); - } - - if (order.status !== "pending") { - return res.status(400).json({ error: "Order already processed" }); - } - - // Create transaction message - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount: order.amount, - asset: order.currency || "TON", - recipientAddr: "", - senderAddr, - commentToSender: `Payment for Order ${order.id}`, - commentToRecipient: `Order ${order.id} - ${order.description}`, - }, - { - chain: "testnet", - apiKey: "TONPAY_API_KEY", // optional - } - ); - - // Store tracking identifiers - await db.payments.create({ - orderId: order.id, - reference, - bodyBase64Hash, - amount: order.amount, - asset: order.currency || "TON", - senderAddr, - status: "pending", - createdAt: new Date(), - }); - - // Return message to client - res.json({ message }); - } catch (error) { - console.error("Failed to create transaction:", error); - res.status(500).json({ error: "Failed to create transaction" }); - } -}); -``` - -### Frontend implementation - -```tsx expandable -export function ServerManagedPayment({ orderId }: { orderId: string }) { - const address = useTonAddress(true); - const { open } = useTonConnectModal(); - const [tonConnectUI] = useTonConnectUI(); - const [loading, setLoading] = useState(false); - - const handlePayment = async () => { - if (!address) { - open(); - return; - } - - setLoading(true); - - try { - // Request transaction message from server - const response = await fetch("/api/create-transaction", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ orderId, senderAddr: address }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to create transaction"); - } - - const { message } = await response.json(); - - // Send transaction - const result = await tonConnectUI.sendTransaction({ - messages: [message], - validUntil: Math.floor(Date.now() / 1000) + 300, - from: address, - }); - - console.log("Transaction completed:", result.boc); - - // Navigate to success page - window.location.href = `/orders/${orderId}/success`; - } catch (error) { - console.error("Payment error:", error); - alert(error.message || "Payment failed"); - } finally { - setLoading(false); - } - }; - - return ( - - ); -} -``` - -## Vanilla JavaScript implementation - -For non-React applications, use the TON Connect SDK directly: - -```javascript expandable -import TonConnectUI from "@tonconnect/ui"; -import { createTonPayTransfer } from "@ton-pay/api"; - -const tonConnectUI = new TonConnectUI({ - manifestUrl: "https://yourdomain.com/tonconnect-manifest.json", -}); - -// Connect wallet -async function connectWallet() { - await tonConnectUI.connectWallet(); -} - -// Send payment -async function sendPayment(amount, orderId) { - const wallet = tonConnectUI.wallet; - - if (!wallet) { - await connectWallet(); - return; - } - - try { - // Create transaction message - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr: "", - senderAddr: wallet.account.address, - commentToSender: `Order ${orderId}`, - }, - { chain: "testnet" } - ); - - // Store tracking data - await fetch("/api/store-payment", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ reference, bodyBase64Hash, orderId }), - }); - - // Send transaction - const result = await tonConnectUI.sendTransaction({ - messages: [message], - validUntil: Math.floor(Date.now() / 1000) + 300, - from: wallet.account.address, - }); - - console.log("Payment successful:", result.boc); - } catch (error) { - console.error("Payment failed:", error); - } -} - -// Listen for connection changes -tonConnectUI.onStatusChange((wallet) => { - if (wallet) { - console.log("Wallet connected:", wallet.account.address); - document.getElementById("wallet-address").textContent = wallet.account.address; - } else { - console.log("Wallet disconnected"); - document.getElementById("wallet-address").textContent = "Not connected"; - } -}); -``` - -## Transaction parameters - -### Message structure - -The message object passed to `sendTransaction` must include these fields: - - - Recipient wallet address in [user-friendly format](/foundations/addresses/formats). - - - - Amount to send in nanotons. - - - - Base64-encoded message payload containing transfer details and tracking information. - - -### Transaction options - - - Unix timestamp indicating when the transaction expires. Typically is 5 minutes. - - ```typescript - validUntil: Math.floor(Date.now() / 1000) + 300 - ``` - - - - Sender's wallet address. Must match the connected wallet address. - - - - Network identifier. Usually omitted as it is inferred from the connected wallet. - - -## Error handling - -```typescript -async function handleTransaction() { - try { - const result = await tonConnectUI.sendTransaction({ - messages: [message], - validUntil: Math.floor(Date.now() / 1000) + 300, - from: address, - }); - - return result; - } catch (error) { - // User rejected the transaction - if (error.message?.includes("rejected")) { - console.log("User cancelled the transaction"); - return null; - } - - // Wallet not connected - if (error.message?.includes("Wallet is not connected")) { - console.log("Connect the wallet first"); - tonConnectUI.connectWallet(); - return null; - } - - // Transaction expired - if (error.message?.includes("expired")) { - console.log("Transaction expired, please try again"); - return null; - } - - // Network or other errors - console.error("Transaction failed:", error); - throw error; - } -} -``` - -## Best practices - -- Check wallet connection status before attempting to send transactions. Provide clear UI feedback for connection state. - - ```typescript - const wallet = tonConnectUI.wallet; - - if (!wallet) { - // Show connect button - return; - } - - // Proceed with transaction - ``` - -- Use a reasonable `validUntil` value, typically 5 minutes, to prevent stale transactions while allowing enough time for user confirmation. - - ```typescript - const validUntil = Math.floor(Date.now() / 1000) + 300; // 5 minutes - ``` - -- Ensure the sender address from TON Connect matches the format expected by the backend. Use the [user-friendly format](/foundations/addresses/formats) consistently. - - ```typescript - const address = useTonAddress(true); // true = user-friendly format - ``` - -- Always persist `reference` and `bodyBase64Hash` before sending the transaction. This allows payment reconciliation through webhooks even if the client flow fails after submission. - - ```typescript - // Good: Store first, then send - await storePaymentTracking(reference, bodyBase64Hash); - await tonConnectUI.sendTransaction(...); - - // Bad: Send first, then store - await tonConnectUI.sendTransaction(...); - await storePaymentTracking(reference, bodyBase64Hash); // Might not execute - ``` - -- Implement connection state listeners to update the UI and handle wallet disconnections. - - ```typescript - useEffect(() => { - const unsubscribe = tonConnectUI.onStatusChange((wallet) => { - if (wallet) { - setConnectedWallet(wallet.account.address); - } else { - setConnectedWallet(null); - } - }); - - return unsubscribe; - }, [tonConnectUI]); - ``` - -- Handle transaction rejection explicitly. Treat user rejection as a normal cancellation, not as an error. - - ```typescript - try { - await tonConnectUI.sendTransaction(transaction); - } catch (error) { - if (error.message?.includes("rejected")) { - // Don't show error - user intentionally cancelled - console.log("Transaction cancelled by user"); - } else { - // Show error for unexpected failures - showErrorMessage("Transaction failed"); - } - } - ``` - -## Troubleshooting - - - Ensure the wallet is connected before calling `sendTransaction`: - - ```typescript - if (!tonConnectUI.wallet) { - await tonConnectUI.connectWallet(); - // Wait for connection before proceeding - } - ``` - - Add a connection state check: - - ```typescript - const isConnected = tonConnectUI.wallet !== null; - ``` - - - - 1. Ensure the `from` address matches the connected wallet address. - 1. Verify that the recipient address is [a valid TON address](/foundations/addresses/formats). - 1. Use the wallet address provided by the SDK to avoid format mismatches: - - ```typescript - const address = useTonAddress(true); // Ensure consistent format - ``` - - - - The `validUntil` timestamp may be too short. Increase the validity period to give the user more time to confirm the transaction: - - ```typescript - // Increase from 5 to 10 minutes if needed - validUntil: Math.floor(Date.now() / 1000) + 600 - ``` - - - - Check for the following common issues: - - - The URL is not publicly accessible. - - [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers are not configured correctly. - - The manifest JSON is malformed. - - The URL is not HTTPS; required in production. - - Open the manifest URL directly in a browser to verify that it is accessible. - - - - Ensure that the status change subscription is created once and remains active for the lifetime of the component. - - ```typescript - useEffect(() = >{ - const unsubscribe = tonConnectUI.onStatusChange(handleWalletChange); - return () = >unsubscribe(); // Clean up subscription - }, - [tonConnectUI]); - ``` - - - - `connectWallet()` may be called more than once. Track the connection state: - - ```typescript - const[isConnecting, setIsConnecting] = useState(false); - - const connect = async() = >{ - if (isConnecting) return; - setIsConnecting(true); - try { - await tonConnectUI.connectWallet(); - } finally { - setIsConnecting(false); - } - }; - ``` - - -## Optional API key configuration - -When using TON Connect with server-side message building, [the optional API key can be included](/ecosystem/ton-pay/payment-integration/payments-tonconnect#api-key-configuration) in the backend: - -```typescript -import { createTonPayTransfer } from "@ton-pay/api"; - -app.post("/api/create-transaction", async (req, res) => { - const { orderId, senderAddr } = req.body; - const order = await db.orders.findById(orderId); - - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount: order.amount, - asset: "TON", - recipientAddr: "", - senderAddr: "", - }, - { - chain: "testnet", - apiKey: "TONPAY_API_KEY", // optional - } - ); - - await db.payments.create({ orderId, reference, bodyBase64Hash }); - res.json({ message }); -}); -``` - -## Testnet configuration - - - -### Set up testnet - -Configure the environment to use testnet: - -```bash -# .env.development -TON_CHAIN=testnet -MERCHANT_WALLET_ADDRESS=TESTNET_ADDRESS -``` - -```tsx -import { useTonAddress, useTonConnectUI } from "@tonconnect/ui-react"; -import { createTonPayTransfer } from "@ton-pay/api"; - -export function TestnetPayment({ amount }: { amount: number }) { - const address = useTonAddress(true); - const [tonConnectUI] = useTonConnectUI(); - - const handlePayment = async () => { - if (!address) { - tonConnectUI.connectWallet(); - return; - } - - const { message } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr: "", - senderAddr: "", - }, - { chain: "testnet" } // Use testnet - ); - - const result = await tonConnectUI.sendTransaction({ - messages: [message], - validUntil: Math.floor(Date.now() / 1000) + 300, - from: address, - }); - - console.log("Testnet transaction:", result.boc); - }; - - return ; -} -``` - -## Next steps - - - - Receive real-time notifications when payments complete. - - - - Query payment status using reference or body hash. - - diff --git a/ecosystem/ton-pay/payment-integration/status-info.mdx b/ecosystem/ton-pay/payment-integration/status-info.mdx deleted file mode 100644 index 3157ee74d..000000000 --- a/ecosystem/ton-pay/payment-integration/status-info.mdx +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: "How to check status and retrieve info" -sidebarTitle: "Check status and retrieve info" ---- - -import { Aside } from '/snippets/aside.jsx'; - -After sending, look up the final result and details. The SDK exposes two lookup functions. - -## By body hash - -Use the Base64 hash of the signed message body payload. - -```ts -import { getTonPayTransferByBodyHash } from "@ton-pay/api"; - -const info = await getTonPayTransferByBodyHash(bodyBase64Hash, { - chain: "testnet", -}); -``` - -## By reference - -Use the `reference` returned by `createTonPayTransfer`. - -```ts -import { getTonPayTransferByReference } from "@ton-pay/api"; - -const info = await getTonPayTransferByReference(reference, { - chain: "testnet", -}); -``` - -### Return shape - -```ts -type CompletedTonPayTransferInfo = { - amount: string; - rawAmount: string; - senderAddr: string; - recipientAddr: string; - asset: string; - assetTicker?: string; - status: string; - reference: string; - bodyBase64Hash: string; - txHash: string; - traceId: string; - commentToSender?: string; - commentToRecipient?: string; - date: string; - errorCode?: number; - errorMessage?: string; -}; -``` - -Use `status` to drive UI state and use `reference`, `bodyBase64Hash`, and `txHash` for reconciliation. - - - -## Response fields - - - Human-readable amount with decimals. - - - - Amount in base units; nano for TON and jetton base units. - - - - Sender wallet address. - - - - Recipient wallet address. - - - - Asset address. "TON" for coin or jetton master address. - - - - Possible values: - - - `pending` – the transfer is created and awaits confirmation on the blockchain. - - `success` – the transfer is completed successfully; the trace completes without errors. - - `error` – the transfer fails; the trace completes with an error. - - - - Tracking reference returned at creation time. - - - - Base64 hash of the signed message body content (payload). Used for lookups. - - - - Transaction hash assigned by the network. - - - - Trace identifier for explorer. - - - - Optional note shown to the payer while signing. Public on-chain; avoid confidential data and keep under 120 characters to reduce gas. - - - - Optional note shown to the payee after receipt. Public on-chain; avoid confidential data and keep under 120 characters to reduce gas. - diff --git a/ecosystem/ton-pay/payment-integration/transfer.mdx b/ecosystem/ton-pay/payment-integration/transfer.mdx deleted file mode 100644 index 3ec293c69..000000000 --- a/ecosystem/ton-pay/payment-integration/transfer.mdx +++ /dev/null @@ -1,443 +0,0 @@ ---- -title: "How to build a transfer" -sidebarTitle: "Build a transfer" ---- - -import { Aside } from '/snippets/aside.jsx'; - -Use this server-side helper to generate a canonical TON message payload and a tracking reference. - -```ts -import { createTonPayTransfer } from "@ton-pay/api"; - -const { message, bodyBase64Hash, reference } = await createTonPayTransfer( - { - amount: 10.5, - asset: "TON", - recipientAddr: "", - senderAddr: "", - commentToSender: "Payment for Order #1234", - commentToRecipient: "Order #1234", - }, - { - chain: "testnet", - apiKey: "", // optional - } -); -``` - - - -## Function signature - -```ts -createTonPayTransfer( - params: CreateTonPayTransferParams, - options?: APIOptions -): Promise -``` - -### Transfer parameters - -```ts -type CreateTonPayTransferParams = { - amount: number; - asset: string; - recipientAddr?: string; - senderAddr: string; - queryId?: number; - commentToSender?: string; - commentToRecipient?: string; -}; -``` - -### API options - -```ts -type APIOptions = { - chain?: "mainnet" | "testnet"; - apiKey?: string; // optional -}; -``` - - - Target blockchain network. Use `"mainnet"` for production or `"testnet"` for development and testing. - - - - The optional TON Pay API key from the Merchant Dashboard. When provided, it enables: - - - Transaction visibility in the TON Pay Merchant Dashboard. - - Webhook notifications for completed transactions. - - Receiving wallet management from the Merchant Dashboard. - - -## Parameter details - - - Human-readable payment amount. Decimals are allowed, for example, 10.5 TON. - - - - Asset to transfer. Use "TON" for Toncoin or a jetton master address or constant, for example, USDT. - - - - - - Payee wallet address. Optional if an API key is provided. Defaults to the merchant’s default wallet address configured in the Merchant Dashboard. - - - - Payer wallet address. In UI flows, obtain it from TON Connect. - - - - Jetton only. Numeric identifier embedded into the jetton payload for idempotency and tracking. Ignored for Toncoin payments. - - - - Short note visible to the user in the wallet while signing. - - - - - - Note visible to the recipient after the transfer is received. - - - - -## Predefined asset constants - -Built-in constants can be used instead of raw addresses. - -```ts -import { createTonPayTransfer, TON, USDT } from "@ton-pay/api"; - -// Toncoin transfer -await createTonPayTransfer( - { - amount: 1, - asset: TON, // same as "TON" - recipientAddr: "", // shortened example; replace with a full wallet address - senderAddr: "", // shortened example; replace with a full wallet address - }, - { - chain: "testnet", - apiKey: "", // optional - } -); - -// USDT jetton transfer -await createTonPayTransfer( - { - amount: 25, - asset: USDT, // mainnet USDT jetton master address - recipientAddr: "", // shortened example; replace with a full wallet address - senderAddr: "", // shortened example; replace with a full wallet address - }, - { - chain: "testnet", - apiKey: "", // optional - } -); -``` - - - -Response: - -```ts -type CreateTonPayTransferResponse = { - message: { - address: string; - amount: string; - payload: string; - }; - bodyBase64Hash: string; - reference: string; -}; -``` - -## Response fields - - - Prebuilt TON Connect transaction message. Intended to be passed to `sendTransaction` as `messages: [message]`. - - - - Base64 hash of the signed message body content (payload). Used with `getTonPayTransferByBodyHash`. - - - - Tracking reference string. Used with `getTonPayTransferByReference`. - - -The SDK call returns a ready-to-send message along with identifiers for subsequent status lookups. Always persist tracking identifiers server-side before sending the transaction to the user. - -## Optional API key configuration - -The TON Pay API key is optional but enables merchant features, including transaction visibility in the dashboard, webhook notifications, and centralized wallet management in the TON Pay Merchant Dashboard. - -### Obtain an optional API key - - - - Open the TON Pay Merchant Dashboard and sign in to the merchant account. - - - - Navigate to DeveloperAPI Keys sections to set up the optional API key. - - - - Generate a new API key (optional) or copy an existing one and store it securely. - - - -### Usage in code - -```typescript -import { createTonPayTransfer } from "@ton-pay/api"; - -const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount: 10.5, - asset: "TON", - // recipientAddr is optional when API key is provided - // Will use merchant's default wallet from the Merchant Dashboard - senderAddr: userWalletAddress, - }, - { - chain: "testnet", - apiKey: "", // optional - } -); -``` - -### Optional API key capabilities - -| Capability | With optional API key | Without API key | -| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| Transaction visibility and monitoring | Transfers appear in the TON Pay Merchant Dashboard with status tracking and full transaction history. | Transfers are processed on-chain but are not visible in the Merchant dashboard. | -| Webhook notifications | Real-time HTTP POST notifications are sent for completed and failed payments. | No webhook notifications; payment status must be obtained through the manual polling. | -| Receiving wallet management | Receiving wallets are managed from the TON Pay Merchant Dashboard; addresses can be updated without code changes. | Receiving wallet addresses must be hard-coded in the application. | -| `recipientAddr` handling | Optional; when omitted, the merchant’s default wallet from the Merchant Dashboard is used automatically. | Required for every transfer. | - -With optional API key: `recipientAddr` is optional. - -```ts -const result = await createTonPayTransfer( - { - amount: 10, - asset: "TON", - senderAddr: "", - }, - { - chain: "testnet", - apiKey: "", // optional - } -); -``` - -Without API key (the API key remains optional): `recipientAddr` is required. - -```ts -// Works without API key (it is optional) - transaction processes normally -const result = await createTonPayTransfer( -{ - amount: 10, - asset: "TON", - recipientAddr: "", - senderAddr: "", -}, -{ chain: "testnet" } // No optional apiKey - transaction works but no dashboard features -); -``` - -## Testnet configuration - - - -### Set up testnet - - - - Configure the `chain` option to `"testnet"` in the API calls: - - ```typescript - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount: 5.0, - asset: "TON", - recipientAddr: "", - senderAddr: "", - }, - { - chain: "testnet", // Use testnet - apiKey: "", // optional - } - ); - ``` - - - - Ensure all wallet addresses are valid [testnet addresses](/foundations/addresses/formats). - - - - - Use testnet wallet account: [Tonkeeper](/ecosystem/wallet-apps/tonkeeper) or other TON wallets. - - [Get testnet TON](/ecosystem/wallet-apps/get-coins) from a faucet to test transactions. - - - - If testing with jettons, use the correct testnet jetton master addresses; not mainnet addresses. - - ```typescript - // Example: Testnet USDT (use actual testnet address) - await createTonPayTransfer( - { - amount: 10, - asset: "", // Testnet jetton address - recipientAddr: "", - senderAddr: "", - }, - { chain: "testnet" } - ); - ``` - - - -### Configure environment - -Use environment variables to switch between mainnet and testnet: - -```typescript -// .env.development -TON_CHAIN=testnet -MERCHANT_WALLET_ADDRESS=EQC...TESTNET_ADDRESS - -// .env.production -TON_CHAIN=mainnet -MERCHANT_WALLET_ADDRESS=EQC...MAINNET_ADDRESS -``` - -```typescript -// In the application code -const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { - amount: orderAmount, - asset: "TON", - recipientAddr: "", - senderAddr: "", - }, - { - chain: "testnet", - apiKey: "", // optional - } -); -``` - -### Verify testnet transactions - -Verify testnet transactions using testnet block [explorers](/ecosystem/explorers/overview): - -- [Testnet Tonviewer](https://testnet.tonviewer.com/) -- [Testnet Tonscan](https://testnet.tonscan.org/) - -```typescript -// Replace txHash with the actual transaction hash. -// After transaction completes: -console.log(`View on explorer: https://testnet.tonscan.org/tx/${txHash}`); -``` - -### Apply testing best practices - -- Test all payment outcomes on testnet, including success, failures, user rejections, and edge cases. -- Verify webhook endpoint correctly processes testnet webhooks; payload structure matches mainnet. -- Validate behavior across different amounts, including small, large, and fractional values. -- Ensure `reference` and `bodyBase64Hash` are persisted and usable for status lookups. -- Exercise error paths such as insufficient balance, invalid addresses, and network issues. diff --git a/ecosystem/ton-pay/quick-start.mdx b/ecosystem/ton-pay/quick-start.mdx index 139ace179..caab250a4 100644 --- a/ecosystem/ton-pay/quick-start.mdx +++ b/ecosystem/ton-pay/quick-start.mdx @@ -5,395 +5,133 @@ sidebarTitle: "Quick start" import { Aside } from '/snippets/aside.jsx'; -## TON Connect manifest - -Before installing and setting up the TON Pay SDK, the application must provide a TON Connect manifest, which is a JSON file that defines application metadata. Wallets use the [TON Connect manifest](/ecosystem/ton-connect/manifest) to discover the application. - -## First payment +## Create the first widget - - - ```bash - # API - npm i @ton-pay/api - - # UI (install separately from API) - npm i @ton-pay/ui-react @tonconnect/ui-react - ``` - + Install `@ton-pay/ui-react`. - - ```bash - # API - npm i @ton-pay/api - - # UI (install separately from API) - npm i @ton-pay/ui @tonconnect/ui - ``` - - + ```bash + npm install @ton-pay/ui-react + ``` - TON Pay UI uses TON Connect UI for wallet communication. - - The application must be wrapped with `TonConnectUIProvider` and configured with an absolute URL to the TON Connect manifest. Add `TonConnectUIProvider` at the root of the application. + Add `TonPayWidget` to the page and pass the required props. ```tsx - import { TonConnectUIProvider } from '@tonconnect/ui-react'; - import AppContent from "./AppContent"; + import { TonPayWidget } from "@ton-pay/ui-react"; - export default function App() { - return ( - - - - ); - } + ``` - - - Add a `TonPayButton` and provide a handler. The handler uses `useTonPay` to connect a wallet if needed, send a transaction through TON Connect, and return tracking data for the next step. + Required props: - - `TonPayButton` wraps wallet connect and disconnect UX and invokes the provided handler. - - `useTonPay` accepts an async message factory that receives `senderAddr` and returns `{ message }` along with any tracking fields to propagate. - - Return `reference` from `createTonPayTransfer` so it can be used later with `getTonPayTransferByReference`. + - `amount` – payment amount in asset units. + - `recipientWalletAddress` – recipient wallet address. - The returned `{ message }` is a TON Connect transaction message. `useTonPay` forwards it to the wallet through TON Connect and initiates the transaction send; direct calls to the wallet SDK are not required. + Main checkout props: - In the examples below, replace `` with the recipient wallet address, `` with an optional dashboard API key, and `` with an order label or ID. + - `asset` – asset identifier. Use `TON`, `USDT`, or a jetton master address. + - `chain` – TON Pay environment. Defaults to `mainnet`. + - `itemTitle` – UI label displayed in the widget. + - `comment` – payment comment added to the blockchain transaction. - ```tsx - import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; - import { createTonPayTransfer } from "@ton-pay/api"; + The full list of props is available in the [API reference](/ecosystem/ton-pay/api-reference). - const recipientAddr = ""; - const orderReference = ""; - - // Set chain to "mainnet" in production. - const options = { - chain: "testnet", - - // Pass an API key from the dashboard when available. - apiKey: "", - } as const; - - export default function PayButton() { - const { pay } = useTonPay(); - - async function createMessage(senderAddr: string) { - const { message, reference } = await createTonPayTransfer( - { - amount: 12.34, - asset: "TON", - recipientAddr, - senderAddr, - commentToSender: orderReference, - }, - options - ); - return { message, reference }; - } - - return ( - pay(createMessage)} /> - ); - } - ``` - - - - - - ```ts - // Backend: POST /api/create-payment - import { createTonPayTransfer, TON } from "@ton-pay/api"; - - const recipientAddr = ""; - - // Set chain to "mainnet" in production. - const options = { - chain: "testnet", - - // Pass an API key from the dashboard when available. - apiKey: "", - } as const; - - app.post("/api/create-payment", async (req, res) => { - const { productId, senderAddr } = req.body; - - // Create an order and calculate the amount from the product price. - const amount = 12.23; - const orderId = 1; - - // Create the transfer and get tracking identifiers. - const { message, reference, bodyBase64Hash } = await createTonPayTransfer( - { amount, asset: TON, recipientAddr, senderAddr }, - options - ); - - // Persist identifiers in the database immediately. - - // Return only the message to the client. - res.json({ message }); - }); - ``` - - ```tsx - // Frontend - import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; - - export function PayOrder({ productId }: { productId: string }) { - const { pay } = useTonPay(); - - async function createMessage(senderAddr: string) { - const response = await fetch("/api/create-payment", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ productId, senderAddr }), - }); - const { message } = await response.json(); - return { message }; - } - - return pay(createMessage)} />; - } - ``` - - - - + - `TonPayWidget` renders the payment button and opens the hosted TON Pay widget. + - The host app passes checkout parameters. + - TON Pay handles session bootstrap, checkout routing, wallet connection, transaction signing, and result handling. - Wallet approval does not mean the payment is finalized yet. After `pay(createMessage)` resolves, use the `reference` returned by `createTonPayTransfer` to query TON Pay until the transfer leaves the `pending` state. + Start the React app: - The example below uses React. The same flow applies in other clients: keep the `reference`, poll status, and update the UI when the transfer leaves `pending`. - - Use the SDK flow below: - - 1. Return `reference` from the `createMessage` function together with `message`. - 1. Call `pay(createMessage)` and, once it resolves, read the propagated `reference` from its return value. - 1. Call `getTonPayTransferByReference(reference, options)` with the same `chain` and optional `apiKey` used during transfer creation. - 1. While the SDK returns `status: "pending"`, keep the UI in a loading or "confirming payment" state. - 1. When the status becomes `success`, show the confirmation UI and persist any result fields the application needs, such as `txHash` or `traceId`. - 1. When the status becomes `error`, show the failure state and capture `errorCode` or `errorMessage` for diagnostics. - - In a client-only flow, persist the `reference` after `pay(createMessage)` resolves and returns it. This is enough to resume status checks after a reload during polling, but it does not cover the period while the wallet approval screen is open. To avoid that gap, create the transfer on the server and persist the `reference` before opening the wallet. - - ```tsx title="Result handling example" expandable - import { useState } from "react"; - import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; - import { - createTonPayTransfer, - getTonPayTransferByReference, - type CompletedTonPayTransferInfo, - } from "@ton-pay/api"; - - type PaymentState = "idle" | "sending" | "pending" | "success" | "error"; - - const amount = 12.34; - const recipientAddr = ""; - - // Set chain to "mainnet" in production. - const options = { - chain: "testnet", - - // Pass an API key from the dashboard when available. - apiKey: "", - } as const; - - export default function Checkout() { - const { pay } = useTonPay(); - const [paymentState, setPaymentState] = useState("idle"); - const [reference, setReference] = useState(null); - const [result, setResult] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - - async function createMessage(senderAddr: string) { - const { message, reference } = await createTonPayTransfer( - { - amount, - asset: "TON", - recipientAddr, - senderAddr, - }, - options - ); - - setReference(reference); - return { message, reference }; - } - - // Polls TON Pay until the transfer gets a final status. - async function waitForTransferResult(reference: string) { - for (;;) { - const transfer = await getTonPayTransferByReference(reference, options); - - if (transfer.status === "pending") { - await new Promise((resolve) => setTimeout(resolve, 1000)); - continue; - } - - return transfer; - } - } - - async function handlePay() { - setPaymentState("sending"); - setErrorMessage(null); - setReference(null); - setResult(null); - - try { - const { reference } = await pay(createMessage); - setPaymentState("pending"); - - const transfer = await waitForTransferResult(reference); - - if (transfer.status === "success") { - setResult(transfer); - setPaymentState("success"); - return; - } - - setPaymentState("error"); - setErrorMessage(transfer.errorMessage ?? "Payment failed"); - } catch (error) { - setPaymentState("error"); - setErrorMessage(error instanceof Error ? error.message : "Payment failed"); - } - } - - return ( - <> - - - {paymentState === "pending" && reference && ( -
- Payment submitted. Waiting for blockchain confirmation. Reference: {reference} -
- )} - - {paymentState === "success" && result && ( -
- Payment confirmed. Tx hash: {result.txHash} -
- )} - - {paymentState === "error" && errorMessage && ( -
- Payment failed: {errorMessage} -
- )} - - ); - } + ```bash + npm run dev ``` - The [status lookup guide](/ecosystem/ton-pay/payment-integration/status-info) describes the response fields and lookup methods used in this step. + Open the local app URL and click the button to launch the widget.
+ + ## Full example -This minimal example scaffolds a React app, installs TON Pay dependencies, and renders a working button wired to TON Connect. Replace the manifest URL and `recipientAddr` with the necessary values. +This example scaffolds a React app with Vite, installs `@ton-pay/ui-react`, and renders the `TonPayWidget` with common checkout props and result callbacks. + +Replace `` with a recipient wallet address. ```bash -npx create-react-app my-app --template typescript -cd my-app -npm i @ton-pay/api @ton-pay/ui-react @tonconnect/ui-react +npm create vite@latest tonpay-widget-demo -- --template react-ts +cd tonpay-widget-demo +npm install +npm install @ton-pay/ui-react ``` ```tsx // src/App.tsx -import { TonConnectUIProvider } from "@tonconnect/ui-react"; -import { TonPayButton, useTonPay } from "@ton-pay/ui-react"; -import { createTonPayTransfer } from "@ton-pay/api"; - -const recipientAddr = ""; -const commentToSender = "Order #123"; - -// Set chain to "mainnet" in production. -const options = { - chain: "testnet", - - // Pass an API key from the dashboard when available. - apiKey: "", -} as const; - -function AppContent() { - const { pay } = useTonPay(); - - async function createMessage(senderAddr: string) { - const { message, reference } = await createTonPayTransfer( - { - amount: 12.34, - asset: "TON", - recipientAddr, - senderAddr, - commentToSender, - }, - options - ); - return { message, reference }; - } - - return ( - pay(createMessage)} /> - ); -} +import { TonPayWidget } from "@ton-pay/ui-react"; export default function App() { return ( - - - +
+
+

TON Pay checkout

+ + { + console.error(payload.code, payload.message); + }} + onSuccess={(payload) => { + console.log("Payment completed", payload); + }} + /> +
+
); } ``` +Common optional props: + +- `chain` – TON Pay environment. Defaults to `mainnet`. +- `comment` – payment comment added to the blockchain transaction. +- `itemTitle` – UI-only text displayed in the widget. +- `onSuccess` – callback invoked when the widget completes successfully. +- `onError` – callback invoked on widget runtime or session bootstrap errors. + ## See also -- [Build a transfer](/ecosystem/ton-pay/payment-integration/transfer) -- [Send payments using React](/ecosystem/ton-pay/payment-integration/payments-react) -- [Webhooks](/ecosystem/ton-pay/webhooks) +- [How to add a TON Pay widget using React](/ecosystem/ton-pay/ui-integration/button-react) +- [How to send payments using TON Pay React widget](/ecosystem/ton-pay/payment-integration/payments-react) +- [Server-side integration](/ecosystem/ton-pay/payment-integration/payments-server-side) - [API reference](/ecosystem/ton-pay/api-reference) diff --git a/ecosystem/ton-pay/ui-integration/button-js.mdx b/ecosystem/ton-pay/ui-integration/button-js.mdx index f903a6ef8..4c5f761cb 100644 --- a/ecosystem/ton-pay/ui-integration/button-js.mdx +++ b/ecosystem/ton-pay/ui-integration/button-js.mdx @@ -1,12 +1,12 @@ --- -title: "How to add a TON Pay button using JS" -sidebarTitle: "Add TON Pay button JS" +title: "How to add a TON Pay widget using JS" +sidebarTitle: "Add a TON Pay widget JS" --- import { Aside } from "/snippets/aside.jsx"; import { Image } from "/snippets/image.jsx"; -Add a TON Pay button to a plain HTML/JavaScript page using the embed script or TON Pay client. The default button appearance is shown below. +`@ton-pay/ui` is the framework-agnostic hosted widget SDK for plain HTML/JS pages or non-React integrations. It renders a TON Pay button when needed and opens the checkout UI in a fullscreen iframe appended to `document.body`.
-## Installation +## Install - - - Choose one of the installation methods: +Choose one of the supported delivery methods: - 1. npm - - ```bash - npm install @ton-pay/ui @tonconnect/ui - ``` - - 1. CDN: no installation needed - - ```html - - - ``` - - 1. Local copy - - After npm install, copy `node_modules/@ton-pay/ui/dist/ton-pay-embed.js` to a public assets folder. For example, the site's `public/` directory. - Then include it with a ` - ``` - - - - ```html - - ``` - - - - ```html - - ``` - - +```bash +npm install @ton-pay/ui +``` - +Or load the browser bundle directly: -## Option 2: use TON Pay client - -Use `createTonPay` for custom UI and payment flow control. - - - - ```html - - - - My TON Payment App - - - - -
- - - ``` -
- - - ```html - - ``` - - - - ```html - - ``` - - - -
- -## Create messages with createTonPayTransfer - -Use `createTonPayTransfer` to build a canonical payment message with tracking identifiers. - -Combine the snippets in this section into one HTML page. - - - - ```html - - ``` - - - - ```html - - ``` - - +```html + +``` -## Embed script parameters - -| Parameter | Type | Default | Description | -| -------------- | --------------------------- | ----------------- | -------------------------------------------------------------- | -| `containerId` | string | `"ton-pay-btn"` | Target element ID where the button renders. | -| `preset` | `"default"` \| `"gradient"` | - | Built-in theme preset. | -| `bgColor` | string | `"#0098EA"` | Background color in hex or CSS gradient. URL-encode the value. | -| `textColor` | string | `"#FFFFFF"` | Text and icon color. | -| `variant` | `"long"` \| `"short"` | `"long"` | Button text variant. | -| `text` | string | - | Custom button text. Overrides `variant`. | -| `loadingText` | string | `"Processing..."` | Text shown during loading. | -| `borderRadius` | number | `8` | Border radius in pixels. | -| `fontFamily` | string | `"inherit"` | CSS font-family value. | -| `width` | number | `300` | Button width in pixels. | -| `height` | number | `44` | Button height in pixels. | -| `showMenu` | boolean | `true` | Show dropdown menu with wallet actions. | -| `callback` | string | - | Global function name called on click. | - -### Examples - - - ```html Default blue button - - ``` - - ```html Gradient button - - ``` - - ```html Custom colors - - ``` - - ```html Custom size - - ``` - - - - -## TonPayEmbed API - -The embed script exposes a global `TonPayEmbed` object for programmatic control. - -### `TonPayEmbed.mount(config)` +## Option 1: render the built-in button from a script tag -Update button configuration dynamically. - -```js -TonPayEmbed.mount({ - preset: "gradient", - variant: "short", - borderRadius: 12, - width: 350 -}); -``` - -### `TonPayEmbed.setCallback(functionName)` - -Change the callback function. +```html +
-```js -TonPayEmbed.setCallback("newPaymentHandler"); + + ``` -### `TonPayEmbed.setAddress(address)` +## Option 2: use the ESM API with a bundler -Update the displayed wallet address in the menu. +```ts +import { render } from "@ton-pay/ui"; -```js -TonPayEmbed.setAddress(""); +const widget = render("#tonpay-checkout", { + amount: 25, + chain: "mainnet", + comment: "Order #8451", + currency: "USDT", + itemTitle: "Premium plan", + recipientWalletAddress: "", + onReady() { + console.log("Widget ready"); + } +}); ``` -### `TonPayEmbed.click()` +## Option 3: headless mode with a custom trigger -Trigger a button click programmatically. - -```js -TonPayEmbed.click(); -``` - -### Example: dynamic configuration +Use `createWidget()` when the host application renders its own button or CTA. ```html -
- - - + + ``` -## TonPay client API - -`createTonPay` returns a client for wallet connection and payments. - -### Properties - -- `address: string | null` +## API - Current wallet address. +### `TonPay.render(target, options)` - ```js - const tonPay = createTonPay({ manifestUrl: "https:///tonconnect-manifest.json" }); - console.log(tonPay.address); // null or wallet address - ``` +Create a widget instance and mount the built-in button immediately. -### Methods - -- `waitForWalletConnection(): Promise` - - Wait for a wallet connection and open the modal if needed. - - ```js - try { - const address = await tonPay.waitForWalletConnection(); - console.log("Connected:", address); - } catch (error) { - console.error("Connection failed:", error); - } - ``` - -- `pay(getMessage): Promise` +```js +const widget = TonPay.render("#tonpay-checkout", { + amount: 15, + recipientWalletAddress: "" +}); +``` - Execute a payment transaction. +### `TonPay.createWidget(options)` - ```js - const result = await tonPay.pay(async (senderAddr) => { - const message = { - address: "", - amount: "1000000" - }; - return { message }; - }); +Create a widget instance without mounting a button. - console.log(result.txResult); - ``` +```js +const widget = TonPay.createWidget({ + amount: 15, + recipientWalletAddress: "" +}); +``` -- `disconnect(): Promise` +### Instance methods - Disconnect the current wallet. +- `mount(target)` renders the built-in TON Pay button. +- `unmount()` removes the built-in button but keeps the widget instance alive. +- `open()` opens the fullscreen hosted widget. +- `close()` closes the hosted widget. +- `update(partialOptions)` updates checkout or button options. +- `destroy()` removes the button, iframe, and event listeners. - ```js - await tonPay.disconnect(); - ``` +## Options -## Framework integration +### Checkout options -### WordPress +| Option | Type | Default | Description | +| ------------------------ | ------------------------ | ------------------- | ------------------------------------------------------------------------------------- | +| `amount` | `number \| string` | required | Payment amount in asset units. | +| `recipientWalletAddress` | `string` | required | Recipient wallet address. | +| `chain` | `"mainnet" \| "testnet"` | `"mainnet"` | TON Pay environment used for session bootstrap. | +| `currency` | `string` | - | Asset symbol shown to the user. | +| `asset` | `string` | `currency ?? "TON"` | Asset identifier, including jettons. | +| `itemTitle` | `string` | - | UI-only text displayed in the widget. | +| `comment` | `string` | - | Payment comment added to the blockchain transaction. It maps to `commentToRecipient`. | -```html - -
- - - -``` +### Button options -### Plain HTML/CSS/JS +| Option | Type | Default | +| -------------- | ------------------- | ----------- | +| `variant` | `"long" \| "short"` | `"long"` | +| `text` | `string` | - | +| `bgColor` | `string` | `"#0098EA"` | +| `textColor` | `string` | `"#FFFFFF"` | +| `borderRadius` | `number \| string` | `8` | +| `width` | `number \| string` | `"100%"` | +| `height` | `number \| string` | `44` | +| `fontFamily` | `string` | `"inherit"` | -Use the complete example of the embed script in a single HTML file. +### Lifecycle callbacks -### Build tools: Webpack and Vite +| Option | Type | Description | +| --------------- | ------------------- | ------------------------------------------------------- | +| `onReady` | `() => void` | Called when the visible widget is ready. | +| `onClose` | `() => void` | Called when the widget closes. | +| `onRouteChange` | `(payload) => void` | Called when the iframe route changes. | +| `onError` | `(payload) => void` | Called for session bootstrap and runtime widget errors. | +| `onSuccess` | `(payload) => void` | Called when the widget finishes successfully. | -```js -import { createTonPay } from "@ton-pay/ui/vanilla"; +## Fullscreen behavior -const tonPay = createTonPay({ - manifestUrl: "https:///tonconnect-manifest.json" -}); +The hosted widget always renders as a fullscreen overlay under `document.body`. This means: -// Use tonPay.pay() in event handlers -``` +- It is not clipped by the button container. +- It is not limited by the host page layout. +- It behaves the same on plain sites and [Telegram Mini Apps](/ecosystem/tma/overview). ## Best practices -- Wrap payment calls in try-catch blocks and display clear error messages. -- Update the UI during payment processing to prevent repeated clicks. -- Check amounts, addresses, and input before calling the payment function. -- Use HTTPS in production. TON Connect requires HTTPS for the manifest URL and callbacks. -- Save `reference` and `bodyBase64Hash` from `createTonPayTransfer` to track payments via webhooks. -- Test with testnet first and handle error scenarios before going live. +- Use `chain="testnet"` during local integration. +- Wrap callbacks with logging or UI feedback for error handling. +- Persist callback payload identifiers for checkout tracking on the merchant side. +- Use headless mode when the host app needs a fully custom trigger. ## Troubleshooting - - - 1. Verify that `containerId` matches the target div ID - 1. Check the browser console for errors - 1. Ensure the script loads after the container div - 1. Verify that the script URL is correct and accessible - - - - 1. Ensure the callback function is defined on the `window` object - 1. Verify that the function name matches the `callback` parameter exactly - 1. Define the callback before the embed script runs - - - - 1. Add the import map before any module scripts - 1. Verify that the `@tonconnect/ui` version is compatible - 1. Check that the import map syntax is valid - - - - 1. Serve the manifest from the same domain or enable CORS - 1. Use HTTPS in production - 1. Verify that CDN resources are accessible - - + + - Ensure the target selector exists before calling `render()` or `mount()`. + - Check the browser console for script loading or runtime errors. + + + + - Verify that the page can reach the TON Pay API origin for the selected `chain`. + - Inspect `onError` payloads for bootstrap failures. + + + + - The widget should not be constrained by host containers. + - If clipping occurs, check whether custom host CSS overrides global iframe or `position: fixed` behavior. + diff --git a/ecosystem/ton-pay/ui-integration/button-react.mdx b/ecosystem/ton-pay/ui-integration/button-react.mdx index ecf6748fc..4e308dc8c 100644 --- a/ecosystem/ton-pay/ui-integration/button-react.mdx +++ b/ecosystem/ton-pay/ui-integration/button-react.mdx @@ -1,12 +1,11 @@ --- -title: "How to add a TON Pay button using React" -sidebarTitle: "Add TON Pay button React" +title: "How to add a TON Pay widget using React" +sidebarTitle: "Add a TON Pay widget React" --- -import { Aside } from "/snippets/aside.jsx"; import { Image } from "/snippets/image.jsx"; -`TonPayButton` is a ready-to-use React component that handles wallet connection and payment flow with configurable styling for TON payments. The default button appearance is shown below. +`TonPayWidget` is the default React integration for TON Pay in version `0.4`. It renders a button and opens a hosted widget that owns session bootstrap, checkout routing, wallet logic, and result handling.
-## Install packages +## Install - - - ```bash - npm install @ton-pay/ui-react @ton-pay/api @tonconnect/ui-react - ``` - - - - Create [`tonconnect-manifest.json`](/ecosystem/ton-connect/manifest) in the app public directory: - - ```json - { - "url": "", - "name": "", - "iconUrl": "" - } - ``` - - - - Wrap the root component with `TonConnectUIProvider`. - - ```tsx - import { TonConnectUIProvider } from "@tonconnect/ui-react"; - - function App() { - return ( - - - - ); - } - ``` - - - - - -## Option 1: use useTonPay hook - -The `useTonPay` hook simplifies wallet connection and transaction handling. Use it for a direct integration path. - - - - ```tsx - import { TonPayButton } from "@ton-pay/ui-react"; - import { useTonPay } from "@ton-pay/ui-react"; - import { createTonPayTransfer } from "@ton-pay/api"; - import { useState } from "react"; - ``` - - - - ```tsx - function PaymentComponent() { - const { pay } = useTonPay(); - const [isLoading, setIsLoading] = useState(false); - ``` - - - - ```tsx - 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: "", - 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); - } - }; - ``` - - - - - - ```tsx - return ( - - ); - } - ``` - - - -## Option 2: use TON Connect directly - -Use TON Connect hooks directly when full control over the connection flow is required. - - - - ```tsx - 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"; - ``` - - - - ```tsx - function DirectPaymentComponent() { - const address = useTonAddress(true); - const modal = useTonConnectModal(); - const [tonConnectUI] = useTonConnectUI(); - const [isLoading, setIsLoading] = useState(false); - ``` - - - - ```tsx - 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: "", - senderAddr: "", - 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); - } - }; - ``` - - - - ```tsx - return ; - } - ``` - - - -## TonPayButton props - -All props are optional except `handlePay`. - -| Prop | Type | Default | Description | -| ----------------------- | --------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | -| `handlePay` | `() => Promise` | required | Payment handler called when the button is selected. | -| `isLoading` | `boolean` | `false` | Shows 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`. | -| `showErrorNotification` | `boolean` | `true` | Shows the built-in error notification popup on error. | -| `bgColor` | `string` | `"#0098EA"` | Background color (hex) or CSS gradient. | -| `textColor` | `string` | `"#FFFFFF"` | Text and icon color (hex). | -| `borderRadius` | `number` \| `string` | `8` | Border radius in pixels or CSS value. | -| `fontFamily` | `string` | `"inherit"` | Font family for button text. | -| `width` | `number` \| `string` | `300` | Button width in pixels or CSS value. | -| `height` | `number` \| `string` | `44` | Button height in pixels or CSS value. | -| `loadingText` | `string` | `"Processing..."` | Text shown during loading state. | -| `showMenu` | `boolean` | `true` | Shows the dropdown menu with wallet actions. | -| `disabled` | `boolean` | `false` | Disables the button. | -| `style` | `Record` | - | Additional inline styles. | -| `className` | `string` | - | Additional CSS class name. | - -## Customization - -### Button variants - -[Use `useTonPay`](/ecosystem/ton-pay/ui-integration/button-react#option-1:-using-usetonpay-hook-recommended) for a complete example. - - - ```tsx Long variant (default) - - // Displays: "Pay with [TON icon] Pay" - ``` - - ```tsx Short variant - - // Displays: "[TON icon] Pay" - ``` - - -### Presets - -[Use `useTonPay`](/ecosystem/ton-pay/ui-integration/button-react#option-1:-using-usetonpay-hook-recommended) for a complete example. - - - ```tsx Default preset - - // Blue theme: #0098EA - ``` - - ```tsx Gradient preset - - // Gradient: #2A82EB to #0355CF - ``` - - -### Custom styling - -[Use `useTonPay`](/ecosystem/ton-pay/ui-integration/button-react#option-1:-using-usetonpay-hook-recommended) for a complete example. - -```tsx - -``` - -CSS gradients can be used in `bgColor`. For example: `"linear-gradient(135deg, #667eea 0%, #764ba2 100%)"`. - -### Button states - -[Use `useTonPay`](/ecosystem/ton-pay/ui-integration/button-react#option-1:-using-usetonpay-hook-recommended) for a complete example. - -```tsx - -``` - -## Advanced patterns - -### Error handling - -```tsx -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(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: "", - 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 ( -
- - {error &&
{error}
} -
- ); -} +```bash +npm install @ton-pay/ui-react ``` -#### 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. +## Minimal example ```tsx -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: "", - senderAddr, - }, - { chain: "testnet" } - ); - return { message }; - }); - } finally { - setIsLoading(false); - } - }; +import { TonPayWidget } from "@ton-pay/ui-react"; +export function PaymentButton() { return ( - { - 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. +## What the widget handles + +With `TonPayWidget`, the host app passes checkout parameters and TON Pay handles: + +- selecting the environment-specific base URL from `chain`; +- preloading the iframe state; +- session bootstrap; +- routing the checkout flow; +- wallet interaction; +- on-ramp and top-up steps when available. + +## Props + +### Checkout props + +| Prop | Type | Default | Description | +| ------------------------ | ------------------------ | ------------------- | ------------------------------------------------------------------------------------- | +| `amount` | `number \| string` | required | Payment amount in asset units. | +| `recipientWalletAddress` | `string` | required | Recipient wallet address. | +| `chain` | `"mainnet" \| "testnet"` | `"mainnet"` | TON Pay environment used for session bootstrap. | +| `currency` | `string` | - | Asset symbol shown to the user. | +| `asset` | `string` | `currency ?? "TON"` | Asset identifier, including jetton master addresses. | +| `itemTitle` | `string` | - | UI-only text displayed in the widget. | +| `comment` | `string` | - | Payment comment added to the blockchain transaction. It maps to `commentToRecipient`. | + +### Button props + +| Prop | Type | Default | Description | +| -------------- | ------------------- | ----------- | ---------------------------------------- | +| `variant` | `"long" \| "short"` | `"long"` | Button label variant. | +| `text` | `string` | - | Override the default button label. | +| `bgColor` | `string` | `"#0098EA"` | Button background color. | +| `textColor` | `string` | `"#FFFFFF"` | Button text color. | +| `borderRadius` | `number \| string` | `8` | Button border radius. | +| `width` | `number \| string` | `"100%"` | Button width. | +| `height` | `number \| string` | `44` | Button height. | +| `fontFamily` | `string` | `"inherit"` | Button font family. | +| `disabled` | `boolean` | `false` | Disable the button. | +| `isLoading` | `boolean` | `false` | Force a loading state from the host app. | +| `className` | `string` | - | Extra button class name. | +| `style` | `CSSProperties` | - | Extra inline styles for the button. | + +### Callbacks + +| Prop | Type | Description | +| --------------- | ------------------- | -------------------------------------------------------------- | +| `onReady` | `() => void` | Called when the visible widget is ready. | +| `onClose` | `() => void` | Called when the widget closes. | +| `onRouteChange` | `(payload) => void` | Called when the iframe route changes. | +| `onError` | `(payload) => void` | Called for widget runtime errors and session bootstrap errors. | +| `onSuccess` | `(payload) => void` | Called when the widget finishes successfully. | + +## Styling examples ```tsx -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(null); - - const handlePayment = async () => { - setIsLoading(true); - setError(null); - try { - await pay(async (senderAddr: string) => { - const { message } = await createTonPayTransfer( - { - amount: 2.5, - asset: "TON", - recipientAddr: "", - 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 ( -
- - {error &&
{error}
} -
- ); -} -``` - -## Server-side payment creation - -Create the payment message on a backend to store tracking identifiers and validate parameters before sending. - - - - Build an endpoint that returns the message for `TonPayButton`. - - ```ts - // /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: "", - senderAddr, - commentToSender: `Order ${orderId}`, - }, - { chain: "testnet" } - ); - - // Store reference and bodyBase64Hash in the database - await db.savePayment({ orderId, reference, bodyBase64Hash }); - - res.json({ message }); - }); - ``` - - - - ```tsx - 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: "", - }), - }); - const data = await response.json(); - return { message: data.message }; - }); - } finally { - setIsLoading(false); - } - }; - - return ; - } - ``` - - - - - -## Test the integration - -Run the interactive button showcase to test variants and styling. - -```bash -npm run test:button-react -# or -bun test:button-react + ``` - +## Migration from version 0.3.2 or lower -## Best practices +The current React integration is based on `TonPayWidget`, not the legacy `TonPayButton` + `useTonPay` integration. The hosted widget handles wallet connection, session bootstrap, checkout routing, and result processing, removing most legacy TON Connect logic from the host app. -- 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](/ecosystem/ton-pay/webhooks). -- After successful payment, show a confirmation message, redirect to a success page, or update the UI. +- Use the [migration guide](/ecosystem/ton-pay/payment-integration/migration) for the upgrade checklist and before-and-after examples. diff --git a/ecosystem/ton-pay/webhooks.mdx b/ecosystem/ton-pay/webhooks.mdx deleted file mode 100644 index aa4083f6e..000000000 --- a/ecosystem/ton-pay/webhooks.mdx +++ /dev/null @@ -1,692 +0,0 @@ ---- -title: "TON Pay webhooks" -sidebarTitle: "Webhooks" -tag: "beta" ---- - -import { Aside } from '/snippets/aside.jsx'; - - - -TON Pay uses webhooks to notify applications in real-time about transfer events. - -## How webhooks work - -1. A transfer is completed on-chain and indexed by TON Pay. -1. TON Pay generates a webhook payload and signs it using [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) and the optional API key when it is configured. -1. TON Pay sends a `POST` request to the webhook URL with the signed payload. The request includes the `X-TonPay-Signature` header for verification. -1. Verify the signature and process the event payload. Return a `2xx` status code to acknowledge receipt. - - - -## Configure the webhook URL - -Configure the endpoint in TON Pay Merchant Dashboard. - -1. Open the TON Pay Merchant Dashboard and sign in to the merchant account. -1. Navigate to the Developer section, then select Webhooks. -1. Enter the webhook URL. In production, the endpoint must use HTTPS. For example, `https://thedomain.com/webhooks/tonpay`. -1. Save the configuration and use the test feature to verify delivery. - - - -## Webhook payload - -### Event types - -```ts -import { - type WebhookPayload, - type WebhookEventType, - type TransferCompletedWebhookPayload, - type TransferRefundedWebhookPayload, // Coming soon -} from "@ton-pay/api"; - -// Event types -type WebhookEventType = - | "transfer.completed" - | "transfer.refunded"; // Coming soon - -// Union type for all webhook payloads -type WebhookPayload = - | TransferCompletedWebhookPayload - | TransferRefundedWebhookPayload; // Coming soon -``` - -### `transfer.completed` - -```ts -interface TransferCompletedWebhookPayload { - event: "transfer.completed"; - timestamp: string; - data: CompletedTonPayTransferInfo; // See API reference -} -``` - - - -### Example payloads - -Successful transfer: - -```json -{ - "event": "transfer.completed", - "timestamp": "2024-01-15T14:30:00.000Z", - "data": { - "amount": "10.5", - "rawAmount": "10500000000", - "senderAddr": "", - "recipientAddr": "", - "asset": "TON", - "assetTicker": "TON", - "status": "success", - "reference": "", - "bodyBase64Hash": "", - "txHash": "", - "traceId": "", - "commentToSender": "Thanks for the purchase", - "commentToRecipient": "Payment for order #1234", - "date": "2024-01-15T14:30:00.000Z" - } -} -``` - -Failed transfer: - -```json -{ - "event": "transfer.completed", - "timestamp": "2024-01-15T14:35:00.000Z", - "data": { - "amount": "10.5", - "rawAmount": "10500000000", - "senderAddr": "", - "recipientAddr": "", - "asset": "TON", - "assetTicker": "TON", - "status": "failed", - "reference": "", - "bodyBase64Hash": "", - "txHash": "", - "traceId": "", - "commentToSender": "Transaction failed", - "commentToRecipient": "Payment for order #5678", - "date": "2024-01-15T14:35:00.000Z", - "errorCode": 36, - "errorMessage": "Not enough TON" - } -} -``` - -Placeholders: - -- `` - sender wallet address. -- `` - recipient wallet address. -- `` - transfer reference returned by `createTonPayTransfer`. -- `` - Base64 hash of the transfer body. -- `` - transaction hash. -- `` - trace identifier. - -## Payload fields - - - Event type. `transfer.completed` indicates that on-chain processing is finished. The transfer may be successful or failed; check `data.status` for the result. - - - - [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp indicating when the event occurred. - - - - Transfer details such as amount, addresses, and the transaction hash. - - - - Human-readable payment amount with decimals. For example, 10.5. - - - - Amount in base units; nanotons for TON. - - - - Sender wallet address on TON blockchain. - - - - Recipient wallet address on TON blockchain. - - - - Asset identifier. Use "TON" for TON coin or a jetton master address for tokens. - - - - Human-readable asset ticker. For example, TON or USDT. - - - - Transfer status: `success` or `failed`. Process only `success` status. Log `failed` status for investigation. - - - - Unique transfer reference. Use this to match the webhook with the internal order or transaction. - - - - Base64 hash of the transfer body. Use for advanced verification. - - - - Transaction hash on-chain. Use this to verify the transaction if needed. - - - - Trace ID for tracking the transaction across systems. - - - - Optional note visible to the sender during signing. - - - - Optional note visible to the recipient. Use for order IDs or invoice numbers. - - - - ISO 8601 timestamp of the transfer completion. - - - - Error code if the transfer failed. - - - - Error message if the transfer failed. - - - - -## Verify webhook signatures - -Every webhook request includes an `X-TonPay-Signature` header containing an HMAC-SHA256 signature. Verify this signature to ensure the request comes from TON Pay. - - - -```ts -import { verifySignature, type WebhookPayload } from "@ton-pay/api"; - -app.post("/webhook", (req, res) => { - const signature = req.headers["x-tonpay-signature"]; - - if (!verifySignature(req.body, signature, process.env.TONPAY_API_SECRET)) { - return res.status(401).json({ error: "Invalid signature" }); - } - - const webhookData: WebhookPayload = req.body; - - // Process the webhook in application-specific logic. - res.status(200).json({ received: true }); -}); -``` - - - -## Validate webhook requests - -Validate fields against the expected transaction data before marking an order as paid. - - - -1. Verify the `X-TonPay-Signature` header and reject invalid requests. - - ```ts - if (!verifySignature(payload, signature, apiKey)) { // apiKey is optional and used when configured - return res.status(401).json({ error: "Invalid signature" }); - } - ``` - -1. Verify the event type. - - ```ts - if (webhookData.event !== "transfer.completed") { - return res.status(400).json({ error: "Unexpected event type" }); - } - ``` - -1. Check that the reference matches a transaction created earlier. - - ```ts - const order = await db.getOrderByReference(webhookData.data.reference); - if (!order) { - console.error("Order not found:", webhookData.data.reference); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - ``` - -1. Verify the amount matches the expected payment amount. - - ```ts - if (parseFloat(webhookData.data.amount) !== order.expectedAmount) { - console.error("Amount mismatch:", { - expected: order.expectedAmount, - received: webhookData.data.amount, - }); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - ``` - -1. Verify the payment currency or token. - - ```ts - if (webhookData.data.asset !== order.expectedAsset) { - console.error("Asset mismatch:", { - expected: order.expectedAsset, - received: webhookData.data.asset, - }); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - ``` - -1. Check status and process only successful transfers. - - ```ts - if (webhookData.data.status !== "success") { - await db.markOrderAsFailed(order.id, webhookData.data.txHash); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - ``` - -1. Prevent duplicate processing using the reference. - - ```ts - if (order.status === "completed") { - return res.status(200).json({ received: true, duplicate: true }); - } - ``` - -### Complete validation example - -```ts expandable -import { verifySignature, type WebhookPayload } from "@ton-pay/api"; - -app.post("/webhooks/tonpay", async (req, res) => { - try { - // 1. Verify signature - const signature = req.headers["x-tonpay-signature"] as string; - - if (!verifySignature(req.body, signature, process.env.TONPAY_API_SECRET!)) { - console.error("Invalid webhook signature"); - return res.status(401).json({ error: "Invalid signature" }); - } - - const webhookData: WebhookPayload = req.body; - - // 2. Verify event type - if (webhookData.event !== "transfer.completed") { - return res.status(400).json({ error: "Unexpected event type" }); - } - - // 3. Find the order - const order = await db.getOrderByReference(webhookData.data.reference); - if (!order) { - console.error("Order not found:", webhookData.data.reference); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - - // 4. Check for duplicate processing - if (order.status === "completed") { - console.log("Order already processed:", order.id); - return res.status(200).json({ received: true, duplicate: true }); - } - - // 5. Verify transfer status - if (webhookData.data.status !== "success") { - await db.updateOrder(order.id, { - status: "failed", - txHash: webhookData.data.txHash, - failureReason: "Transfer failed on blockchain", - }); - return res.status(200).json({ received: true }); - } - - // 6. CRITICAL: Verify amount - const receivedAmount = parseFloat(webhookData.data.amount); - if (receivedAmount !== order.expectedAmount) { - console.error("Amount mismatch:", { - orderId: order.id, - expected: order.expectedAmount, - received: receivedAmount, - }); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - - // 7. CRITICAL: Verify asset/currency - const expectedAsset = order.expectedAsset || "TON"; - if (webhookData.data.asset !== expectedAsset) { - console.error("Asset mismatch:", { - orderId: order.id, - expected: expectedAsset, - received: webhookData.data.asset, - }); - return res.status(200).json({ received: true }); // Acknowledge to prevent retry - } - - // All validations passed - acknowledge receipt - res.status(200).json({ received: true }); - - // Process order asynchronously - await processOrderCompletion(order.id, { - txHash: webhookData.data.txHash, - senderAddr: webhookData.data.senderAddr, - completedAt: webhookData.timestamp, - }); - } catch (error) { - console.error("Webhook processing error:", error); - return res.status(500).json({ error: "Internal server error" }); - } -}); -``` - -## Retry behavior - -TON Pay retries failed webhook deliveries. - - - - Up to 3 automatic retries with exponential backoff. - - - - 1s, 5s, and 15s. - - - -### Delivery outcomes - -- Success responses: `2xx`. No retry; webhook marked as delivered. -- All other responses: `4xx`, `5xx`, or network errors. Automatic retry with exponential backoff. - - - -## Best practices - -- Validate the following fields before marking an order as paid: - - - Signature: authentication; - - Reference: the transaction exists in the system; - - Amount: the expected payment is matched; - - Asset: correct currency or token; - - Wallet ID: correct receiving wallet; - - Status: the transaction succeeded. - - ```ts - // Avoid: trust without verification - await markOrderAsPaid(webhook.data.reference); - - // Prefer: validate and then process - if (isValidWebhook(webhook, order)) { - await markOrderAsPaid(order.id); - } - ``` - -- Return `2xx` only after successful validation, then process the webhook to avoid timeouts. - - ```ts - app.post("/webhook", async (req, res) => { - // Validate signature first - if (!verifyWebhookSignature(...)) { - return res.status(401).json({ error: "Invalid signature" }); - } - - // Quick validations - if (!isValidWebhook(req.body)) { - return res.status(400).json({ error: "Invalid webhook data" }); - } - - // Acknowledge after validation - res.status(200).json({ received: true }); - - // Process in background - processWebhookAsync(req.body).catch(console.error); - }); - ``` - -- Implement idempotency. Webhooks may be delivered more than once. Use the `reference` field to prevent duplicate processing. - - ```ts - async function processWebhook(payload: WebhookPayload) { - const order = await db.getOrder(payload.reference); - - // Check if already processed - if (order.status === "completed") { - console.log("Order already completed:", payload.reference); - return; // Skip processing - } - - // Process and update status atomically - await db.transaction(async (tx) => { - await tx.updateOrder(order.id, { status: "completed" }); - await tx.createPaymentRecord(payload); - }); - } - ``` - -- Log all webhook attempts. Log each request for debugging and security auditing. - - ```ts - await logger.info("Webhook received", { - timestamp: new Date(), - signature: req.headers["x-tonpay-signature"], - reference: payload.data.reference, - event: payload.event, - amount: payload.data.amount, - asset: payload.data.asset, - status: payload.data.status, - validationResult: validationResult, - }); - ``` - -- To receive webhooks in production, ensure corresponding endpoints use HTTPS. Regular HTTP endpoints would be ignored by TON Pay. - -- Monitor delivery failures. Set up alerts and review webhook delivery logs in the Merchant Dashboard - -- Store transaction hashes. Save `txHash` to support on-chain verification for disputes. - - ```ts - await db.updateOrder(order.id, { - status: "completed", - txHash: webhookData.data.txHash, - senderAddr: webhookData.data.senderAddr, - completedAt: webhookData.timestamp, - }); - ``` - -## Troubleshooting - - - 1. Verify the webhook URL configured in the Merchant Dashboard. - 1. Ensure the endpoint is publicly reachable and not blocked by a firewall. - 1. Use the dashboard test feature to send a sample webhook. - 1. Review webhook attempt logs in the Merchant Dashboard. - 1. Ensure the endpoint returns a `2xx` status code for valid requests. - 1. Ensure the endpoint responds within 10 seconds to avoid timeouts. - - - - 1. Use the optional API key from the Merchant Dashboard at DeveloperAPI when webhook signing is configured. - 1. Compute the HMAC over the raw JSON string. - 1. Do not modify the request body before verification. - 1. Verify the header name is `x-tonpay-signature` in lowercase. - 1. Use the `HMAC-SHA256` algorithm. - 1. Ensure the signature value starts with `sha256=`. - - - - 1. Wrong order lookup or reference mapping. - 1. Price changed after order creation. - 1. Currency mismatch between order and transfer. - 1. Potential attack or spoofed request. - - - - 1. Implement idempotency using the `reference` field. - 1. Use database transactions to prevent race conditions. - 1. Check order status before processing. - - ```ts - if (order.status === "completed") { - return; // Already processed - } - ``` - - - - 1. Respond within 10 seconds. - 1. Process webhooks asynchronously after quick validation. - 1. Return `2xx` after validation passes. - - -## Next steps - - - - Create a TON Pay transfer message. - - - - Query transfer status and details. - -