August 21, 2026 · Mobile Tutorial
Add E-Signatures to Flutter (Dart) Apps via REST API (2026 Tutorial)
Building cross-platform mobile apps for iOS, Android, and Web often requires capturing legal digital signatures on invoices, NDAs, or field-service work orders. Discover how to build a smooth bezier curve signature pad in Flutter 3+, manage credentials securely, and orchestrate automated contract dispatch via the Signbee REST API.
Founder, Signbee
TL;DR
You do not need heavy legacy vendor SDKs to capture and execute legally binding electronic signatures in Flutter. By pairing a responsive CustomPainter with midpoint quadratic bezier curve smoothing and communicating directly with Signbee's lightweight REST API using Dart's http package, you can generate dynamic Markdown agreements, collect smooth signatures on touchscreens, and track audit-trail certificates across iOS, Android, and Web with minimal binary overhead.
Why Modern Flutter Apps Need a Lightweight E-Signature Architecture
Flutter has become the gold standard framework for cross-platform engineering, empowering teams to ship high-performance mobile, desktop, and web applications from a single Dart codebase. However, integrating electronic signatures into mobile workflows has traditionally been a headache:
- Bloated Native SDKs: Legacy providers (like DocuSign or Adobe Sign) require complex native iOS CocoaPods and Android Gradle wrappers that bloat your app bundle size by dozens of megabytes.
- Clunky User Experience: Forcing mobile users out of your application into an unoptimized desktop-first web portal breaks onboarding conversion and spikes abandonment rates.
- Gesture Collisions: Capturing finger or stylus input inside standard Flutter scrollable views causes frustrating jitter when vertical drags conflict with the parent scroll controller.
- Security Vulnerabilities: Storing hardcoded API credentials in Dart source code leaves keys exposed to reverse engineering via decompiled APKs or iOS binary strings.
In this comprehensive tutorial, we will build a production-grade Flutter e-signature integration from the ground up. You will learn how to securely manage API tokens with flutter_secure_storage, construct a typed Dart REST client for Signbee, build a silky-smooth bezier curve signature drawing widget, and trigger in-app signing ceremonies using webview_flutter and url_launcher.
Prerequisites & Environment Setup
Add the necessary dependencies to your Flutter project's pubspec.yaml file:
name: flutter_esignature_demo
description: "A production Flutter e-signature integration using Signbee API."
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dependencies:
flutter:
sdk: flutter
http: ^1.2.1
flutter_secure_storage: ^9.2.2
webview_flutter: ^4.7.0
url_launcher: ^6.3.0
path_provider: ^2.1.3
json_annotation: ^4.9.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
build_runner: ^2.4.9
json_serializable: ^6.8.0Run flutter pub get in your terminal to fetch the packages.
Section 1: Data Models & Serialization with json_serializable
Robust mobile architectures require strong typing. Rather than passing loose Map<String, dynamic> dictionaries across your application, we define immutable Dart models for the Signbee API payload and response structures.
import 'package:json_annotation/json_annotation.dart';
part 'signbee_models.g.dart';
@JsonSerializable()
class SignbeeSendRequest {
final String title;
@JsonKey(name: 'recipient_name')
final String recipientName;
@JsonKey(name: 'recipient_email')
final String recipientEmail;
final String markdown;
@JsonKey(name: 'callback_url')
final String? callbackUrl;
@JsonKey(name: 'expires_in_days')
final int? expiresInDays;
final Map<String, dynamic>? metadata;
const SignbeeSendRequest({
required this.title,
required this.recipientName,
required this.recipientEmail,
required this.markdown,
this.callbackUrl,
this.expiresInDays = 30,
this.metadata,
});
factory SignbeeSendRequest.fromJson(Map<String, dynamic> json) =>
_$SignbeeSendRequestFromJson(json);
Map<String, dynamic> toJson() => _$SignbeeSendRequestToJson(this);
}
@JsonSerializable()
class SignbeeSendResponse {
@JsonKey(name: 'document_id')
final String documentId;
final String status;
@JsonKey(name: 'signing_url')
final String signingUrl;
@JsonKey(name: 'created_at')
final String createdAt;
const SignbeeSendResponse({
required this.documentId,
required this.status,
required this.signingUrl,
required this.createdAt,
});
factory SignbeeSendResponse.fromJson(Map<String, dynamic> json) =>
_$SignbeeSendResponseFromJson(json);
Map<String, dynamic> toJson() => _$SignbeeSendResponseToJson(this);
}
@JsonSerializable()
class SignbeeDocumentStatus {
@JsonKey(name: 'document_id')
final String documentId;
final String title;
final String status;
@JsonKey(name: 'recipient_email')
final String recipientEmail;
@JsonKey(name: 'signed_at')
final String? signedAt;
@JsonKey(name: 'pdf_url')
final String? pdfUrl;
@JsonKey(name: 'audit_trail_url')
final String? auditTrailUrl;
const SignbeeDocumentStatus({
required this.documentId,
required this.title,
required this.status,
required this.recipientEmail,
this.signedAt,
this.pdfUrl,
this.auditTrailUrl,
});
factory SignbeeDocumentStatus.fromJson(Map<String, dynamic> json) =>
_$SignbeeDocumentStatusFromJson(json);
Map<String, dynamic> toJson() => _$SignbeeDocumentStatusToJson(this);
}Generate the serialization code by running:
dart run build_runner build --delete-conflicting-outputs
Section 2: Secure Credential Management with flutter_secure_storage
Never hardcode production API secrets in your client-side Flutter code. For client-to-backend mobile operations, credentials should either be stored in the device's hardware-backed secure enclave (iOS Keychain and Android KeyStore) or proxied through your own backend service (such as a Node.js API or Go backend).
Here is a secure token management class that leverages flutter_secure_storage with strict platform-specific security policies:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SignbeeAuthStorage {
static const String _apiKeyKey = 'signbee_api_key';
final FlutterSecureStorage _storage;
SignbeeAuthStorage({FlutterSecureStorage? storage})
: _storage = storage ??
const FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
resetOnError: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
Future<void> saveApiKey(String apiKey) async {
await _storage.write(key: _apiKeyKey, value: apiKey);
}
Future<String?> getApiKey() async {
return await _storage.read(key: _apiKeyKey);
}
Future<void> deleteApiKey() async {
await _storage.delete(key: _apiKeyKey);
}
Future<bool> hasApiKey() async {
final key = await getApiKey();
return key != null && key.trim().isNotEmpty;
}
}Section 3: Typed Dart Signbee REST API Service
With secure storage and data models in place, we build the core SignbeeApiService. This service encapsulates HTTP communication, header injection, timeout policies, and standardized error parsing.
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../models/signbee_models.dart';
import 'signbee_auth_storage.dart';
class SignbeeApiException implements Exception {
final String message;
final int? statusCode;
final dynamic details;
SignbeeApiException(this.message, {this.statusCode, this.details});
@override
String toString() => 'SignbeeApiException(status: $statusCode, message: $message)';
}
class SignbeeApiService {
static const String _baseUrl = 'https://signb.ee/api/v1';
final http.Client _client;
final SignbeeAuthStorage _authStorage;
SignbeeApiService({
http.Client? client,
SignbeeAuthStorage? authStorage,
}) : _client = client ?? http.Client(),
_authStorage = authStorage ?? SignbeeAuthStorage();
Future<Map<String, String>> _getHeaders() async {
final apiKey = await _authStorage.getApiKey();
if (apiKey == null || apiKey.isEmpty) {
throw SignbeeApiException('Missing Signbee API Key. Authenticate before making requests.');
}
return {
HttpHeaders.contentTypeHeader: 'application/json',
HttpHeaders.authorizationHeader: 'Bearer $apiKey',
HttpHeaders.acceptHeader: 'application/json',
'User-Agent': 'Signbee-Flutter-SDK/2026.1',
};
}
/// Dispatches a new contract formatted in Markdown to Signbee
Future<SignbeeSendResponse> sendContract(SignbeeSendRequest request) async {
final url = Uri.parse('$_baseUrl/send');
final headers = await _getHeaders();
try {
final response = await _client
.post(
url,
headers: headers,
body: jsonEncode(request.toJson()),
)
.timeout(const Duration(seconds: 25));
final responseBody = jsonDecode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return SignbeeSendResponse.fromJson(responseBody);
} else {
final errorMessage = responseBody['error']?['message'] ??
responseBody['message'] ??
'HTTP ${response.statusCode} Request Failed';
throw SignbeeApiException(errorMessage, statusCode: response.statusCode, details: responseBody);
}
} on SocketException {
throw SignbeeApiException('No internet connection. Please verify your mobile network.');
} on http.ClientException catch (e) {
throw SignbeeApiException('HTTP transport error: ${e.message}');
}
}
/// Polls or fetches the current status of a document
Future<SignbeeDocumentStatus> getDocumentStatus(String documentId) async {
final url = Uri.parse('$_baseUrl/documents/$documentId');
final headers = await _getHeaders();
final response = await _client.get(url, headers: headers).timeout(const Duration(seconds: 15));
final responseBody = jsonDecode(response.body);
if (response.statusCode == 200) {
return SignbeeDocumentStatus.fromJson(responseBody);
} else {
throw SignbeeApiException(
responseBody['message'] ?? 'Failed to retrieve document status',
statusCode: response.statusCode,
);
}
}
void dispose() {
_client.close();
}
}Section 4: Custom Smooth Signature Drawing Canvas (CustomPainter)
Many naive mobile signature implementations simply draw straight lines between consecutive touch coordinates. This produces jagged, robotic lines that look unprofessional on high-density iPhone and OLED Android displays.
To deliver an authentic, fountain-pen feel, we use quadratic bezier curve interpolation. When the user moves their finger or Apple Pencil / stylus across the screen, the painter calculates the midpoint between consecutive touch points and computes a smooth curve through the control point.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class SignaturePoint {
final Offset offset;
final double pressure;
SignaturePoint(this.offset, {this.pressure = 1.0});
}
class SignatureStroke {
final List<SignaturePoint> points;
final Color color;
final double strokeWidth;
SignatureStroke({
required this.points,
this.color = Colors.black,
this.strokeWidth = 3.0,
});
}
class SignaturePadWidget extends StatefulWidget {
final Color strokeColor;
final double strokeWidth;
final Color backgroundColor;
final Function(bool hasContent)? onStrokeChanged;
const SignaturePadWidget({
super.key,
this.strokeColor = const Color(0xFF1E293B),
this.strokeWidth = 3.0,
this.backgroundColor = Colors.white,
this.onStrokeChanged,
});
@override
State<SignaturePadWidget> createState() => SignaturePadWidgetState();
}
class SignaturePadWidgetState extends State<SignaturePadWidget> {
final List<SignatureStroke> _strokes = [];
SignatureStroke? _currentStroke;
bool get isEmpty => _strokes.isEmpty && (_currentStroke == null || _currentStroke!.points.isEmpty);
void clear() {
setState(() {
_strokes.clear();
_currentStroke = null;
});
widget.onStrokeChanged?.call(false);
}
void undo() {
if (_strokes.isNotEmpty) {
setState(() {
_strokes.removeLast();
});
widget.onStrokeChanged?.call(!isEmpty);
}
}
Future<ui.Image?> exportImage({double pixelRatio = 3.0}) async {
if (isEmpty) return null;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final size = context.size ?? const Size(400, 200);
final painter = _SignatureCustomPainter(
strokes: _strokes,
currentStroke: _currentStroke,
backgroundColor: widget.backgroundColor,
);
painter.paint(canvas, size);
final picture = recorder.endRecording();
return await picture.toImage((size.width * pixelRatio).toInt(), (size.height * pixelRatio).toInt());
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
clipBehavior: Clip.antiAlias,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: (details) {
setState(() {
_currentStroke = SignatureStroke(
points: [SignaturePoint(details.localPosition)],
color: widget.strokeColor,
strokeWidth: widget.strokeWidth,
);
});
},
onPanUpdate: (details) {
setState(() {
_currentStroke?.points.add(SignaturePoint(details.localPosition));
});
widget.onStrokeChanged?.call(true);
},
onPanEnd: (details) {
if (_currentStroke != null && _currentStroke!.points.isNotEmpty) {
setState(() {
_strokes.add(_currentStroke!);
_currentStroke = null;
});
widget.onStrokeChanged?.call(true);
}
},
child: CustomPaint(
size: Size.infinite,
painter: _SignatureCustomPainter(
strokes: _strokes,
currentStroke: _currentStroke,
backgroundColor: widget.backgroundColor,
),
),
),
);
}
}
class _SignatureCustomPainter extends CustomPainter {
final List<SignatureStroke> strokes;
final SignatureStroke? currentStroke;
final Color backgroundColor;
_SignatureCustomPainter({
required this.strokes,
this.currentStroke,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
// 1. Draw signature baseline guides
final guidePaint = Paint()
..color = Colors.black.withOpacity(0.08)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final baselineY = size.height * 0.75;
canvas.drawLine(Offset(24, baselineY), Offset(size.width - 24, baselineY), guidePaint);
// 2. Render all committed strokes with midpoint quadratic bezier interpolation
for (final stroke in strokes) {
_paintStroke(canvas, stroke);
}
// 3. Render active stroke in real time
if (currentStroke != null) {
_paintStroke(canvas, currentStroke!);
}
}
void _paintStroke(Canvas canvas, SignatureStroke stroke) {
final points = stroke.points;
if (points.isEmpty) return;
final paint = Paint()
..color = stroke.color
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..strokeWidth = stroke.strokeWidth
..style = PaintingStyle.stroke;
if (points.length == 1) {
canvas.drawCircle(points.first.offset, stroke.strokeWidth / 2, paint..style = PaintingStyle.fill);
return;
}
final path = Path();
path.moveTo(points[0].offset.dx, points[0].offset.dy);
for (int i = 1; i < points.length - 1; i++) {
final p0 = points[i].offset;
final p1 = points[i + 1].offset;
// Calculate midpoint for smooth bezier control point
final midPoint = Offset((p0.dx + p1.dx) / 2, (p0.dy + p1.dy) / 2);
path.quadraticBezierTo(p0.dx, p0.dy, midPoint.dx, midPoint.dy);
}
// Connect final segment
path.lineTo(points.last.offset.dx, points.last.offset.dy);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _SignatureCustomPainter oldDelegate) => true;
}If you are also building a cross-platform React Native variant of this signature canvas, see our companion tutorial on React Native Mobile E-Signature Canvas.
Section 5: In-App Signing Ceremony with webview_flutter & Deep Links
When a contract is dispatched to Signbee, the API returns a responsive signing_url. Depending on your UX requirements, you can open this ceremony inside your Flutter app using an embedded WebView or redirect the user to their default system browser via url_launcher.
Method A: Embedded In-App Signing Screen with WebView
Using webview_flutter provides a seamless experience without throwing users out of your application. You can monitor navigation requests to intercept deep link redirects when the document signing ceremony finishes.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class InAppSigningScreen extends StatefulWidget {
final String signingUrl;
final String documentTitle;
final VoidCallback onCompleted;
const InAppSigningScreen({
super.key,
required this.signingUrl,
required this.documentTitle,
required this.onCompleted,
});
@override
State<InAppSigningScreen> createState() => _InAppSigningScreenState();
}
class _InAppSigningScreenState extends State<InAppSigningScreen> {
late final WebViewController _controller;
bool _isLoading = true;
double _loadingProgress = 0.0;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0xFF09090B))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
setState(() {
_loadingProgress = progress / 100.0;
});
},
onPageStarted: (url) {
setState(() => _isLoading = true);
},
onPageFinished: (url) {
setState(() => _isLoading = false);
},
onNavigationRequest: (NavigationRequest request) {
// Intercept custom return callback scheme
if (request.url.startsWith('signbee://') || request.url.contains('/completed')) {
widget.onCompleted();
Navigator.of(context).pop(true);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.signingUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF09090B),
appBar: AppBar(
backgroundColor: const Color(0xFF18181B),
elevation: 0,
title: Text(
widget.documentTitle,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white70),
onPressed: () => Navigator.of(context).pop(false),
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
LinearProgressIndicator(
value: _loadingProgress > 0 ? _loadingProgress : null,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFFF59E0B)),
),
],
),
);
}
}Method B: External Browser Redirection via url_launcher
If you prefer launching the native Safari View Controller on iOS or Chrome Custom Tabs on Android, use url_launcher with LaunchMode.inAppBrowserView:
import 'package:url_launcher/url_launcher.dart';
Future<void> launchSigningCeremony(String signingUrl) async {
final uri = Uri.parse(signingUrl);
if (!await launchUrl(
uri,
mode: LaunchMode.inAppBrowserView,
browserConfiguration: const BrowserConfiguration(showTitle: true),
)) {
throw Exception('Could not launch signing URL: $signingUrl');
}
}Section 6: Complete End-to-End Flutter Contract Dispatch Screen
Now we assemble all components into a cohesive, production-ready screen where an operations manager or sales representative can generate a dynamic contract, draw an authorization signature, submit it to Signbee, and monitor real-time completion.
import 'package:flutter/material.dart';
import '../models/signbee_models.dart';
import '../services/signbee_api_service.dart';
import '../widgets/signature_pad_widget.dart';
import 'in_app_signing_screen.dart';
class ContractDispatchScreen extends StatefulWidget {
const ContractDispatchScreen({super.key});
@override
State<ContractDispatchScreen> createState() => _ContractDispatchScreenState();
}
class _ContractDispatchScreenState extends State<ContractDispatchScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController(text: 'Alex Rivers');
final _emailController = TextEditingController(text: 'alex@example.com');
final _projectController = TextEditingController(text: 'Mobile App Refactor');
final _amountController = TextEditingController(text: '$8,500.00');
final GlobalKey<SignaturePadWidgetState> _signatureKey = GlobalKey<SignaturePadWidgetState>();
final SignbeeApiService _apiService = SignbeeApiService();
bool _isSubmitting = false;
String? _statusMessage;
SignbeeDocumentStatus? _activeStatus;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_projectController.dispose();
_amountController.dispose();
_apiService.dispose();
super.dispose();
}
String _buildContractMarkdown() {
return """
# Master Services & Consulting Agreement
**Effective Date:** ${DateTime.now().toIso8601String().substring(0, 10)}
**Client:** ${_nameController.text.trim()} (${_emailController.text.trim()})
**Project Scope:** ${_projectController.text.trim()}
**Fixed Engagement Fee:** ${_amountController.text.trim()} USD
---
### Terms & Deliverables
1. **Scope of Work:** The Consultant agrees to perform mobile architectural design and engineering services as outlined in the Statement of Work.
2. **Payment Schedule:** 50% upon execution, 50% upon final acceptance testing.
3. **Intellectual Property:** All custom code, assets, and documentation transfer to the Client upon receipt of full payment.
4. **Governing Law:** This agreement shall be governed by the laws of the State of Delaware.
---
*Sign below to execute this legally binding agreement.*
""";
}
Future<void> _handleSendContract() async {
if (!_formKey.currentState!.validate()) return;
if (_signatureKey.currentState?.isEmpty ?? true) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please sign in the signature pad before submitting.')),
);
return;
}
setState(() {
_isSubmitting = true;
_statusMessage = 'Compiling contract and dispatching to Signbee...';
});
try {
final request = SignbeeSendRequest(
title: 'Consulting Agreement - ${_projectController.text.trim()}',
recipientName: _nameController.text.trim(),
recipientEmail: _emailController.text.trim(),
markdown: _buildContractMarkdown(),
);
final response = await _apiService.sendContract(request);
setState(() {
_statusMessage = 'Contract created! Document ID: ${response.documentId}';
});
if (!mounted) return;
// Open in-app signing ceremony
final result = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (context) => InAppSigningScreen(
signingUrl: response.signingUrl,
documentTitle: 'Sign Agreement: ${request.title}',
onCompleted: () => _fetchStatus(response.documentId),
),
),
);
if (result == true) {
_fetchStatus(response.documentId);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(backgroundColor: Colors.red.shade900, content: Text('Error: ${e.toString()}')),
);
} finally {
setState(() => _isSubmitting = false);
}
}
Future<void> _fetchStatus(String documentId) async {
try {
final status = await _apiService.getDocumentStatus(documentId);
setState(() {
_activeStatus = status;
});
} catch (_) {}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF09090B),
appBar: AppBar(
backgroundColor: const Color(0xFF18181B),
title: const Text('New E-Signature Contract', style: TextStyle(color: Colors.white)),
),
body: SafeArea(
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(20),
children: [
const Text('Recipient Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(height: 12),
TextFormField(
controller: _nameController,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Recipient Full Name'),
validator: (v) => v == null || v.isEmpty ? 'Required' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Recipient Email Address'),
validator: (v) => v == null || !v.contains('@') ? 'Enter a valid email' : null,
),
const SizedBox(height: 24),
const Text('Agreement Terms', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(height: 12),
TextFormField(
controller: _projectController,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Project Scope / Title'),
),
const SizedBox(height: 12),
TextFormField(
controller: _amountController,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Engagement Total ($ USD)'),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Authorization Signature', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
TextButton.icon(
onPressed: () => _signatureKey.currentState?.clear(),
icon: const Icon(Icons.refresh, size: 16, color: Color(0xFFF59E0B)),
label: const Text('Clear', style: TextStyle(color: Color(0xFFF59E0B))),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 180,
child: SignaturePadWidget(key: _signatureKey),
),
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF59E0B),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: _isSubmitting ? null : _handleSendContract,
child: _isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black),
)
: const Text('Generate & Dispatch Agreement', style: TextStyle(fontWeight: FontWeight.bold)),
),
if (_statusMessage != null) ...[
const SizedBox(height: 16),
Text(_statusMessage!, style: const TextStyle(color: Colors.white60, fontSize: 13), textAlign: TextAlign.center),
],
if (_activeStatus != null) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.04),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withOpacity(0.08)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Status: ${_activeStatus!.status.toUpperCase()}', style: const TextStyle(color: Color(0xFF10B981), fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('Recipient: ${_activeStatus!.recipientEmail}', style: const TextStyle(color: Colors.white70)),
if (_activeStatus!.pdfUrl != null) ...[
const SizedBox(height: 8),
Text('PDF Download: ${_activeStatus!.pdfUrl}', style: const TextStyle(color: Color(0xFFF59E0B), fontSize: 12)),
]
],
),
),
],
],
),
),
),
);
}
InputDecoration _inputDecoration(String label) {
return InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.white50),
filled: true,
fillColor: Colors.white.withOpacity(0.03),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFF59E0B))),
);
}
}Section 7: Mobile Security & Performance Best Practices
When deploying electronic signature capture in high-security production environments (such as healthcare, banking, and legal tech), observe the following architectural rules:
1. Prevent Gesture Arena Collisions in Scroll Views
If your signature widget is nested inside a ListView or SingleChildScrollView, vertical touch motions can be claimed by the scroll view's drag recognizer. Always wrap your CustomPaint widget in a dedicated GestureDetector with HitTestBehavior.opaque or disable parent scroll physics during active strokes to guarantee smooth bezier curve rendering.
2. Screen Privacy & Screenshot Protection
Financial and legal agreements often contain sensitive Personally Identifiable Information (PII). On Android, protect screens during signing using FLAG_SECURE via platform channels or plugins to prevent background app preview caching and unauthorized screenshots. On iOS, blank the screen snapshot in applicationDidEnterBackground.
3. Direct REST API vs Legacy Heavy SDKs
Below is an architectural comparison between integrating Signbee's REST API versus legacy e-signature SDKs in Flutter apps:
| Metric / Feature | Legacy Vendor Mobile SDKs | Signbee Direct REST API |
|---|---|---|
| Flutter Binary Overhead | +25MB to +45MB (Native C++/Java/Obj-C) | < 150KB (Pure Dart HTTP) |
| Cross-Platform Support | Often iOS / Android only (Breaks Flutter Web) | 100% Cross-Platform (iOS, Android, Web, Desktop) |
| Document Definition | Complex XML/JSON coordinate positioning | Dynamic Markdown text rendering |
| Authentication Complexity | OAuth 2.0 JWT assertion & RSA Keypairs | Standard Bearer API Key |
| Developer Integration Time | 2 to 3 weeks of configuration | Under 30 minutes |
Frequently Asked Questions
How do I handle signature drawing gestures inside a scrollable Flutter screen without gesture conflicts?
When embedding a signature drawing canvas inside a scrollable Flutter layout (such as a SingleChildScrollView or ListView), touch events frequently trigger vertical screen scrolling rather than smooth pen strokes. To resolve this gesture collision, wrap your CustomPaint signature widget inside a RawGestureDetector configured with an EagerGestureRecognizer, or wrap the parent scroll view in a physics controller that disables scrolling during active pan gestures. Alternatively, set PanGestureRecognizer callbacks directly on a dedicated drawing container and call gestureArena.hold() or manage a boolean isDrawing state that sets the parent ScrollView physics to const NeverScrollableScrollPhysics() on onPanStart and restores it on onPanEnd. This guarantees that your quadratic bezier curve calculation retains exclusive capture of finger or stylus movement across iOS and Android.
Should I embed the signing ceremony in a Flutter WebView or launch the external system browser?
For most mobile workflows, embedding the ceremony using webview_flutter provides the cleanest user experience because users remain inside your branded application without context switching. With webview_flutter, you can intercept URL navigation delegates to detect completion redirects (such as custom app scheme callbacks like signbee://signed?document_id=123) or inject JavaScript channels to capture signing completion events in real time. However, if your application runs on Flutter Web or requires strict third-party biometric passkey authentication managed by Safari / Chrome system cookies, launching the external browser using url_launcher with LaunchMode.inAppBrowserView or LaunchMode.externalApplication provides seamless operating system integration and automatic deep linking back into your Flutter application upon completion.
How are e-signatures captured in a Flutter mobile app made legally binding under ESIGN and eIDAS?
Capturing a raw touch path on a mobile canvas is only the visual component of an electronic signature; legal enforceability under the US ESIGN Act, UETA, and EU eIDAS regulations requires an immutable, verifiable audit trail and document tamper evidence. When you integrate your Flutter app with the Signbee API, Signbee compiles the agreement Markdown and captured signature metadata into a standardized PDF with cryptographic SHA-256 document hashing. Signbee timestamps the signing action (compliant with RFC 3161), logs the signer's IP address, device user-agent, email verification status, and exact UTC timestamp, and attaches a tamper-evident Certificate of Completion. If the document is modified after signing, cryptographic hash verification will immediately flag the alteration, ensuring full legal admissibility in court.
Related Tutorials & Cross-Platform Guides
Building unified multi-client architectures or backend dispatch microservices? Explore our in-depth developer guides: