This commit is contained in:
zhangjianjun 2026-07-15 11:23:07 +08:00
parent c7638f240a
commit 65d0127291
1 changed files with 118 additions and 33 deletions

View File

@ -11,7 +11,8 @@ const PAY_METHOD_LIST = [
const ORDER_POLLING_INTERVAL = 2000 const ORDER_POLLING_INTERVAL = 2000
const ORDER_POLLING_MAX_COUNT = 60 const ORDER_POLLING_MAX_COUNT = 60
const PENDING_PAYMENT_STORAGE_KEY = 'pending-payment-order' const PENDING_PAYMENT_STORAGE_KEY = 'pending-payment-order'
const PENDING_PAYMENT_MAX_AGE = ORDER_POLLING_INTERVAL * ORDER_POLLING_MAX_COUNT const PENDING_PAYMENT_COOKIE_KEY = 'pending-payment-order'
const PENDING_PAYMENT_MAX_AGE = 15 * 60 * 1000
type PayMethodInfo = (typeof PAY_METHOD_LIST)[number] type PayMethodInfo = (typeof PAY_METHOD_LIST)[number]
@ -20,15 +21,13 @@ type PendingPayment = {
orderId: string orderId: string
} }
function getPendingPayment(): PendingPayment | null { function parsePendingPayment(value: string | null): PendingPayment | null {
if (!value) {
return null
}
try { try {
const rawPendingPayment = window.localStorage.getItem(PENDING_PAYMENT_STORAGE_KEY) const pendingPayment: unknown = JSON.parse(value)
if (!rawPendingPayment) {
return null
}
const pendingPayment: unknown = JSON.parse(rawPendingPayment)
if ( if (
!pendingPayment || !pendingPayment ||
typeof pendingPayment !== 'object' || typeof pendingPayment !== 'object' ||
@ -36,7 +35,6 @@ function getPendingPayment(): PendingPayment | null {
typeof (pendingPayment as PendingPayment).expiresAt !== 'number' || typeof (pendingPayment as PendingPayment).expiresAt !== 'number' ||
(pendingPayment as PendingPayment).expiresAt <= Date.now() (pendingPayment as PendingPayment).expiresAt <= Date.now()
) { ) {
window.localStorage.removeItem(PENDING_PAYMENT_STORAGE_KEY)
return null return null
} }
@ -46,29 +44,86 @@ function getPendingPayment(): PendingPayment | null {
} }
} }
function savePendingPayment(orderId: string) { function getPendingPaymentCookie() {
try { const cookiePrefix = `${PENDING_PAYMENT_COOKIE_KEY}=`
const pendingPayment: PendingPayment = { const cookie = document.cookie.split('; ').find((value) => value.startsWith(cookiePrefix))
orderId,
expiresAt: Date.now() + PENDING_PAYMENT_MAX_AGE,
}
window.localStorage.setItem(PENDING_PAYMENT_STORAGE_KEY, JSON.stringify(pendingPayment)) if (!cookie) {
return null
}
try {
return decodeURIComponent(cookie.slice(cookiePrefix.length))
} catch { } catch {
// The current page can still poll when browser storage is unavailable. return null
} }
} }
function clearPendingPayment(orderId?: string) { function getPendingPayment(): PendingPayment | null {
try { let storedPendingPayment: PendingPayment | null = null
const pendingPayment = getPendingPayment()
if (!orderId || !pendingPayment || pendingPayment.orderId === orderId) { try {
window.localStorage.removeItem(PENDING_PAYMENT_STORAGE_KEY) storedPendingPayment = parsePendingPayment(window.localStorage.getItem(PENDING_PAYMENT_STORAGE_KEY))
}
} catch { } catch {
// Ignore storage cleanup failures. // Fall back to the cookie when localStorage is unavailable.
} }
const pendingPayment = storedPendingPayment ?? parsePendingPayment(getPendingPaymentCookie())
if (!pendingPayment) {
clearPendingPayment()
}
return pendingPayment
}
function savePendingPayment(orderId: string) {
const pendingPayment: PendingPayment = {
orderId,
expiresAt: Date.now() + PENDING_PAYMENT_MAX_AGE,
}
const serializedPendingPayment = JSON.stringify(pendingPayment)
try {
window.localStorage.setItem(PENDING_PAYMENT_STORAGE_KEY, serializedPendingPayment)
} catch {
// Cookie storage below still allows the return page to recover the order.
}
document.cookie = `${PENDING_PAYMENT_COOKIE_KEY}=${encodeURIComponent(serializedPendingPayment)}; Max-Age=${Math.ceil(
PENDING_PAYMENT_MAX_AGE / 1000,
)}; Path=/; SameSite=Lax${window.location.protocol === 'https:' ? '; Secure' : ''}`
}
function clearPendingPayment(orderId?: string) {
let storedPendingPayment: PendingPayment | null = null
try {
storedPendingPayment = parsePendingPayment(window.localStorage.getItem(PENDING_PAYMENT_STORAGE_KEY))
} catch {
// Cookie cleanup below still runs when localStorage is unavailable.
}
const pendingPayment = storedPendingPayment ?? parsePendingPayment(getPendingPaymentCookie())
if (!orderId || !pendingPayment || pendingPayment.orderId === orderId) {
try {
window.localStorage.removeItem(PENDING_PAYMENT_STORAGE_KEY)
} catch {
// Cookie cleanup below still runs when localStorage is unavailable.
}
document.cookie = `${PENDING_PAYMENT_COOKIE_KEY}=; Max-Age=0; Path=/; SameSite=Lax${
window.location.protocol === 'https:' ? '; Secure' : ''
}`
}
}
function isIOSSafari() {
const userAgent = navigator.userAgent
const isIOS = /iPad|iPhone|iPod/.test(userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
return isIOS && /Safari/.test(userAgent) && !/CriOS|FxiOS|EdgiOS|OPiOS/.test(userAgent)
} }
function getSupportedPayMethods(goods?: GoodsItem) { function getSupportedPayMethods(goods?: GoodsItem) {
@ -248,7 +303,6 @@ function PayPage() {
} }
stopOrderPolling() stopOrderPolling()
clearPendingPayment(orderId)
if (isMountedRef.current) { if (isMountedRef.current) {
setIsPaying(false) setIsPaying(false)
} }
@ -257,7 +311,6 @@ function PayPage() {
if (pollingCountRef.current >= ORDER_POLLING_MAX_COUNT) { if (pollingCountRef.current >= ORDER_POLLING_MAX_COUNT) {
stopOrderPolling() stopOrderPolling()
clearPendingPayment(orderId)
if (isMountedRef.current) { if (isMountedRef.current) {
setIsPaying(false) setIsPaying(false)
} }
@ -276,7 +329,7 @@ function PayPage() {
[navigate, stopOrderPolling], [navigate, stopOrderPolling],
) )
useEffect(() => { const resumePendingPaymentPolling = useCallback(() => {
const pendingPayment = getPendingPayment() const pendingPayment = getPendingPayment()
if (!pendingPayment) { if (!pendingPayment) {
@ -287,12 +340,32 @@ function PayPage() {
startOrderPolling(pendingPayment.orderId) startOrderPolling(pendingPayment.orderId)
}, [startOrderPolling]) }, [startOrderPolling])
useEffect(() => {
const handlePageVisible = () => {
if (!document.hidden) {
resumePendingPaymentPolling()
}
}
resumePendingPaymentPolling()
window.addEventListener('pageshow', resumePendingPaymentPolling)
window.addEventListener('focus', resumePendingPaymentPolling)
document.addEventListener('visibilitychange', handlePageVisible)
return () => {
window.removeEventListener('pageshow', resumePendingPaymentPolling)
window.removeEventListener('focus', resumePendingPaymentPolling)
document.removeEventListener('visibilitychange', handlePageVisible)
}
}, [resumePendingPaymentPolling])
const handlePay = useCallback(async () => { const handlePay = useCallback(async () => {
if (isPaying || !selectedPlan || !selectedPayType) { if (isPaying || !selectedPlan || !selectedPayType) {
return return
} }
const alipayWindow = selectedPayType === 'alipay' ? window.open('', '_blank') : null const openAlipayInCurrentTab = selectedPayType === 'alipay' && isIOSSafari()
const alipayWindow = selectedPayType === 'alipay' && !openAlipayInCurrentTab ? window.open('', '_blank') : null
const paymentAttemptId = paymentAttemptRef.current + 1 const paymentAttemptId = paymentAttemptRef.current + 1
paymentAttemptRef.current = paymentAttemptId paymentAttemptRef.current = paymentAttemptId
setIsPaying(true) setIsPaying(true)
@ -318,26 +391,38 @@ function PayPage() {
throw new Error('Missing Alipay orderId or payParam') throw new Error('Missing Alipay orderId or payParam')
} }
savePendingPayment(data.orderId)
startOrderPolling(data.orderId)
if (openAlipayInCurrentTab) {
window.location.assign(data.payParam)
return
}
if (alipayWindow) { if (alipayWindow) {
alipayWindow.location.href = data.payParam alipayWindow.location.href = data.payParam
} else if (!window.open(data.payParam, '_blank')) { return
}
if (!window.open(data.payParam, '_blank')) {
clearPendingPayment(data.orderId)
stopOrderPolling()
window.alert('支付宝页面打开失败,请允许浏览器弹窗后重试') window.alert('支付宝页面打开失败,请允许浏览器弹窗后重试')
throw new Error('Failed to open Alipay window') throw new Error('Failed to open Alipay window')
} }
savePendingPayment(data.orderId)
startOrderPolling(data.orderId)
return return
} }
navigate('/pay-result?success=true') navigate('/pay-result?success=true')
} catch { } catch {
alipayWindow?.close() alipayWindow?.close()
stopOrderPolling()
if (isMountedRef.current && paymentAttemptRef.current === paymentAttemptId) { if (isMountedRef.current && paymentAttemptRef.current === paymentAttemptId) {
setIsPaying(false) setIsPaying(false)
} }
} }
}, [isPaying, navigate, selectedPayType, selectedPlan, startOrderPolling]) }, [isPaying, navigate, selectedPayType, selectedPlan, startOrderPolling, stopOrderPolling])
const handleCurrentIndexChange = useCallback( const handleCurrentIndexChange = useCallback(
(nextIndex: number | ((prevIndex: number) => number)) => { (nextIndex: number | ((prevIndex: number) => number)) => {