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() const token = getToken()
let response: Response let response: Response
try { 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}`, { response = await fetch(`${baseUrl}${path}`, {
...init, ...init,
cache: 'no-store',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
...(token ? { Authorization: `Bearer ${token}` } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),
...(init?.headers ?? {}), ...(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 { apiGet, apiPatch } from '../../api/client'
import { ENDPOINTS } from '../../api/config' import { ENDPOINTS } from '../../api/config'
import type { ApiWithdrawal, WalletTxnStatus } from '../../api/types' 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. */ /** All owner/customer withdrawals from the FunZone admin withdrawals API. */
export function usePendingCustomerWithdrawals(enabled = true) { export function usePendingCustomerWithdrawals(enabled = true) {
const [withdrawals, setWithdrawals] = useState<ApiWithdrawal[]>([]) const [withdrawals, setWithdrawals] = useState<ApiWithdrawal[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [updatingId, setUpdatingId] = useState<string | null>(null) const [updatingId, setUpdatingId] = useState<string | null>(null)
const [error, setError] = 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 () => { const load = useCallback(async (opts?: { soft?: boolean }) => {
setLoading(true) if (!enabledRef.current || inFlightRef.current) return
const soft = Boolean(opts?.soft)
inFlightRef.current = true
if (!soft) setLoading(true)
setError(null) setError(null)
try { try {
const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS) const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
const list = [...(res.withdrawals ?? [])].sort( setWithdrawals(sortWithdrawalsNewestFirst(res.withdrawals ?? []))
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
setWithdrawals(list)
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'خطا در بارگذاری برداشت‌ها') 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 { } finally {
inFlightRef.current = false
setLoading(false) setLoading(false)
} }
}, []) }, [])
useEffect(() => { 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]) }, [enabled, load])
const toggleStatus = useCallback(async (withdrawal: ApiWithdrawal) => { 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}` export const withdrawalNoteTag = (withdrawalId: string): string => `withdrawal:${withdrawalId}`

View File

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