SDKsTypeScriptUI Components

UI Components

T402 provides UI packages for building payment flows in web applications. Three packages cover different use cases:

PackagePurposeFramework
@t402/paywallServer-rendered paywall pageFramework-agnostic (HTML)
@t402/reactReact components and hooksReact 18+
@t402/vueVue composables and componentsVue 3.3+

Installation

# Paywall (server-side, used with middleware)
pnpm add @t402/paywall
 
# React components
pnpm add @t402/react
 
# Vue components
pnpm add @t402/vue

@t402/paywall

A universal, server-rendered paywall component that generates standalone HTML pages for browser-based payment flows. Used with server middleware (@t402/express, @t402/hono, etc.) to serve a payment UI when browsers hit protected endpoints.

Builder Pattern

import { createPaywall, evmPaywall, svmPaywall, tonPaywall } from '@t402/paywall'
 
const paywall = createPaywall()
  .withNetwork(evmPaywall)
  .withNetwork(svmPaywall)
  .withNetwork(tonPaywall)
  .withConfig({
    appName: 'My API',
    appLogo: 'https://example.com/logo.png',
    theme: { mode: 'dark' }
  })
  .build()

Network Handlers

HandlerNetworksWallet Support
evmPaywalleip155:*MetaMask, WalletConnect, Coinbase
svmPaywallsolana:*Phantom, Solflare
tonPaywallton:*TON Connect
tronPaywalltron:*TronLink
stacksPaywallstacks:*Hiro Wallet, Leather
nearPaywallnear:*NEAR Wallet, MyNearWallet
cosmosPaywallcosmos:*Keplr

Configuration

interface PaywallConfig {
  appName?: string;               // Display name in paywall header
  appLogo?: string;               // Logo URL
  currentUrl?: string;            // Current page URL (for redirect after payment)
  testnet?: boolean;              // Show testnet indicators
  walletConnectProjectId?: string; // WalletConnect v2 project ID (EVM mobile)
  theme?: PaywallTheme;
}
 
interface PaywallTheme {
  mode?: 'light' | 'dark' | 'auto';
  colors?: {
    primary?: string;
    background?: string;
    containerBackground?: string;
    text?: string;
    secondaryText?: string;
    border?: string;
  };
  borderRadius?: string;
  fontFamily?: string;
}

Integration with Middleware

import { paymentMiddleware } from '@t402/express'
import { createPaywall, evmPaywall, svmPaywall } from '@t402/paywall'
 
const paywall = createPaywall()
  .withNetwork(evmPaywall)
  .withNetwork(svmPaywall)
  .build()
 
app.use(paymentMiddleware(routes, server, {
  paywallConfig: {
    appName: 'Premium API',
    theme: { mode: 'dark' }
  },
  paywall  // PaywallProvider instance
}))

When a browser (Accept: text/html) hits a protected endpoint without payment, the middleware returns the paywall HTML page instead of a JSON 402 response.

Generating HTML Directly

const html = paywall.generateHtml(paymentRequired, {
  appName: 'My Service',
  currentUrl: 'https://example.com/premium',
  testnet: false
})
 
// Serve as response
res.setHeader('Content-Type', 'text/html')
res.send(html)

The paywall generates a self-contained HTML page with inline CSS and JavaScript. No external dependencies are loaded at runtime — wallet SDKs are bundled in.


@t402/react

React components, hooks, and utilities for building custom payment UIs.

PaymentProvider

Wrap your app (or payment section) with PaymentProvider to share payment state:

import { PaymentProvider } from '@t402/react'
 
function App() {
  return (
    <PaymentProvider testnet={false}>
      <PaymentFlow />
    </PaymentProvider>
  )
}

Access shared state via usePaymentContext:

import { usePaymentContext } from '@t402/react'
 
function PaymentFlow() {
  const {
    status,
    error,
    paymentRequired,
    selectedRequirement,
    isTestnet,
    setPaymentRequired,
    selectRequirement,
    setStatus,
    reset
  } = usePaymentContext()
 
  // ...
}

Hooks

usePaymentRequired

Fetches a resource and handles 402 responses:

import { usePaymentRequired } from '@t402/react'
 
