React 集成
在 React 应用中集成 Payve 支付功能
12 分钟阅读
更新于 2025/8/13
ReactSDK前端集成
本指南展示如何在 React 应用中集成 Payve 支付功能,包括支付按钮、支付弹窗、状态管理等完整实现。
安装
# 使用 npm
npm install @payve/react @payve/js-sdk
# 使用 yarn
yarn add @payve/react @payve/js-sdk
# 使用 pnpm
pnpm add @payve/react @payve/js-sdk快速开始
1. 初始化 PayveProvider
在应用根组件配置 PayveProvider:
// App.jsx
import React from 'react';
import { PayveProvider } from '@payve/react';
function App() {
return (
<PayveProvider
apiKey="your-api-key"
environment="production" // 或 'sandbox' 用于测试
locale="zh" // 语言设置
>
{/* 您的应用组件 */}
<YourApp />
</PayveProvider>
);
}
export default App;JSX
代码已折叠
共 18 行
2. 使用支付按钮
最简单的集成方式 - 使用预置的支付按钮组件:
import React from 'react';
import { PayveButton } from '@payve/react';
function ProductPage() {
const handlePaymentSuccess = (payment) => {
console.log('支付成功!', payment);
// 处理支付成功逻辑
};
const handlePaymentError = (error) => {
console.error('支付失败:', error);
// 处理支付失败
};
return (
<div>
<h1>商品详情</h1>
<p>价格:100 USDT</p>
<PayveButton
amount={100}
currency="USDT"
description="商品购买"
metadata={{
productId: 'PROD-123',
orderId: 'ORDER-456'
}}
onSuccess={handlePaymentSuccess}
onError={handlePaymentError}
buttonText="立即支付"
className="bg-blue-500 text-white px-6 py-3 rounded-lg"
/>
</div>
);
}JSX
代码已折叠
共 35 行
进阶用法
自定义支付流程
使用 hooks 实现自定义支付流程:
import React, { useState } from 'react';
import { usePayve } from '@payve/react';
function CustomPayment() {
const { createPayment, getPaymentStatus } = usePayve();
const [loading, setLoading] = useState(false);
const [payment, setPayment] = useState(null);
const [selectedCurrency, setSelectedCurrency] = useState('USDT');
const supportedCurrencies = [
{ code: 'USDT', name: 'Tether USD', network: ['TRC20', 'ERC20', 'BEP20'] },
{ code: 'USDC', name: 'USD Coin', network: ['ERC20', 'BEP20'] },
{ code: 'BTC', name: 'Bitcoin', network: ['Bitcoin'] },
{ code: 'ETH', name: 'Ethereum', network: ['Ethereum'] },
{ code: 'BNB', name: 'Binance Coin', network: ['BEP20'] },
{ code: 'SOL', name: 'Solana', network: ['Solana'] }
];
const handleCreatePayment = async () => {
setLoading(true);
try {
const paymentData = await createPayment({
amount: 100,
currency: selectedCurrency,
description: '自定义支付',
metadata: {
userId: 'USER-123',
orderId: 'ORDER-789'
}
});
setPayment(paymentData);
// 开始轮询支付状态
pollPaymentStatus(paymentData.orderId);
} catch (error) {
console.error('创建支付失败:', error);
} finally {
setLoading(false);
}
};
const pollPaymentStatus = async (orderId) => {
const interval = setInterval(async () => {
try {
const status = await getPaymentStatus(orderId);
if (status.status === 'COMPLETED') {
clearInterval(interval);
handlePaymentComplete(status);
} else if (status.status === 'FAILED' || status.status === 'EXPIRED') {
clearInterval(interval);
handlePaymentFailed(status);
}
} catch (error) {
console.error('查询状态失败:', error);
}
}, 5000); // 每5秒查询一次
// 30分钟后停止轮询
setTimeout(() => clearInterval(interval), 30 * 60 * 1000);
};
const handlePaymentComplete = (payment) => {
console.log('支付完成!', payment);
alert('支付成功!');
};
const handlePaymentFailed = (payment) => {
console.log('支付失败', payment);
alert('支付失败或已过期');
};
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">选择支付方式</h2>
{/* 币种选择 */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">选择币种</label>
<select
value={selectedCurrency}
onChange={(e) => setSelectedCurrency(e.target.value)}
className="w-full p-2 border rounded"
>
{supportedCurrencies.map(currency => (
<option key={currency.code} value={currency.code}>
{currency.name} ({currency.code})
</option>
))}
</select>
</div>
{/* 创建支付按钮 */}
{!payment && (
<button
onClick={handleCreatePayment}
disabled={loading}
className="w-full bg-blue-500 text-white py-3 rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? '创建中...' : `支付 100 ${selectedCurrency}`}
</button>
)}
{/* 支付信息显示 */}
{payment && (
<div className="mt-6 p-4 border rounded">
<h3 className="font-bold mb-2">支付信息</h3>
<p className="text-sm mb-2">订单号:{payment.orderId}</p>
<p className="text-sm mb-2">金额:{payment.amount} {payment.currency}</p>
<p className="text-sm mb-2">网络:{payment.network}</p>
<p className="text-sm mb-4">地址:{payment.address}</p>
{/* 二维码 */}
{payment.qrCode && (
<div className="flex justify-center mb-4">
<img src={payment.qrCode} alt="Payment QR Code" className="w-48 h-48" />
</div>
)}
<p className="text-center text-sm text-gray-500">
请使用支持 {payment.currency} 的钱包扫描二维码完成支付
</p>
</div>
)}
</div>
);
}JSX
代码已折叠
共 127 行
支付弹窗组件
创建可复用的支付弹窗组件:
import React, { useState, useEffect } from 'react';
import { usePayve } from '@payve/react';
import { X, Copy, CheckCircle, Clock, AlertCircle } from 'lucide-react';
function PaymentModal({ isOpen, onClose, amount, currency, metadata }) {
const { createPayment, subscribeToPayment } = usePayve();
const [payment, setPayment] = useState(null);
const [status, setStatus] = useState('idle'); // idle, loading, pending, completed, failed
const [copied, setCopied] = useState(false);
const [timeLeft, setTimeLeft] = useState(1800); // 30分钟
useEffect(() => {
if (isOpen && status === 'idle') {
initPayment();
}
}, [isOpen]);
useEffect(() => {
if (payment && status === 'pending') {
// 倒计时
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 0) {
setStatus('failed');
return 0;
}
return prev - 1;
});
}, 1000);
// 订阅支付状态
const unsubscribe = subscribeToPayment(payment.orderId, (update) => {
if (update.status === 'COMPLETED') {
setStatus('completed');
clearInterval(timer);
} else if (update.status === 'FAILED' || update.status === 'EXPIRED') {
setStatus('failed');
clearInterval(timer);
}
});
return () => {
clearInterval(timer);
unsubscribe();
};
}
}, [payment, status]);
const initPayment = async () => {
setStatus('loading');
try {
const paymentData = await createPayment({
amount,
currency,
metadata
});
setPayment(paymentData);
setStatus('pending');
} catch (error) {
console.error('创建支付失败:', error);
setStatus('failed');
}
};
const copyAddress = () => {
navigator.clipboard.writeText(payment.address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
{/* 头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">加密货币支付</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"
>
<X size={24} />
</button>
</div>
{/* 加载状态 */}
{status === 'loading' && (
<div className="py-12 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600">正在创建支付订单...</p>
</div>
)}
{/* 支付信息 */}
{status === 'pending' && payment && (
<div>
{/* 倒计时 */}
<div className="flex items-center justify-center mb-4 text-sm">
<Clock size={16} className="mr-1" />
<span>剩余时间:{formatTime(timeLeft)}</span>
</div>
{/* 金额信息 */}
<div className="bg-gray-50 rounded p-4 mb-4">
<div className="flex justify-between items-center">
<span className="text-gray-600">应付金额</span>
<span className="text-xl font-bold">
{payment.amount} {payment.currency}
</span>
</div>
<div className="flex justify-between items-center mt-2 text-sm">
<span className="text-gray-500">网络</span>
<span>{payment.network}</span>
</div>
</div>
{/* 二维码 */}
<div className="flex justify-center mb-4">
<div className="p-4 bg-white border-2 border-gray-200 rounded">
<img
src={payment.qrCode}
alt="Payment QR"
className="w-48 h-48"
/>
</div>
</div>
{/* 地址 */}
<div className="mb-4">
<label className="block text-sm text-gray-600 mb-1">
收款地址
</label>
<div className="flex items-center">
<input
type="text"
value={payment.address}
readOnly
className="flex-1 p-2 border rounded-l text-sm"
/>
<button
onClick={copyAddress}
className="px-3 py-2 bg-blue-500 text-white rounded-r hover:bg-blue-600"
>
{copied ? <CheckCircle size={20} /> : <Copy size={20} />}
</button>
</div>
</div>
{/* 提示信息 */}
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 text-sm">
<p className="flex items-start">
<AlertCircle size={16} className="mr-2 mt-0.5 flex-shrink-0 text-yellow-600" />
<span>
请确保发送准确的金额,否则可能导致支付失败。
支付完成后,我们将自动确认您的订单。
</span>
</p>
</div>
</div>
)}
{/* 成功状态 */}
{status === 'completed' && (
<div className="py-12 text-center">
<CheckCircle size={64} className="text-green-500 mx-auto mb-4" />
<h3 className="text-xl font-bold mb-2">支付成功!</h3>
<p className="text-gray-600 mb-4">您的支付已确认</p>
<button
onClick={onClose}
className="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
完成
</button>
</div>
)}
{/* 失败状态 */}
{status === 'failed' && (
<div className="py-12 text-center">
<AlertCircle size={64} className="text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-bold mb-2">支付失败</h3>
<p className="text-gray-600 mb-4">
{timeLeft === 0 ? '支付超时' : '支付未完成'}
</p>
<button
onClick={() => {
setStatus('idle');
setTimeLeft(1800);
initPayment();
}}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
重试
</button>
</div>
)}
</div>
</div>
);
}
export default PaymentModal;JSX
代码已折叠
共 209 行
使用支付弹窗
import React, { useState } from 'react';
import PaymentModal from './PaymentModal';
function CheckoutPage() {
const [showPayment, setShowPayment] = useState(false);
const [orderData] = useState({
items: [
{ id: 1, name: '商品A', price: 50, quantity: 1 },
{ id: 2, name: '商品B', price: 30, quantity: 2 }
],
total: 110
});
return (
<div className="container mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">结账</h1>
{/* 订单详情 */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">订单详情</h2>
{orderData.items.map(item => (
<div key={item.id} className="flex justify-between py-2 border-b">
<span>{item.name} x {item.quantity}</span>
<span>${item.price * item.quantity}</span>
</div>
))}
<div className="flex justify-between pt-4 font-bold">
<span>总计</span>
<span>${orderData.total}</span>
</div>
</div>
{/* 支付按钮 */}
<button
onClick={() => setShowPayment(true)}
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
使用加密货币支付
</button>
{/* 支付弹窗 */}
<PaymentModal
isOpen={showPayment}
onClose={() => setShowPayment(false)}
amount={orderData.total}
currency="USDT"
metadata={{
orderId: 'ORDER-' + Date.now(),
items: orderData.items
}}
/>
</div>
);
}JSX
代码已折叠
共 54 行
状态管理
使用 Context 管理支付状态
import React, { createContext, useContext, useState, useCallback } from 'react';
import { usePayve } from '@payve/react';
// 创建支付上下文
const PaymentContext = createContext();
export function PaymentProvider({ children }) {
const { createPayment, getPaymentStatus } = usePayve();
const [payments, setPayments] = useState([]);
const [activePayment, setActivePayment] = useState(null);
const initiatePayment = useCallback(async (options) => {
try {
const payment = await createPayment(options);
setPayments(prev => [...prev, payment]);
setActivePayment(payment);
return payment;
} catch (error) {
console.error('Payment creation failed:', error);
throw error;
}
}, [createPayment]);
const updatePaymentStatus = useCallback(async (orderId) => {
try {
const status = await getPaymentStatus(orderId);
setPayments(prev => prev.map(p =>
p.orderId === orderId ? { ...p, ...status } : p
));
if (activePayment?.orderId === orderId) {
setActivePayment(prev => ({ ...prev, ...status }));
}
return status;
} catch (error) {
console.error('Status update failed:', error);
throw error;
}
}, [getPaymentStatus, activePayment]);
const value = {
payments,
activePayment,
initiatePayment,
updatePaymentStatus,
setActivePayment
};
return (
<PaymentContext.Provider value={value}>
{children}
</PaymentContext.Provider>
);
}
export function usePaymentContext() {
const context = useContext(PaymentContext);
if (!context) {
throw new Error('usePaymentContext must be used within PaymentProvider');
}
return context;
}JSX
代码已折叠
共 66 行
使用 Redux 管理支付状态
// paymentSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { PayveSDK } from '@payve/js-sdk';
const payve = new PayveSDK({ apiKey: process.env.REACT_APP_PAYVE_API_KEY });
// 异步 actions
export const createPayment = createAsyncThunk(
'payment/create',
async (paymentData) => {
const response = await payve.createPayment(paymentData);
return response;
}
);
export const fetchPaymentStatus = createAsyncThunk(
'payment/fetchStatus',
async (orderId) => {
const response = await payve.getPaymentStatus(orderId);
return response;
}
);
// Slice
const paymentSlice = createSlice({
name: 'payment',
initialState: {
payments: [],
activePayment: null,
loading: false,
error: null
},
reducers: {
setActivePayment: (state, action) => {
state.activePayment = action.payload;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
builder
// 创建支付
.addCase(createPayment.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(createPayment.fulfilled, (state, action) => {
state.loading = false;
state.payments.push(action.payload);
state.activePayment = action.payload;
})
.addCase(createPayment.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// 获取状态
.addCase(fetchPaymentStatus.fulfilled, (state, action) => {
const index = state.payments.findIndex(
p => p.orderId === action.payload.orderId
);
if (index !== -1) {
state.payments[index] = action.payload;
}
if (state.activePayment?.orderId === action.payload.orderId) {
state.activePayment = action.payload;
}
});
}
});
export const { setActivePayment, clearError } = paymentSlice.actions;
export default paymentSlice.reducer;JAVASCRIPT
代码已折叠
共 73 行
WebSocket 实时更新
使用 WebSocket 接收实时支付状态更新:
import React, { useEffect, useState } from 'react';
import { usePayveWebSocket } from '@payve/react';
function PaymentTracker({ orderId }) {
const [status, setStatus] = useState('PENDING');
const [confirmations, setConfirmations] = useState(0);
const { subscribe, unsubscribe } = usePayveWebSocket();
useEffect(() => {
// 订阅支付状态更新
const subscription = subscribe(`payment.${orderId}`, (event) => {
console.log('收到支付更新:', event);
switch (event.type) {
case 'payment.confirming':
setStatus('CONFIRMING');
setConfirmations(event.data.confirmations);
break;
case 'payment.succeeded':
setStatus('COMPLETED');
setConfirmations(event.data.confirmations);
// 触发成功处理
handlePaymentSuccess(event.data);
break;
case 'payment.failed':
setStatus('FAILED');
// 触发失败处理
handlePaymentFailure(event.data);
break;
default:
console.log('未知事件类型:', event.type);
}
});
// 清理函数
return () => {
unsubscribe(subscription);
};
}, [orderId]);
const handlePaymentSuccess = (data) => {
console.log('支付成功!', data);
// 显示成功通知
// 更新订单状态
// 跳转到成功页面
};
const handlePaymentFailure = (data) => {
console.log('支付失败', data);
// 显示失败通知
// 提供重试选项
};
const getStatusDisplay = () => {
switch (status) {
case 'PENDING':
return { text: '等待支付', color: 'text-yellow-600' };
case 'CONFIRMING':
return { text: `确认中 (${confirmations}/6)`, color: 'text-blue-600' };
case 'COMPLETED':
return { text: '支付成功', color: 'text-green-600' };
case 'FAILED':
return { text: '支付失败', color: 'text-red-600' };
default:
return { text: '未知状态', color: 'text-gray-600' };
}
};
const statusDisplay = getStatusDisplay();
return (
<div className="p-4 border rounded-lg">
<h3 className="font-semibold mb-2">支付状态</h3>
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full mr-2 ${
status === 'COMPLETED' ? 'bg-green-500' :
status === 'FAILED' ? 'bg-red-500' :
status === 'CONFIRMING' ? 'bg-blue-500' :
'bg-yellow-500'
} ${status !== 'COMPLETED' && status !== 'FAILED' ? 'animate-pulse' : ''}`} />
<span className={statusDisplay.color}>
{statusDisplay.text}
</span>
</div>
{status === 'CONFIRMING' && (
<div className="mt-3">
<div className="bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${(confirmations / 6) * 100}%` }}
/>
</div>
<p className="text-xs text-gray-600 mt-1">
需要 6 次确认,当前 {confirmations} 次
</p>
</div>
)}
</div>
);
}JSX
代码已折叠
共 104 行
测试模式
在开发环境使用沙盒模式测试:
// 开发环境配置
import { PayveProvider } from '@payve/react';
function App() {
const isDevelopment = process.env.NODE_ENV === 'development';
return (
<PayveProvider
apiKey={process.env.REACT_APP_PAYVE_API_KEY}
environment={isDevelopment ? 'sandbox' : 'production'}
debug={isDevelopment} // 开启调试日志
>
<YourApp />
</PayveProvider>
);
}
// 使用测试数据
function TestPayment() {
const { createPayment } = usePayve();
const handleTestPayment = async () => {
const payment = await createPayment({
amount: 0.01, // 使用小额测试
currency: 'USDT',
testMode: true, // 标记为测试模式
metadata: {
test: true,
timestamp: Date.now()
}
});
console.log('测试支付创建:', payment);
// 在沙盒环境,可以使用模拟支付完成
if (payment.testMode) {
setTimeout(() => {
console.log('模拟支付完成');
// 触发支付成功逻辑
}, 3000);
}
};
return (
<button onClick={handleTestPayment}>
创建测试支付
</button>
);
}JSX
代码已折叠
共 49 行
错误处理
完善的错误处理提升用户体验:
import React, { useState } from 'react';
import { usePayve } from '@payve/react';
function PaymentWithErrorHandling() {
const { createPayment } = usePayve();
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const handlePayment = async (retrying = false) => {
try {
setError(null);
const payment = await createPayment({
amount: 100,
currency: 'USDT'
});
// 成功处理
console.log('支付创建成功:', payment);
setRetryCount(0);
} catch (err) {
console.error('支付错误:', err);
// 根据错误类型显示不同提示
if (err.code === 'NETWORK_ERROR') {
setError({
type: 'network',
message: '网络连接失败,请检查您的网络连接',
canRetry: true
});
} else if (err.code === 'RATE_LIMIT') {
setError({
type: 'rateLimit',
message: '请求过于频繁,请稍后再试',
canRetry: true,
retryAfter: err.retryAfter || 60
});
} else if (err.code === 'INVALID_AMOUNT') {
setError({
type: 'validation',
message: '支付金额无效',
canRetry: false
});
} else if (err.code === 'UNAUTHORIZED') {
setError({
type: 'auth',
message: 'API密钥无效或已过期',
canRetry: false
});
} else {
setError({
type: 'unknown',
message: err.message || '支付创建失败,请稍后重试',
canRetry: true
});
}
// 自动重试逻辑
if (!retrying && retryCount < 3 && err.code === 'NETWORK_ERROR') {
setRetryCount(prev => prev + 1);
setTimeout(() => {
handlePayment(true);
}, 2000 * (retryCount + 1)); // 递增延迟
}
}
};
return (
<div className="p-6">
{error && (
<div className={`p-4 rounded-lg mb-4 ${
error.type === 'network' ? 'bg-yellow-50 border-yellow-200' :
error.type === 'auth' ? 'bg-red-50 border-red-200' :
'bg-orange-50 border-orange-200'
} border`}>
<p className="font-semibold mb-1">
{error.type === 'network' ? '网络错误' :
error.type === 'auth' ? '认证错误' :
error.type === 'validation' ? '输入错误' :
'支付错误'}
</p>
<p className="text-sm">{error.message}</p>
{error.canRetry && (
<button
onClick={() => handlePayment()}
className="mt-2 text-sm text-blue-600 hover:text-blue-800"
>
重试 {retryCount > 0 && `(${retryCount}/3)`}
</button>
)}
{error.retryAfter && (
<p className="text-xs text-gray-600 mt-1">
请在 {error.retryAfter} 秒后重试
</p>
)}
</div>
)}
<button
onClick={() => handlePayment()}
className="px-6 py-3 bg-blue-500 text-white rounded hover:bg-blue-600"
>
创建支付
</button>
</div>
);
}JSX
代码已折叠
共 110 行
最佳实践
-
性能优化
- 使用 React.memo 避免不必要的重渲染
- 懒加载支付组件
- 缓存支付状态避免重复请求
-
安全考虑
- 不要在前端存储 API Secret
- 使用环境变量管理配置
- 验证所有用户输入
-
用户体验
- 提供清晰的加载和错误状态
- 实时更新支付进度
- 支持多种支付币种
-
代码组织
- 将支付逻辑抽象为自定义 hooks
- 使用 TypeScript 提供类型安全
- 分离业务逻辑和UI组件
