From 65d012729106b08ac991c72d7ad288b2dcbaa893 Mon Sep 17 00:00:00 2001 From: zhangjianjun Date: Wed, 15 Jul 2026 11:23:07 +0800 Subject: [PATCH] ud --- src/pages/PayPage.tsx | 151 +++++++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 33 deletions(-) diff --git a/src/pages/PayPage.tsx b/src/pages/PayPage.tsx index adf17c6..a4f57c3 100644 --- a/src/pages/PayPage.tsx +++ b/src/pages/PayPage.tsx @@ -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)) => {