full_identity_verification 0.1.11
full_identity_verification: ^0.1.11 copied to clipboard
Flutter plugin for Blusalt Full Identity Verification SDK that supports both Android and iOS platforms.
example/lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:full_identity_verification/enums.dart';
import 'package:full_identity_verification/full_identity_verification.dart';
import 'package:full_identity_verification/model.dart';
void main() {
runApp(const MyApp());
}
enum FullIdentitySDKType {
documentPresent,
docAbsentCustomSelector,
docAbsentIdNumber,
none
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _fullIdentityPlugin = BlusaltFullIdentityVerification();
// Controllers
TextEditingController clientIdController = TextEditingController(text: '');
TextEditingController appNameController = TextEditingController(text: '');
TextEditingController apiKeyController = TextEditingController(text: '');
TextEditingController webHookUrlController = TextEditingController();
TextEditingController referenceController = TextEditingController();
TextEditingController thresholdController = TextEditingController();
TextEditingController timeoutDurationController = TextEditingController();
TextEditingController documentNumberController = TextEditingController();
// Settings
bool isDev = false;
// Document type selection
List<DocumentType> selectedDocumentTypes = [
DocumentType.bvn,
DocumentType.nin,
DocumentType.passport,
];
DocumentType selectedSingleDocType = DocumentType.bvn;
LivenessFacialComparisonType selectedLivenessType =
LivenessFacialComparisonType.motional;
// Result
FullIdentitySDKType sdkType = FullIdentitySDKType.none;
BlusaltFullIdentityResultResponse? resultResponse;
@override
void dispose() {
super.dispose();
clientIdController.dispose();
appNameController.dispose();
apiKeyController.dispose();
webHookUrlController.dispose();
referenceController.dispose();
thresholdController.dispose();
timeoutDurationController.dispose();
documentNumberController.dispose();
}
Future<void> startFullIdentitySDK() async {
if (!_validateInputs()) return;
resultResponse = await _fullIdentityPlugin.startFullIdentitySDK(
apiKey: apiKeyController.text,
appName: appNameController.text,
clientId: clientIdController.text,
isDev: isDev,
webhookUrl: webHookUrlController.text.isEmpty
? null
: webHookUrlController.text,
reference: referenceController.text.isEmpty
? null
: referenceController.text,
documentTypeList: selectedDocumentTypes,
timeoutDurationInSec: _validateDurationNumber(timeoutDurationController.text),
);
if (resultResponse?.blusaltFullIdentityProcess ==
BlusaltFullIdentityProcess.completed) {
setState(() {
sdkType = FullIdentitySDKType.documentPresent;
});
} else {
debugPrint('Error Code: ${resultResponse?.code ?? ''}');
debugPrint('Error Message: ${resultResponse?.message ?? ''}');
_showErrorDialog(resultResponse?.message ?? 'Unknown error occurred');
}
}
Future<void> startDocAbsentWithCustomSelector() async {
if (!_validateInputs()) return;
resultResponse =
await _fullIdentityPlugin.startDocAbsentWithCustomSelector(
apiKey: apiKeyController.text,
appName: appNameController.text,
clientId: clientIdController.text,
isDev: isDev,
documentTypeList: selectedDocumentTypes,
webhookUrl: webHookUrlController.text.isEmpty
? null
: webHookUrlController.text,
reference: referenceController.text.isEmpty
? null
: referenceController.text,
livenessFacialComparisonType: selectedLivenessType,
thresholdInPercent: _validateNumber(thresholdController.text),
timeoutDurationInSec: _validateDurationNumber(timeoutDurationController.text),
);
if (resultResponse?.blusaltFullIdentityProcess ==
BlusaltFullIdentityProcess.completed) {
setState(() {
sdkType = FullIdentitySDKType.docAbsentCustomSelector;
});
} else {
debugPrint('Error Code: ${resultResponse?.code ?? ''}');
debugPrint('Error Message: ${resultResponse?.message ?? ''}');
_showErrorDialog(resultResponse?.message ?? 'Unknown error occurred');
}
}
Future<void> startDocAbsentWithIdNumber() async {
if (!_validateInputs()) return;
if (documentNumberController.text.isEmpty) {
_showErrorDialog('Please enter document number');
return;
}
resultResponse = await _fullIdentityPlugin.startDocAbsentWithIdNumber(
apiKey: apiKeyController.text,
appName: appNameController.text,
clientId: clientIdController.text,
isDev: isDev,
documentType: selectedSingleDocType,
documentNumber: documentNumberController.text,
webhookUrl: webHookUrlController.text.isEmpty
? null
: webHookUrlController.text,
reference: referenceController.text.isEmpty
? null
: referenceController.text,
livenessFacialComparisonType: selectedLivenessType,
thresholdInPercent: _validateNumber(thresholdController.text),
timeoutDurationInSec: _validateDurationNumber(timeoutDurationController.text),
);
if (resultResponse?.blusaltFullIdentityProcess ==
BlusaltFullIdentityProcess.completed) {
setState(() {
sdkType = FullIdentitySDKType.docAbsentIdNumber;
});
} else {
debugPrint('Error Code: ${resultResponse?.code ?? ''}');
debugPrint('Error Message: ${resultResponse?.message ?? ''}');
_showErrorDialog(resultResponse?.message ?? 'Unknown error occurred');
}
}
bool _validateInputs() {
if (clientIdController.text.isEmpty ||
appNameController.text.isEmpty ||
apiKeyController.text.isEmpty) {
_showErrorDialog('Please fill in Client ID, App Name, and API Key');
return false;
}
return true;
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
double? _validateNumber(String value) {
if (value.isEmpty) {
return null;
}
return double.tryParse(value);
}
int? _validateDurationNumber(String value) {
if (value.isEmpty) {
return null;
}
return int.tryParse(value);
}
String _getSDKTypeName() {
switch (sdkType) {
case FullIdentitySDKType.documentPresent:
return 'Full Identity (Document Present)';
case FullIdentitySDKType.docAbsentCustomSelector:
return 'Doc Absent (Custom Selector)';
case FullIdentitySDKType.docAbsentIdNumber:
return 'Doc Absent (ID Number)';
case FullIdentitySDKType.none:
return 'None';
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Full Identity Verification Example'),
backgroundColor: Colors.blue,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
// Credentials Section
const Text(
'SDK Credentials',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: clientIdController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Client ID",
hintText: "Enter client id",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
onChanged: (text) => setState(() {}),
),
const SizedBox(height: 14),
TextField(
controller: appNameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "App Name",
hintText: "Enter app name",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
onChanged: (text) => setState(() {}),
),
const SizedBox(height: 14),
TextField(
controller: apiKeyController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "API Key",
hintText: "Enter api key",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
onChanged: (text) => setState(() {}),
),
const SizedBox(height: 20),
// Optional Fields Section
const Text(
'Optional Configuration',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: webHookUrlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Webhook URL (Optional)",
hintText: "Enter webhook url",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
onChanged: (text) => setState(() {}),
),
const SizedBox(height: 14),
TextField(
controller: referenceController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Reference (Optional)",
hintText: "Enter reference",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
onChanged: (text) => setState(() {}),
),
const SizedBox(height: 14),
TextField(
controller: timeoutDurationController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Timeout Duration (seconds)",
hintText:
"Enter timeout duration in seconds (Optional, default 120s)",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
maxLines: 2,
),
const SizedBox(height: 4),
CustomCheckbox(
label: "Point to Development Environment?",
value: isDev,
onCheck: (value) => setState(() => isDev = value),
),
const SizedBox(height: 20),
// Document Absent Specific Options
const Text(
'Document Absent Options',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: thresholdController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Threshold Percent (0-100)",
hintText:
"Enter threshold for facial comparison 0-100 (Optional)",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
maxLines: 2,
),
const SizedBox(height: 14),
const Text(
'Liveness Type:',
style: TextStyle(fontWeight: FontWeight.w500),
),
RadioListTile<LivenessFacialComparisonType>(
title: const Text('Motional'),
value: LivenessFacialComparisonType.motional,
groupValue: selectedLivenessType,
onChanged: (value) {
setState(() {
selectedLivenessType = value!;
});
},
),
RadioListTile<LivenessFacialComparisonType>(
title: const Text('Still'),
value: LivenessFacialComparisonType.still,
groupValue: selectedLivenessType,
onChanged: (value) {
setState(() {
selectedLivenessType = value!;
});
},
),
const SizedBox(height: 20),
// Document Type Selection
const Text(
'Document Types (for Full Identity & Custom Selector)',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
..._buildDocumentTypeCheckboxes(),
const SizedBox(height: 20),
// ID Number Specific Section
const Text(
'Document Absent with ID Number Options',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
'Select Document Type:',
style: TextStyle(fontWeight: FontWeight.w500),
),
DropdownButton<DocumentType>(
value: selectedSingleDocType,
isExpanded: true,
items: DocumentType.values.map((DocumentType type) {
return DropdownMenuItem<DocumentType>(
value: type,
child: Text(_getDocumentTypeName(type)),
);
}).toList(),
onChanged: (DocumentType? newValue) {
if (newValue != null) {
setState(() {
selectedSingleDocType = newValue;
});
}
},
),
const SizedBox(height: 14),
TextField(
controller: documentNumberController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Document Number",
hintText: "Enter document number (e.g., BVN: 12345678901)",
hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
),
),
const SizedBox(height: 24),
// SDK Launch Buttons
const Text(
'Launch SDK',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: startFullIdentitySDK,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(double.infinity, 50),
),
child: const Text(
"Start Full Identity SDK\n(Document Present)",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: startDocAbsentWithCustomSelector,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
minimumSize: const Size(double.infinity, 50),
),
child: const Text(
"Start Doc Absent SDK\n(Custom Selector)",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: startDocAbsentWithIdNumber,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
minimumSize: const Size(double.infinity, 50),
),
child: const Text(
"Start Doc Absent SDK\n(ID Number)",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
),
// Results Section
if (sdkType != FullIdentitySDKType.none) ...[
const SizedBox(height: 34),
Text(
"Result: (${_getSDKTypeName()})",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
JsonViewerWidget(jsonData: resultResponse?.toJson()),
],
const SizedBox(height: 34),
const Text(
'Powered by Blusalt',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
List<Widget> _buildDocumentTypeCheckboxes() {
return DocumentType.values.map((type) {
return CustomCheckbox(
label: _getDocumentTypeName(type),
value: selectedDocumentTypes.contains(type),
onCheck: (value) {
setState(() {
if (value) {
if (!selectedDocumentTypes.contains(type)) {
selectedDocumentTypes.add(type);
}
} else {
selectedDocumentTypes.remove(type);
}
});
},
);
}).toList();
}
String _getDocumentTypeName(DocumentType type) {
switch (type) {
case DocumentType.bvn:
return 'BVN (Bank Verification Number)';
case DocumentType.nin:
return 'NIN (National Identification Number)';
case DocumentType.passport:
return 'International Passport';
case DocumentType.driverLicense:
return "Driver's License";
case DocumentType.pvc:
return "PVC (Permanent Voter's Card)";
}
}
}
class CustomCheckbox extends StatelessWidget {
final String label;
final bool value;
final Function(bool) onCheck;
const CustomCheckbox({
Key? key,
required this.label,
required this.value,
required this.onCheck,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: value,
onChanged: (newValue) => onCheck(newValue ?? false),
),
Expanded(
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
],
);
}
}
class JsonViewerWidget extends StatelessWidget {
final dynamic jsonData;
const JsonViewerWidget({
super.key,
required this.jsonData,
});
@override
Widget build(BuildContext context) {
final prettyJson = const JsonEncoder.withIndent(' ').convert(jsonData);
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
Container(
color: const Color(0xFF282C34),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
prettyJson,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Colors.white,
),
),
),
),
),
),
),
Positioned(
top: 8,
right: 8,
child: IconButton(
icon: const Icon(
Icons.copy,
color: Colors.white70,
size: 20,
),
onPressed: () {
_copyToClipboard(context, prettyJson);
},
tooltip: 'Copy to clipboard',
constraints: const BoxConstraints(),
padding: const EdgeInsets.all(8),
),
),
],
),
);
}
void _copyToClipboard(BuildContext context, String prettyJson) {
Clipboard.setData(ClipboardData(text: prettyJson));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('JSON copied to clipboard'),
duration: Duration(seconds: 2),
),
);
}
}