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_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)) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue