sign-free.openapps.xyz

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.
Owned app · rev 528326727848
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.
← All files
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
5283267278 · 5 hours ago
Bounded source sync
15f0fa2e27 · 1 day ago
Bounded source sync
d3aa1ca65b · 1 day ago
Bounded source sync
8e2ca0b773 · 1 day ago
Bounded source sync
5cfa84715c · 1 day ago
Bounded source sync
c849e7b286 · 1 day ago
Bounded source sync
68c1cabe03 · 1 day ago
Bounded source sync
62ae8944f0 · 1 day ago
Bounded source sync
7ed54e7381 · 1 day ago