Files
soul/components/payment-modal.tsx

444 lines
19 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, useEffect } from "react"
import { X, CheckCircle, Bitcoin, Globe, Copy, Check, QrCode, Shield, Users } from "lucide-react"
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="M20.422 13.066c-.198-.07-.405-.137-.62-.202.107-.263.204-.534.29-.814h-3.32v-.93h3.927v-.627h-3.927v-1.18h-1.637c.07-.138.131-.28.184-.425l-1.483-.326a4.091 4.091 0 0 1-.405.75h-2.78v1.181H7.72v.627h2.932v.93H7.205v.652h5.784a9.296 9.296 0 0 1-.608.814 13.847 13.847 0 0 0-2.76-.93l-.483.652c1.038.273 1.96.608 2.766 1.008a8.483 8.483 0 0 1-3.603 1.484l.43.652c1.71-.378 3.103-1.03 4.18-1.957.665.395 1.223.835 1.67 1.32l.608-.652c-.44-.43-.984-.836-1.637-1.215a9.6 9.6 0 0 0 .72-.93c.182-.264.345-.53.488-.798.587.168 1.12.35 1.598.544 1.956.8 2.82 1.614 2.82 2.665 0 .727-.587 1.277-2.21 1.277-1.193 0-2.524-.203-3.996-.609l-.103.75c1.445.378 2.843.567 4.196.567 2.158 0 3.204-.748 3.204-2.013 0-1.382-1.183-2.437-3.58-3.413z" />
<path d="M21.714 4H2.286A2.286 2.286 0 0 0 0 6.286v11.428A2.286 2.286 0 0 0 2.286 20h19.428A2.286 2.286 0 0 0 24 17.714V6.286A2.286 2.286 0 0 0 21.714 4zM2.286 5.143h19.428c.631 0 1.143.512 1.143 1.143v8.08c-.957-.454-2.222-.903-3.75-1.346a9.8 9.8 0 0 0 .607-2.02h-4.571V9.286h5.143V8.143h-5.143V6.286H13.43v1.857H8.286v1.143h5.143V11H8.286v1.143h6.356a11.54 11.54 0 0 1-.916 1.512 16.648 16.648 0 0 0-3.3-1.12l-.576.78c1.242.328 2.348.73 3.31 1.21a10.175 10.175 0 0 1-4.317 1.78l.514.78c2.048-.454 3.718-1.237 5.008-2.344.796.472 1.464 1 2.004 1.583l.726-.78c-.527-.516-1.179-.996-1.96-1.455.327-.407.627-.839.9-1.295.264-.447.495-.907.694-1.38.7.2 1.341.412 1.916.637 2.343.96 3.378 1.935 3.378 3.195 0 .872-.703 1.532-2.647 1.532-1.43 0-3.023-.244-4.786-.732l-.123.9c1.73.454 3.407.68 5.03.68 2.585 0 3.84-.899 3.84-2.416 0-1.166-.69-2.152-2.066-2.96v-.001c-.24-.14-.495-.276-.77-.408V6.286a1.143 1.143 0 0 0-1.143-1.143z" />
</svg>
)
type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal" | "stripe" | "bank"
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>("alipay")
const [isProcessing, setIsProcessing] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [showQRCode, setShowQRCode] = useState(false)
const [copied, setCopied] = useState(false)
const { purchaseSection, purchaseFullBook, user, settings } = useStore()
useEffect(() => {
if (isOpen) {
setShowQRCode(false)
setIsSuccess(false)
setIsProcessing(false)
}
}, [isOpen])
const paymentConfig = settings?.paymentMethods || {
wechat: { enabled: true, qrCode: "", account: "", groupQrCode: "" },
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 () => {
setShowQRCode(true)
if (paymentMethod === "wechat" && paymentConfig.wechat?.qrCode) {
const link = paymentConfig.wechat.qrCode
if (link.startsWith("http") || link.startsWith("weixin://")) {
window.open(link, "_blank")
}
} else if (paymentMethod === "alipay" && paymentConfig.alipay?.qrCode) {
const link = paymentConfig.alipay.qrCode
if (link.startsWith("http") || link.startsWith("alipays://")) {
window.open(link, "_blank")
}
}
}
const confirmPayment = 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)
if (success) {
setIsSuccess(true)
const groupUrl = paymentConfig.wechat?.groupQrCode
if (groupUrl) {
setTimeout(() => {
window.open(groupUrl, "_blank")
}, 800)
}
setTimeout(() => {
onSuccess()
onClose()
setIsSuccess(false)
setShowQRCode(false)
}, 2500)
}
}
if (!isOpen) return null
const paymentMethods: {
id: PaymentMethod
name: string
icon: React.ReactNode
color: string
iconBg: string
enabled: boolean
extra?: string
}[] = [
{
id: "wechat",
name: "微信支付",
icon: <WechatIcon />,
color: "#07C160",
iconBg: "rgba(7, 193, 96, 0.15)",
enabled: paymentConfig.wechat?.enabled ?? true,
},
{
id: "alipay",
name: "支付宝",
icon: <AlipayIcon />,
color: "#1677FF",
iconBg: "rgba(22, 119, 255, 0.15)",
enabled: paymentConfig.alipay?.enabled ?? true,
},
{
id: "usdt",
name: `USDT (${paymentConfig.usdt?.network || "TRC20"})`,
icon: <Bitcoin className="w-5 h-5" />,
color: "#26A17B",
iconBg: "rgba(38, 161, 123, 0.15)",
enabled: paymentConfig.usdt?.enabled ?? true,
extra: `$${usdtAmount}`,
},
{
id: "paypal",
name: "PayPal",
icon: <Globe className="w-5 h-5" />,
color: "#003087",
iconBg: "rgba(0, 48, 135, 0.15)",
enabled: paymentConfig.paypal?.enabled ?? false,
extra: `$${paypalAmount}`,
},
]
const availableMethods = paymentMethods.filter((m) => m.enabled)
// 二维码/详情页面 - iOS毛玻璃风格
if (showQRCode) {
const isCrypto = paymentMethod === "usdt"
const isPaypal = paymentMethod === "paypal"
const isWechat = paymentMethod === "wechat"
const isAlipay = paymentMethod === "alipay"
let address = ""
let displayAmount = `¥${amount.toFixed(2)}`
let title = "扫码支付"
let hint = "支付完成后,请点击下方按钮确认"
let qrCodeUrl = ""
if (isCrypto) {
address = paymentConfig.usdt?.address || ""
displayAmount = `$${usdtAmount} USDT`
title = "USDT支付"
hint = "请转账到以下地址,完成后点击确认"
} else if (isPaypal) {
address = paymentConfig.paypal?.email || ""
displayAmount = `$${paypalAmount} USD`
title = "PayPal支付"
hint = "请转账到以下PayPal账户"
} else if (isWechat) {
title = "微信支付"
qrCodeUrl = paymentConfig.wechat?.qrCode || ""
hint = "请使用微信扫码支付"
} else if (isAlipay) {
title = "支付宝支付"
qrCodeUrl = paymentConfig.alipay?.qrCode || ""
hint = "请使用支付宝扫码支付"
}
return (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" onClick={onClose} />
<div className="relative w-full sm:max-w-md glass-modal overflow-hidden safe-bottom modal-content sm:m-4">
{/* 顶部把手 - 仅移动端 */}
<div className="flex justify-center pt-3 pb-2 sm:hidden">
<div className="w-9 h-1 rounded-full bg-[var(--app-text-tertiary)]" />
</div>
<button
onClick={onClose}
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center text-[var(--app-text-secondary)] hover:text-white z-10 touch-feedback"
>
<X className="w-4 h-4" />
</button>
<div className="p-6 pt-4 sm:pt-10">
{/* 标题 */}
<h3 className="text-xl font-semibold text-white text-center mb-2">{title}</h3>
{/* 金额显示 */}
<div className="text-center mb-6">
<p className="text-4xl font-bold text-[var(--app-brand)] glow-text">{displayAmount}</p>
</div>
{/* QR Code Display */}
{(isWechat || isAlipay) && (
<div className="flex flex-col items-center mb-6">
<div className="w-52 h-52 bg-white rounded-2xl p-4 mb-4 flex items-center justify-center shadow-lg">
{qrCodeUrl ? (
<img
src={qrCodeUrl || "/placeholder.svg"}
alt="支付二维码"
className="w-full h-full object-contain rounded-lg"
/>
) : (
<div className="flex flex-col items-center text-gray-400">
<QrCode className="w-16 h-16 mb-2" />
<span className="text-sm text-center"></span>
</div>
)}
</div>
<p className="text-[var(--app-text-tertiary)] text-sm">{hint}</p>
</div>
)}
{/* Crypto/PayPal Address */}
{(isCrypto || isPaypal) && (
<div className="mb-6">
<div className="glass-card p-4">
<p className="text-[var(--app-text-tertiary)] text-xs 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 bg-[var(--app-bg-secondary)] p-3 rounded-lg">
{address || "请在后台配置收款地址"}
</p>
{address && (
<button
onClick={() => handleCopyAddress(address)}
className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center text-[var(--app-brand)] touch-feedback"
>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</button>
)}
</div>
</div>
<p className="text-[var(--app-text-tertiary)] text-sm mt-3 text-center">{hint}</p>
</div>
)}
{/* 提示信息 */}
<div className="glass-card p-4 mb-6 border-[var(--app-brand)]/20">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
<Shield className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<p className="text-[var(--app-text-secondary)] text-sm flex-1">
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<button
onClick={() => setShowQRCode(false)}
className="btn-ios-secondary flex-1"
>
</button>
<button
onClick={confirmPayment}
disabled={isProcessing}
className="btn-ios flex-1 glow disabled:opacity-50"
>
{isProcessing ? (
<div className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
</div>
) : (
"已完成支付"
)}
</button>
</div>
</div>
</div>
</div>
)
}
// 支付成功页面
if (isSuccess) {
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-md modal-overlay" />
<div className="relative w-full max-w-sm glass-modal overflow-hidden modal-content">
<div className="p-8 text-center">
{/* 成功动画 */}
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-[var(--app-brand-light)] flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-[var(--app-brand)]" />
</div>
<h3 className="text-2xl font-semibold text-white mb-2"></h3>
<p className="text-[var(--app-text-secondary)] mb-4">
{type === "fullbook" ? "您已解锁全部内容" : "您已解锁本节内容"}
</p>
{paymentConfig.wechat?.groupQrCode && (
<div className="glass-card p-4 mt-4">
<div className="flex items-center justify-center gap-2 text-[#07C160]">
<Users className="w-5 h-5" />
<span className="text-sm">...</span>
</div>
</div>
)}
</div>
</div>
</div>
)
}
// 主支付选择页面 - iOS风格
return (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" onClick={onClose} />
<div className="relative w-full sm:max-w-md glass-modal overflow-hidden safe-bottom modal-content sm:m-4 max-h-[90vh] overflow-y-auto scrollbar-hide">
{/* 顶部把手 - 仅移动端 */}
<div className="flex justify-center pt-3 pb-2 sm:hidden sticky top-0 bg-transparent">
<div className="w-9 h-1 rounded-full bg-[var(--app-text-tertiary)]" />
</div>
<button
onClick={onClose}
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center text-[var(--app-text-secondary)] hover:text-white z-10 touch-feedback"
>
<X className="w-4 h-4" />
</button>
{/* Header */}
<div className="px-6 pt-2 sm:pt-10 pb-4">
<h3 className="text-xl font-semibold text-white mb-1"></h3>
<p className="text-[var(--app-text-tertiary)] text-sm">
{type === "fullbook" ? "购买整本书,解锁全部内容" : `购买: ${sectionTitle}`}
</p>
</div>
{/* Amount */}
<div className="px-6 py-6 text-center border-y border-[var(--app-separator)]">
<p className="text-[var(--app-text-tertiary)] text-sm mb-2"></p>
<p className="text-5xl font-bold text-white">¥{amount.toFixed(2)}</p>
{(paymentMethod === "usdt" || paymentMethod === "paypal") && (
<p className="text-[var(--app-brand)] text-sm mt-2">
${paymentMethod === "usdt" ? usdtAmount : paypalAmount} USD
</p>
)}
{user?.referredBy && (
<div className="inline-flex items-center gap-2 mt-4 px-4 py-2 rounded-full bg-[var(--app-brand-light)]">
<Users className="w-4 h-4 text-[var(--app-brand)]" />
<span className="text-[var(--app-brand)] text-sm">
{settings?.distributorShare || 90}%
</span>
</div>
)}
</div>
{/* Payment Methods */}
<div className="p-6">
<p className="text-[var(--app-text-tertiary)] text-sm mb-4"></p>
<div className="space-y-3">
{availableMethods.map((method) => (
<button
key={method.id}
onClick={() => setPaymentMethod(method.id)}
className={`w-full p-4 rounded-xl flex items-center gap-4 transition-all touch-feedback ${
paymentMethod === method.id
? "glass-card-light border-[var(--app-brand)]/30"
: "glass-card hover:border-[var(--glass-border-light)]"
}`}
>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center"
style={{ backgroundColor: method.iconBg, color: method.color }}
>
{method.icon}
</div>
<div className="flex-1 text-left">
<span className="text-white font-medium">{method.name}</span>
{method.extra && (
<span className="text-[var(--app-text-tertiary)] text-sm ml-2">{method.extra}</span>
)}
</div>
{paymentMethod === method.id && (
<div className="w-6 h-6 rounded-full bg-[var(--app-brand)] flex items-center justify-center">
<Check className="w-4 h-4 text-white" />
</div>
)}
</button>
))}
{availableMethods.length === 0 && (
<p className="text-[var(--app-text-tertiary)] text-center py-8"></p>
)}
</div>
</div>
{/* Submit Button */}
<div className="p-6 pt-0">
<button
onClick={handlePayment}
disabled={isProcessing || availableMethods.length === 0}
className="btn-ios w-full glow text-lg disabled:opacity-50"
>
{isProcessing ? (
<div className="flex items-center justify-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-[var(--app-text-tertiary)] text-xs text-center mt-4">
</p>
</div>
</div>
</div>
)
}