function ResourceFetcher() {
  const {
    paymentRequired,
    status,
    error,
    fetchResource,
    reset
  } = usePaymentRequired({
    onSuccess: (response) => console.log('Got resource!'),
    onError: (err) => console.error('Failed:', err)
  })
 
  const handleFetch = async () => {
    const response = await fetchResource('https://api.example.com/data')
    if (response) {
      const data = await response.json()
      // Resource obtained (either free or after payment)
    }
  }
 
  if (paymentRequired) {
    return <div>Payment needed: {paymentRequired.accepts[0].amount}</div>
  }
 
  return <button onClick={handleFetch}>Fetch Data</button>
}

usePaymentStatus

Manages payment status with timed messages:

import { usePaymentStatus } from '@t402/react'
 
function PaymentUI() {
  const {
    status,
    message,
    setSuccess,
    setError,
    setInfo,
    reset
  } = usePaymentStatus()
 
  const handlePay = async () => {
    try {
      setInfo('Signing payment...')
      await signPayment()
      setSuccess('Payment confirmed!', 5000) // Auto-clear after 5s
    } catch (err) {
      setError(err.message)
    }
  }
 
  return (
    <div>
      {message && <div className={message.type}>{message.text}</div>}
      <button onClick={handlePay}>Pay</button>
    </div>
  )
}

useAsyncPayment

Wraps async payment operations with loading/success/error states:

import { useAsyncPayment } from '@t402/react'
 
function PayButton({ requirement }) {
  const {
    execute,
    isLoading,
    isSuccess,
    isError,
    error,
    result
  } = useAsyncPayment({
    paymentFn: () => submitPayment(requirement),
    onSuccess: (receipt) => console.log('Tx:', receipt.hash),
    onError: (err) => console.error(err)
  })
 
  return (
    <button onClick={execute} disabled={isLoading}>
      {isLoading ? 'Processing...' : isSuccess ? 'Done!' : 'Pay'}
    </button>
  )
}

Components

PaymentButton

import { PaymentButton } from '@t402/react'
 
<PaymentButton
  onClick={handlePayment}
  loading={isProcessing}
  disabled={!isConnected}
  variant="primary"
  size="md"
>
  Pay $0.01
</PaymentButton>

Props:

PropTypeDefaultDescription
onClick() => void | Promise<void>-Click handler
loadingbooleanfalseShow loading spinner
disabledbooleanfalseDisable interaction
variant'primary' | 'secondary' | 'outline''primary'Visual style
size'sm' | 'md' | 'lg''md'Button size

PaymentDetails

import { PaymentDetails } from '@t402/react'
 
<PaymentDetails
  requirement={selectedRequirement}
  showNetwork
  showAsset
  showRecipient
/>

AddressDisplay

import { AddressDisplay } from '@t402/react'
 
<AddressDisplay
  address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
  startChars={6}
  endChars={4}
  copyable
/>
// Renders: 0x8335...2913 [copy icon]

Spinner

import { Spinner } from '@t402/react'
 
<Spinner size="md" />

Utilities

import {
  // Network detection
  isEvmNetwork,
  isSvmNetwork,
  isTonNetwork,
  isTronNetwork,
  isTestnetNetwork,
  getNetworkDisplayName,
 
  // Payment helpers
  normalizePaymentRequirements,
  getPreferredNetworks,
  choosePaymentRequirement,
 
  // Formatters
  truncateAddress,
  formatTokenAmount,
  getAssetDisplayName
} from '@t402/react'
 
// Examples
isEvmNetwork('eip155:8453')           // true
getNetworkDisplayName('eip155:8453')   // "Base"
truncateAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913')  // "0x8335...2913"
formatTokenAmount('1000000')           // "1.00"

Full Example

import {
  PaymentProvider,
  usePaymentContext,
  useAsyncPayment,
  PaymentButton,
  PaymentDetails,
  AddressDisplay
} from '@t402/react'
 
