"use client"; import { Check, Loader2, QrCode, Smartphone, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import QRCodeSvg from "react-qr-code"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { ApiError } from "@/lib/api-client"; import { commitWalletShare, type Patient, pollWalletShare, requestWalletPairing, requestWalletShare, type WalletShareRequest, } from "@/lib/patients"; import { notify } from "@/lib/toast"; import { cn } from "@/lib/utils"; type Phase = | "form" | "requesting" | "waiting" | "approved" | "denied" | "expired" | "error"; type Mode = "number" | "qr"; const DURATIONS = [ { hours: 1, key: "hours", count: 1 }, { hours: 24, key: "days", count: 1 }, { hours: 168, key: "days", count: 7 }, ] as const; const POLL_INTERVAL = 2500; const POLL_TIMEOUT = 3 * 60 * 1000; export function ImportFromWalletDialog({ open, onOpenChange, onImported, }: { open: boolean; onOpenChange: (open: boolean) => void; onImported?: (fileNumber: string) => void; }) { const { t } = useTranslation(); const [mode, setMode] = useState("number"); const [walletNumber, setWalletNumber] = useState(""); const [temporary, setTemporary] = useState(false); const [durationHours, setDurationHours] = useState(24); const [phase, setPhase] = useState("form"); const [error, setError] = useState(null); const [request, setRequest] = useState(null); const [pairUri, setPairUri] = useState(null); const [reviewOpen, setReviewOpen] = useState(false); const pollTimer = useRef | null>(null); const stopPolling = () => { if (pollTimer.current) { clearInterval(pollTimer.current); pollTimer.current = null; } }; // Reset everything whenever the dialog is (re)opened. useEffect(() => { if (open) { setMode("number"); setWalletNumber(""); setTemporary(false); setDurationHours(24); setPhase("form"); setError(null); setRequest(null); setPairUri(null); setReviewOpen(false); } return stopPolling; }, [open]); // Poll the request until the patient approves/denies on their device. useEffect(() => { if (phase !== "waiting" || !request) return; const startedAt = Date.now(); pollTimer.current = setInterval(async () => { try { const next = await pollWalletShare(request.id); if (next.status === "approved") { stopPolling(); setRequest(next); setPhase("approved"); } else if (next.status === "denied") { stopPolling(); setPhase("denied"); } else if ( next.status === "expired" || Date.now() - startedAt > POLL_TIMEOUT ) { stopPolling(); setPhase("expired"); } } catch { /* transient — keep polling until timeout */ if (Date.now() - startedAt > POLL_TIMEOUT) { stopPolling(); setPhase("expired"); } } }, POLL_INTERVAL); return stopPolling; }, [phase, request]); const sendRequest = async () => { setPhase("requesting"); setError(null); try { const req = await requestWalletShare({ walletNumber: walletNumber.trim(), mode: temporary ? "temporary" : "permanent", durationHours: temporary ? durationHours : undefined, }); setRequest(req); setPhase("waiting"); } catch (err) { if (err instanceof ApiError && err.status === 400) { setError(t("patients.importApp.invalidWallet")); } else if (err instanceof ApiError && err.status === 409) { setError(t("patients.importApp.networkOff")); } else { setError(t("patients.importApp.error")); } setPhase("error"); } }; // QR flow: create a pairing request (no wallet number) and build the // `temetro-pair:` URI the app scans (this clinic's relay URL + request + key). const startPairing = async () => { setPhase("requesting"); setError(null); try { const pairing = await requestWalletPairing({ mode: temporary ? "temporary" : "permanent", durationHours: temporary ? durationHours : undefined, }); const params = new URLSearchParams({ // Use the server-resolved, phone-reachable relay URL — NOT API_BASE_URL, // which is often http://localhost:4000 (the phone itself) and never // connects from a real device. relay: pairing.relayUrl, rid: pairing.id, epk: pairing.ephemeralPubKey, mode: pairing.shareMode, }); if (temporary) params.set("dur", String(durationHours)); setPairUri(`temetro-pair:?${params.toString()}`); setRequest(pairing); setPhase("waiting"); } catch (err) { if (err instanceof ApiError && err.status === 409) { setError(t("patients.importApp.networkOff")); } else { setError(t("patients.importApp.error")); } setPhase("error"); } }; const commitDraft = async (record: Patient) => { if (!request) return; try { const saved = await commitWalletShare(request.id, record); setReviewOpen(false); onOpenChange(false); onImported?.(saved.fileNumber); notify.success( t("patients.importApp.savedTitle"), t("patients.importApp.savedBody", { name: saved.name }), ); } catch (err) { notify.error( t("patients.importApp.errorTitle"), err instanceof Error ? err.message : t("patients.importApp.error"), ); } }; const durationLabel = (d: (typeof DURATIONS)[number]) => t(`patients.importApp.${d.key}`, { count: d.count }); return ( <> {t("patients.importApp.title")} {t("patients.importApp.description")} {phase === "waiting" && mode === "qr" && pairUri ? (

{t("patients.importApp.qrCaption")}

{t("patients.importApp.waitingTitle")}

) : phase === "waiting" ? (

{t("patients.importApp.waitingTitle")}

{t("patients.importApp.waitingBody")}

) : phase === "approved" ? (

{t("patients.importApp.approvedTitle")}

{t("patients.importApp.approvedBody")}

) : phase === "denied" || phase === "expired" ? (

{t(`patients.importApp.${phase}Title`)}

{t(`patients.importApp.${phase}Body`)}

) : ( <>
{mode === "number" ? ( ) : (

{t("patients.importApp.qrHint")}

)}

{t("patients.importApp.tempLabel")}

{t("patients.importApp.tempHint")}

setTemporary(v)} />
{temporary ? (
{t("patients.importApp.durationLabel")}
{DURATIONS.map((d) => ( ))}
) : null} {error ? (

{error}

) : null} )}
}> {phase === "approved" || phase === "denied" || phase === "expired" ? t("patients.importApp.close") : t("patients.importApp.cancel")} {(phase === "form" || phase === "requesting" || phase === "error") && mode === "number" ? ( ) : (phase === "form" || phase === "requesting" || phase === "error") && mode === "qr" ? ( ) : null}
{/* Review the shared record in the full patient form (review mode — the form emits the draft, we commit it via the wallet-share endpoint). */} {request?.draft ? ( ) : null} ); }