This commit is contained in:
parent
c7638f240a
commit
65d0127291
|
|
@ -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)) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue