@@ -93,11 +93,14 @@ export default function PaymentMetrics({
9393 const [ hiddenAssets , setHiddenAssets ] = useState < Set < string > > ( new Set ( ) ) ;
9494 const [ range , setRange ] = useState < TimeRange > ( "7D" ) ;
9595 const [ loading , setLoading ] = useState ( true ) ;
96+ const [ isRefreshing , setIsRefreshing ] = useState ( false ) ;
9697 const [ error , setError ] = useState < string | null > ( null ) ;
98+ const [ nonBlockingError , setNonBlockingError ] = useState < string | null > ( null ) ;
9799 const [ refreshToken , setRefreshToken ] = useState ( 0 ) ;
98100 const apiKey = useMerchantApiKey ( ) ;
99101 const hydrated = useMerchantHydrated ( ) ;
100102 const chartContainerRef = useRef < HTMLDivElement > ( null ) ;
103+ const hasLoadedDataRef = useRef ( false ) ;
101104 const chartTitleId = useId ( ) ;
102105 const chartDescriptionId = useId ( ) ;
103106 const chartSummaryId = useId ( ) ;
@@ -114,9 +117,15 @@ export default function PaymentMetrics({
114117 const controller = new AbortController ( ) ;
115118 const apiUrl = process . env . NEXT_PUBLIC_API_URL || "http://localhost:4000" ;
116119 let isCancelled = false ;
117-
118- setLoading ( true ) ;
119- setError ( null ) ;
120+ const hasCachedData = hasLoadedDataRef . current ;
121+
122+ setNonBlockingError ( null ) ;
123+ if ( hasCachedData ) {
124+ setIsRefreshing ( true ) ;
125+ } else {
126+ setLoading ( true ) ;
127+ setError ( null ) ;
128+ }
120129
121130 async function fetchMetrics ( ) {
122131 try {
@@ -150,6 +159,7 @@ export default function PaymentMetrics({
150159
151160 setSummary ( summaryData ) ;
152161 setVolumeData ( volumePayload ) ;
162+ hasLoadedDataRef . current = true ;
153163 // Keep only hidden assets that still exist in the refreshed payload.
154164 setHiddenAssets ( ( prev ) => {
155165 const available = new Set ( volumePayload . assets ?? [ ] ) ;
@@ -159,14 +169,19 @@ export default function PaymentMetrics({
159169 if ( fetchError instanceof Error && fetchError . name === "AbortError" ) {
160170 return ;
161171 }
162- setError (
172+ const nextError =
163173 fetchError instanceof Error
164174 ? fetchError . message
165- : t ( "fetchMetricsFailed" ) ,
166- ) ;
175+ : t ( "fetchMetricsFailed" ) ;
176+ if ( hasCachedData ) {
177+ setNonBlockingError ( nextError ) ;
178+ } else {
179+ setError ( nextError ) ;
180+ }
167181 } finally {
168182 if ( ! isCancelled ) {
169183 setLoading ( false ) ;
184+ setIsRefreshing ( false ) ;
170185 }
171186 }
172187 }
@@ -319,6 +334,14 @@ export default function PaymentMetrics({
319334 </ div >
320335
321336 < div className = "flex flex-wrap items-center gap-2" >
337+ { isRefreshing && (
338+ < span
339+ className = "rounded-full border border-[#E8E8E8] bg-[#F5F5F5] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-[#6B6B6B]"
340+ aria-live = "polite"
341+ >
342+ Updating...
343+ </ span >
344+ ) }
322345 < div className = "flex gap-0.5 rounded-md border border-[#E8E8E8] bg-[#F5F5F5] p-0.5" >
323346 { TIME_RANGES . map ( ( nextRange ) => (
324347 < button
@@ -338,6 +361,14 @@ export default function PaymentMetrics({
338361 </ div >
339362 </ div >
340363 </ div >
364+ { nonBlockingError && (
365+ < p
366+ className = "rounded-md border border-yellow-200 bg-yellow-50 px-3 py-2 text-xs text-yellow-700"
367+ role = "status"
368+ >
369+ { nonBlockingError }
370+ </ p >
371+ ) }
341372
342373 { assets . length > 0 && (
343374 < div
0 commit comments