function PaymentFlow() {
  const { paymentRequired, selectedRequirement, selectRequirement } = usePaymentContext()
 
  const { execute, isLoading, isSuccess } = useAsyncPayment({
    paymentFn: async () => {
      // Sign and submit payment using your wallet SDK
      return await walletClient.signPayment(selectedRequirement)
    }
  })
 
  if (!paymentRequired) return null
 
  return (
    <div>
      <h2>Payment Required</h2>
 
      {/* Show available payment options */}
      {paymentRequired.accepts.map((req, i) => (
        <div key={i} onClick={() => selectRequirement(req)}>
          <PaymentDetails requirement={req} showNetwork showAsset />
        </div>
      ))}
 
      {/* Selected payment details */}
      {selectedRequirement && (
        <>
          <p>Pay to: <AddressDisplay address={selectedRequirement.payTo} copyable /></p>
          <PaymentButton onClick={execute} loading={isLoading}>
            {isSuccess ? 'Paid!' : `Pay ${formatTokenAmount(selectedRequirement.amount)}`}
          </PaymentButton>
        </>
      )}
    </div>
  )
}
 
export default function App() {
  return (
    <PaymentProvider>
      <PaymentFlow />
    </PaymentProvider>
  )
}

@t402/vue

Vue 3 composables and components with the same API surface as @t402/react.

Composables

usePaymentRequired

<script setup lang="ts">
import { usePaymentRequired } from '@t402/vue'
 
const { paymentRequired, status, error, fetchResource, reset } = usePaymentRequired({
  onSuccess: (response) => console.log('Got resource!'),
  onError: (err) => console.error(err)
})
 
async function handleFetch() {
  const response = await fetchResource('https://api.example.com/data')
  if (response) {
    const data = await response.json()
  }
}
</script>
 
<template>
  <div v-if="paymentRequired">
    Payment required: {{ paymentRequired.accepts[0].amount }}
  </div>
  <button v-else @click="handleFetch">Fetch Data</button>
</template>

usePaymentStatus

<script setup lang="ts">
import { usePaymentStatus } from '@t402/vue'
 
const { status, message, setSuccess, setError, setInfo } = usePaymentStatus()
 
async function handlePay() {
  setInfo('Signing payment...')
  try {
    await signPayment()
    setSuccess('Payment confirmed!', 5000)
  } catch (err) {
    setError(err.message)
  }
}
</script>
 
<template>
  <div v-if="message" :class="message.type">{{ message.text }}</div>
  <button @click="handlePay">Pay</button>
</template>

useAsyncPayment

<script setup lang="ts">
import { useAsyncPayment } from '@t402/vue'
 
const { execute, isLoading, isSuccess, error } = useAsyncPayment({
  paymentFn: () => submitPayment(requirement),
  onSuccess: (receipt) => console.log('Tx:', receipt.hash)
})
</script>
 
<template>
  <button @click="execute" :disabled="isLoading">
    {{ isLoading ? 'Processing...' : isSuccess ? 'Done!' : 'Pay' }}
  </button>
</template>

Components

<script setup lang="ts">
import {
  PaymentButton,
  PaymentDetails,
  AddressDisplay,
  Spinner
} from '@t402/vue'
</script>
 
<template>
  <PaymentButton
    @click="handlePay"
    :loading="isProcessing"
    :disabled="!isConnected"
    variant="primary"
  >
    Pay $0.01
  </PaymentButton>
 
  <PaymentDetails
    :requirement="selectedRequirement"
    show-network
    show-asset
  />
 
  <AddressDisplay
    address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
    :start-chars="6"
    :end-chars="4"
    copyable
  />
 
  <Spinner size="md" />
</template>

Utilities

Same utilities are exported from @t402/vue:

import {
  isEvmNetwork,
  getNetworkDisplayName,
  truncateAddress,
  formatTokenAmount,
  choosePaymentRequirement
} from '@t402/vue'

API Comparison

Feature@t402/paywall@t402/react@t402/vue
RenderingServer-side HTMLClient-side JSXClient-side SFC
Wallet integrationBuilt-in (all chains)Bring your ownBring your own
CustomizationTheme configFull component controlFull component control
Use caseServer middlewareCustom React appsCustom Vue apps
State managementSelf-containedContext + hooksComposables
Bundle sizeLarger (includes wallets)Smaller (UI only)Smaller (UI only)

Choose @t402/paywall when you want a turnkey solution that handles wallet connections and payment execution. Choose @t402/react or @t402/vue when building a custom payment UI with your own wallet integration.

Further Reading