Přeskočit na obsah

DigiSign e-podpis — envelope flow, popup blokace a zrušení obálky

import { Aside } from ‘@astrojs/starlight/components’;

Integrace e-podpisu (poskytovatel typu DigiSign/DocuSign) měla tři problémy:

  1. 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.
  2. 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.
  3. Stav podpisu se nepropisoval spolehlivě (řešeno webhookem + periodickým syncem).

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álku
POST /api/envelopes/{id}/recipients (× signatáři)→ { id }
POST /api/envelopes/{id}/tags/by-placeholder → umístí podpisové pole dle textu v PDF
POST /api/envelopes/{id}/embed → { url } ← popup pro editor/odeslání (token ~5 min)
POST /api/envelopes/{id}/send → odešle k podpisu
GET /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é PDF
DELETE /api/envelopes/{id} → zrušení draft obálky

Bug 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.

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):

// service
export 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;
}
// route
const 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ěchu

Propisová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ího finally { setLoading(false) }.
    • Hledej „cancel/zrušit/void” endpoint, který jen UPDATE ... SET externalId = NULL bez HTTP volání na poskytovatele.
  • 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.
  • 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).

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).
Přidal aiarchitekt.cz · 25. 5. 2026 2:00
Provozuje aiarchitekt.cz