personal_validation_system 1.0.0
personal_validation_system: ^1.0.0 copied to clipboard
A Flutter plugin for identity verification and validation. Provides a complete solution for validating personal identification documents, performing face matching, and verifying personal information.
example/lib/main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:personal_validation_system/personal_validation_system.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dotted_border/dotted_border.dart';
import 'verification_results_page.dart';
import 'models/validation_data.dart';
import 'services/validation_api_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Identity Verification',
theme: CupertinoThemeData(
primaryColor: CupertinoColors.activeBlue,
brightness: Brightness.light,
textTheme: CupertinoTextThemeData(
textStyle: GoogleFonts.poppins(
color: CupertinoColors.black,
fontSize: 16,
),
),
scaffoldBackgroundColor: CupertinoColors.white,
),
home: const IdentityVerificationScreen(),
);
}
}
class IdentityVerificationScreen extends StatefulWidget {
const IdentityVerificationScreen({super.key});
@override
State<IdentityVerificationScreen> createState() =>
_IdentityVerificationScreenState();
}
class _IdentityVerificationScreenState extends State<IdentityVerificationScreen>
implements GestureArenaMember {
final _personalValidationSystemPlugin = PersonalValidationSystem();
final _fullNameController = TextEditingController();
final _phoneNumberController = TextEditingController();
final _idNumberController = TextEditingController();
final _imagePicker = ImagePicker();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
File? _selfieImage;
File? _frontIDImage;
File? _backIDImage;
@override
void initState() {
super.initState();
// Initialize the plugin
_initPlugin();
}
Future<void> _initPlugin() async {
try {
await _personalValidationSystemPlugin.getPlatformVersion();
} on PlatformException {
// Handle initialization error
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Set a higher touch slop for better performance on MIUI devices
GestureBinding.instance.pointerRouter.addGlobalRoute((PointerEvent event) {
if (event is PointerDownEvent) {
GestureBinding.instance.gestureArena.add(event.pointer, this);
}
});
}
@override
void dispose() {
_fullNameController.dispose();
_phoneNumberController.dispose();
_idNumberController.dispose();
if (_selfieImage != null && _selfieImage!.existsSync())
_selfieImage!.deleteSync();
if (_frontIDImage != null && _frontIDImage!.existsSync())
_frontIDImage!.deleteSync();
if (_backIDImage != null && _backIDImage!.existsSync())
_backIDImage!.deleteSync();
GestureBinding.instance.pointerRouter.removeGlobalRoute(
(PointerEvent event) {},
);
super.dispose();
}
Future<void> _takeSelfie() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.front,
imageQuality: 80,
);
if (image != null) {
setState(() {
_selfieImage = File(image.path);
});
}
} catch (e) {
print('Error taking selfie: $e');
}
}
Future<void> _takeFrontID() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.rear,
imageQuality: 80,
);
if (image != null) {
setState(() {
_frontIDImage = File(image.path);
});
}
} catch (e) {
print('Error taking front ID photo: $e');
}
}
Future<void> _takeBackID() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.rear,
imageQuality: 80,
);
if (image != null) {
setState(() {
_backIDImage = File(image.path);
});
}
} catch (e) {
print('Error taking back ID photo: $e');
}
}
Future<void> _submitVerification() async {
// if (_formKey.currentState == null || !_formKey.currentState!.validate()) {
// return;
// }
// Show loading dialog and store reference
final loadingDialog = showCupertinoDialog(
context: context,
barrierDismissible: false,
builder:
(context) => CupertinoAlertDialog(
title: Text(
'Processing',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16),
CupertinoActivityIndicator(),
SizedBox(height: 16),
Text('Submitting your verification data...'),
],
),
),
),
);
try {
// Create validation data object
final validationData = ValidationData(
fullName:
_fullNameController.text.isEmpty
? 'Georg Mathu'
: _fullNameController.text,
phoneNumber:
_phoneNumberController.text.isEmpty
? '+254798325826'
: _phoneNumberController.text,
idNumber:
_idNumberController.text.isEmpty
? '35788580'
: _idNumberController.text,
);
// Call the API service in a separate isolate if possible
final response = await ValidationApiService().submitValidation(
data: validationData,
selfieImage: _selfieImage,
frontIDImage: _frontIDImage,
backIDImage: _backIDImage,
);
// Dismiss loading dialog
Navigator.of(context).pop();
await loadingDialog;
if (response['status'] == 'success') {
// Handle success
if (mounted) {
// Navigate to results page with the API response
if (!mounted) return;
Navigator.push(
context,
CupertinoPageRoute(
builder:
(context) => VerificationResultsPage(
name: response['data']['name'] ?? validationData.fullName,
idNumber:
response['data']['id_number'] ??
validationData.idNumber,
phoneNumber:
response['data']['phone_number'] ??
validationData.phoneNumber,
verificationTime: DateTime.now(),
apiResponse: {
'data': {
'is_verified': response['is_verified'] ?? false,
'id_validation': {
'valid':
response['validation_details']?['id_validation']?['is_valid'] ??
false,
'message':
response['validation_details']?['id_validation']?['message'] ??
'ID validation not performed',
},
'ocr_extraction': {
'valid':
response['validation_details']?['ocr_extraction']?['name_found'] ??
false &&
response['validation_details']?['ocr_extraction']?['id_number_found'] ??
false,
'message': 'Name and ID extracted',
},
'face_detection': {
'valid':
response['validation_details']?['face_detection']?['faces_match'] ??
false,
'message':
response['validation_details']?['face_detection']?['faces_match'] ==
true
? 'Faces match'
: 'Faces do not match',
},
'phone_validation': {
'valid':
response['validation_details']?['phone_validation']?['is_valid'] ??
false,
'message':
response['validation_details']?['phone_validation']?['message'] ??
'Phone validation not performed',
},
},
},
),
),
);
}
} else {
// Handle error
if (mounted) {
showCupertinoDialog(
context: context,
builder:
(BuildContext context) => CupertinoAlertDialog(
title: const Text('Error'),
content: Text(response['message'] ?? 'Verification failed'),
actions: [
CupertinoDialogAction(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
}
} catch (e) {
// Ensure dialog is dismissed even on error
Navigator.of(context).pop();
await loadingDialog;
print('Error submitting verification: $e');
} finally {
// Clear image references to free memory
if (!mounted) return;
setState(() {
_selfieImage = null;
_frontIDImage = null;
_backIDImage = null;
});
}
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(
'Identity Verification',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
leading: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const Icon(CupertinoIcons.back),
),
),
child: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Please provide your information',
style: GoogleFonts.poppins(
fontSize: 16,
color: CupertinoColors.systemGrey,
),
),
const SizedBox(height: 12),
// Progress indicator
Stack(
children: [
Container(
height: 4,
decoration: BoxDecoration(
color: CupertinoColors.systemGrey5,
borderRadius: BorderRadius.circular(2),
),
),
Container(
height: 4,
width: MediaQuery.of(context).size.width * 0.7,
decoration: BoxDecoration(
color: CupertinoColors.activeBlue,
borderRadius: BorderRadius.circular(2),
),
),
],
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: Text(
'Step 1 of 1',
style: GoogleFonts.poppins(
fontSize: 14,
color: CupertinoColors.systemGrey,
),
),
),
const SizedBox(height: 24),
// Full Name Field
Text(
'Full Name',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
CupertinoTextField(
controller: _fullNameController,
placeholder: 'Enter your full name',
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xffF1F1F1)),
borderRadius: BorderRadius.circular(8),
),
style: GoogleFonts.poppins(),
),
const SizedBox(height: 20),
// Phone Number Field
Text(
'Phone Number',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
CupertinoTextField(
controller: _phoneNumberController,
placeholder: '+1 (XXX) XXX-XXXX',
keyboardType: TextInputType.phone,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xffF1F1F1)),
borderRadius: BorderRadius.circular(8),
),
style: GoogleFonts.poppins(),
),
const SizedBox(height: 20),
// ID Number Field
Text(
'ID Number',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
CupertinoTextField(
controller: _idNumberController,
placeholder: 'Enter your ID number',
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xffF1F1F1)),
borderRadius: BorderRadius.circular(8),
),
style: GoogleFonts.poppins(),
),
const SizedBox(height: 20),
// Take a Selfie
Text(
'Take a Selfie',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Center(
child: GestureDetector(
onTap: _takeSelfie,
child:
_selfieImage != null
? Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: FileImage(_selfieImage!),
fit: BoxFit.cover,
),
),
)
: DottedBorder(
borderType: BorderType.Circle,
color: CupertinoColors.activeBlue,
strokeWidth: 1.5,
dashPattern: const [6, 3],
child: Container(
width: 90,
height: 90,
decoration: const BoxDecoration(
color: CupertinoColors.systemGrey6,
shape: BoxShape.circle,
),
child: const Icon(
CupertinoIcons.camera,
color: CupertinoColors.activeBlue,
size: 32,
),
),
),
),
),
const SizedBox(height: 8),
Center(
child: Text(
'Face should be clear and well-lit',
style: GoogleFonts.poppins(
fontSize: 14,
color: CupertinoColors.systemGrey,
),
),
),
const SizedBox(height: 20),
// ID Document Images
Text(
'ID Document Images',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: _takeFrontID,
child:
_frontIDImage != null
? Container(
height: 120,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: FileImage(_frontIDImage!),
fit: BoxFit.cover,
),
),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
color: CupertinoColors.black.withOpacity(
0.5,
),
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
'Front of ID',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
color: CupertinoColors.white,
fontSize: 12,
),
),
),
),
)
: DottedBorder(
borderType: BorderType.RRect,
radius: const Radius.circular(12),
color: CupertinoColors.activeBlue,
strokeWidth: 1.5,
dashPattern: const [6, 3],
child: Container(
height: 120,
width: double.infinity,
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.creditcard,
color: CupertinoColors.activeBlue,
size: 32,
),
const SizedBox(height: 8),
Text(
'Front of ID',
style: GoogleFonts.poppins(
color: CupertinoColors.activeBlue,
),
),
],
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: _takeBackID,
child:
_backIDImage != null
? Container(
height: 120,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: FileImage(_backIDImage!),
fit: BoxFit.cover,
),
),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
color: CupertinoColors.black.withOpacity(
0.5,
),
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
'Back of ID',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
color: CupertinoColors.white,
fontSize: 12,
),
),
),
),
)
: DottedBorder(
borderType: BorderType.RRect,
radius: const Radius.circular(12),
color: CupertinoColors.activeBlue,
strokeWidth: 1.5,
dashPattern: const [6, 3],
child: Container(
height: 120,
width: double.infinity,
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.creditcard,
color: CupertinoColors.activeBlue,
size: 32,
),
const SizedBox(height: 8),
Text(
'Back of ID',
style: GoogleFonts.poppins(
color: CupertinoColors.activeBlue,
),
),
],
),
),
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Clear, unobstructed photos required',
style: GoogleFonts.poppins(
fontSize: 12,
color: CupertinoColors.systemGrey,
),
),
const SizedBox(height: 32),
// Submit Button
SizedBox(
width: double.infinity,
height: 50,
child: CupertinoButton(
onPressed: _submitVerification,
color: CupertinoColors.activeBlue,
borderRadius: BorderRadius.circular(8),
padding: EdgeInsets.zero,
child: Text(
'Submit Verification',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: CupertinoColors.white,
),
),
),
),
const SizedBox(height: 20),
],
),
),
),
),
);
}
@override
void acceptGesture(int pointer) {}
@override
void rejectGesture(int pointer) {}
}