2026-03-10 11:52:37 +08:00
2026-03-01 03:04:24 +08:00
'use client' ;
import { useSearchParams } from 'next/navigation' ;
import { useEffect , useState , Suspense } from 'react' ;
2026-03-10 11:52:37 +08:00
import { applyLocaleToSearchParams , pickLocaleText , resolveLocale , type Locale } from '@/lib/locale' ;
import type { PublicOrderStatusSnapshot } from '@/lib/order/status' ;
type WindowWithAlipayBridge = Window & {
AlipayJSBridge ? : {
call : ( name : string , params? : unknown , callback ? : ( . . . args : unknown [ ] ) = > void ) = > void ;
} ;
} ;
function tryCloseViaAlipayBridge ( ) : boolean {
const bridge = ( window as WindowWithAlipayBridge ) . AlipayJSBridge ;
if ( ! bridge ? . call ) {
return false ;
}
try {
bridge . call ( 'closeWebview' ) ;
return true ;
} catch {
return false ;
}
}
function closeCurrentWindow() {
if ( tryCloseViaAlipayBridge ( ) ) {
return ;
}
let settled = false ;
const handleBridgeReady = ( ) = > {
if ( settled ) {
return ;
}
settled = true ;
document . removeEventListener ( 'AlipayJSBridgeReady' , handleBridgeReady ) ;
if ( ! tryCloseViaAlipayBridge ( ) ) {
window . close ( ) ;
}
} ;
document . addEventListener ( 'AlipayJSBridgeReady' , handleBridgeReady , { once : true } ) ;
window . setTimeout ( ( ) = > {
if ( settled ) {
return ;
}
settled = true ;
document . removeEventListener ( 'AlipayJSBridgeReady' , handleBridgeReady ) ;
window . close ( ) ;
} , 250 ) ;
}
function buildOrderStatusUrl ( orderId : string , accessToken? : string | null ) : string {
const query = new URLSearchParams ( ) ;
if ( accessToken ) {
query . set ( 'access_token' , accessToken ) ;
}
const suffix = query . toString ( ) ;
return suffix ? ` /api/orders/ ${ orderId } ? ${ suffix } ` : ` /api/orders/ ${ orderId } ` ;
}
function getStatusConfig ( order : PublicOrderStatusSnapshot | null , locale : Locale , hasAccessToken : boolean ) {
if ( ! order ) {
return locale === 'en'
? { label : 'Payment Error' , color : 'text-red-600' , icon : '✗' , message : hasAccessToken ? 'Unable to load the order status. Please try again later.' : 'Missing order access token. Please go back to the recharge page.' }
: { label : '支付异常' , color : 'text-red-600' , icon : '✗' , message : hasAccessToken ? '未查询到订单状态,请稍后重试。' : '订单访问凭证缺失,请返回原充值页查看订单结果。' } ;
}
if ( order . rechargeSuccess ) {
return locale === 'en'
? { label : 'Recharge Successful' , color : 'text-green-600' , icon : '✓' , message : 'Your balance has been credited successfully.' }
: { label : '充值成功' , color : 'text-green-600' , icon : '✓' , message : '余额已成功到账!' } ;
}
if ( order . paymentSuccess ) {
if ( order . rechargeStatus === 'paid_pending' || order . rechargeStatus === 'recharging' ) {
return locale === 'en'
? { label : 'Top-up Processing' , color : 'text-blue-600' , icon : '⟳' , message : 'Payment succeeded, and the balance top-up is being processed.' }
: { label : '充值处理中' , color : 'text-blue-600' , icon : '⟳' , message : '支付成功,余额正在充值中...' } ;
}
if ( order . rechargeStatus === 'failed' ) {
return locale === 'en'
? { label : 'Payment Successful' , color : 'text-amber-600' , icon : '!' , message : 'Payment succeeded, but the balance top-up has not completed yet. Please check again later or contact the administrator.' }
: { label : '支付成功' , color : 'text-amber-600' , icon : '!' , message : '支付成功,但余额充值暂未完成,请稍后查看订单结果或联系管理员。' } ;
}
}
if ( order . status === 'PENDING' ) {
return locale === 'en'
? { label : 'Awaiting Payment' , color : 'text-yellow-600' , icon : '⏳' , message : 'The order has not been paid yet.' }
: { label : '等待支付' , color : 'text-yellow-600' , icon : '⏳' , message : '订单尚未完成支付。' } ;
}
if ( order . status === 'EXPIRED' ) {
return locale === 'en'
? { label : 'Order Expired' , color : 'text-gray-500' , icon : '⏰' , message : 'This order has expired. Please create a new order.' }
: { label : '订单已超时' , color : 'text-gray-500' , icon : '⏰' , message : '订单已超时,请重新充值。' } ;
}
if ( order . status === 'CANCELLED' ) {
return locale === 'en'
? { label : 'Order Cancelled' , color : 'text-gray-500' , icon : '✗' , message : 'This order has been cancelled.' }
: { label : '订单已取消' , color : 'text-gray-500' , icon : '✗' , message : '订单已被取消。' } ;
}
return locale === 'en'
? { label : 'Payment Error' , color : 'text-red-600' , icon : '✗' , message : 'Please contact the administrator.' }
: { label : '支付异常' , color : 'text-red-600' , icon : '✗' , message : '请联系管理员处理。' } ;
}
2026-03-01 03:04:24 +08:00
function ResultContent() {
const searchParams = useSearchParams ( ) ;
2026-03-01 17:58:08 +08:00
const outTradeNo = searchParams . get ( 'out_trade_no' ) || searchParams . get ( 'order_id' ) ;
2026-03-10 11:52:37 +08:00
const accessToken = searchParams . get ( 'access_token' ) ;
2026-03-04 10:58:07 +08:00
const isPopup = searchParams . get ( 'popup' ) === '1' ;
2026-03-07 04:16:01 +08:00
const theme = searchParams . get ( 'theme' ) === 'dark' ? 'dark' : 'light' ;
2026-03-09 18:33:57 +08:00
const locale = resolveLocale ( searchParams . get ( 'lang' ) ) ;
2026-03-07 04:16:01 +08:00
const isDark = theme === 'dark' ;
2026-03-01 03:04:24 +08:00
2026-03-09 18:33:57 +08:00
const text = {
checking : pickLocaleText ( locale , '查询支付结果中...' , 'Checking payment result...' ) ,
back : pickLocaleText ( locale , '返回' , 'Back' ) ,
2026-03-10 11:52:37 +08:00
closeSoon : pickLocaleText ( locale , '此窗口将在 3 秒后自动关闭' , 'This window will close automatically in 3 seconds' ) ,
closeNow : pickLocaleText ( locale , '立即关闭窗口' , 'Close now' ) ,
2026-03-09 18:33:57 +08:00
orderId : pickLocaleText ( locale , '订单号' , 'Order ID' ) ,
unknown : pickLocaleText ( locale , '未知' , 'Unknown' ) ,
} ;
2026-03-10 11:52:37 +08:00
const [ orderState , setOrderState ] = useState < PublicOrderStatusSnapshot | null > ( null ) ;
2026-03-01 03:04:24 +08:00
const [ loading , setLoading ] = useState ( true ) ;
2026-03-04 10:58:07 +08:00
const [ isInPopup , setIsInPopup ] = useState ( false ) ;
useEffect ( ( ) = > {
if ( isPopup || window . opener ) {
setIsInPopup ( true ) ;
}
} , [ isPopup ] ) ;
2026-03-01 03:04:24 +08:00
useEffect ( ( ) = > {
2026-03-10 11:52:37 +08:00
if ( ! outTradeNo || ! accessToken ) {
2026-03-01 03:04:24 +08:00
setLoading ( false ) ;
return ;
}
const checkOrder = async ( ) = > {
try {
2026-03-10 11:52:37 +08:00
const res = await fetch ( buildOrderStatusUrl ( outTradeNo , accessToken ) ) ;
2026-03-01 03:04:24 +08:00
if ( res . ok ) {
2026-03-10 11:52:37 +08:00
const data = ( await res . json ( ) ) as PublicOrderStatusSnapshot ;
setOrderState ( data ) ;
2026-03-01 03:04:24 +08:00
}
} catch {
} finally {
setLoading ( false ) ;
}
} ;
checkOrder ( ) ;
const timer = setInterval ( checkOrder , 3000 ) ;
const timeout = setTimeout ( ( ) = > clearInterval ( timer ) , 30000 ) ;
return ( ) = > {
clearInterval ( timer ) ;
clearTimeout ( timeout ) ;
} ;
2026-03-10 11:52:37 +08:00
} , [ outTradeNo , accessToken ] ) ;
2026-03-01 03:04:24 +08:00
2026-03-10 11:52:37 +08:00
const shouldAutoClose = Boolean ( orderState ? . paymentSuccess ) ;
2026-03-04 10:58:07 +08:00
2026-03-07 16:55:49 +08:00
const goBack = ( ) = > {
if ( isInPopup ) {
2026-03-10 11:52:37 +08:00
closeCurrentWindow ( ) ;
2026-03-09 18:33:57 +08:00
return ;
}
if ( window . history . length > 1 ) {
2026-03-07 16:55:49 +08:00
window . history . back ( ) ;
2026-03-09 18:33:57 +08:00
return ;
2026-03-07 16:55:49 +08:00
}
2026-03-09 18:33:57 +08:00
const params = new URLSearchParams ( ) ;
params . set ( 'theme' , theme ) ;
applyLocaleToSearchParams ( params , locale ) ;
window . location . replace ( ` /pay? ${ params . toString ( ) } ` ) ;
2026-03-07 16:55:49 +08:00
} ;
useEffect ( ( ) = > {
2026-03-10 11:52:37 +08:00
if ( ! isInPopup || ! shouldAutoClose ) return ;
const timer = setTimeout ( ( ) = > {
closeCurrentWindow ( ) ;
} , 3000 ) ;
return ( ) = > clearTimeout ( timer ) ;
} , [ isInPopup , shouldAutoClose ] ) ;
2026-03-04 10:58:07 +08:00
2026-03-01 03:04:24 +08:00
if ( loading ) {
return (
2026-03-07 04:16:01 +08:00
< div className = { ` flex min-h-screen items-center justify-center ${ isDark ? 'bg-slate-950' : 'bg-slate-50' } ` } >
2026-03-09 18:33:57 +08:00
< div className = { isDark ? 'text-slate-400' : 'text-gray-500' } > { text . checking } < / div >
2026-03-01 03:04:24 +08:00
< / div >
) ;
}
2026-03-10 11:52:37 +08:00
const display = getStatusConfig ( orderState , locale , Boolean ( accessToken ) ) ;
2026-03-01 03:04:24 +08:00
return (
2026-03-07 04:16:01 +08:00
< div className = { ` flex min-h-screen items-center justify-center p-4 ${ isDark ? 'bg-slate-950' : 'bg-slate-50' } ` } >
< div
className = { [
'w-full max-w-md rounded-xl p-8 text-center shadow-lg' ,
isDark ? 'bg-slate-900 text-slate-100' : 'bg-white' ,
] . join ( ' ' ) }
>
2026-03-10 11:52:37 +08:00
< div className = { ` text-6xl ${ display . color } ` } > { display . icon } < / div >
< h1 className = { ` mt-4 text-xl font-bold ${ display . color } ` } > { display . label } < / h1 >
< p className = { isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500' } > { display . message } < / p >
{ isInPopup ? (
shouldAutoClose && (
2026-03-07 16:55:49 +08:00
< div className = "mt-4 space-y-2" >
2026-03-10 11:52:37 +08:00
< p className = { isDark ? 'text-sm text-slate-500' : 'text-sm text-gray-400' } > { text . closeSoon } < / p >
2026-03-07 16:55:49 +08:00
< button
type = "button"
2026-03-10 11:52:37 +08:00
onClick = { closeCurrentWindow }
2026-03-07 16:55:49 +08:00
className = "text-sm text-blue-600 underline hover:text-blue-700"
>
2026-03-10 11:52:37 +08:00
{ text . closeNow }
2026-03-07 16:55:49 +08:00
< / button >
< / div >
2026-03-10 11:52:37 +08:00
)
2026-03-01 03:04:24 +08:00
) : (
2026-03-10 11:52:37 +08:00
< button
type = "button"
onClick = { goBack }
className = "mt-4 text-sm text-blue-600 underline hover:text-blue-700"
>
{ text . back }
< / button >
2026-03-01 03:04:24 +08:00
) }
2026-03-07 04:16:01 +08:00
< p className = { isDark ? 'mt-4 text-xs text-slate-500' : 'mt-4 text-xs text-gray-400' } >
2026-03-09 18:33:57 +08:00
{ text . orderId } : { outTradeNo || text . unknown }
2026-03-07 04:16:01 +08:00
< / p >
2026-03-01 03:04:24 +08:00
< / div >
< / div >
) ;
}
2026-03-09 18:33:57 +08:00
function ResultPageFallback() {
const searchParams = useSearchParams ( ) ;
const locale = resolveLocale ( searchParams . get ( 'lang' ) ) ;
return (
< div className = "flex min-h-screen items-center justify-center bg-slate-50" >
< div className = "text-gray-500" > { pickLocaleText ( locale , '加载中...' , 'Loading...' ) } < / div >
< / div >
) ;
}
2026-03-01 03:04:24 +08:00
export default function PayResultPage() {
return (
2026-03-09 18:33:57 +08:00
< Suspense fallback = { < ResultPageFallback / > } >
2026-03-01 03:04:24 +08:00
< ResultContent / >
< / Suspense >
) ;
}