fix(cf-modal): inline auto-connect + credential form — no parcel-sync hop
Marius: don't kick the user out to /parcel-sync just to connect ePay,
do everything inside the modal. The parcel-sync page also wasn't
helpful when reached (UAT selector empty), so the redirect was a
dead-end anyway.
State machine rewrite:
loading-status → GET /api/ancpi/session
not-connected → DELETED (replaced by transparent flow)
connecting → POST /api/ancpi/session with {} — server picks up
ANCPI_USERNAME/ANCPI_PASSWORD from env (Infisical
has them in /architools), connects silently
need-credentials → only if env creds are missing OR invalid: shows
an inline form (username / password / Conectează
button + privacy note "nu sunt păstrate la noi
după sfârșitul sesiunii")
no-credits / ready / placing / processing / done / error — as before
Flow for the happy path (Marius's case): user clicks "Comandă CF" →
modal shows "Conectare la ePay ANCPI…" for ~1s → "Verificare credite"
done → "Ești sigur? 1 credit, mai ai X" → confirm → animated steps
→ done. Zero page navigations.
Flow for the no-env case (other tenants or first-run): user sees
inline form, types credentials, presses Conectează → server stores
them in the in-memory session for the lifetime of the request,
modal continues straight to "ready".
Removed:
- goToParcelSync() handler + "Conectează ePay" deep-link button
- "not-connected" UI panel
- Phase value "not-connected" (no longer reachable)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,8 @@ import { cn } from "@/shared/lib/utils";
|
||||
|
||||
type Phase =
|
||||
| "loading-status"
|
||||
| "not-connected"
|
||||
| "connecting"
|
||||
| "need-credentials"
|
||||
| "no-credits"
|
||||
| "ready"
|
||||
| "placing"
|
||||
@@ -79,6 +80,8 @@ export function CfOrderModal({
|
||||
const [orderId, setOrderId] = useState<string | null>(null);
|
||||
const [documentUrl, setDocumentUrl] = useState<string | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const startedAtRef = useRef<number>(0);
|
||||
const pollAbortRef = useRef<AbortController | null>(null);
|
||||
@@ -110,25 +113,69 @@ export function CfOrderModal({
|
||||
return () => clearInterval(id);
|
||||
}, [phase]);
|
||||
|
||||
const evaluateStatus = useCallback((data: SessionStatus) => {
|
||||
setStatus(data);
|
||||
if (!data.connected) return "need-connect" as const;
|
||||
if (data.credits != null && data.credits < 1) {
|
||||
setPhase("no-credits");
|
||||
return "no-credits" as const;
|
||||
}
|
||||
setPhase("ready");
|
||||
return "ready" as const;
|
||||
}, []);
|
||||
|
||||
const tryAutoConnect = useCallback(
|
||||
async (creds?: { username: string; password: string }) => {
|
||||
setPhase("connecting");
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(creds ?? {}),
|
||||
});
|
||||
const data = (await res.json()) as {
|
||||
success?: boolean;
|
||||
credits?: number | null;
|
||||
error?: string;
|
||||
};
|
||||
if (!res.ok || data.error) {
|
||||
// 400 with empty body = no server-side env creds. Show the
|
||||
// inline credential form. 401 = bad credentials.
|
||||
if (creds) {
|
||||
setError(data.error ?? "Credențiale ePay invalide");
|
||||
}
|
||||
setPhase("need-credentials");
|
||||
return;
|
||||
}
|
||||
// Re-fetch status to confirm
|
||||
const sRes = await fetch("/api/ancpi/session", { cache: "no-store" });
|
||||
const sData = (await sRes.json()) as SessionStatus;
|
||||
evaluateStatus(sData);
|
||||
} catch {
|
||||
setPhase("need-credentials");
|
||||
setError("Nu s-a putut conecta automat. Introdu credențialele manual.");
|
||||
}
|
||||
},
|
||||
[evaluateStatus],
|
||||
);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/session", { cache: "no-store" });
|
||||
const data = (await res.json()) as SessionStatus;
|
||||
setStatus(data);
|
||||
if (!data.connected) {
|
||||
setPhase("not-connected");
|
||||
return;
|
||||
const result = evaluateStatus(data);
|
||||
if (result === "need-connect") {
|
||||
// Session not active yet — silently try the server-side env
|
||||
// credentials (ANCPI_USERNAME / ANCPI_PASSWORD). Most installs
|
||||
// have these in Infisical so connection happens transparently.
|
||||
await tryAutoConnect();
|
||||
}
|
||||
if (data.credits != null && data.credits < 1) {
|
||||
setPhase("no-credits");
|
||||
return;
|
||||
}
|
||||
setPhase("ready");
|
||||
} catch {
|
||||
setError("Nu pot verifica sesiunea ePay.");
|
||||
setPhase("error");
|
||||
}
|
||||
}, []);
|
||||
}, [evaluateStatus, tryAutoConnect]);
|
||||
|
||||
const placeOrder = useCallback(async () => {
|
||||
setPhase("placing");
|
||||
@@ -228,11 +275,6 @@ export function CfOrderModal({
|
||||
[cadastralRef],
|
||||
);
|
||||
|
||||
const goToParcelSync = useCallback(() => {
|
||||
const url = `/parcel-sync?tab=epay&cad=${encodeURIComponent(cadastralRef)}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}, [cadastralRef]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
@@ -284,29 +326,76 @@ export function CfOrderModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not connected */}
|
||||
{phase === "not-connected" && (
|
||||
{/* Connecting to ePay (auto, env creds) */}
|
||||
{phase === "connecting" && (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>Conectare la ePay ANCPI…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Need credentials — server didn't have them or auto-connect failed */}
|
||||
{phase === "need-credentials" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-50 p-3 text-sm dark:bg-amber-950/30">
|
||||
<div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-50 p-3 text-xs dark:bg-amber-950/30">
|
||||
<Plug className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
<p className="font-medium text-amber-900 dark:text-amber-200">
|
||||
ePay neconectat
|
||||
Conectează-te la ePay ANCPI
|
||||
</p>
|
||||
<p className="text-xs text-amber-800/80 dark:text-amber-200/80">
|
||||
Conectează-te în secțiunea Parcel Sync → ePay pentru
|
||||
a putea comanda extrase CF.
|
||||
<p className="text-amber-800/80 dark:text-amber-200/80">
|
||||
{error ?? "Introdu credențialele tale ePay pentru a continua."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goToParcelSync}
|
||||
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md border bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
<form
|
||||
className="space-y-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!username || !password) return;
|
||||
void tryAutoConnect({ username, password });
|
||||
}}
|
||||
>
|
||||
Conectează ePay
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<label className="block">
|
||||
<span className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
||||
Utilizator
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary"
|
||||
placeholder="email@exemplu.ro"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
||||
Parolă
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!username || !password}
|
||||
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
<Plug className="h-3.5 w-3.5" />
|
||||
Conectează
|
||||
</button>
|
||||
<p className="pt-1 text-[10px] text-muted-foreground">
|
||||
Credențialele sunt trimise direct la ANCPI; nu sunt
|
||||
păstrate la noi după sfârșitul sesiunii.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -462,7 +551,10 @@ export function CfOrderModal({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(phase === "loading-status" || phase === "no-credits") && (
|
||||
{(phase === "loading-status" ||
|
||||
phase === "connecting" ||
|
||||
phase === "no-credits" ||
|
||||
phase === "need-credentials") && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -471,15 +563,6 @@ export function CfOrderModal({
|
||||
Închide
|
||||
</button>
|
||||
)}
|
||||
{phase === "not-connected" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Renunță
|
||||
</button>
|
||||
)}
|
||||
{(phase === "placing" || phase === "processing") && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user