Files
soul/components/modules/payment/payment-modal.tsx

344 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import type React from "react"
import { useState } from "react"
import { X, CheckCircle, Bitcoin, Globe, Copy, Check } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useStore } from "@/lib/store"
const WechatIcon = () => (
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.406-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z" />
</svg>
)
const AlipayIcon = () => (
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M8.77 20.62l9.92-4.33c-.12-.33-.24-.66-.38-.99-.14-.33-.3-.66-.47-.99H8.08c-2.2 0-3.99-1.79-3.99-3.99V8.08c0-2.2 1.79-3.99 3.99-3.99h7.84c2.2 0 3.99 1.79 3.99 3.99v2.24h-8.66c-.55 0-1 .45-1 1s.45 1 1 1h10.66c-.18 1.73-.71 3.36-1.53 4.83l-2.76 1.2c-.74-1.69-1.74-3.24-2.93-4.6-.52-.59-1.11-1.13-1.76-1.59H4.09v4.24c0 2.2 1.79 3.99 3.99 3.99h.69v.23z" />
</svg>
)
type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal"
interface PaymentModalProps {
isOpen: boolean
onClose: () => void
type: "section" | "fullbook"
sectionId?: string
sectionTitle?: string
amount: number
onSuccess: () => void
}
export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, amount, onSuccess }: PaymentModalProps) {
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat")
const [isProcessing, setIsProcessing] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [showPaymentDetails, setShowPaymentDetails] = useState(false)
const [copied, setCopied] = useState(false)
const { purchaseSection, purchaseFullBook, user, settings } = useStore()
const paymentConfig = settings?.paymentMethods || {
wechat: { enabled: true, qrCode: "", account: "" },
alipay: { enabled: true, qrCode: "", account: "" },
usdt: { enabled: true, network: "TRC20", address: "", exchangeRate: 7.2 },
paypal: { enabled: false, email: "", exchangeRate: 7.2 },
}
const usdtAmount = (amount / (paymentConfig.usdt.exchangeRate || 7.2)).toFixed(2)
const paypalAmount = (amount / (paymentConfig.paypal.exchangeRate || 7.2)).toFixed(2)
const handleCopyAddress = (address: string) => {
navigator.clipboard.writeText(address)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const handlePayment = async () => {
if (paymentMethod === "usdt" || paymentMethod === "paypal" || paymentMethod === "wechat" || paymentMethod === "alipay") {
setShowPaymentDetails(true)
return
}
setIsProcessing(true)
await new Promise((resolve) => setTimeout(resolve, 1500))
let success = false
if (type === "section" && sectionId) {
success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
} else if (type === "fullbook") {
success = await purchaseFullBook(paymentMethod)
}
setIsProcessing(false)
if (success) {
setIsSuccess(true)
setTimeout(() => {
onSuccess()
onClose()
setIsSuccess(false)
}, 1500)
}
}
const confirmCryptoPayment = async () => {
setIsProcessing(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
let success = false
if (type === "section" && sectionId) {
success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
} else if (type === "fullbook") {
success = await purchaseFullBook(paymentMethod)
}
setIsProcessing(false)
setShowPaymentDetails(false)
if (success) {
setIsSuccess(true)
setTimeout(() => {
onSuccess()
onClose()
setIsSuccess(false)
}, 1500)
}
}
if (!isOpen) return null
const paymentMethods: {
id: PaymentMethod
name: string
icon: React.ReactNode
color: string
enabled: boolean
extra?: string
}[] = [
{
id: "wechat",
name: "微信支付",
icon: <WechatIcon />,
color: "bg-[#07C160]",
enabled: paymentConfig.wechat.enabled,
},
{
id: "alipay",
name: "支付宝",
icon: <AlipayIcon />,
color: "bg-[#1677FF]",
enabled: paymentConfig.alipay.enabled,
},
{
id: "usdt",
name: `USDT (${paymentConfig.usdt.network || "TRC20"})`,
icon: <Bitcoin className="w-5 h-5" />,
color: "bg-[#26A17B]",
enabled: paymentConfig.usdt.enabled,
extra: `$${usdtAmount}`,
},
{
id: "paypal",
name: "PayPal",
icon: <Globe className="w-5 h-5" />,
color: "bg-[#003087]",
enabled: paymentConfig.paypal.enabled,
extra: `$${paypalAmount}`,
},
]
const availableMethods = paymentMethods.filter((m) => m.enabled)
// Payment details view
if (showPaymentDetails) {
const isCrypto = paymentMethod === "usdt"
const isPayPal = paymentMethod === "paypal"
const isWechat = paymentMethod === "wechat"
const isAlipay = paymentMethod === "alipay"
let title = ""
let address = ""
let displayAmount = `¥${amount.toFixed(2)}`
let qrCodeUrl = ""
if (isCrypto) {
title = "USDT支付"
address = paymentConfig.usdt.address
displayAmount = `$${usdtAmount} USDT`
} else if (isPayPal) {
title = "PayPal支付"
address = paymentConfig.paypal.email
displayAmount = `$${paypalAmount} USD`
} else if (isWechat) {
title = "微信支付"
qrCodeUrl = paymentConfig.wechat.qrCode || "/images/wechat-pay.png"
} else if (isAlipay) {
title = "支付宝支付"
qrCodeUrl = paymentConfig.alipay.qrCode || "/images/alipay.png"
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-md bg-[#0f2137] rounded-2xl border border-gray-700/50 overflow-hidden">
<button onClick={onClose} className="absolute top-4 right-4 p-2 text-gray-400 hover:text-white z-10">
<X className="w-5 h-5" />
</button>
<div className="p-6">
<h3 className="text-lg font-semibold text-white mb-4 text-center">{title}</h3>
<div className="bg-[#0a1628] rounded-xl p-4 mb-4 text-center">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-3xl font-bold text-[#38bdac]">{displayAmount}</p>
{(isCrypto || isPayPal) && <p className="text-gray-500 text-sm"> ¥{amount.toFixed(2)}</p>}
</div>
{(isWechat || isAlipay) ? (
<div className="flex flex-col items-center justify-center mb-6">
<div className="bg-white p-2 rounded-xl mb-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={qrCodeUrl} alt={title} className="w-48 h-48 object-contain" />
</div>
<p className="text-gray-400 text-sm">使{title === "微信支付" ? "微信" : "支付宝"}</p>
</div>
) : (
<div className="bg-[#0a1628] rounded-xl p-4 mb-4">
<p className="text-gray-400 text-sm mb-2">
{isCrypto ? `收款地址 (${paymentConfig.usdt.network})` : "PayPal账户"}
</p>
<div className="flex items-center gap-2">
<p className="text-white text-sm break-all flex-1 font-mono">
{address || "请联系客服获取"}
</p>
{address && (
<button onClick={() => handleCopyAddress(address)} className="text-[#38bdac] hover:text-[#4fd4c4]">
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</button>
)}
</div>
</div>
)}
<div className="bg-orange-500/10 border border-orange-500/30 rounded-xl p-4 mb-6">
<p className="text-orange-400 text-sm text-center">
"我已支付"<br/>
</p>
</div>
<div className="flex gap-3">
<Button
onClick={() => setShowPaymentDetails(false)}
variant="outline"
className="flex-1 border-gray-600 text-white hover:bg-gray-700/50 bg-transparent"
>
</Button>
<Button
onClick={confirmCryptoPayment}
disabled={isProcessing}
className="flex-1 bg-[#38bdac] hover:bg-[#2da396] text-white"
>
{isProcessing ? "处理中..." : "已完成支付"}
</Button>
</div>
</div>
</div>
</div>
)
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-md bg-[#0f2137] rounded-2xl border border-gray-700/50 overflow-hidden">
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 text-gray-400 hover:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
{isSuccess ? (
<div className="p-8 text-center">
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-[#38bdac]/20 flex items-center justify-center">
<CheckCircle className="w-10 h-10 text-[#38bdac]" />
</div>
<h3 className="text-xl font-semibold text-white mb-2"></h3>
<p className="text-gray-400">{type === "fullbook" ? "您已解锁全部内容" : "您已解锁本节内容"}</p>
</div>
) : (
<>
<div className="p-6 border-b border-gray-700/50">
<h3 className="text-lg font-semibold text-white mb-1"></h3>
<p className="text-gray-400 text-sm">
{type === "fullbook" ? "购买整本书,解锁全部内容" : `购买: ${sectionTitle}`}
</p>
</div>
<div className="p-6 border-b border-gray-700/50 text-center">
<p className="text-gray-400 text-sm mb-1"></p>
<p className="text-4xl font-bold text-white">¥{amount.toFixed(2)}</p>
{(paymentMethod === "usdt" || paymentMethod === "paypal") && (
<p className="text-[#38bdac] text-sm mt-1">
${paymentMethod === "usdt" ? usdtAmount : paypalAmount} USD
</p>
)}
{user?.referredBy && (
<p className="text-[#38bdac] text-sm mt-2">
,{settings?.distributorShare || 90}%
</p>
)}
</div>
<div className="p-6 space-y-3">
<p className="text-gray-400 text-sm mb-3"></p>
{availableMethods.map((method) => (
<button
key={method.id}
onClick={() => setPaymentMethod(method.id)}
className={`w-full p-4 rounded-xl border flex items-center gap-4 transition-all ${
paymentMethod === method.id
? "border-[#38bdac] bg-[#38bdac]/10"
: "border-gray-700 hover:border-gray-600"
}`}
>
<div className={`w-10 h-10 rounded-lg ${method.color} flex items-center justify-center text-white`}>
{method.icon}
</div>
<div className="flex-1 text-left">
<span className="text-white">{method.name}</span>
{method.extra && <span className="text-gray-400 text-sm ml-2">{method.extra}</span>}
</div>
{paymentMethod === method.id && <CheckCircle className="w-5 h-5 text-[#38bdac]" />}
</button>
))}
{availableMethods.length === 0 && <p className="text-gray-500 text-center py-4"></p>}
</div>
<div className="p-6 pt-0">
<Button
onClick={handlePayment}
disabled={isProcessing || availableMethods.length === 0}
className="w-full bg-[#38bdac] hover:bg-[#2da396] text-white py-6 text-lg"
>
{isProcessing ? (
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
</div>
) : (
`确认支付 ¥${amount.toFixed(2)}`
)}
</Button>
<p className="text-gray-500 text-xs text-center mt-3"></p>
</div>
</>
)}
</div>
</div>
)
}