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 =
|
type Phase =
|
||||||
| "loading-status"
|
| "loading-status"
|
||||||
| "not-connected"
|
| "connecting"
|
||||||
|
| "need-credentials"
|
||||||
| "no-credits"
|
| "no-credits"
|
||||||
| "ready"
|
| "ready"
|
||||||
| "placing"
|
| "placing"
|
||||||
@@ -79,6 +80,8 @@ export function CfOrderModal({
|
|||||||
const [orderId, setOrderId] = useState<string | null>(null);
|
const [orderId, setOrderId] = useState<string | null>(null);
|
||||||
const [documentUrl, setDocumentUrl] = useState<string | null>(null);
|
const [documentUrl, setDocumentUrl] = useState<string | null>(null);
|
||||||
const [elapsed, setElapsed] = useState(0);
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const startedAtRef = useRef<number>(0);
|
const startedAtRef = useRef<number>(0);
|
||||||
const pollAbortRef = useRef<AbortController | null>(null);
|
const pollAbortRef = useRef<AbortController | null>(null);
|
||||||
@@ -110,25 +113,69 @@ export function CfOrderModal({
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [phase]);
|
}, [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 () => {
|
const loadStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ancpi/session", { cache: "no-store" });
|
const res = await fetch("/api/ancpi/session", { cache: "no-store" });
|
||||||
const data = (await res.json()) as SessionStatus;
|
const data = (await res.json()) as SessionStatus;
|
||||||
setStatus(data);
|
const result = evaluateStatus(data);
|
||||||
if (!data.connected) {
|
if (result === "need-connect") {
|
||||||
setPhase("not-connected");
|
// Session not active yet — silently try the server-side env
|
||||||
return;
|
// 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 {
|
} catch {
|
||||||
setError("Nu pot verifica sesiunea ePay.");
|
setError("Nu pot verifica sesiunea ePay.");
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
}
|
}
|
||||||
}, []);
|
}, [evaluateStatus, tryAutoConnect]);
|
||||||
|
|
||||||
const placeOrder = useCallback(async () => {
|
const placeOrder = useCallback(async () => {
|
||||||
setPhase("placing");
|
setPhase("placing");
|
||||||
@@ -228,11 +275,6 @@ export function CfOrderModal({
|
|||||||
[cadastralRef],
|
[cadastralRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToParcelSync = useCallback(() => {
|
|
||||||
const url = `/parcel-sync?tab=epay&cad=${encodeURIComponent(cadastralRef)}`;
|
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
|
||||||
}, [cadastralRef]);
|
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -284,29 +326,76 @@ export function CfOrderModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not connected */}
|
{/* Connecting to ePay (auto, env creds) */}
|
||||||
{phase === "not-connected" && (
|
{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="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" />
|
<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">
|
<p className="font-medium text-amber-900 dark:text-amber-200">
|
||||||
ePay neconectat
|
Conectează-te la ePay ANCPI
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-amber-800/80 dark:text-amber-200/80">
|
<p className="text-amber-800/80 dark:text-amber-200/80">
|
||||||
Conectează-te în secțiunea Parcel Sync → ePay pentru
|
{error ?? "Introdu credențialele tale ePay pentru a continua."}
|
||||||
a putea comanda extrase CF.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<form
|
||||||
type="button"
|
className="space-y-2"
|
||||||
onClick={goToParcelSync}
|
onSubmit={(e) => {
|
||||||
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"
|
e.preventDefault();
|
||||||
|
if (!username || !password) return;
|
||||||
|
void tryAutoConnect({ username, password });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Conectează ePay
|
<label className="block">
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
<span className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
||||||
</button>
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -462,7 +551,10 @@ export function CfOrderModal({
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(phase === "loading-status" || phase === "no-credits") && (
|
{(phase === "loading-status" ||
|
||||||
|
phase === "connecting" ||
|
||||||
|
phase === "no-credits" ||
|
||||||
|
phase === "need-credentials") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -471,15 +563,6 @@ export function CfOrderModal({
|
|||||||
Închide
|
Închide
|
||||||
</button>
|
</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") && (
|
{(phase === "placing" || phase === "processing") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user