Skip to content

feat: headless useESignature hook and ESignature theming for RN and web - #60

Merged
grimen merged 10 commits into
mainfrom
feat/headless-hook
Sep 4, 2026
Merged

grimen merged 10 commits into
mainfrom
feat/headless-hook

Conversation

@grimen

@grimen grimen commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Why

The ESignature component hardcoded every color and string (iOS-blue buttons, "Review and sign your document", "Cancel", ...) and the only customization was label. There was also no way to start the signing flow from a host's own UI: the state machine (offline check, source.start(), WebView/iframe embed, session-expiry restart, success delay, error mapping) was welded to the built-in screens. Esaú hit the first problem ("it would be great if the library allowed changing the button colors"); the second is the underlying design issue: the library should own the protocol and the state machine, not the chrome.

What

Headless hook, both packages. The state machine moves into useESignature(options), exported from @blinkbitcoin/esign-react-native (and its Apollo-free /webform subpath) and @blinkbitcoin/esign-react. It returns status, error, isSessionExpired, the actions sign / cancel / retry / restart / checkConnection, and an embed handle:

  • RN: webViewProps to spread onto the host's own WebView (plus isCheckingConnection).
  • Web: embed, either { kind: 'iframe', iframeProps } or { kind: 'mount', ref } for DocuSign.js sources. The mount effect now waits for the container to attach via a callback ref instead of assuming it exists on the first signing render.

Thin default UI with theming. ESignature is now a consumer of the hook and gains three optional props: theme (primaryColor, primaryTextColor, mutedTextColor, successColor, errorColor, warningColor), styles (per-element overrides), and labels (copy overrides; title/sign default to label). Precedence is base < theme < styles. Esaú's ask becomes theme={{ primaryColor: '#F7931A' }}.

No breaking change. Existing props keep working and the default look and copy are byte-identical. All testIDs are unchanged, so Maestro and Playwright flows are untouched.

Demos. Both demo apps gain a HookSigning screen (host-owned buttons + WebView/iframe over the hook) behind a toolbar toggle; the default UI stays the initial view and the E2E target.

Docs. READMEs gain Theming and Headless usage sections; architecture pages, source tree, integration notes, CLAUDE.md/AGENTS.md, and the component-hierarchy diagram reflect the split.

Tests

  • RN package: 74 Jest tests (was 36), web package: 89 (was 43), both at the enforced 100% coverage. State-machine tests moved to useESignature.test.tsx, component tests cover screens + wiring + theming, theme.test.tsx covers the resolvers, and each package has a hook-only consumer test that never imports ESignature.
  • Demos: RN 22 tests, web 14, above their coverage floors.
  • npm run typecheck, npm run lint, npm test (all workspaces), npm run build && npm run check:packages, make docs-check all green locally.

Extract the signing state machine (acquisition, offline check, WebView
events, session-expiry restart, success delay) into useESignature so a
host can trigger signing from its own UI and place the WebView itself.
ESignature becomes a thin default UI over the hook and gains theme,
styles, and labels props; the default look and copy are unchanged.
Mirror of the RN change: the signing state machine moves into
useESignature, which hands back an embed descriptor (iframe props, or a
callback ref for DocuSign.js-mounted sources) so a host can render its
own UI. The DocuSign.js mount now waits for the container to attach
instead of assuming it exists on the first signing render. ESignature
becomes a thin default UI with theme, styles, and labels props; the
default look and copy are unchanged.
Each demo gains a HookSigning screen built on useESignature with the
host's own buttons and WebView/iframe, toggled from the toolbar. The
default ESignature UI stays the initial view so the Maestro and
Playwright flows are unchanged.
READMEs gain Theming and Headless usage sections; architecture pages,
source tree, integration notes, CLAUDE.md/AGENTS.md, and the component
hierarchy diagram reflect the hook + default-UI split.
Three uniform SVGs (default, themed, headless) under docs/images, each
showing the idle screen and which layers the library versus the host
owns. Static assets, not mermaid, so they live outside docs/diagrams.
Move the path illustrations to docs/assets (where readme-hero.svg
already lives) and reference them from the root README as well.
Two-line rows so labels and notes never collide, five identical slots
in every column (the unused ESignature slot is drawn dashed on the
headless path), and a taller canvas with the legend in its own band.
The toolbar now cycles default → themed → hook. Themed renders the
stock ESignature with Blink's palette (orange primary, pill buttons)
and the label a consumer actually asked for, so the theme props can be
seen on a device without any host code beyond three props.
@grimen

grimen commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Demo of the themed path on the iOS simulator (mock provider): stock component in the default blue → toolbar switch to the themed mode → same component with Blink's palette (#fc5805, pill buttons) and the label Sign the agreement → signing page in the WebView → success → host alert.

Themed ESignature on iOS

The demo app's toolbar now cycles default → themed → hook (dfa3cb6); themed is three props on the stock component:

<ESignature
  source={source}
  label="Sign the agreement"
  theme={{ primaryColor: '#fc5805', primaryTextColor: '#FFFFFF', mutedTextColor: '#9292A0', successColor: '#00A700', warningColor: '#ffad0d' }}
  styles={{ button: { borderRadius: 50, paddingHorizontal: 32 } }}
/>

GIF lives on the assets/pr-60-demo branch (delete after merge).

grimen and others added 2 commits September 4, 2026 20:12
Cover buildSource for every ESIGN_MODE by mocking the config module per
test, the Web Forms fetch success and failure paths, the in-flight
reconnect label, and the non-dev recipient. The RN mode resolver becomes
a function because Babel inlines ESIGN_MODE at transform time. Coverage
thresholds on both demos move from floors to 100%; docs updated.
@grimen
grimen merged commit a990939 into main Sep 4, 2026
12 of 14 checks passed
@grimen
grimen deleted the feat/headless-hook branch September 4, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant