Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 1da91c033c738ab74538d13d
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/SignPage.tsx
// A signer's view, opened from their private link: fill in their fields, agree, sign.
import { useEffect, useMemo, useRef, useState } from "react";
import { get, setMany, useQuery, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { Modal, toast } from "./ui";
import { Pages } from "./Pages";
import { PageSkeleton, PagePlaceholder } from "./Skeleton";
import { SignaturePad } from "./SignaturePad";
import { FilledBox, LoadError, SideLabel } from "./DocPage";
import { openPdf, type PdfDoc } from "./lib/pdfjs";
import { loadBase, type Base } from "./lib/docdata";
import { buildSignedPdf, download } from "./lib/finalpdf";
import { SITE_URL } from "../functions/lib/config";
import { FIELD_LABEL, SIGNER_COLORS, parseJson, randomId, type Doc, type Field, type Signer, type Signature, type SignEvent } from "../functions/lib/doc";
type Row<T> = T & { id: string };
const when = (sec: number) => new Date(sec * 1000).toLocaleString(undefined, { month: "long", day: "numeric", hour: "numeric", minute: "2-digit" });
export function SignPage({ docId, signerId }: { docId: string; signerId: string }) {
const [state, setState] = useState<"loading" | "ready" | "invalid" | "other" | "error">("loading");
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
(async () => {
try {
const uid = await me();
const storeKey = `sign:t:${docId}:${signerId}`;
let stored: string | null = null;
try { stored = localStorage.getItem(storeKey); } catch { /* storage blocked */ }
const token = new URLSearchParams(location.hash.slice(1)).get("t") || stored;
const joined = (await get(`docs/${docId}/signerjoins/${uid}`)) as { signerId: string } | null;
if (joined && joined.signerId !== signerId) { if (!cancelled) setState("other"); return; }
if (!joined) {
if (!token) { if (!cancelled) setState("invalid"); return; }
try {
await setMany([
{ path: `signerclaims/${docId}/s/${signerId}/c/${uid}`, document: { token } },
{ path: `docs/${docId}/signerjoins/${uid}`, document: { signerId, at: serverTimestamp() } },
{ path: `docs/${docId}/events/${randomId(12)}`, document: { type: "viewed", signerId, at: serverTimestamp(), by: uid } },
]);
} catch {
if (!cancelled) setState("invalid");
return;
}
}
if (token) {
try { localStorage.setItem(storeKey, token); } catch { /* storage blocked */ }
history.replaceState(null, "", `/s/${docId}/${signerId}`);
}
if (!cancelled) setState("ready");
} catch (e) {
if (!cancelled) { setError(explain(e)); setState("error"); }
}
})();
return () => { cancelled = true; };
}, [docId, signerId]);
if (state === "loading") return <PageSkeleton />;
if (state === "invalid") return (
<Stop title="This signing link does not work" text="It may be missing its end, or the sender voided the document. Ask the sender to send the link again, and open all of it." />
);
if (state === "other") return (
<Stop title="This browser is signing as someone else" text="It already opened another signer's link for this document. Open this link in another browser, a private window or the signer's own device." />
);
if (state === "error") return <LoadError text={error} />;
return <Signing docId={docId} signerId={signerId} />;
}
function Stop({ title, text }: { title: string; text: string }) {
return (
<div className="wrap page-pad narrow enter">
<div className="empty-state">
<img className="art" src="/art/card-sign.webp" alt="" width="150" height="125" />
<h1>{title}</h1>
<p>{text}</p>
<a className="btn btn-line" href="/" onClick={(e) => { e.preventDefault(); navigate("/"); }}>What is Sign?</a>
</div>
</div>
);
}
function Signing({ docId, signerId }: { docId: string; signerId: string }) {
const { data: doc, error: docError } = useQuery(`docs/${docId}`) as { data: Doc | null | undefined; error: Error | null };
const { data: sigRows } = useQuery(`docs/${docId}/signatures`) as { data: Row<Signature>[] | undefined };
const signers = useMemo(() => parseJson<Signer[]>(doc?.signers, []), [doc?.signers]);
const [base, setBase] = useState<Base | null>(null);
const [pdf, setPdf] = useState<PdfDoc | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [values, setValues] = useState<Record<string, string>>({});
const [padFor, setPadFor] = useState<Field | null>(null);
const [textFor, setTextFor] = useState<Field | null>(null);
const [adopted, setAdopted] = useState<{ signature?: string; initials?: string }>({});
const [finishing, setFinishing] = useState(false);
const [started, setStarted] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const pagesRef = useRef<HTMLDivElement>(null);
const meSigner = signers.find((s) => s.id === signerId);
useEffect(() => {
if (!doc || base) return;
let cancelled = false;
(async () => {
try {
const b = await loadBase(docId, doc, signers);
const p = await openPdf(b.bytes);
if (!cancelled) { setBase(b); setPdf(p); }
} catch (e) { if (!cancelled) setLoadError(explain(e)); }
})();
return () => { cancelled = true; };
}, [doc, base, docId, signers]);
// This signer's fields in reading order, which is the order "Next field" walks them.
const mine = useMemo(() => (base?.fields ?? []).filter((f) => f.signerId === signerId)
.sort((a, b) => a.page - b.page || a.y - b.y || a.x - b.x), [base, signerId]);
// Prefill what we can: today's date and the signer's name.
useEffect(() => {
if (!mine.length || !meSigner) return;
setValues((v) => {
const next = { ...v };
for (const f of mine) {
if (next[f.id]) continue;
if (f.type === "date") next[f.id] = new Date().toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" });
if (f.type === "name") next[f.id] = meSigner.name;
}
return next;
});
}, [mine, meSigner]);
useEffect(() => { if (doc?.title) document.title = `Sign: ${doc.title}`; }, [doc?.title]);
if (docError && doc === undefined) return <LoadError text={explain(docError)} />;
if (doc === undefined || sigRows === undefined) return <PageSkeleton />;
if (!doc || !meSigner) return <LoadError text="This document could not be found." />;
const signatures = Object.fromEntries(sigRows.map((s) => [s.id, s]));
const mySig = signatures[signerId];
const complete = signers.length > 0 && signers.every((s) => signatures[s.id]);
const open = doc.status === "sent" && !mySig;
const waitingFor = signers.filter((s) => !signatures[s.id]).length;
const required = mine.filter((f) => f.required !== false);
const remaining = required.filter((f) => !values[f.id]);
const color = SIGNER_COLORS[Math.max(0, signers.findIndex((s) => s.id === signerId)) % SIGNER_COLORS.length];
const fill = (f: Field) => {
if (!open) return;
setStarted(true);
if (f.type === "signature" || f.type === "initials") {
const prev = adopted[f.type];
if (prev && !values[f.id]) { setValues((v) => ({ ...v, [f.id]: prev })); return; }
setPadFor(f);
} else if (f.type === "checkbox") {
setValues((v) => ({ ...v, [f.id]: v[f.id] === "true" ? "" : "true" }));
} else if (f.type === "text" || f.type === "name") {
setTextFor(f);
}
};
const next = () => {
setStarted(true);
const f = remaining[0];
if (!f) return;
const el = pagesRef.current?.querySelector(`[data-field="${f.id}"]`) as HTMLElement | null;
el?.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "center" });
el?.focus({ preventScroll: true });
if (f.type !== "checkbox") setTimeout(() => fill(f), 300);
};
const sign = async () => {
setBusy(true);
setError(null);
try {
const uid = await me();
const clean = Object.fromEntries(mine.filter((f) => values[f.id]).map((f) => [f.id, values[f.id]]));
await setMany([
{ path: `docs/${docId}/signatures/${signerId}`, document: { values: JSON.stringify(clean), consent: true, signedAt: serverTimestamp(), by: uid, ua: navigator.userAgent.slice(0, 400) } },
{ path: `docs/${docId}/events/${randomId(12)}`, document: { type: "signed", signerId, at: serverTimestamp(), by: uid } },
]);
setFinishing(false);
toast("Signed");
window.scrollTo({ top: 0, behavior: "smooth" });
} catch (e) {
setError(explain(e));
} finally {
setBusy(false);
}
};
const downloadSigned = async () => {
if (!base) return;
setBusy(true);
try {
const events = ((await get(`docs/${docId}/events`, { limit: 500 })) as { data?: SignEvent[] })?.data ?? [];
download(await buildSignedPdf({ docId, doc, base: base.bytes, fields: base.fields, signers, signatures, events, siteUrl: SITE_URL }), `${doc.title} (signed).pdf`);
} catch (e) { toast(explain(e)); } finally { setBusy(false); }
};
const overlay = (page: number) => (base?.fields ?? []).filter((f) => f.page === page).map((f) => {
if (f.signerId !== signerId || !open) {
const sig = signatures[f.signerId];
const value = sig ? parseJson<Record<string, string>>(sig.values, {})[f.id] : undefined;
return <FilledBox key={f.id} field={f} value={value} color={SIGNER_COLORS[Math.max(0, signers.findIndex((s) => s.id === f.signerId)) % SIGNER_COLORS.length]} />;
}
const v = values[f.id];
const label = f.label || FIELD_LABEL[f.type];
const auto = f.type === "date";
const prompt = f.type === "signature" ? "Sign here" : f.type === "initials" ? "Initial here" : label;
return (
<div key={f.id} data-field={f.id} className={`fieldbox mine${f.type === "checkbox" ? " checkbox" : ""}${v ? " has-value" : ""}${auto ? " auto" : ""}`}
style={{ left: `${f.x * 100}%`, top: `${f.y * 100}%`, width: `${f.w * 100}%`, height: `${f.h * 100}%`, ["--c" as string]: color }}
onClick={() => fill(f)} role={auto ? undefined : f.type === "checkbox" ? "checkbox" : "button"} tabIndex={auto ? undefined : 0}
aria-checked={f.type === "checkbox" ? v === "true" : undefined}
aria-label={auto ? undefined : `${label}${f.required !== false ? ", required" : ", optional"}${v && f.type !== "checkbox" ? ", filled in" : ""}`}
onKeyDown={(e) => { if (!auto && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); fill(f); } }}>
{f.type === "checkbox" ? <>
{v && <span className="fill-text">✕</span>}
{f.label && <SideLabel field={f} text={f.label} />}
</> : v ? (
f.type === "signature" || f.type === "initials" ? <img src={v} alt="" /> : <span className="fill-text">{v}</span>
) : <span className="fieldbox-label">{prompt}</span>}
</div>
);
});
const primary = open ? (
remaining.length
? <button className="btn btn-lg" type="button" onClick={next} disabled={!pdf}>{started ? "Next field" : "Start"}</button>
: <button className="btn btn-lg" type="button" onClick={() => setFinishing(true)} disabled={!pdf}>Finish and sign</button>
) : mySig && complete ? <button className="btn btn-lg" type="button" onClick={downloadSigned} disabled={!base} aria-busy={busy || undefined}>Download signed PDF</button> : null;
const progress = open && remaining.length > 0 ? <span className="progress muted small" aria-live="polite">{remaining.length === 1 ? "1 field left" : `${remaining.length} fields left`}</span> : null;
return (
<div className="doc enter">
<header className="doc-head">
<div className="wrap doc-head-inner">
<div className="grow">
<p className={`eyebrow${doc.status === "voided" ? " status-voided" : mySig ? " status-done" : ""}`}>{doc.status === "voided" ? "Voided" : mySig ? (complete ? "Completed" : "You signed") : `For ${meSigner.name}`}</p>
<h1 className="doc-title">{doc.title}</h1>
</div>
{primary && <div className="head-actions hide-phone">{progress}{primary}</div>}
</div>
</header>
<div className="wrap sign-body">
{doc.status === "voided" && <p className="notice notice-error">The sender voided this document, so it can no longer be signed.</p>}
{mySig && (
<div className={`card done-card enter`} role="status">
<h2 className="h3">{complete ? "Everyone has signed" : waitingFor === 1 ? "Waiting for 1 more signer" : `Waiting for ${waitingFor} more signers`}</h2>
<p className="small">{complete
? "Download the signed PDF with its completion certificate. This link keeps working, so you can download it again later."
: `You signed on ${when(mySig.signedAt)}. Keep this link: the signed PDF appears here once everyone has signed.`}</p>
</div>
)}
{open && (
<div className="sign-intro">
{doc.message ? <blockquote className="sign-message">{doc.message}</blockquote> : null}
<p className="muted">Read the document, fill in the {required.length === 1 ? "field" : "fields"} marked for you, then sign. No account needed.</p>
</div>
)}
<div ref={pagesRef}>
{loadError ? <p className="notice notice-error">{loadError}</p> : pdf ? <Pages pdf={pdf} overlay={overlay} /> : <PagePlaceholder />}
</div>
</div>
{primary && <div className="action-bar phone-bar">{progress}{primary}</div>}
<SignaturePad open={Boolean(padFor)} kind={padFor?.type === "initials" ? "initials" : "signature"} name={meSigner.name} onClose={() => setPadFor(null)}
onDone={(png) => {
const kind = padFor?.type === "initials" ? "initials" : "signature";
setAdopted((a) => ({ ...a, [kind]: png }));
// Fill every field of the same kind with the adopted signature.
setValues((v) => ({ ...v, ...Object.fromEntries(mine.filter((m) => m.type === kind).map((m) => [m.id, png])) }));
setPadFor(null);
}} />
<TextDialog field={textFor} value={textFor ? values[textFor.id] ?? "" : ""} onClose={() => setTextFor(null)}
onSave={(v) => { if (textFor) setValues((s) => ({ ...s, [textFor.id]: v })); setTextFor(null); }} />
<FinishSheet open={finishing} name={meSigner.name} busy={busy} error={error} onClose={() => setFinishing(false)} onSign={sign} />
</div>
);
}
function TextDialog({ field, value, onClose, onSave }: { field: Field | null; value: string; onClose: () => void; onSave: (v: string) => void }) {
const [text, setText] = useState(value);
useEffect(() => { if (field) setText(value); }, [field, value]);
const label = field ? field.label || FIELD_LABEL[field.type] : "";
return (
<Modal open={Boolean(field)} title={label} onClose={onClose} footer={<>
<button className="btn btn-line" type="button" onClick={onClose}>Cancel</button>
<button className="btn" type="submit" form="text-field-form">Done</button>
</>}>
<form id="text-field-form" onSubmit={(e) => { e.preventDefault(); onSave(text.trim().slice(0, 200)); }}>
<label className="field"><span className="sr-only">{label}</span>
<input className="input" data-autofocus value={text} onChange={(e) => setText(e.target.value)} maxLength={200} enterKeyHint="done" />
</label>
</form>
</Modal>
);
}
function FinishSheet({ open, name, busy, error, onClose, onSign }: { open: boolean; name: string; busy: boolean; error: string | null; onClose: () => void; onSign: () => void }) {
const [agree, setAgree] = useState(false);
return (
<Modal open={open} title="Sign the document" onClose={onClose} footer={<>
<button className="btn btn-line" type="button" onClick={onClose}>Back</button>
<button className="btn" type="button" onClick={onSign} disabled={!agree} aria-busy={busy || undefined}>Sign</button>
</>}>
<label className="check consent">
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />
<span>I, {name}, agree to sign this document electronically, and that my electronic signature is as valid as my handwritten one.</span>
</label>
<p className="muted small">Sign records when you sign, your browser, and a fingerprint of the exact document, and prints them on a completion certificate that everyone receives.</p>
{error && <p className="notice notice-error" role="alert">{error}</p>}
</Modal>
);
}
Change history
Quality pass: phone signing, edit signers, copy to send again
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync