mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { ShoppingBag } from "lucide-react"
|
||||
|
||||
import type { GetMerchantOrderResponse } from "@/lib/services"
|
||||
|
||||
|
||||
/**
|
||||
* 加载中骨架组件
|
||||
* 显示活动信息加载中的骨架
|
||||
*/
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-6 text-center md:text-left">
|
||||
<Skeleton className="h-4 w-32 mb-2 bg-white/10 mx-auto md:mx-0" />
|
||||
<Skeleton className="h-8 w-40 bg-white/10 mx-auto md:mx-0" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl bg-white/5 p-4 space-y-4">
|
||||
<div className="flex justify-between items-start pb-4">
|
||||
<div className="flex flex-col items-start space-y-1.5">
|
||||
<Skeleton className="h-4 w-24 bg-white/10" />
|
||||
<Skeleton className="h-3 w-16 bg-white/10" />
|
||||
</div>
|
||||
<div className="flex flex-col items-end space-y-1.5">
|
||||
<Skeleton className="h-4 w-20 bg-white/10" />
|
||||
<Skeleton className="h-3 w-24 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<Skeleton className="h-6 w-20 bg-white/10" />
|
||||
<Skeleton className="h-6 w-24 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 px-1">
|
||||
<Skeleton className="h-3 w-full bg-white/10" />
|
||||
<Skeleton className="h-3 w-3/4 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动信息组件
|
||||
* 显示活动信息
|
||||
*/
|
||||
function OrderContent({ orderInfo, forceMobile = false }: { orderInfo: GetMerchantOrderResponse, forceMobile?: boolean }) {
|
||||
const amount = parseFloat(orderInfo.order.amount).toFixed(2)
|
||||
const feeRateValue = orderInfo.user_pay_config?.fee_rate
|
||||
const feeRate = feeRateValue ? (parseFloat(feeRateValue) * 100).toFixed(2) : '0.00'
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className={`mb-4 text-center ${ forceMobile ? '' : 'md:mb-6 md:text-left' }`}>
|
||||
<div className={`inline-flex items-center justify-center gap-1.5 mb-1 text-white/50 text-[10px] font-medium uppercase tracking-wider ${ forceMobile ? '' : 'md:justify-start md:gap-2 md:mb-1.5 md:text-xs' }`}>
|
||||
<span>服务方</span>
|
||||
<span className="w-1 h-1 rounded-full bg-white/30" />
|
||||
<span className="text-white hover:text-blue-400 transition-colors cursor-pointer font-bold">{orderInfo.merchant.app_name}</span>
|
||||
</div>
|
||||
<h1 className={`text-2xl font-bold text-white tracking-tight flex items-baseline justify-center gap-1 ${ forceMobile ? '' : 'md:text-4xl md:justify-start' }`}>
|
||||
<span className={`text-lg text-white/50 font-normal ${ forceMobile ? '' : 'md:text-2xl' }`}>LDC</span>
|
||||
{amount}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className={`space-y-3 ${ forceMobile ? '' : 'md:space-y-4' }`}>
|
||||
<div className={`rounded-2xl bg-white/5 p-3 backdrop-blur-sm transition-colors ${ forceMobile ? '' : 'md:p-4' }`}>
|
||||
<div className={`flex justify-between items-start pb-3 mb-3 ${ forceMobile ? '' : 'md:pb-4 md:mb-4' }`}>
|
||||
<div className={`flex flex-col items-start pr-3 ${ forceMobile ? '' : 'md:pr-4' }`}>
|
||||
<p className={`text-xs font-medium text-white mb-0.5 line-clamp-2 ${ forceMobile ? '' : 'md:text-sm' }`}>{orderInfo.order.order_name}</p>
|
||||
<p className="text-white/40 text-[10px]">数量: 1</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end text-right whitespace-nowrap">
|
||||
<p className={`text-xs font-medium text-white mb-0.5 ${ forceMobile ? '' : 'md:text-sm' }`}>LDC {amount}</p>
|
||||
<p className="text-white/40 text-[10px]">LDC {amount} / 个</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full text-xs text-left mb-3 text-white/40 hover:text-white transition-colors duration-200 flex items-center gap-2 group">
|
||||
<span>添加优惠码</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] bg-white/5 text-white/30">暂未开放</span>
|
||||
</button>
|
||||
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<p className="text-base font-bold text-white">积分合计</p>
|
||||
<p className="text-lg font-bold text-white tracking-tight">LDC {amount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-2">
|
||||
<p className="text-[10px] text-white/30 space-y-0.5 leading-relaxed">
|
||||
* 包含 {feeRate}% 的服务手续费 (由服务方承担)。
|
||||
<br />
|
||||
服务手续费率根据服务方等级和平台活动动态调整,不会消耗您(消费者)的积分。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动信息组件
|
||||
* 显示活动信息
|
||||
*/
|
||||
export function PayingInfo({ orderInfo, loading = false, forceMobile = false }: { orderInfo: GetMerchantOrderResponse, loading: boolean, forceMobile?: boolean }) {
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full p-4 justify-center bg-gradient-to-br from-neutral-900/95 to-black dark:from-neutral-900 dark:to-black relative text-white rounded-t-3xl ${ forceMobile ? '' : 'md:w-[45%] md:p-8 md:rounded-l-3xl md:rounded-tr-none' }`}>
|
||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none opacity-30">
|
||||
<div className="absolute -top-[20%] -left-[10%] w-[70%] h-[70%] bg-blue-900/20 rounded-full blur-3xl" />
|
||||
<div className="absolute top-[40%] -right-[10%] w-[60%] h-[60%] bg-purple-900/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className={`w-full max-w-xs mx-auto relative z-10 flex flex-col justify-center ${ forceMobile ? '' : 'md:h-full md:min-h-[360px]' }`}>
|
||||
<div className={`text-center mb-2 ${ forceMobile ? '' : 'md:hidden' }`}>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Skeleton className="h-3 w-32 bg-white/10" />
|
||||
<div className="flex items-baseline justify-center gap-1">
|
||||
<Skeleton className="h-8 w-24 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-white/50 text-[10px] uppercase tracking-wider mb-1">服务方 {orderInfo.merchant.app_name}</div>
|
||||
<div className="text-3xl font-bold text-white tracking-tight flex items-baseline justify-center gap-1">
|
||||
<span className="text-lg text-white/50 font-normal">LDC</span>
|
||||
{(parseFloat(orderInfo.order.amount)).toFixed(2)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`mb-10 flex justify-center hidden ${ forceMobile ? '' : 'md:block md:justify-start' }`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 flex items-center justify-center shadow-sm backdrop-blur-sm">
|
||||
{loading ? (
|
||||
<Skeleton className="w-5 h-5 rounded-full bg-white/20" />
|
||||
) : (
|
||||
<ShoppingBag className="size-4 text-white/80" />
|
||||
)}
|
||||
</div>
|
||||
{loading ? (
|
||||
<Skeleton className="h-5 w-24 bg-white/10" />
|
||||
) : (
|
||||
<span className="text-xs font-bold text-white/60">积分流转信息</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`hidden ${ forceMobile ? '' : 'md:block' }`}>
|
||||
{loading ? <LoadingSkeleton /> : <OrderContent orderInfo={orderInfo} forceMobile={forceMobile} />}
|
||||
</div>
|
||||
|
||||
{!loading && (
|
||||
<div className={`text-center ${ forceMobile ? '' : 'md:hidden' }`}>
|
||||
<div className="inline-flex items-center gap-1 text-[10px] text-white/40 bg-white/5 px-2 py-1 rounded-full">
|
||||
<span>{orderInfo.order.order_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`hidden mt-auto pt-8 ${ forceMobile ? '' : 'md:block' }`}>
|
||||
<div className="text-[10px] text-white/20 font-mono">
|
||||
ORDER: {orderInfo?.order?.order_no || 'LOADING...'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { useSearchParams, useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import { motion } from "motion/react"
|
||||
import { PayingNow } from "@/components/common/pay/paying/paying-now"
|
||||
import { PayingInfo } from "@/components/common/pay/paying/paying-info"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import type { GetMerchantOrderResponse } from "@/lib/services"
|
||||
|
||||
|
||||
/**
|
||||
* 积分认证主页面组件
|
||||
* 通过 order_no 查询订单信息并完成积分认证
|
||||
*/
|
||||
export function PayingMain() {
|
||||
/** 获取URL参数中的认证编号 */
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const encryptedOrderNo = searchParams.get('order_no')
|
||||
|
||||
/** 组件状态管理 */
|
||||
const [orderInfo, setOrderInfo] = useState<GetMerchantOrderResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
const [paying, setPaying] = useState(false)
|
||||
const [payKey, setPayKey] = useState("")
|
||||
const [currentStep, setCurrentStep] = useState<'method' | 'pay'>('method')
|
||||
const [selectedMethod, setSelectedMethod] = useState<string>('')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 统一的服务错误处理函数 */
|
||||
const handleServiceError = (error: unknown, operation: string) => {
|
||||
console.error(`${ operation }失败:`, error)
|
||||
|
||||
let errorMessage = `${ operation }失败`
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
errorMessage = String(error.message)
|
||||
}
|
||||
|
||||
const errorMap: Record<string, string> = {
|
||||
'订单不存在': "此积分流转服务不存在或已过期",
|
||||
'未登录': "请先登录后再进行认证",
|
||||
'余额不足': "您的积分余额不足",
|
||||
'安全密码': "安全密码错误",
|
||||
'不能认证自己的订单': "不能认证自己创建的积分流转服务",
|
||||
'每日限额': "已达到每日认证限额",
|
||||
'已完成': "认证完成"
|
||||
}
|
||||
|
||||
const userMessage = Object.entries(errorMap).find(([key]) =>
|
||||
errorMessage.includes(key)
|
||||
)?.[1] || errorMessage
|
||||
|
||||
toast.error(userMessage, { id: 'payment-error' })
|
||||
}
|
||||
|
||||
/** 组件初始化时查询订单信息 */
|
||||
useEffect(() => {
|
||||
if (encryptedOrderNo) {
|
||||
setError(false)
|
||||
handleQueryOrder(encryptedOrderNo)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setError(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [encryptedOrderNo])
|
||||
|
||||
/** 查询订单信息 */
|
||||
const handleQueryOrder = async (queryOrderNo?: string) => {
|
||||
const targetOrderNo = queryOrderNo || encryptedOrderNo
|
||||
if (!targetOrderNo) {
|
||||
toast.error("缺少认证编号", { id: 'missing-order-no' })
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await services.merchant.getMerchantOrder({ order_no: targetOrderNo })
|
||||
setOrderInfo(data)
|
||||
setError(false)
|
||||
} catch (error: unknown) {
|
||||
handleServiceError(error, "查询认证信息")
|
||||
setOrderInfo(null)
|
||||
setError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行积分认证操作 */
|
||||
const handlePayOrder = async () => {
|
||||
if (!orderInfo) return
|
||||
|
||||
if (!payKey.trim()) {
|
||||
toast.error("请输入安全密码")
|
||||
return
|
||||
}
|
||||
|
||||
if (payKey.length !== 6) {
|
||||
toast.error("安全密码长度为6位,请重新输入")
|
||||
return
|
||||
}
|
||||
|
||||
setPaying(true)
|
||||
try {
|
||||
const freshOrderInfo = await services.merchant.getMerchantOrder({
|
||||
order_no: encryptedOrderNo!
|
||||
})
|
||||
|
||||
if (freshOrderInfo.order.status !== 'pending') {
|
||||
toast.error("此积分流转服务状态已变更,无法认证")
|
||||
setOrderInfo(freshOrderInfo)
|
||||
return
|
||||
}
|
||||
|
||||
await services.merchant.payMerchantOrder({
|
||||
order_no: encryptedOrderNo!,
|
||||
pay_key: payKey
|
||||
})
|
||||
|
||||
toast.success("积分流转服务认证成功!", { id: 'payment-success' })
|
||||
|
||||
/** 支付成功后立即更新订单状态为成功 */
|
||||
if (orderInfo) {
|
||||
setOrderInfo({
|
||||
...orderInfo,
|
||||
order: {
|
||||
...orderInfo.order,
|
||||
status: 'success'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 5秒后跳转到redirect_uri或刷新页面 */
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
const redirectUri = orderInfo?.merchant?.redirect_uri
|
||||
if (redirectUri && redirectUri.trim()) {
|
||||
window.location.href = redirectUri
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/home')
|
||||
}, 5000)
|
||||
} catch (error: unknown) {
|
||||
handleServiceError(error, "认证")
|
||||
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'message' in error &&
|
||||
String(error.message).includes('已完成')
|
||||
) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (!isMountedRef.current) return
|
||||
window.location.reload()
|
||||
}, 500)
|
||||
}
|
||||
} finally {
|
||||
setPaying(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="relative min-h-screen w-full flex flex-col items-center justify-center bg-background">
|
||||
<div className="relative z-10 w-full h-full flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
className="relative z-10 flex flex-col items-center justify-center min-h-screen text-center px-4"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
>
|
||||
<div className="w-full rounded-3xl p-8 max-w-lg">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-6 bg-red-500/10">
|
||||
<div className="size-8 text-red-500 font-bold text-4xl flex items-center justify-center">!</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground mb-4">
|
||||
出了点问题
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
您访问的认证编号已经完成认证或不存在。请检查您的积分流转服务链接是否正确,对此如有疑问请联系社区技术支持。
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full font-sans text-foreground overflow-hidden bg-background selection:bg-primary/30">
|
||||
<div className="relative min-h-screen w-full flex flex-col items-center justify-center bg-background">
|
||||
<div className="relative z-10 w-full h-full flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row w-full max-w-4xl bg-card/70 backdrop-blur-2xl border border-border/50 rounded-3xl overflow-hidden shadow-2xl"
|
||||
style={{ minHeight: 'auto' }}
|
||||
>
|
||||
<PayingInfo orderInfo={orderInfo!} loading={loading} />
|
||||
<PayingNow
|
||||
orderInfo={orderInfo}
|
||||
paying={paying}
|
||||
payKey={payKey}
|
||||
currentStep={currentStep}
|
||||
selectedMethod={selectedMethod}
|
||||
isOpen={isOpen}
|
||||
loading={loading}
|
||||
onPayKeyChange={setPayKey}
|
||||
onCurrentStepChange={setCurrentStep}
|
||||
onSelectedMethodChange={setSelectedMethod}
|
||||
onIsOpenChange={setIsOpen}
|
||||
onPayOrder={handlePayOrder}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
import * as React from "react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Send } from "@/components/animate-ui/icons/send"
|
||||
import { Check } from "@/components/animate-ui/icons/check"
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/animate-ui/primitives/radix/collapsible"
|
||||
import { AlertCircle, CreditCard, ArrowLeft, ShieldCheck, Lock } from "lucide-react"
|
||||
|
||||
import type { GetMerchantOrderResponse } from "@/lib/services"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
|
||||
|
||||
type PaymentStep = typeof PAYMENT_STEPS[keyof typeof PAYMENT_STEPS]
|
||||
|
||||
const PAYMENT_METHODS = {
|
||||
LINUX_DO_PAY: 'linux-do-pay'
|
||||
} as const
|
||||
|
||||
const PAYMENT_STEPS = {
|
||||
METHOD: 'method' as const,
|
||||
Credit: 'pay' as const
|
||||
} as const
|
||||
|
||||
const ORDER_STATUSES = {
|
||||
SUCCESS: 'success',
|
||||
PENDING: 'pending'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 加载中骨架组件
|
||||
* 显示支付方式加载中的骨架
|
||||
*/
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-xs mx-auto">
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-6 w-32 bg-muted/50" />
|
||||
<Skeleton className="h-4 w-48 bg-muted/50" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-xl border border-border/50 p-3 bg-card/50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Skeleton className="w-9 h-9 rounded-lg bg-muted/50" />
|
||||
<Skeleton className="h-3.5 w-24 bg-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border/50 p-3 bg-card/50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Skeleton className="w-9 h-9 rounded-lg bg-muted/50" />
|
||||
<Skeleton className="h-3.5 w-20 bg-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Skeleton className="h-10 w-full rounded-full bg-muted/50" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功状态组件
|
||||
* 显示认证成功状态
|
||||
*/
|
||||
function SuccessState() {
|
||||
const [countdown, setCountdown] = React.useState(5)
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer)
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: "easeOut",
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}}
|
||||
className="text-center py-10"
|
||||
>
|
||||
<div className="relative inline-block mb-6">
|
||||
<div className="absolute inset-0 bg-green-500/20 blur-xl rounded-full" />
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
duration: 0.5,
|
||||
type: "spring",
|
||||
stiffness: 200
|
||||
}}
|
||||
className="relative w-10 h-10 bg-green-500 rounded-full flex items-center justify-center "
|
||||
>
|
||||
<Check
|
||||
className="size-6 text-white"
|
||||
animate={true}
|
||||
loop={false}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.4 }}
|
||||
className="text-xl font-bold text-foreground mb-2"
|
||||
>
|
||||
认证成功
|
||||
</motion.h3>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6, duration: 0.4 }}
|
||||
className="text-muted-foreground text-sm"
|
||||
>
|
||||
认证已安全完成,{countdown > 0 ? `${ countdown }秒后自动跳转...` : '正在跳转...'}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证方式选择步骤组件
|
||||
* 显示认证方式选择步骤
|
||||
*/
|
||||
function MethodSelectionStep({
|
||||
orderInfo,
|
||||
selectedMethod,
|
||||
isOpen,
|
||||
onSelectedMethodChange,
|
||||
onIsOpenChange,
|
||||
onCurrentStepChange,
|
||||
forceMobile = false
|
||||
}: {
|
||||
orderInfo: GetMerchantOrderResponse
|
||||
selectedMethod: string
|
||||
isOpen: boolean
|
||||
onSelectedMethodChange: (method: string) => void
|
||||
onIsOpenChange: (isOpen: boolean) => void
|
||||
onCurrentStepChange: (step: PaymentStep) => void
|
||||
forceMobile?: boolean
|
||||
}) {
|
||||
const { user } = useUser()
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="method-step"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6 w-full max-w-xs mx-auto"
|
||||
>
|
||||
<div className={`space-y-2 text-center ${ forceMobile ? '' : 'md:text-left' }`}>
|
||||
<h2 className="text-xl font-bold text-foreground tracking-tight">选择认证平台</h2>
|
||||
<p className="text-xs text-muted-foreground">请选择您想要使用的服务认证平台</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Collapsible open={isOpen} onOpenChange={onIsOpenChange}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div
|
||||
className={`group cursor-pointer rounded-xl p-2.5 transition-all duration-300 border backdrop-blur-sm ${ selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY
|
||||
? 'border-primary/50 bg-primary/5'
|
||||
: 'border-border/50 bg-card hover:bg-muted/50 hover:border-border'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY) {
|
||||
onSelectedMethodChange('')
|
||||
onIsOpenChange(false)
|
||||
} else {
|
||||
onSelectedMethodChange(PAYMENT_METHODS.LINUX_DO_PAY)
|
||||
onIsOpenChange(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300 ${ selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground group-hover:bg-muted/80'
|
||||
}`}>
|
||||
<CreditCard className="size-4" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-bold text-xs text-foreground">LINUX DO Credit</div>
|
||||
<div className="text-[9px] text-muted-foreground mt-0.5">即时安全认证</div>
|
||||
</div>
|
||||
<div className={`w-5 h-5 rounded-full border flex items-center justify-center transition-all duration-300 ${ selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border'
|
||||
}`}>
|
||||
{selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY && <Check className="size-3" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{selectedMethod === PAYMENT_METHODS.LINUX_DO_PAY && (
|
||||
<CollapsibleContent>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="pt-3 px-1"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-xs text-muted-foreground mb-4 bg-muted/50 p-2.5 rounded-lg border">
|
||||
<div className="p-1 rounded">
|
||||
<Lock className="size-3 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] opacity-70">消费方账户</span>
|
||||
<span className="font-medium text-foreground">{orderInfo.order.payer_username || user?.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onCurrentStepChange(PAYMENT_STEPS.Credit)}
|
||||
className="w-full rounded-full font-bold text-sm shadow-md transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
确认并继续
|
||||
</Button>
|
||||
</motion.div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证步骤组件
|
||||
* 显示认证步骤
|
||||
*/
|
||||
function PaymentStep({
|
||||
orderInfo,
|
||||
payKey,
|
||||
paying,
|
||||
onPayKeyChange,
|
||||
onCurrentStepChange,
|
||||
onPayOrder
|
||||
}: {
|
||||
orderInfo: GetMerchantOrderResponse
|
||||
payKey: string
|
||||
paying: boolean
|
||||
onPayKeyChange: (value: string) => void
|
||||
onCurrentStepChange: (step: PaymentStep) => void
|
||||
onPayOrder: () => void
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
key="pay-step"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6 w-full max-w-xs mx-auto"
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 text-muted-foreground hover:text-foreground mb-1"
|
||||
onClick={() => onCurrentStepChange(PAYMENT_STEPS.METHOD)}
|
||||
>
|
||||
<ArrowLeft className="size-3.5 mr-1" /> 返回
|
||||
</Button>
|
||||
<h2 className="text-xl font-bold text-foreground tracking-tight">身份验证</h2>
|
||||
<p className="text-xs text-muted-foreground">请输入您的6位安全密码</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center py-4">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={payKey}
|
||||
onChange={onPayKeyChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && payKey.length === 6 && !paying) {
|
||||
onPayOrder()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputOTPGroup className="gap-2">
|
||||
{[0, 1, 2, 3, 4, 5].map((index) => (
|
||||
<InputOTPSlot
|
||||
key={index}
|
||||
index={index}
|
||||
className="size-11 bg-background border-border text-foreground text-lg font-bold transition-all focus:border-primary focus:ring-4 focus:ring-primary/10 ring-offset-0"
|
||||
/>
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={onPayOrder}
|
||||
disabled={paying || payKey.length !== 6}
|
||||
className="w-full rounded-full font-bold text-sm shadow-md transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
{paying ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Send className="size-4" animate={true} loop={true} loopDelay={2000} /> 处理中...
|
||||
</span>
|
||||
) : (
|
||||
`消耗 LDC ${ (parseFloat(orderInfo.order.amount)).toFixed(2) }`
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误状态组件
|
||||
* 显示错误状态
|
||||
*/
|
||||
function ErrorState({ status }: { status: string }) {
|
||||
return (
|
||||
<div className="text-center py-10">
|
||||
<div className="w-14 h-14 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-4 border border-red-500/20">
|
||||
<AlertCircle className="size-6 text-red-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-1">认证出现问题</h3>
|
||||
<p className="text-muted-foreground text-sm">状态:<span className="font-medium text-foreground">{status}</span></p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 底部组件
|
||||
* 显示底部
|
||||
*/
|
||||
function Footer({ loading }: { loading?: boolean }) {
|
||||
return loading ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 mt-8 opacity-50">
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 text-[10px] uppercase font-bold text-muted-foreground/40 mt-10">
|
||||
<ShieldCheck className="size-3 text-green-500" />
|
||||
<span>由 LINUX DO Credit 提供安全认证</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PayingNowProps {
|
||||
orderInfo: GetMerchantOrderResponse | null
|
||||
paying: boolean
|
||||
payKey: string
|
||||
currentStep: PaymentStep
|
||||
selectedMethod: string
|
||||
isOpen: boolean
|
||||
loading?: boolean
|
||||
onPayKeyChange: (value: string) => void
|
||||
onCurrentStepChange: (step: PaymentStep) => void
|
||||
onSelectedMethodChange: (method: string) => void
|
||||
onIsOpenChange: (isOpen: boolean) => void
|
||||
onPayOrder: () => void
|
||||
forceMobile?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付现在组件
|
||||
* 显示支付现在组件
|
||||
*/
|
||||
export function PayingNow({
|
||||
orderInfo,
|
||||
paying,
|
||||
payKey,
|
||||
currentStep,
|
||||
selectedMethod,
|
||||
isOpen,
|
||||
loading = false,
|
||||
onPayKeyChange,
|
||||
onCurrentStepChange,
|
||||
onSelectedMethodChange,
|
||||
onIsOpenChange,
|
||||
onPayOrder,
|
||||
forceMobile = false
|
||||
}: PayingNowProps) {
|
||||
const renderContent = () => {
|
||||
if (loading) return <LoadingSkeleton />
|
||||
|
||||
if (!orderInfo) return null
|
||||
|
||||
switch (orderInfo.order.status) {
|
||||
case ORDER_STATUSES.SUCCESS:
|
||||
return <SuccessState />
|
||||
|
||||
case ORDER_STATUSES.PENDING:
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{currentStep === PAYMENT_STEPS.METHOD ? (
|
||||
<MethodSelectionStep
|
||||
orderInfo={orderInfo}
|
||||
selectedMethod={selectedMethod}
|
||||
isOpen={isOpen}
|
||||
onSelectedMethodChange={onSelectedMethodChange}
|
||||
onIsOpenChange={onIsOpenChange}
|
||||
onCurrentStepChange={onCurrentStepChange}
|
||||
forceMobile={forceMobile}
|
||||
/>
|
||||
) : (
|
||||
<PaymentStep
|
||||
orderInfo={orderInfo}
|
||||
payKey={payKey}
|
||||
paying={paying}
|
||||
onPayKeyChange={onPayKeyChange}
|
||||
onCurrentStepChange={onCurrentStepChange}
|
||||
onPayOrder={onPayOrder}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
||||
default:
|
||||
return <ErrorState status={orderInfo.order.status} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex-1 flex flex-col items-center justify-center p-6 relative overflow-hidden bg-white dark:bg-neutral-900 transition-colors duration-500 ${ forceMobile ? '' : 'md:p-8' }`}>
|
||||
<div className="w-full relative z-10 flex flex-col h-full justify-center">
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
{renderContent()}
|
||||
</div>
|
||||
<Footer loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import { motion } from "motion/react"
|
||||
import { PayingNow } from "@/components/common/pay/paying/paying-now"
|
||||
import { PayingInfo } from "@/components/common/pay/paying/paying-info"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import { ConfigService } from "@/lib/services"
|
||||
import type { PaymentLink, GetMerchantOrderResponse, UserPayConfig } from "@/lib/services"
|
||||
|
||||
|
||||
/**
|
||||
* 支付链接支付页面组件
|
||||
* 通过 token 查询支付链接信息并完成支付
|
||||
*/
|
||||
export function PayingOnline() {
|
||||
/* 获取URL参数中的Token */
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const { user } = useUser()
|
||||
|
||||
/* 组件状态管理 */
|
||||
const [paymentLink, setPaymentLink] = useState<PaymentLink | null>(null)
|
||||
const [orderInfo, setOrderInfo] = useState<GetMerchantOrderResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
const [paying, setPaying] = useState(false)
|
||||
const [payKey, setPayKey] = useState("")
|
||||
const [currentStep, setCurrentStep] = useState<'method' | 'pay'>('method')
|
||||
const [selectedMethod, setSelectedMethod] = useState<string>('')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
/* 获取用户支付配置 */
|
||||
const [userPayConfigs, setUserPayConfigs] = useState<UserPayConfig[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
const configs = await ConfigService.getUserPayConfigs()
|
||||
setUserPayConfigs(configs)
|
||||
} catch (error) {
|
||||
console.error("Failed to load user pay configs:", error)
|
||||
}
|
||||
}
|
||||
loadConfigs()
|
||||
}, [])
|
||||
|
||||
/* 统一的服务错误处理函数 */
|
||||
const handleServiceError = (error: unknown, operation: string) => {
|
||||
console.error(`${ operation }失败:`, error)
|
||||
|
||||
let errorMessage = `${ operation }失败`
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
errorMessage = String(error.message)
|
||||
}
|
||||
|
||||
const errorMap: Record<string, string> = {
|
||||
'不存在': "此积分流转服务不存在或已失效",
|
||||
'未登录': "请先登录后再进行认证",
|
||||
'余额不足': "账户积分不足",
|
||||
'支付密码': "安全密码错误",
|
||||
'不能支付': "不能支付自己创建的积分流转服务",
|
||||
'每日限额': "已达到每日认证限额",
|
||||
}
|
||||
|
||||
const userMessage = Object.entries(errorMap).find(([key]) =>
|
||||
errorMessage.includes(key)
|
||||
)?.[1] || errorMessage
|
||||
|
||||
toast.error(userMessage, { id: 'payment-error' })
|
||||
}
|
||||
|
||||
/* 组件初始化时查询支付链接信息 */
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setError(false)
|
||||
handleQueryPaymentLink(token)
|
||||
} else {
|
||||
setLoading(false)
|
||||
setError(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token, userPayConfigs.length, user?.pay_level])
|
||||
|
||||
/* 查询支付链接信息 */
|
||||
const handleQueryPaymentLink = async (queryToken?: string) => {
|
||||
const targetToken = queryToken || token
|
||||
if (!targetToken) {
|
||||
toast.error("缺少支付链接", { id: 'missing-token' })
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await services.merchant.getPaymentLinkByToken(targetToken)
|
||||
setPaymentLink(data)
|
||||
|
||||
/* 将支付链接信息转换为订单信息格式,以复用PayingInfo和PayingNow组件 */
|
||||
const mockOrderInfo: GetMerchantOrderResponse = {
|
||||
merchant: {
|
||||
app_name: data.app_name || "商户",
|
||||
redirect_uri: data.redirect_uri || "",
|
||||
},
|
||||
order: {
|
||||
id: '0',
|
||||
order_no: `LINK-${ data.id }`,
|
||||
order_name: data.product_name,
|
||||
payer_username: user?.username || "",
|
||||
payee_username: "",
|
||||
amount: data.amount,
|
||||
status: "pending",
|
||||
type: "payment",
|
||||
payment_type: "link",
|
||||
remark: data.remark || "",
|
||||
client_id: "",
|
||||
return_url: "",
|
||||
notify_url: "",
|
||||
trade_time: null,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
},
|
||||
user_pay_config: (() => {
|
||||
const currentUserConfig = userPayConfigs.find(c => user?.pay_level !== undefined && c.level === user.pay_level)
|
||||
return currentUserConfig ? {
|
||||
...currentUserConfig,
|
||||
fee_rate: String(currentUserConfig.fee_rate)
|
||||
} : {
|
||||
id: '0',
|
||||
level: user?.pay_level ?? 0,
|
||||
min_score: 0,
|
||||
max_score: null,
|
||||
daily_limit: null,
|
||||
fee_rate: "0.00",
|
||||
created_at: "",
|
||||
updated_at: ""
|
||||
}
|
||||
})(),
|
||||
}
|
||||
|
||||
setOrderInfo(mockOrderInfo)
|
||||
setError(false)
|
||||
} catch (error: unknown) {
|
||||
handleServiceError(error, "查询积分流转服务")
|
||||
setPaymentLink(null)
|
||||
setOrderInfo(null)
|
||||
setError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/* 执行支付操作 */
|
||||
const handlePayOrder = async () => {
|
||||
if (!paymentLink || !token) return
|
||||
|
||||
if (!payKey.trim()) {
|
||||
toast.error("请输入安全密码")
|
||||
return
|
||||
}
|
||||
|
||||
if (payKey.length < 6 || payKey.length > 10) {
|
||||
toast.error("安全密码长度必须为6位")
|
||||
return
|
||||
}
|
||||
|
||||
setPaying(true)
|
||||
try {
|
||||
await services.merchant.payByLink({
|
||||
token: token,
|
||||
pay_key: payKey,
|
||||
remark: paymentLink.remark || undefined
|
||||
})
|
||||
|
||||
toast.success("积分流转服务认证成功!", { id: 'payment-success' })
|
||||
|
||||
/* 支付成功后更新订单状态 */
|
||||
if (orderInfo) {
|
||||
setOrderInfo({
|
||||
...orderInfo,
|
||||
order: {
|
||||
...orderInfo.order,
|
||||
status: 'success'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 5秒后跳转到首页或回调地址 */
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
const redirectUri = orderInfo?.merchant?.redirect_uri
|
||||
if (redirectUri && redirectUri.trim()) {
|
||||
window.location.href = redirectUri
|
||||
return
|
||||
}
|
||||
|
||||
window.location.href = '/home'
|
||||
}, 5000)
|
||||
} catch (error: unknown) {
|
||||
handleServiceError(error, "认证")
|
||||
} finally {
|
||||
setPaying(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="relative min-h-screen w-full flex flex-col items-center justify-center bg-background">
|
||||
<div className="relative z-10 w-full h-full flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
className="relative z-10 flex flex-col items-center justify-center min-h-screen text-center px-4"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
>
|
||||
<div className="w-full rounded-3xl p-8 max-w-lg">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-6 bg-red-500/10">
|
||||
<div className="size-8 text-red-500 font-bold text-4xl flex items-center justify-center">!</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground mb-4">
|
||||
出了点问题
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
您访问的积分流转服务不存在或已失效。请检查链接地址是否正确,对此如有疑问请联系社区技术支持。
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full font-sans text-foreground overflow-hidden bg-background selection:bg-primary/30">
|
||||
<div className="relative min-h-screen w-full flex flex-col items-center justify-center bg-background">
|
||||
<div className="relative z-10 w-full h-full flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row w-full max-w-4xl bg-card/70 backdrop-blur-2xl border border-border/50 rounded-3xl overflow-hidden shadow-2xl"
|
||||
style={{ minHeight: 'auto' }}
|
||||
>
|
||||
<PayingInfo orderInfo={orderInfo!} loading={loading} />
|
||||
<PayingNow
|
||||
orderInfo={orderInfo}
|
||||
paying={paying}
|
||||
payKey={payKey}
|
||||
currentStep={currentStep}
|
||||
selectedMethod={selectedMethod}
|
||||
isOpen={isOpen}
|
||||
loading={loading}
|
||||
onPayKeyChange={setPayKey}
|
||||
onCurrentStepChange={setCurrentStep}
|
||||
onSelectedMethodChange={setSelectedMethod}
|
||||
onIsOpenChange={setIsOpen}
|
||||
onPayOrder={handlePayOrder}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user