@t402/vue
Vue components and composables for building payment UIs with the t402 protocol. Pre-built components with customizable styling and composables for custom implementations.
Installation
npm install @t402/vueQuick Start
<script setup lang="ts">
import { usePaymentRequired, PaymentButton, PaymentDetails } from '@t402/vue';
const { paymentRequired, status, fetchResource } = usePaymentRequired({
onSuccess: () => console.log('Access granted!'),
});
const handleAccess = () => fetchResource('/api/premium');
const handlePayment = () => processPayment(paymentRequired.value);
</script>
<template>
<div v-if="paymentRequired">
<PaymentDetails :requirements="paymentRequired.accepts" />
<PaymentButton @click="handlePayment">
Pay Now
</PaymentButton>
</div>
<button v-else @click="handleAccess">
Access Premium Content
</button>
</template>Composables
usePaymentRequired
Composable to fetch resources and capture 402 Payment Required responses.
function usePaymentRequired(options?: UsePaymentRequiredOptions): UsePaymentRequiredResultOptions
interface UsePaymentRequiredOptions {
onSuccess?: (response: Response) => void;
onError?: (error: Error) => void;
}Returns
interface UsePaymentRequiredResult {
paymentRequired: Ref<PaymentRequired | null>;
status: Ref<PaymentStatus>;
error: Ref<string | null>;
fetchResource: (url: string, options?: RequestInit) => Promise<Response | null>;
reset: () => void;
}Example
<script setup lang="ts">
import { usePaymentRequired } from '@t402/vue';
const { paymentRequired, status, error, fetchResource, reset } = usePaymentRequired({
onSuccess: (response) => console.log('Access granted!'),
onError: (error) => console.error('Failed:', error),
});
async function handleFetch() {
const response = await fetchResource('/api/protected');
if (response?.ok) {
const data = await response.json();
// Handle data
}
}
</script>
<template>
<div v-if="status === 'loading'">Loading...</div>
<div v-else-if="status === 'error'">Error: {{ error }}</div>
<PaymentUI v-else-if="paymentRequired" :data="paymentRequired" @paid="reset" />
<button v-else @click="handleFetch">Access Resource</button>
</template>usePaymentStatus
Track payment transaction status.
function usePaymentStatus(): UsePaymentStatusResultReturns
interface UsePaymentStatusResult {
status: Ref<PaymentStatus>;
setStatus: (status: PaymentStatus) => void;
isLoading: ComputedRef<boolean>;
isSuccess: ComputedRef<boolean>;
isError: ComputedRef<boolean>;
}Example
<script setup lang="ts">
import { usePaymentStatus } from '@t402/vue';
const { status, setStatus, isLoading } = usePaymentStatus();
async function handlePayment() {
setStatus('loading');
try {
await processPayment();
setStatus('success');
} catch {
setStatus('error');
}
}
</script>
<template>
<div>
<p>Status: {{ status }}</p>
<button @click="handlePayment" :disabled="isLoading">
Pay
</button>
</div>
</template>useAsyncPayment
Handle async payment flows with retry logic.
function useAsyncPayment(options: UseAsyncPaymentOptions): UseAsyncPaymentResultExample
<script setup lang="ts">
import { useAsyncPayment } from '@t402/vue';
const props = defineProps<{ paymentRequired: PaymentRequired }>();
const { execute, status, error, retry } = useAsyncPayment({
onSuccess: () => window.location.reload(),
maxRetries: 3,
});
</script>
<template>
<div>
<button @click="execute(props.paymentRequired)" :disabled="status === 'loading'">
{{ status === 'loading' ? 'Processing...' : 'Pay Now' }}
</button>
<div v-if="status === 'error'">
<p>Error: {{ error }}</p>
<button @click="retry">Retry</button>
</div>
</div>
</template>useGaslessPayment
Composable for gasless ERC-4337 payments via WDK smart accounts.
function useGaslessPayment(options: GaslessPaymentOptions): GaslessPaymentResultOptions
interface GaslessPaymentOptions {
payFn: (params: { to: string; amount: bigint; token?: string }) => Promise<{
userOpHash: string;
sender: string;
sponsored: boolean;
wait: () => Promise<{ txHash: string; success: boolean }>;
}>;
onSuccess?: (receipt: { txHash: string; success: boolean }) => void;
onError?: (error: Error) => void;
autoWait?: boolean; // Default: true
}Returns
interface GaslessPaymentResult {
pay: (params: { to: string; amount: bigint; token?: string }) => Promise<void>;
status: Ref<PaymentStatus>;
userOpHash: Ref<string | null>;
txHash: Ref<string | null>;
sponsored: Ref<boolean | null>;
error: Ref<string | null>;
isLoading: ComputedRef<boolean>;
isSuccess: ComputedRef<boolean>;
isError: ComputedRef<boolean>;
reset: () => void;
}Example
<script setup lang="ts">
import { useGaslessPayment } from '@t402/vue';
const props = defineProps<{ client: GaslessClient }>();
const { pay, isLoading, isSuccess, txHash, sponsored, error } = useGaslessPayment({
payFn: (params) => props.client.pay(params),
onSuccess: (receipt) => console.log('Confirmed:', receipt.txHash),
autoWait: true,
});
</script>
<template>
<div>
<button @click="pay({ to: '0x...', amount: 1000000n })" :disabled="isLoading">
{{ isLoading ? 'Processing...' : 'Pay Gasless' }}
</button>
<p v-if="isSuccess">TX: {{ txHash }} {{ sponsored ? '(Sponsored)' : '' }}</p>
<p v-if="error">Error: {{ error }}</p>
</div>
</template>useBridgePayment
Composable for cross-chain USDT0 bridging via LayerZero OFT.
function useBridgePayment(options: BridgePaymentOptions): BridgePaymentResultReturns
type BridgeStatus = 'idle' | 'quoting' | 'bridging' | 'waiting' | 'success' | 'error';
interface BridgePaymentResult {
bridge: (params: BridgeParams) => Promise<void>;
autoBridge: (params: AutoBridgeParams) => Promise<void>;
status: Ref<BridgeStatus>;
txHash: Ref<string | null>;
messageGuid: Ref<string | null>;
dstTxHash: Ref<string | null>;
error: Ref<string | null>;
isLoading: ComputedRef<boolean>;
isSuccess: ComputedRef<boolean>;
isError: ComputedRef<boolean>;
reset: () => void;
}Example
<script setup lang="ts">
import { useBridgePayment } from '@t402/vue';
const { autoBridge, isLoading, isSuccess, txHash, error } = useBridgePayment({
bridgeFn: (params) => bridgeClient.bridge(params),
autoBridgeFn: (params) => bridgeClient.autoBridge(params),
onSuccess: (result) => console.log('Bridged:', result),
});
</script>
<template>
<button
@click="autoBridge({ toChain: 'arbitrum', amount: 100_000000n, recipient: '0x...' })"
:disabled="isLoading"
>
{{ isLoading ? 'Bridging...' : 'Bridge USDT0' }}
</button>
</template>useMultiSigPayment
Composable for multi-signature Safe payments with threshold signature collection.
function useMultiSigPayment(options: MultiSigPaymentOptions): MultiSigPaymentResultReturns
type MultiSigStatus = 'idle' | 'initiating' | 'collecting' | 'submitting' | 'success' | 'error';
interface MultiSigPaymentResult {
initiate: (params: { to: string; amount: bigint; token?: string }) => Promise<void>;
submit: () => Promise<void>;
status: Ref<MultiSigStatus>;
requestId: Ref<string | null>;
userOpHash: Ref<string | null>;
txHash: Ref<string | null>;
threshold: Ref<number>;
collectedCount: Ref<number>;
isReady: Ref<boolean>;
error: Ref<string | null>;
isLoading: ComputedRef<boolean>;
isSuccess: ComputedRef<boolean>;
isError: ComputedRef<boolean>;
reset: () => void;
}Example
<script setup lang="ts">
import { useMultiSigPayment } from '@t402/vue';
const { initiate, submit, status, isReady, threshold, collectedCount, error } =
useMultiSigPayment({
initiateFn: (params) => multiSigClient.initiatePayment(params),
submitFn: (id) => multiSigClient.submitRequest(id),
onSuccess: (receipt) => console.log('Confirmed:', receipt.txHash),
autoWait: true,
});
</script>
<template>
<div>
<button v-if="status === 'idle'" @click="initiate({ to: '0x...', amount: 1000000n })">
Initiate Payment
</button>
<p v-if="status === 'collecting'">
Collecting signatures: {{ collectedCount }}/{{ threshold }}
</p>
<button v-if="isReady" @click="submit">Submit Transaction</button>
<p v-if="error">Error: {{ error }}</p>
</div>
</template>Components
PaymentButton
Styled payment button with loading state.
interface PaymentButtonProps {
disabled?: boolean;
loading?: boolean;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}Example
<script setup lang="ts">
import { ref } from 'vue';
import { PaymentButton } from '@t402/vue';
const loading = ref(false);
async function handlePayment() {
loading.value = true;
await processPayment();
loading.value = false;
}
</script>
<template>
<PaymentButton
@click="handlePayment"
:loading="loading"
variant="primary"
size="lg"
>
Pay $10.00
</PaymentButton>
</template>PaymentDetails
Display payment requirements.
interface PaymentDetailsProps {
requirements: PaymentRequirements[];
selectedIndex?: number;
showNetworkSelector?: boolean;
}Events
| Event | Payload | Description |
|---|---|---|
select | number | Emitted when a network is selected |
Example
<script setup lang="ts">
import { ref } from 'vue';
import { PaymentDetails } from '@t402/vue';
const props = defineProps<{ paymentRequired: PaymentRequired }>();
const selected = ref(0);
</script>
<template>
<PaymentDetails
:requirements="props.paymentRequired.accepts"
:selected-index="selected"
@select="selected = $event"
show-network-selector
/>
</template>PaymentStatusDisplay
Show payment status with icons.
interface PaymentStatusDisplayProps {
status: PaymentStatus;
message?: string;
txHash?: string;
network?: string;
}Example
<script setup lang="ts">
import { PaymentStatusDisplay } from '@t402/vue';
const props = defineProps<{
status: PaymentStatus;
txHash?: string;
}>();
</script>
<template>
<PaymentStatusDisplay
:status="props.status"
:tx-hash="props.txHash"
network="eip155:8453"
:message="props.status === 'success' ? 'Payment confirmed!' : undefined"
/>
</template>AddressDisplay
Display blockchain addresses with truncation.
interface AddressDisplayProps {
address: string;
network?: string;
showCopy?: boolean;
showExplorer?: boolean;
}Example
<script setup lang="ts">
import { AddressDisplay } from '@t402/vue';
const props = defineProps<{
payTo: string;
network: string;
}>();
</script>
<template>
<AddressDisplay
:address="props.payTo"
:network="props.network"
show-copy
show-explorer
/>
</template>Spinner
Loading spinner component.
interface SpinnerProps {
size?: 'sm' | 'md' | 'lg';
color?: string;
}Utilities
Network Detection
import {
isEvmNetwork,
isSvmNetwork,
isTonNetwork,
isTronNetwork,
isTestnetNetwork,
getNetworkDisplayName,
} from '@t402/vue';
// Check network type
isEvmNetwork('eip155:8453'); // true
isSvmNetwork('solana:5eykt...'); // true
isTonNetwork('ton:mainnet'); // true
isTestnetNetwork('eip155:84532'); // true
// Get display name
getNetworkDisplayName('eip155:8453'); // "Base"
getNetworkDisplayName('ton:mainnet'); // "TON"Payment Helpers
import {
normalizePaymentRequirements,
getPreferredNetworks,
choosePaymentRequirement,
} from '@t402/vue';
// Normalize requirements to array
const requirements = normalizePaymentRequirements(paymentRequired);
// Get user's preferred networks
const preferred = getPreferredNetworks();
// Auto-select best requirement
const selected = choosePaymentRequirement(requirements, preferred);Formatters
import {
truncateAddress,
formatTokenAmount,
getAssetDisplayName,
} from '@t402/vue';
truncateAddress('0x1234...5678'); // "0x1234...5678"
formatTokenAmount('1000000', 6); // "1.00"
getAssetDisplayName('eip155:8453', 'USDT'); // "USDT on Base"Complete Example
<script setup lang="ts">
import { ref } from 'vue';
import {
usePaymentRequired,
PaymentButton,
PaymentDetails,
PaymentStatusDisplay,
} from '@t402/vue';
import { t402Client } from '@t402/fetch';
import { ExactEvmClient } from '@t402/evm/exact/client';
// Create client
const client = new t402Client()
.register('eip155:*', new ExactEvmClient(walletClient));
const content = ref(null);
const { paymentRequired, status, fetchResource, reset } = usePaymentRequired({
onSuccess: async (response) => {
content.value = await response.json();
},
});
async function handlePayment() {
// Process payment with client...
await client.createPaymentPayload(paymentRequired.value);
reset();
await fetchResource('/api/premium');
}
</script>
<template>
<div v-if="content">
{{ JSON.stringify(content) }}
</div>
<div v-else-if="paymentRequired" class="payment-modal">
<h2>Payment Required</h2>
<PaymentDetails :requirements="paymentRequired.accepts" />
<PaymentButton @click="handlePayment" size="lg">
Pay Now
</PaymentButton>
<PaymentStatusDisplay :status="status" />
</div>
<button v-else @click="fetchResource('/api/premium')">
Access Premium Content
</button>
</template>
<style scoped>
.payment-modal {
padding: 2rem;
border-radius: 8px;
background: #f5f5f5;
}
</style>Nuxt Integration
For Nuxt 3, create a plugin:
// plugins/t402.client.ts
import { t402Client } from '@t402/fetch';
import { ExactEvmClient } from '@t402/evm/exact/client';
export default defineNuxtPlugin(() => {
const client = new t402Client()
.register('eip155:*', new ExactEvmClient(walletClient));
return {
provide: {
t402Client: client,
},
};
});<script setup lang="ts">
const { $t402Client } = useNuxtApp();
</script>Type Exports
// Components
export {
Spinner,
PaymentButton,
PaymentStatusDisplay,
PaymentDetails,
AddressDisplay,
} from '@t402/vue';
// Composables
export {
usePaymentRequired,
usePaymentStatus,
useAsyncPayment,
useGaslessPayment,
useBridgePayment,
useMultiSigPayment,
} from '@t402/vue';
// Types
export type {
PaymentStatus,
PaymentState,
PaymentButtonProps,
PaymentStatusDisplayProps,
PaymentDetailsProps,
SpinnerProps,
AddressDisplayProps,
PaymentRequired,
PaymentRequirements,
} from '@t402/vue';Related
- @t402/react - React components
- @t402/paywall - Server-side paywall
- @t402/fetch - Fetch wrapper
- @t402/wdk-gasless - Gasless payments
- @t402/wdk-bridge - Cross-chain bridging
- @t402/wdk-multisig - Multi-sig payments
- Client Guide - Client implementation