August 20, 2026 · Mobile Architecture & SDKs
Add E-Signatures to React Native & Expo Mobile Apps via API (2026)
Build high-performance mobile contract signing for iOS and Android: Bézier touch smoothing, dynamic landscape orientation locks, FaceID/TouchID biometric intent, and direct dispatch to Signbee's REST API with tamper-resistant device telemetry.
Founder, Signbee
ARCHITECTURAL BLUEPRINT
Embedding heavy WebViews or web-based iframe signers inside native mobile apps is a recipe for frustration. On iOS and Android, webview-based e-signing suffers from touch gesture latency, accidental scroll-hijacking, and zero access to native security hardware. In this comprehensive guide, we construct a 100% native React Native & Expo signing component featuring mathematical Bézier smoothing, landscape locking, Secure Enclave biometric verification (FaceID / TouchID / BiometricPrompt), and direct REST ingestion into Signbee's single-endpoint document signing engine.
Why Legacy Mobile E-Signature Workflows Fail
For years, mobile developers needing e-signature capabilities were forced to choose between two subpar paths:
- The In-App Browser / WebView Hack: Spawning a
react-native-webviewpointing to a legacy vendor's hosted signing page (e.g., DocuSign or Adobe Sign). This introduces massive bundle overhead, 2–4 second cold-start delays, authentication cookie drops on iOS Safari WebKit, and constant pinch-to-zoom collisions when users attempt to write their name on a small glass viewport. - Bloated Native Vendor SDKs: Pulling in multi-megabyte binary SDKs that require native bridging, complicate CocoaPods and Gradle compilation, and force your mobile UI into rigid, dated enterprise modals that cannot be branded.
In modern mobile engineering, the ideal pattern is decoupled: the native mobile app owns the signature capture and biometric validation UI, while an API-first document infrastructure like Signbee handles dynamic PDF synthesis from Markdown, cryptographic timestamping (RFC 3161), SHA-256 audit trails, and multi-party email dispatch.
If you have previously read our tutorial on building a React Web signing component or our deep dive on building a signature UI that feels alive, this mobile architecture translates those principles directly to native iOS and Android touch digitizers.
The Mobile Signing Architecture
Here is the operational lifecycle of our native mobile e-signature pipeline:
| Layer | Technology | Responsibility |
|---|---|---|
| Touch Digitizer | PanResponder + SVG | Capture 60/120Hz touch events with quadratic Bézier smoothing |
| Orientation Lock | expo-screen-orientation | Force landscape aspect ratio for ergonomic signature area |
| Biometric Intent | expo-local-authentication | Hardware Secure Enclave FaceID/TouchID challenge |
| Device Telemetry | expo-device + expo-crypto | Model, OS version, client SHA-256 agreement hash |
| Document Engine | Signbee REST API (/api/v1/send) | Markdown to PDF, audit certificate, and webhook dispatch |
Step 1: Installing Expo Dependencies & Native Permissions
In your React Native project (bare workflow or managed Expo SDK 51/52+), install the core mobile libraries. We avoid bloated native wrappers by relying on battle-tested Expo modules:
# Install Expo core dependencies for graphics, biometrics, and orientation npx expo install react-native-svg expo-screen-orientation expo-local-authentication expo-device expo-crypto expo-network
Next, update your app.json or Info.plist to declare biometric permissions for iOS and Android. Modern app store guidelines require explicit permission strings before invoking biometric hardware:
{
"expo": {
"name": "FieldContractSigner",
"slug": "field-contract-signer",
"version": "1.0.0",
"ios": {
"supportsTablet": true,
"infoPlist": {
"NSFaceIDUsageDescription": "Confirm your identity and seal the legal agreement using Face ID."
}
},
"android": {
"permissions": [
"USE_BIOMETRIC",
"USE_FINGERPRINT"
]
},
"plugins": [
[
"expo-local-authentication",
{
"faceIDPermission": "Confirm your identity and seal the legal agreement using Face ID."
}
]
]
}
}Step 2: The Mathematics of Bézier Stroke Smoothing
When a user signs their name with a finger or stylus on mobile glass, the device digitizer emits discrete points: (x0, y0), (x1, y1), (x2, y2), .... If you draw straight lines between these raw points, the resulting signature looks sharp, jagged, and digitized.
To achieve a silky, realistic ink feel, we calculate the midpoint between consecutive coordinates and draw a Quadratic Bézier Curve using the prior point as the control anchor.
The Midpoint Bézier Formula
Given current point P1 = (x1, y1) and next point P2 = (x2, y2), the curve endpoint M is the midpoint:
M_x = (x1 + x2) / 2, M_y = (y1 + y2) / 2
The SVG path command becomes Q x1 y1 Mx My. This guarantees continuous tangential velocity across stroke segments, eliminating jarring angular artifacts while retaining lightweight SVG vector data.
Step 3: Locking Landscape Orientation for Signing
Signing a legally binding agreement on a vertical portrait smartphone (390px wide) leaves almost zero horizontal clearance for a standard signature, causing signers to produce truncated or illegible squiggles.
When our signature modal mounts, we immediately trigger ScreenOrientation.lockAsync() to rotate the screen into landscape mode, expanding the usable signing width to over 840px. When the modal dismisses or completes, we restore portrait mode:
import * as ScreenOrientation from "expo-screen-orientation";
import { useEffect } from "react";
export function useLandscapeSigningLock(isOpen: boolean) {
useEffect(() => {
async function updateOrientation() {
if (isOpen) {
// Lock to landscape for optimal signature aspect ratio
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
);
} else {
// Revert back to portrait default upon exit
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
}
}
updateOrientation();
return () => {
// Safety cleanup on unmount
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
};
}, [isOpen]);
}Step 4: Hardware Biometric Intent Verification
Under ESIGN and eIDAS standards, proving intent to sign and attributing the signature to an authenticated individual are primary requirements. Touch vectors alone prove someone touched the screen; adding a biometric hardware prompt binds the action directly to the registered device owner.
Using expo-local-authentication, we verify whether the device possesses biometric sensors (FaceID, TouchID, Android Biometrics) and execute an authenticated challenge before sealing the payload:
import * as LocalAuthentication from "expo-local-authentication";
export async function verifySignerBiometrics(contractTitle: string): Promise<boolean> {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!hasHardware || !isEnrolled) {
// Fallback: Device does not have biometrics configured; proceed with explicit PIN or button confirmation
return true;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: `Authorize and seal signature for: ${contractTitle}`,
fallbackLabel: "Enter Passcode",
cancelLabel: "Cancel Signing",
disableDeviceFallback: false,
});
return result.success;
}Step 5: Harvesting Mobile Telemetry for the Audit Trail
A bulletproof legal audit trail requires contextual metadata. When sending agreements through the Signbee API, we package telemetry gathered from expo-device, expo-network, and expo-crypto.
To understand the deep cryptographic mechanics behind this, review our guide on E-Signature API Security & Cryptographic Audit Trails.
import * as Device from "expo-device";
import * as Network from "expo-network";
import * as Crypto from "expo-crypto";
export interface MobileAuditTelemetry {
deviceModel: string;
osName: string;
osVersion: string;
ipAddress: string;
timestampUtc: string;
contractHashSha256: string;
biometricAuthenticated: boolean;
}
export async function generateMobileAuditRecord(
contractMarkdown: string,
biometricSuccess: boolean
): Promise<MobileAuditTelemetry> {
// Compute SHA-256 digest of contract content on the client
const contractHashSha256 = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
contractMarkdown
);
const ipAddress = await Network.getIpAddressAsync().catch(() => "unknown");
return {
deviceModel: Device.modelName || "Generic Mobile Device",
osName: Device.osName || "Unknown OS",
osVersion: Device.osVersion || "0.0",
ipAddress,
timestampUtc: new Date().toISOString(),
contractHashSha256,
biometricAuthenticated: biometricSuccess,
};
}Step 6: Complete Production React Native Signature Component
Here is the complete, self-contained TypeScript component. It handles touch gestures, Bézier path smoothing, landscape orientation, clear/undo controls, biometric challenge execution, and API dispatch:
import React, { useState, useRef, useCallback } from "react";
import {
View,
Text,
Modal,
TouchableOpacity,
PanResponder,
StyleSheet,
ActivityIndicator,
Alert,
} from "react-native";
import Svg, { Path } from "react-native-svg";
import * as ScreenOrientation from "expo-screen-orientation";
import * as LocalAuthentication from "expo-local-authentication";
import * as Device from "expo-device";
import * as Crypto from "expo-crypto";
import * as Network from "expo-network";
interface Point {
x: number;
y: number;
}
interface MobileSignatureModalProps {
visible: boolean;
contractTitle: string;
contractMarkdown: string;
signerName: string;
signerEmail: string;
signbeeApiKey: string;
onSuccess: (documentId: string) => void;
onCancel: () => void;
}
export function MobileSignatureModal({
visible,
contractTitle,
contractMarkdown,
signerName,
signerEmail,
signbeeApiKey,
onSuccess,
onCancel,
}: MobileSignatureModalProps) {
const [paths, setPaths] = useState<string[]>([]);
const [currentPoints, setCurrentPoints] = useState<Point[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
// 1. Orientation management
React.useEffect(() => {
if (visible) {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT);
} else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
setPaths([]);
setCurrentPoints([]);
}
}, [visible]);
// 2. Convert points array into smooth Bézier SVG path string
const pointsToSvgPath = useCallback((pts: Point[]): string => {
if (pts.length === 0) return "";
if (pts.length === 1) {
return `M ${pts[0].x} ${pts[0].y} L ${pts[0].x + 0.1} ${pts[0].y + 0.1}`;
}
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 1; i < pts.length; i++) {
const p0 = pts[i - 1];
const p1 = pts[i];
const midX = (p0.x + p1.x) / 2;
const midY = (p0.y + p1.y) / 2;
if (i === 1) {
d += ` L ${midX} ${midY}`;
} else {
d += ` Q ${p0.x} ${p0.y} ${midX} ${midY}`;
}
}
const last = pts[pts.length - 1];
d += ` L ${last.x} ${last.y}`;
return d;
}, []);
// 3. PanResponder for zero-latency touch capture
const pointsRef = useRef<Point[]>([]);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: (evt) => {
const { locationX, locationY } = evt.nativeEvent;
pointsRef.current = [{ x: locationX, y: locationY }];
setCurrentPoints([...pointsRef.current]);
},
onPanResponderMove: (evt) => {
const { locationX, locationY } = evt.nativeEvent;
pointsRef.current.push({ x: locationX, y: locationY });
setCurrentPoints([...pointsRef.current]);
},
onPanResponderRelease: () => {
if (pointsRef.current.length > 0) {
const finishedPath = pointsToSvgPath(pointsRef.current);
setPaths((prev) => [...prev, finishedPath]);
}
pointsRef.current = [];
setCurrentPoints([]);
},
})
).current;
const handleClear = () => {
setPaths([]);
setCurrentPoints([]);
pointsRef.current = [];
};
const handleUndo = () => {
setPaths((prev) => prev.slice(0, -1));
};
// 4. Biometric challenge & Signbee REST API dispatch
const handleConfirmAndSign = async () => {
if (paths.length === 0) {
Alert.alert("Signature Required", "Please provide a handwritten signature before proceeding.");
return;
}
try {
setIsSubmitting(true);
// A. Biometric Auth Challenge
const hasBioHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
let biometricVerified = false;
if (hasBioHardware && isEnrolled) {
const bioResult = await LocalAuthentication.authenticateAsync({
promptMessage: `Seal & sign ${contractTitle}`,
fallbackLabel: "Use Device Passcode",
cancelLabel: "Cancel",
});
if (!bioResult.success) {
setIsSubmitting(false);
return;
}
biometricVerified = true;
}
// B. Gather Telemetry & Contract SHA-256 Digest
const contractHash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
contractMarkdown
);
const ipAddress = await Network.getIpAddressAsync().catch(() => "0.0.0.0");
const completeSvgVector = `<svg viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg">${paths
.map((p) => `<path d="${p}" stroke="#09090b" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" />`)
.join("")}</svg>`;
// C. Dispatch to Signbee API
const response = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${signbeeApiKey}`,
},
body: JSON.stringify({
markdown: contractMarkdown,
recipient_name: signerName,
recipient_email: signerEmail,
metadata: {
source: "react-native-mobile",
device_model: Device.modelName || "Mobile Device",
os_name: Device.osName || "Unknown OS",
os_version: Device.osVersion || "Unknown",
ip_address: ipAddress,
biometric_authenticated: biometricVerified,
contract_sha256: contractHash,
signature_svg: completeSvgVector,
},
}),
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Signbee API error (${response.status}): ${errText}`);
}
const data = await response.json();
onSuccess(data.id);
} catch (err: any) {
Alert.alert("Signing Error", err.message || "Failed to submit signature.");
} finally {
setIsSubmitting(false);
}
};
return (
<Modal visible={visible} animationType="slide" transparent={false}>
<View style={styles.container}>
{/* Header Bar */}
<View style={styles.header}>
<View>
<Text style={styles.title}>{contractTitle}</Text>
<Text style={styles.subtitle}>
Signer: {signerName} ({signerEmail})
</Text>
</View>
<View style={styles.headerActions}>
<TouchableOpacity
onPress={handleUndo}
style={[styles.btnSecondary, paths.length === 0 && styles.btnDisabled]}
disabled={paths.length === 0 || isSubmitting}
>
<Text style={styles.btnSecondaryText}>Undo</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleClear}
style={[styles.btnSecondary, paths.length === 0 && styles.btnDisabled]}
disabled={paths.length === 0 || isSubmitting}
>
<Text style={styles.btnSecondaryText}>Clear</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onCancel} style={styles.btnCancel} disabled={isSubmitting}>
<Text style={styles.btnCancelText}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
{/* Touch Canvas Drawing Pad */}
<View style={styles.canvasContainer} {...panResponder.panHandlers}>
<Svg style={StyleSheet.absoluteFill}>
{/* Render committed paths */}
{paths.map((p, idx) => (
<Path
key={idx}
d={p}
stroke="#18181b"
strokeWidth={3}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
))}
{/* Render in-flight active stroke */}
{currentPoints.length > 0 && (
<Path
d={pointsToSvgPath(currentPoints)}
stroke="#d97706"
strokeWidth={3}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
</Svg>
{paths.length === 0 && currentPoints.length === 0 && (
<View style={styles.placeholderOverlay} pointerEvents="none">
<Text style={styles.placeholderText}>Sign on the line above with your finger or stylus</Text>
<View style={styles.baseline} />
</View>
)}
</View>
{/* Footer Actions */}
<View style={styles.footer}>
<Text style={styles.legalNotice}>
By tapping 'Authorize & Sign', you consent to be legally bound by this electronic document under the US ESIGN Act and EU eIDAS regulations.
</Text>
<TouchableOpacity
onPress={handleConfirmAndSign}
style={[styles.btnPrimary, (paths.length === 0 || isSubmitting) && styles.btnDisabled]}
disabled={paths.length === 0 || isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator color="#000000" />
) : (
<Text style={styles.btnPrimaryText}>Authorize & Sign Document</Text>
)}
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#09090b",
paddingHorizontal: 20,
paddingVertical: 12,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
title: {
fontSize: 16,
fontWeight: "700",
color: "#fafafa",
},
subtitle: {
fontSize: 12,
color: "#a1a1aa",
marginTop: 2,
},
headerActions: {
flexDirection: "row",
gap: 8,
},
btnSecondary: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: "#27272a",
borderRadius: 6,
},
btnSecondaryText: {
color: "#e4e4e7",
fontSize: 12,
fontWeight: "600",
},
btnCancel: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: "#ef444420",
borderRadius: 6,
},
btnCancelText: {
color: "#f87171",
fontSize: 12,
fontWeight: "600",
},
canvasContainer: {
flex: 1,
backgroundColor: "#fafafa",
borderRadius: 8,
borderWidth: 1,
borderColor: "#3f3f46",
overflow: "hidden",
position: "relative",
},
placeholderOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "flex-end",
alignItems: "center",
paddingBottom: 40,
},
baseline: {
width: "80%",
height: 1,
backgroundColor: "#d4d4d8",
marginTop: 8,
},
placeholderText: {
color: "#a1a1aa",
fontSize: 13,
fontStyle: "italic",
},
footer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10,
},
legalNotice: {
fontSize: 10,
color: "#71717a",
flex: 1,
marginRight: 16,
},
btnPrimary: {
backgroundColor: "#f59e0b",
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 6,
minWidth: 200,
alignItems: "center",
},
btnPrimaryText: {
color: "#000000",
fontWeight: "700",
fontSize: 13,
},
btnDisabled: {
opacity: 0.4,
},
});Step 7: Handling Offline Queuing & Network Resiliency
Field logistics, construction surveys, and healthcare consultations often occur in areas with spotty cellular coverage. When implementing mobile e-signatures, your app should never fail just because the device lost 5G connectivity.
Follow this standard offline execution strategy:
- Immediate Local Commitment: Once the user completes the signature and passes the biometric prompt, serialize the agreement data, SVG vectors, and timestamp into an encrypted SQLite database or AsyncStorage queue.
- Client Hash Lock: Calculate the SHA-256 hash immediately upon signature capture. This proves the document content was not altered between offline capture and cloud synchronization.
- Background Synchronization: Listen to network status changes using
@react-native-community/netinfoor Expo Network. When connectivity is re-established, flush queued agreements tohttps://signb.ee/api/v1/sendwith exponential backoff.
Comparing Native Mobile Signing vs. Legacy Iframe Wrappers
| Metric | Native + Signbee API | Legacy Iframe / WebView |
|---|---|---|
| Touch Latency | < 8ms (Native 120Hz digitizer) | 45–120ms (Bridge + DOM lag) |
| Biometric Intent | Hardware Secure Enclave (FaceID) | None (Browser sandbox blocked) |
| Orientation Control | Automatic Landscape Lock | Clunky viewport reflows |
| Offline Support | Full offline queue & hash lock | Complete white-screen failure |
| App Bundle Impact | < 150 KB total | 12–25 MB (Heavy SDK dependencies) |
Legal Admissibility: ESIGN, UETA & eIDAS Compliance
Developers frequently ask: “Is a native mobile touch signature captured in our own UI legally valid?”
Under both US federal law (ESIGN Act, 15 U.S.C. § 7001) and state law (UETA), an electronic signature is defined as “an electronic sound, symbol, or process attached to or logically associated with a contract and executed or adopted by a person with the intent to sign.”
Our React Native architecture satisfies all four legal pillars:
- Intent to Sign: Explicit biometric confirmation dialog clearly stating the agreement title and terms.
- Consent to Electronic Business: Prominent on-screen statutory disclosure before authorization.
- Association of Signature to Record: The SVG touch vector and client SHA-256 hash are permanently embedded into the resulting PDF via Signbee's digital sealing engine.
- Tamper-Proof Retention: Signbee generates an immutable audit certificate containing RFC 3161 timestamps, device telemetry, and cryptographic hash verification.
Frequently Asked Questions
How do Bézier curves improve touch signature quality on mobile compared to raw polyline points?
Mobile digitizers sample touch events at discrete intervals (typically 60Hz to 120Hz). Connecting raw $(x, y)$ touch coordinate samples with straight line segments (polylines) produces sharp, jagged corners and unnatural polygon edges, especially during fast loops, curves, and flourishes common in handwritten signatures. By applying quadratic or cubic Bézier interpolation using calculated midpoints between sequential touch points, the canvas generates continuous mathematical curves where tangents match seamlessly at every junction. This reproduces the organic fluid dynamics of a ballpoint or fountain pen gliding over paper while keeping the underlying vector representation lightweight, resolution-independent, and anti-aliased across high-DPI Retina and OLED mobile screens.
Are mobile biometric signatures (FaceID/TouchID) legally binding under ESIGN and eIDAS?
Yes, pairing a handwritten mobile touch signature with hardware-backed biometric authentication (Apple FaceID, TouchID, or Android BiometricPrompt) meets and frequently exceeds the legal requirements established by the US Electronic Signatures in Global and National Commerce Act (ESIGN), the Uniform Electronic Transactions Act (UETA), and EU eIDAS regulation for Advanced Electronic Signatures (AES). Biometric hardware verification on modern mobile devices relies on tamper-proof Secure Enclave / TEE silicon, establishing clear evidence of signer identity and unforgeable intent to sign. When this biometric authentication pass is cryptographically bound alongside high-resolution touch vector data, device hardware telemetry (model, OS, network state), and a SHA-256 document hash in the Signbee audit trail, it produces court-admissible forensic evidence far superior to basic desktop email-link confirmations.
How do you handle mobile offline signing and network reconnects with the Signbee API?
Mobile field applications frequently operate in low-connectivity or air-gapped environments (such as remote field inspections, logistics delivery docks, or flight cabins). In a production React Native architecture, you capture the signature vector path, record the biometric confirmation timestamp, compute the client-side SHA-256 hash of the contract markdown, and persist the serialized signing payload to local encrypted storage using tools like Redux Persist or SQLite. You monitor network reachability with NetInfo or Expo Network. Once an active internet connection resumes, a background sync service dispatches the pending queue with idempotent request keys to Signbee's REST API endpoint. Signbee renders the immutable PDF, timestamps the agreement, logs the mobile telemetry in the audit trail, and dispatches webhook notifications to your backend without risking duplicate contract issuance.
Ready to add native mobile e-signatures to your Expo or React Native app? Get 5 free documents/month.
Last updated: August 20, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.