feat: Stripe 改用 PaymentIntent + Payment Element,iframe 嵌入支付宝弹窗支付
Stripe 集成重构: - 从 Checkout Session 改为 PaymentIntent + Payment Element 模式 - 前端内联渲染 Stripe 支付表单,支持信用卡、支付宝等多种方式 - Webhook 事件改为 payment_intent.succeeded / payment_intent.payment_failed - provider/test 同步更新 iframe 嵌入模式 (ui_mode=embedded): - 支付宝等需跳转的方式改为弹出新窗口处理,避免 X-Frame-Options 冲破 iframe - 信用卡等无跳转方式仍在 iframe 内联完成 - 弹窗使用 confirmAlipayPayment 直接跳转,无需二次操作 - result 页面检测弹窗模式,支付成功后自动关闭窗口 Bug 修复: - 修复配置加载前支付方式闪烁(初始值改为空数组 + loading) - 修复桌面端 PaymentForm 缺少 methodLimits prop - 修复 stripeError 隐藏表单导致无法重试 - 快捷金额增加 1000/2000 选项,过滤低于 minAmount 的选项 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PAYMENT_TYPE_META } from '@/lib/pay-utils';
|
||||
|
||||
export interface MethodLimitInfo {
|
||||
@@ -25,7 +25,7 @@ interface PaymentFormProps {
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
const QUICK_AMOUNTS = [10, 20, 50, 100, 200, 500];
|
||||
const QUICK_AMOUNTS = [10, 20, 50, 100, 200, 500, 1000, 2000];
|
||||
const AMOUNT_TEXT_PATTERN = /^\d*(\.\d{0,2})?$/;
|
||||
|
||||
function hasValidCentPrecision(num: number): boolean {
|
||||
@@ -48,6 +48,13 @@ export default function PaymentForm({
|
||||
const [paymentType, setPaymentType] = useState(enabledPaymentTypes[0] || 'alipay');
|
||||
const [customAmount, setCustomAmount] = useState('');
|
||||
|
||||
// Reset paymentType when enabledPaymentTypes changes (e.g. after config loads)
|
||||
useEffect(() => {
|
||||
if (!enabledPaymentTypes.includes(paymentType)) {
|
||||
setPaymentType(enabledPaymentTypes[0] || 'stripe');
|
||||
}
|
||||
}, [enabledPaymentTypes, paymentType]);
|
||||
|
||||
const handleQuickAmount = (val: number) => {
|
||||
setAmount(val);
|
||||
setCustomAmount(String(val));
|
||||
@@ -159,7 +166,7 @@ export default function PaymentForm({
|
||||
充值金额
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{QUICK_AMOUNTS.filter((val) => val <= effectiveMax).map((val) => (
|
||||
{QUICK_AMOUNTS.filter((val) => val >= minAmount && val <= effectiveMax).map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
type="button"
|
||||
@@ -222,69 +229,71 @@ export default function PaymentForm({
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Payment Type */}
|
||||
<div>
|
||||
<label className={['mb-2 block text-sm font-medium', dark ? 'text-slate-200' : 'text-gray-700'].join(' ')}>
|
||||
支付方式
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
{enabledPaymentTypes.map((type) => {
|
||||
const meta = PAYMENT_TYPE_META[type];
|
||||
const isSelected = paymentType === type;
|
||||
const limitInfo = methodLimits?.[type];
|
||||
const isUnavailable = limitInfo !== undefined && !limitInfo.available;
|
||||
{/* Payment Type — only show when multiple types available */}
|
||||
{enabledPaymentTypes.length > 1 && (
|
||||
<div>
|
||||
<label className={['mb-2 block text-sm font-medium', dark ? 'text-slate-200' : 'text-gray-700'].join(' ')}>
|
||||
支付方式
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
{enabledPaymentTypes.map((type) => {
|
||||
const meta = PAYMENT_TYPE_META[type];
|
||||
const isSelected = paymentType === type;
|
||||
const limitInfo = methodLimits?.[type];
|
||||
const isUnavailable = limitInfo !== undefined && !limitInfo.available;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
disabled={isUnavailable}
|
||||
onClick={() => !isUnavailable && setPaymentType(type)}
|
||||
title={isUnavailable ? '今日充值额度已满,请使用其他支付方式' : undefined}
|
||||
className={[
|
||||
'relative flex h-[58px] flex-1 flex-col items-center justify-center rounded-lg border px-3 transition-all',
|
||||
isUnavailable
|
||||
? dark
|
||||
? 'cursor-not-allowed border-slate-700 bg-slate-800/50 opacity-50'
|
||||
: 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50'
|
||||
: isSelected
|
||||
? `${meta?.selectedBorder || 'border-blue-500'} ${meta?.selectedBg || 'bg-blue-50'} text-slate-900 shadow-sm`
|
||||
: dark
|
||||
? 'border-slate-700 bg-slate-900 text-slate-200 hover:border-slate-500'
|
||||
: 'border-gray-300 bg-white text-slate-700 hover:border-gray-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{renderPaymentIcon(type)}
|
||||
<span className="flex flex-col items-start leading-none">
|
||||
<span className="text-xl font-semibold tracking-tight">{meta?.label || type}</span>
|
||||
{isUnavailable ? (
|
||||
<span className="text-[10px] tracking-wide text-red-400">今日额度已满</span>
|
||||
) : meta?.sublabel ? (
|
||||
<span
|
||||
className={`text-[10px] tracking-wide ${dark && !isSelected ? 'text-slate-400' : 'text-slate-600'}`}
|
||||
>
|
||||
{meta.sublabel}
|
||||
</span>
|
||||
) : null}
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
disabled={isUnavailable}
|
||||
onClick={() => !isUnavailable && setPaymentType(type)}
|
||||
title={isUnavailable ? '今日充值额度已满,请使用其他支付方式' : undefined}
|
||||
className={[
|
||||
'relative flex h-[58px] flex-1 flex-col items-center justify-center rounded-lg border px-3 transition-all',
|
||||
isUnavailable
|
||||
? dark
|
||||
? 'cursor-not-allowed border-slate-700 bg-slate-800/50 opacity-50'
|
||||
: 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50'
|
||||
: isSelected
|
||||
? `${meta?.selectedBorder || 'border-blue-500'} ${meta?.selectedBg || 'bg-blue-50'} text-slate-900 shadow-sm`
|
||||
: dark
|
||||
? 'border-slate-700 bg-slate-900 text-slate-200 hover:border-slate-500'
|
||||
: 'border-gray-300 bg-white text-slate-700 hover:border-gray-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{renderPaymentIcon(type)}
|
||||
<span className="flex flex-col items-start leading-none">
|
||||
<span className="text-xl font-semibold tracking-tight">{meta?.label || type}</span>
|
||||
{isUnavailable ? (
|
||||
<span className="text-[10px] tracking-wide text-red-400">今日额度已满</span>
|
||||
) : meta?.sublabel ? (
|
||||
<span
|
||||
className={`text-[10px] tracking-wide ${dark && !isSelected ? 'text-slate-400' : 'text-slate-600'}`}
|
||||
>
|
||||
{meta.sublabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 当前选中渠道额度不足时的提示 */}
|
||||
{(() => {
|
||||
const limitInfo = methodLimits?.[paymentType];
|
||||
if (!limitInfo || limitInfo.available) return null;
|
||||
return (
|
||||
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
|
||||
所选支付方式今日额度已满,请切换到其他支付方式
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{/* 当前选中渠道额度不足时的提示 */}
|
||||
{(() => {
|
||||
const limitInfo = methodLimits?.[paymentType];
|
||||
if (!limitInfo || limitInfo.available) return null;
|
||||
return (
|
||||
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
|
||||
所选支付方式今日额度已满,请切换到其他支付方式
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fee Detail */}
|
||||
{feeRate > 0 && selectedAmount > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user