fix(epay-ui): stuck connect spinner + order button shows processing not instant-valid
Two UX issues reported from the field:
1. ePay pill spun forever on an already-connected (green) pill. Two causes:
the icon put connecting before connected (so a stuck connecting state
showed the spinner even when connected), and the auto-connect effect leaked
the connecting state — a cancelled early-return skipped clearing it, and
having connecting in the dep array made setConnecting(true) cancel its own
in-flight attempt. Fix: connected takes icon priority; a finally{} always
clears connecting unless retrying; drop connecting from deps.
2. The per-parcel CF button flipped straight to green "Extras CF valid" the
instant the order was queued, while it actually kept processing ~1-2 min in
the background (cart, submit, poll, download). Now it shows a pulsing
"Se proceseaza..." and polls until a completed extract truly exists before
flipping to valid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,17 +141,21 @@ export function EpayConnect({
|
||||
if (cancelled) return;
|
||||
setError("Eroare retea");
|
||||
shouldRetry = attempt < maxRetries;
|
||||
} finally {
|
||||
// ALWAYS clear the connecting spinner unless we're about to retry —
|
||||
// including the `cancelled` early-returns above. Otherwise a re-run
|
||||
// of this effect (e.g. when status.connected flips true) cancels the
|
||||
// in-flight attempt and leaves connecting stuck true → a perpetual
|
||||
// spinner on an already-connected (green) pill.
|
||||
if (!shouldRetry) setConnecting(false);
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (shouldRetry) {
|
||||
// Keep connecting state true during retry wait
|
||||
autoConnectTimerRef.current = setTimeout(() => {
|
||||
void attemptConnect(attempt + 1);
|
||||
}, 3000);
|
||||
} else {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -164,7 +168,11 @@ export function EpayConnect({
|
||||
autoConnectTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [triggerConnect, status.connected, connecting, fetchStatus]);
|
||||
// `connecting` intentionally excluded: setConnecting(true) inside this
|
||||
// effect would otherwise re-trigger it and cancel its own in-flight
|
||||
// attempt. autoConnectAttempted (a ref) already prevents double-starts.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [triggerConnect, status.connected, fetchStatus]);
|
||||
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
@@ -202,10 +210,10 @@ export function EpayConnect({
|
||||
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{connecting ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : status.connected ? (
|
||||
{status.connected ? (
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
) : connecting ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : null}
|
||||
|
||||
<span className="hidden sm:inline">ePay</span>
|
||||
|
||||
@@ -57,6 +57,10 @@ export function EpayOrderButton({
|
||||
|
||||
const [ordering, setOrdering] = useState(false);
|
||||
const [ordered, setOrdered] = useState(false);
|
||||
// After enqueue the order keeps processing in the background (~1–2 min on
|
||||
// the legacy queue): cart → submit → poll → download. Show that instead of
|
||||
// flipping straight to a misleading "valid" the instant it's queued.
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [epayStatus, setEpayStatus] = useState<EpaySessionStatus>({
|
||||
connected: false,
|
||||
@@ -111,7 +115,10 @@ export function EpayOrderButton({
|
||||
});
|
||||
if (mountedRef.current) {
|
||||
if (result.ok) {
|
||||
setOrdered(true);
|
||||
// Queued, not done — enter the processing state and let the poll
|
||||
// effect below flip to "valid" only once the extract is actually
|
||||
// ready (or surface a failure).
|
||||
setProcessing(true);
|
||||
} else {
|
||||
setError(result.error ?? "Eroare comanda");
|
||||
}
|
||||
@@ -119,6 +126,36 @@ export function EpayOrderButton({
|
||||
}
|
||||
}, [nrCadastral, siruta, judetName, uatName, useGisAc]);
|
||||
|
||||
// Poll while processing: flip to "valid" only when a completed extract
|
||||
// actually exists. Caps at ~3 min, then stops (the parent list refresh
|
||||
// will reflect the final state).
|
||||
useEffect(() => {
|
||||
if (!processing) return;
|
||||
let cancelled = false;
|
||||
let attempts = 0;
|
||||
const tick = async () => {
|
||||
attempts += 1;
|
||||
try {
|
||||
const has = await fetchCfHasCompletedForCadastral(useGisAc, nrCadastral);
|
||||
if (cancelled) return;
|
||||
if (has) {
|
||||
setOrdered(true);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* keep polling */
|
||||
}
|
||||
if (!cancelled && attempts >= 36) setProcessing(false); // ~3 min
|
||||
};
|
||||
const id = setInterval(() => void tick(), 5000);
|
||||
void tick();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [processing, useGisAc, nrCadastral]);
|
||||
|
||||
// On the (future) gis.ac path, the orchestrator dispatches ePay calls
|
||||
// through a shared account pool — no personally-connected ePay session
|
||||
// needed. The legacy queue (current route while the guard is on)
|
||||
@@ -143,6 +180,29 @@ export function EpayOrderButton({
|
||||
return tooltipText ?? "Comanda extras CF (1 credit)";
|
||||
};
|
||||
|
||||
if (processing) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 gap-1 px-1.5 text-yellow-600 dark:text-yellow-400"
|
||||
disabled
|
||||
>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<span className="text-[10px]">Se procesează...</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Comanda CF este în curs (coș → plată → descărcare, ~1–2 min)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (ordered) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
|
||||
Reference in New Issue
Block a user