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:
@@ -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 ?? {}),
|
||||
},
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -115,12 +115,34 @@ export function usePartyWithdrawals(kind: PartyKind) {
|
||||
import('../api/types').ApiWithdrawal[]
|
||||
>([])
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const userType = kind === 'supplier' ? 'owner' : 'customer'
|
||||
|
||||
const load = () => {
|
||||
apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
|
||||
.then((res) => {
|
||||
const userType = kind === 'supplier' ? 'owner' : 'customer'
|
||||
if (cancelled) return
|
||||
setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType))
|
||||
})
|
||||
.catch(() => setWithdrawals([]))
|
||||
.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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user