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_MAX_COUNT = 60
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]
@ -20,15 +21,13 @@ type PendingPayment = {
orderId: string
}
function getPendingPayment(): PendingPayment | null {
function parsePendingPayment(value: string | null): PendingPayment | null {
if (!value) {
return null
}
try {
const rawPendingPayment = window.localStorage.getItem(PENDING_PAYMENT_STORAGE_KEY)
if (!rawPendingPayment) {
return null
}
const pendingPayment: unknown = JSON.parse(rawPendingPayment)
const pendingPayment: unknown = JSON.parse(value)
if (
!pendingPayment ||
typeof pendingPayment !== 'object' ||
@ -36,7 +35,6 @@ function getPendingPayment(): PendingPayment | null {
typeof (pendingPayment as PendingPayment).expiresAt !== 'number' ||
(pendingPayment as PendingPayment).expiresAt <= Date.now()
) {
window.localStorage.removeItem(PENDING_PAYMENT_STORAGE_KEY)
return null
}
@ -46,29 +44,86 @@ function getPendingPayment(): PendingPayment | null {
}
}
function savePendingPayment(orderId: string) {
try {
const pendingPayment: PendingPayment = {
orderId,
expiresAt: Date.now() + PENDING_PAYMENT_MAX_AGE,
}
function getPendingPaymentCookie() {
const cookiePrefix = `${PENDING_PAYMENT_COOKIE_KEY}=`
const cookie = document.cookie.split('; ').find((value) => value.startsWith(cookiePrefix))
window.localStorage.setItem(PENDING_PAYMENT_STORAGE_KEY, JSON.stringify(pendingPayment))
if (!cookie) {
return null
}
try {
return decodeURIComponent(cookie.slice(cookiePrefix.length))
} catch {
// The current page can still poll when browser storage is unavailable.
return null
}
}
function clearPendingPayment(orderId?: string) {
try {
const pendingPayment = getPendingPayment()
function getPendingPayment(): PendingPayment | null {
let storedPendingPayment: PendingPayment | null = null
if (!orderId || !pendingPayment || pendingPayment.orderId === orderId) {
window.localStorage.removeItem(PENDING_PAYMENT_STORAGE_KEY)
}
try {
storedPendingPayment = parsePendingPayment(window.localStorage.getItem(PENDING_PAYMENT_STORAGE_KEY))
} 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) {
@ -248,7 +303,6 @@ function PayPage() {
}
stopOrderPolling()
clearPendingPayment(orderId)
if (isMountedRef.current) {
setIsPaying(false)
}
@ -257,7 +311,6 @@ function PayPage() {
if (pollingCountRef.current >= ORDER_POLLING_MAX_COUNT) {
stopOrderPolling()
clearPendingPayment(orderId)
if (isMountedRef.current) {
setIsPaying(false)
}
@ -276,7 +329,7 @@ function PayPage() {
[navigate, stopOrderPolling],
)
useEffect(() => {
const resumePendingPaymentPolling = useCallback(() => {
const pendingPayment = getPendingPayment()
if (!pendingPayment) {
@ -287,12 +340,32 @@ function PayPage() {
startOrderPolling(pendingPayment.orderId)
}, [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 () => {
if (isPaying || !selectedPlan || !selectedPayType) {
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
paymentAttemptRef.current = paymentAttemptId
setIsPaying(true)
@ -318,26 +391,38 @@ function PayPage() {
throw new Error('Missing Alipay orderId or payParam')
}
savePendingPayment(data.orderId)
startOrderPolling(data.orderId)
if (openAlipayInCurrentTab) {
window.location.assign(data.payParam)
return
}
if (alipayWindow) {
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('支付宝页面打开失败,请允许浏览器弹窗后重试')
throw new Error('Failed to open Alipay window')
}
savePendingPayment(data.orderId)
startOrderPolling(data.orderId)
return
}
navigate('/pay-result?success=true')
} catch {
alipayWindow?.close()
stopOrderPolling()
if (isMountedRef.current && paymentAttemptRef.current === paymentAttemptId) {
setIsPaying(false)
}
}
}, [isPaying, navigate, selectedPayType, selectedPlan, startOrderPolling])
}, [isPaying, navigate, selectedPayType, selectedPlan, startOrderPolling, stopOrderPolling])
const handleCurrentIndexChange = useCallback(
(nextIndex: number | ((prevIndex: number) => number)) => {