DigiSign e-podpis — envelope flow, popup blokace a zrušení obálky
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Integrace e-podpisu (poskytovatel typu DigiSign/DocuSign) měla tři problémy:
- Když se podpisové okno (embed popup) nepodařilo otevřít (blokace prohlížečem), tlačítko zůstalo „načítá” a nešlo popup vyvolat znovu.
- Zrušení obálky v aplikaci jen smazalo lokální ID — obálka zůstala aktivní u poskytovatele, klient ji dál viděl a mohl podepsat → nekonzistentní stav.
- Stav podpisu se nepropisoval spolehlivě (řešeno webhookem + periodickým syncem).
Root cause
Sekce “Root cause”Envelope (obálka) je u těchto služeb stavový objekt: draft → sent → completed | cancelled | expired | declined. Typický REST flow pro odeslání k podpisu:
POST /api/auth-token → { token, exp } (Bearer, cache ~5 min)POST /api/envelopes → { id, status:"draft" }POST /api/files (multipart PDF) → { id } (file ref)POST /api/envelopes/{id}/documents → naváže file na obálkuPOST /api/envelopes/{id}/recipients (× signatáři)→ { id }POST /api/envelopes/{id}/tags/by-placeholder → umístí podpisové pole dle textu v PDFPOST /api/envelopes/{id}/embed → { url } ← popup pro editor/odeslání (token ~5 min)POST /api/envelopes/{id}/send → odešle k podpisuGET /api/envelopes/{id} → { status } (polling)GET /api/envelopes/{id}/recipients → kdo podepsal (samostatné volání!)GET /api/envelopes/{id}/download-url?output=combined&include_log=true → odkaz na podepsané PDFDELETE /api/envelopes/{id} → zrušení draft obálkyBug 1 — popup: Embed URL se otevíral přes window.open(). Prohlížeče blokují window.open mimo přímou reakci na klik a vrací null. Bez finally zůstal loading=true → tlačítko natrvalo disabled.
Bug 2 — zrušení: Endpoint pro „zrušit obálku” jen nastavil lokální envelopeId = null v DB. Nezavolal DELETE /api/envelopes/{id} u poskytovatele → obálka zůstala ve stavu draft/sent, klient ji měl pořád ve svém účtu.
Fix
Sekce “Fix”Popup (resetuj loading vždy + detekuj blokaci):
function openPopupSafely(url: string): boolean { const popup = window.open(url, "_blank", "noopener,noreferrer"); if (!popup || popup.closed || typeof popup.closed === "undefined") { toast.error("Pop-up byl prohlížečem zablokován. Povolte vyskakovací okna."); return false; } return true;}
async function openSigner() { setLoading(true); try { const { url } = await fetchEmbedUrl(); // POST /embed openPopupSafely(url); } finally { setLoading(false); // ← KLÍČOVÉ: reset i když popup zablokován }}Zrušení obálky (volej API poskytovatele PŘED smazáním lokální reference):
// serviceexport async function cancelEnvelope(id: string): Promise<boolean> { const res = await fetch(`${API}/api/envelopes/${id}`, { method: "DELETE", headers: await authHeaders() }); if (res.ok || res.status === 404) return true; // 404 = už neexistuje = OK return false;}
// routeconst ok = await cancelEnvelope(contract.envelopeId);if (!ok) return json({ error: "Nepodařilo se zrušit u poskytovatele." }, { status: 502 });await db.contract.update({ where:{id}, data:{ envelopeId: null } }); // až po úspěchuPropisování stavu: kombinace webhooku (recipientSigned, envelopeCompleted, envelopeCancelled, envelopeExpired) + záložní periodický cron (poll GET /api/envelopes/{id} u obálek ve stavu „odesláno”), protože webhook může vypadnout. Recipienti se tahají samostatným voláním /recipients (detail obálky je neobsahuje).
Jak se tomu vyvarovat v jiných systémech
Sekce “Jak se tomu vyvarovat v jiných systémech”- Detection:
- Hledej
window.open(bez navazujícíhofinally { setLoading(false) }. - Hledej „cancel/zrušit/void” endpoint, který jen
UPDATE ... SET externalId = NULLbez HTTP volání na poskytovatele.
- Hledej
- Anti-pattern:
- Otevírání popupu po
await(ztratí se „user gesture”, prohlížeč blokuje) — embed URL si vyzvedni, ale popup otevři co nejdřív po kliku, nebo uživatele vyzvi k druhému kliknutí. - „Smazání” externího objektu jen v lokální DB → drift mezi systémy.
- Spoléhat jen na webhook bez záložního pollingu.
- Otevírání popupu po
- Lepší přístup:
- Loading state vždy v
try/finally. Popup blokaci detekuj (!popup || popup.closed) a řekni uživateli, ať povolí pop-up. - Mutace externího stavu (cancel/void/delete) = nejdřív API poskytovatele, lokální zápis až po úspěchu; jinak vrať chybu (502) a nech referenci být.
- Idempotentní webhook + cron reconciliation. 404 z poskytovatele ber jako „už zrušeno” (úspěch).
- Bearer token cachuj s rezervou (např. −60 s před
exp).
- Loading state vždy v
Sister bugs / související
Sekce “Sister bugs / související”- Auto-navazující akce po dokončení (např. automatické vystavení zálohové faktury při
completed) musí být idempotentní — webhook i cron mohou dorazit oba. - Stažené podepsané PDF ukládej mimo git working tree a zálohuj (viz pasti o uploadech).