innova_id_verify 0.1.0
innova_id_verify: ^0.1.0 copied to clipboard
A Flutter plugin for ID verification
example/lib/main.dart
import 'dart:convert';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:innova_id_verify/innova_id_verify.dart';
import 'package:http/http.dart' as http;
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]).then((_) {
runApp(const MyApp());
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomeScreen());
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFFD1F1FF),
Colors.white,
],
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
toolbarHeight: 100,
backgroundColor: Colors.transparent,
// centerTitle: true,
actions: [
SizedBox(height: 50, child: Image.asset('lib/assets/logo.png')),
SizedBox(
width: 15,
)
],
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 15,
),
const Text(
'InnoTrust',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 32),
),
const Text(
'Seamless eKYC: Verify Instantly, Securely!',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 14),
),
],
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CarouselSlider(
items: [
_buildCarouselItem('lib/assets/liveliness.jpg'),
_buildCarouselItem('lib/assets/ocr.png'),
_buildCarouselItem('lib/assets/face-matching.png'),
],
options: CarouselOptions(
height: 200,
aspectRatio: 9 / 16,
viewportFraction: 0.85,
initialPage: 0,
enableInfiniteScroll: true,
reverse: false,
autoPlay: true,
autoPlayInterval: Duration(seconds: 3),
autoPlayAnimationDuration: Duration(seconds: 2),
autoPlayCurve: Curves.fastOutSlowIn,
enlargeCenterPage: true,
enlargeFactor: 0.2,
scrollDirection: Axis.horizontal,
)),
],
)
],
),
bottomNavigationBar: BottomAppBar(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15), // Shadow color
offset: Offset(0, 4), // Shadow position (x, y)
blurRadius: 3, // Spread of the shadow
spreadRadius: 0, // How much the shadow spreads (optional)
),
],
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF221BC7),
const Color(0xFF0800C9),
],
),
),
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
),
onPressed: () {
try {
Navigator.of(context, rootNavigator: true)
.pushReplacement(MaterialPageRoute(
builder: (context) => const ImageDisplayScreen(),
));
} catch (e) {
print("Error navigating to ImageDisplayScreen: $e");
}
},
child: Text('Launch eKYC',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18)),
),
),
),
),
);
}
Widget _buildCarouselItem(String imagePath) {
return ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image.asset(
imagePath,
fit: BoxFit.fitWidth,
height: double.infinity,
width: double.infinity,
),
);
}
}
class ImageDisplayScreen extends StatefulWidget {
const ImageDisplayScreen({super.key});
@override
State<ImageDisplayScreen> createState() => _ImageDisplayScreenState();
}
class _ImageDisplayScreenState extends State<ImageDisplayScreen> {
Uint8List? _capturedImage;
Uint8List? _frontImage;
Uint8List? _backImage;
String? _fullName;
String? _fcn;
String? _dob;
String? _gender;
String? _nationality;
String? _dateOfExpiry;
String? _phoneNumber;
String? _region;
String? _zone;
String? _woreda;
String? _fin;
String _status = "OCR- FAILED , FACE MATCHING- FAILED";
final _innovaIDVerify = InnovaIDVerify();
int? _mathing_score;
bool _isLoading = false;
String? _error;
String? _currentReferenceNumber;
bool _isInitialLaunch = true;
@override
void initState() {
super.initState();
if (_isInitialLaunch) {
_isInitialLaunch = false;
launchKyc();
}
}
Future<String> generateReferenceNumber({String prefix = 'INNOVAVERIFY'}) async {
final dateTimeNow = DateTime.now();
final dateTimeString =
'${dateTimeNow.year}${dateTimeNow.month.toString().padLeft(2, '0')}${dateTimeNow.day.toString().padLeft(2, '0')}'
'${dateTimeNow.hour.toString().padLeft(2, '0')}${dateTimeNow.minute.toString().padLeft(2, '0')}${dateTimeNow.second.toString().padLeft(2, '0')}${dateTimeNow.millisecond.toString().padLeft(3, '0')}';
return '$prefix$dateTimeString';
}
Future<void> launchKyc() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
_currentReferenceNumber = await generateReferenceNumber();
print("\n=== Starting KYC Process ===");
print("Generated Reference Number: $_currentReferenceNumber");
await _innovaIDVerify.processIDVerification(
referenceNum: _currentReferenceNumber!,
onVerificationComplete: (verificationData) async {
print("Verification complete, starting API polling...");
await startApiPolling(_currentReferenceNumber!);
},
onActivityClosed: () {
if (mounted) {
setState(() {
_isLoading = false;
_error = "KYC process was cancelled";
});
}
},
);
} catch (e, stackTrace) {
print("Error in launchKyc: $e");
print("Stack trace:\n$stackTrace");
if (mounted) {
setState(() {
_error = "Error during KYC process: ${e.toString()}";
_isLoading = false;
});
}
}
}
Future<void> startApiPolling(String referenceNum) async {
int maxAttempts = 10;
int currentAttempt = 0;
const Duration pollInterval = Duration(seconds: 3);
bool isDataProcessed = false;
while (currentAttempt < maxAttempts && !isDataProcessed) {
try {
print("Polling attempt ${currentAttempt + 1} of $maxAttempts");
final response = await makeApiRequest(referenceNum);
if (response['success'] == true && response['data'] != null) {
await processVerificationData(referenceNum);
isDataProcessed = true;
print("Data successfully processed on attempt ${currentAttempt + 1}");
break;
} else {
print("Polling attempt ${currentAttempt + 1}: Data not ready yet");
print("Response: $response");
}
} catch (e) {
print("Error during polling attempt ${currentAttempt + 1}: $e");
}
if (!isDataProcessed && currentAttempt < maxAttempts - 1) {
print("Waiting ${pollInterval.inSeconds} seconds before next attempt...");
await Future.delayed(pollInterval);
}
currentAttempt++;
}
if (mounted) {
setState(() {
_isLoading = false;
if (!isDataProcessed) {
_error = "Failed to retrieve data after $maxAttempts attempts";
}
});
}
}
Future<void> processVerificationData(String referenceNum) async {
try {
print("Processing verification data for reference: $referenceNum");
final response = await makeApiRequest(referenceNum);
print("API Response received, processing data...");
if (response['success'] == true && response['data'] != null) {
final data = response['data'];
// Extract OCR data
final frontOcrData = data['Front_OCR_Data']?['id_analysis'];
final frontDetails = frontOcrData?['front'] ?? {};
final backOcrData = data['Back_OCR_Data']?['id_analysis'];
final backDetails = backOcrData?['back'] ?? {};
if (mounted) {
setState(() {
// Process images
if (data['Front_ID_Image'] != null) {
try {
_frontImage = base64Decode(data['Front_ID_Image']);
} catch (e) {
print("Error decoding front image: $e");
_frontImage = null;
}
}
if (data['Back_ID_Image'] != null) {
try {
_backImage = base64Decode(data['Back_ID_Image']);
} catch (e) {
print("Error decoding back image: $e");
_backImage = null;
}
}
if (data['Selfie_Image'] != null) {
try {
_capturedImage = base64Decode(data['Selfie_Image']);
} catch (e) {
print("Error decoding selfie image: $e");
_capturedImage = null;
}
}
// Process front ID details
_fullName = frontDetails['Full_name'] ?? 'N/A';
_fcn = frontDetails['FCN'] ?? 'N/A';
_dob = frontDetails['Date_of_birth'] ?? 'N/A';
_gender = frontDetails['Sex'] ?? 'N/A';
_nationality = frontDetails['Nationality'] ?? 'N/A';
// Process back ID details
_dateOfExpiry = backDetails['Date_of_Expiry'] ?? 'N/A';
_phoneNumber = backDetails['Phone_Number'] ?? 'N/A';
_region = backDetails['Region_City_Admin'] ?? 'N/A';
_zone = backDetails['Zone_City_Admin_Sub_City'] ?? 'N/A';
_woreda = backDetails['Woreda_City_Admin_Kebele'] ?? 'N/A';
_fin = backDetails['FIN'] ?? 'N/A';
// Process additional data
_mathing_score = data['Score'] ?? 0;
_status = data['STATUS'] ?? 'Unknown Status';
});
}
print("Verification data processing completed successfully");
} else {
throw Exception("Invalid response data structure");
}
} catch (e) {
print("Error processing verification data: $e");
if (mounted) {
setState(() {
_error = "Error processing verification data: ${e.toString()}";
});
}
rethrow;
}
}
Future<Map<String, dynamic>> makeApiRequest(String referenceNum) async {
final url = Uri.parse(
'https://innotrust.innovitegra.online/api/enroll/$referenceNum');
final headers = {'Content-Type': 'application/json'};
try {
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to load data: ${response.statusCode}');
}
} catch (e) {
throw Exception('Failed to make API request: $e');
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFFD1F1FF),
Colors.white,
],
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Color(0xFF221BC7)),
),
SizedBox(height: 20),
Text(
'Processing KYC...',
style: TextStyle(
color: Color(0xFF221BC7),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
)
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 100,
height: 100,
child: Image.asset("lib/assets/fail.png")),
SizedBox(height: 20,),
Text('$_error',style: TextStyle(color: Colors.red,fontSize: 18,fontWeight: FontWeight.bold),),
],
),
)
: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 30, horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 40,
),
Text(
'Status',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 10,
),
Text(
_status,
style: TextStyle(
fontSize: 18,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 10,
),
Divider(
color: Colors.white,
),
SizedBox(
height: 15,
),
Text(
'Name:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_fullName ?? "N/A",
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'FCN:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_fcn ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Gender:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_gender ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Date of Birth:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_dob ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Nationality:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_nationality ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Date of Expiry:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_dateOfExpiry ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Phone Number:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_phoneNumber ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Region:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_region ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Zone:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_zone ?? 'N/A',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Woreda:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_woreda ?? 'NA',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'FIN:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_fin ?? 'N/a',
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 13,
),
Text(
'Face Matching Score:',
style: TextStyle(
fontSize: 15,
color: Colors.black,
),
),
Text(
_mathing_score.toString(),
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 15,
),
if (_frontImage != null && _backImage != null)
Divider(
color: Colors.white,
),
SizedBox(
height: 5,
),
if (_frontImage != null && _backImage != null)
Text(
'ID Card:',
style: TextStyle(
fontSize: 17,
color: Colors.black,
fontWeight: FontWeight.bold),
),
if (_frontImage != null && _backImage != null)
SizedBox(
height: 7,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_frontImage != null)
Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.memory(
_frontImage!,
fit: BoxFit
.contain, // Ensures the entire image fits without cropping
),
),
),
if (_backImage != null)
Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.memory(
_backImage!,
fit: BoxFit
.contain,
),
),
),
],
),
if (_capturedImage != null && _capturedImage != "")
SizedBox(
height: 10,
),
if (_capturedImage != null && _capturedImage != "")
Divider(
color: Colors.white,
),
if (_capturedImage != null && _capturedImage != "")
SizedBox(
height: 10,
),
if (_capturedImage != null && _capturedImage != "")
Text(
'Selfie:',
style: TextStyle(
fontSize: 17,
color: Colors.black,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 7,
),
if (_capturedImage != null && _capturedImage != "")
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SizedBox(
width:
MediaQuery.of(context).size.width / 1.8,
child: Image.memory(_capturedImage!),
),
),
],
)),
),
bottomNavigationBar: _isLoading
? null
: BottomAppBar(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
offset: Offset(0, 4),
blurRadius: 3,
spreadRadius: 0,
),
],
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF221BC7),
const Color(0xFF0800C9),
],
),
),
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
),
onPressed: () {
try {
Navigator.of(context, rootNavigator: true)
.pushReplacement(MaterialPageRoute(
builder: (context) => const HomeScreen(),
));
} catch (e) {
print("Error navigating to ImageDisplayScreen: $e");
}
},
child: Text(_capturedImage != null ? 'Go Home' : 'Retry',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18)),
),
),
),
),
);
}
}