Fix stale withdrawals list on production by bypassing HTTP cache and soft-refreshing.

Keep last known rows on soft errors and refetch on focus/visibility without request loops.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Shayan Azadi
2026-07-30 18:20:50 +03:30
parent bcd7a71c4f
commit 459ae43b0f
3 changed files with 76 additions and 16 deletions

View File

@@ -25,10 +25,15 @@ async function request<T>(path: string, init?: RequestInit, baseUrl: string = ge
const token = getToken()
let response: Response
try {
// Always bypass browser/CDN HTTP caches for live FunZone/accounting data
// (Arvan in front of api/acc can otherwise serve stale withdrawal lists).
response = await fetch(`${baseUrl}${path}`, {
...init,
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(init?.headers ?? {}),
},

View File

@@ -1,34 +1,65 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { apiGet, apiPatch } from '../../api/client'
import { ENDPOINTS } from '../../api/config'
import type { ApiWithdrawal, WalletTxnStatus } from '../../api/types'
const REFRESH_INTERVAL_MS = 30_000
function sortWithdrawalsNewestFirst(list: ApiWithdrawal[]): ApiWithdrawal[] {
return [...list].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
}
/** All owner/customer withdrawals from the FunZone admin withdrawals API. */
export function usePendingCustomerWithdrawals(enabled = true) {
const [withdrawals, setWithdrawals] = useState<ApiWithdrawal[]>([])
const [loading, setLoading] = useState(false)
const [updatingId, setUpdatingId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const inFlightRef = useRef(false)
const enabledRef = useRef(enabled)
enabledRef.current = enabled
const load = useCallback(async () => {
setLoading(true)
const load = useCallback(async (opts?: { soft?: boolean }) => {
if (!enabledRef.current || inFlightRef.current) return
const soft = Boolean(opts?.soft)
inFlightRef.current = true
if (!soft) setLoading(true)
setError(null)
try {
const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
const list = [...(res.withdrawals ?? [])].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
setWithdrawals(list)
setWithdrawals(sortWithdrawalsNewestFirst(res.withdrawals ?? []))
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در بارگذاری برداشت‌ها')
setWithdrawals([])
// Keep last known list on soft refresh / transient CDN errors so the UI
// does not flash empty while production caches catch up.
if (!soft) setWithdrawals([])
} finally {
inFlightRef.current = false
setLoading(false)
}
}, [])
useEffect(() => {
if (enabled) void load()
if (!enabled) return
void load()
const softReload = () => {
if (document.visibilityState === 'visible') void load({ soft: true })
}
document.addEventListener('visibilitychange', softReload)
window.addEventListener('focus', softReload)
const intervalId = window.setInterval(softReload, REFRESH_INTERVAL_MS)
return () => {
document.removeEventListener('visibilitychange', softReload)
window.removeEventListener('focus', softReload)
window.clearInterval(intervalId)
}
}, [enabled, load])
const toggleStatus = useCallback(async (withdrawal: ApiWithdrawal) => {
@@ -55,7 +86,9 @@ export function usePendingCustomerWithdrawals(enabled = true) {
}
}, [])
return { withdrawals, loading, updatingId, error, reload: load, toggleStatus }
const reload = useCallback(() => void load(), [load])
return { withdrawals, loading, updatingId, error, reload, toggleStatus }
}
export const withdrawalNoteTag = (withdrawalId: string): string => `withdrawal:${withdrawalId}`

View File

@@ -115,12 +115,34 @@ export function usePartyWithdrawals(kind: PartyKind) {
import('../api/types').ApiWithdrawal[]
>([])
useEffect(() => {
apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
.then((res) => {
const userType = kind === 'supplier' ? 'owner' : 'customer'
setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType))
})
.catch(() => setWithdrawals([]))
let cancelled = false
const userType = kind === 'supplier' ? 'owner' : 'customer'
const load = () => {
apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
.then((res) => {
if (cancelled) return
setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType))
})
.catch(() => {
// Keep last known list; avoid wiping party stats on a transient/CDN miss.
})
}
load()
const onVisible = () => {
if (document.visibilityState === 'visible') load()
}
document.addEventListener('visibilitychange', onVisible)
window.addEventListener('focus', onVisible)
const intervalId = window.setInterval(onVisible, 30_000)
return () => {
cancelled = true
document.removeEventListener('visibilitychange', onVisible)
window.removeEventListener('focus', onVisible)
window.clearInterval(intervalId)
}
}, [kind])
return withdrawals
}