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