sotaid_pluging 1.1.1+1
sotaid_pluging: ^1.1.1+1 copied to clipboard
A Flutter plugin for integrating SotaBoost's identity verification service into Flutter applications
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:sotaid_pluging/sotaid_plugin.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:async';
import 'dart:io';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SotaID',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const MyHomePage(title: 'SotaID'),
);
}
}
// Verification Status Page
class VerificationStatusPage extends StatelessWidget {
final String verificationType;
final String status;
final String? message;
final String? sessionId; // Add session ID parameter
final bool isSuccess;
const VerificationStatusPage({
Key? key,
required this.verificationType,
required this.status,
this.message,
this.sessionId, // Add session ID parameter
required this.isSuccess,
});
@override
Widget build(BuildContext context) {
final bool isPending = status == 'requires_manual_verification';
final Color statusColor = isSuccess
? Colors.green
: (isPending
? Colors.orange
: Colors.red);
final IconData overlayIcon = isSuccess
? Icons.check_circle
: (isPending ? Icons.schedule : Icons.cancel);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header with back button
Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.arrow_back, color: Colors.black87),
),
SizedBox(width: 16),
Text(
'Status de Vérification',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
SizedBox(height: 40),
// Status Icon
Container(
width: 200,
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
Image.network(
'https://www.sotaid.com/img/verify/reconnaissance-de-visage.png',
width: 200,
height: 200,
errorBuilder: (context, error, stackTrace) => Icon(Icons.image, size: 80, color: Colors.grey.shade400),
),
Positioned(
bottom: 1,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Icon(
overlayIcon,
size: 22,
color: statusColor,
),
),
),
],
),
),
SizedBox(height: 32),
// Status Title
Text(
"${isSuccess ? 'Identité vérifiée avec succès' : (status == 'requires_manual_verification' ? 'Vérification en cours' : 'Échec de la vérification')}",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: statusColor,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
// Verification Type
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.blue.shade200),
),
child: Text(
verificationType,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
),
SizedBox(height: 24),
// Action Buttons
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
child: Text('Terminer'),
),
),
],
),
),
],
),
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final SotaIdPlugin _sotaIdPlugin = SotaIdPlugin();
String _bearerToken = 'sotaboost'; // Test token for sandbox
bool _hasPendingVerification = false; // Track pending verification state
@override
void initState() {
super.initState();
// Initialize plugin with bearer token (defaults to sandbox mode)
_sotaIdPlugin.initialize(bearerToken: _bearerToken);
_checkPendingVerificationOnStartup();
}
void _startCompleteVerification() async {
// Check for pending verification first
await _handlePendingVerificationCheck();
await _sotaIdPlugin.startCompleteVerification(
context: context,
onVerificationComplete: (response) async {
print('=== VERIFICATION RESPONSE (Complete Verification) ===');
print('Status: ${response.status}');
print('Message: ${response.message}');
print('Session ID: ${response.sessionId}');
print('Success: ${response.status == 'success'}');
print('Full Response: $response');
print('=====================================');
// Handle different response types
if (response.status == 'check_pending_status') {
// Handle pending status check - this should be handled by the main app
print('Checking pending verification status for session: ${response.sessionId}');
if (response.sessionId != null) {
await _checkVerificationStatus(response.sessionId!);
}
return;
}
// Handle requires_manual_verification status
if (response.status == 'requires_manual_verification') {
setState(() {
_hasPendingVerification = true;
});
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Complete Verification',
status: response.status,
message: response.message,
sessionId: response.sessionId, // Pass session ID to status page
isSuccess: response.status == 'success',
),
),
);
},
onError: (error) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Complete Verification',
status: 'error',
message: error,
isSuccess: false,
),
),
);
},
);
}
void _startFaceVerification() async {
// Check for pending verification first
await _handlePendingVerificationCheck();
// Navigate to image upload page first
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ImageUploadPage(
bearerToken: _bearerToken,
),
),
);
// Handle result from ImageUploadPage (for pending status check)
if (result != null && result is Map<String, dynamic>) {
if (result['action'] == 'check_pending_status') {
final sessionId = result['session_id'] as String?;
if (sessionId != null) {
await _checkVerificationStatus(sessionId);
}
} else if (result['action'] == 'requires_manual_verification') {
// Set pending verification state
setState(() {
_hasPendingVerification = true;
});
}
}
}
void _startLivenessVerification() async {
// Check for pending verification first
await _handlePendingVerificationCheck();
await _sotaIdPlugin.startVerificationFlow(
context: context,
verificationType: VerificationType.liveness,
onVerificationComplete: (response) async {
// Handle different response types
if (response.status == 'check_pending_status') {
// Handle pending status check - this should be handled by the main app
if (response.sessionId != null) {
await _checkVerificationStatus(response.sessionId!);
}
return;
}
// Handle requires_manual_verification status
if (response.status == 'requires_manual_verification') {
setState(() {
_hasPendingVerification = true;
});
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Liveness',
status: response.status,
message: response.message,
sessionId: response.sessionId, // Pass session ID to status page
isSuccess: response.status == 'success',
),
),
);
},
onError: (error) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Liveness',
status: 'error',
message: error,
isSuccess: false,
),
),
);
},
);
}
void _showSnackBar(String message, {Color? backgroundColor}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
),
);
}
/// Check verification status for a pending session
Future<void> _checkVerificationStatus(String sessionId) async {
try {
setState(() {
});
final status = await _sotaIdPlugin.getVerificationStatus(sessionId);
setState(() {
});
// Show status result
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Status Check',
status: status.status,
message: 'Status checked for session: $sessionId',
sessionId: sessionId,
isSuccess: status.status == 'verified',
),
),
);
// Clear pending state if verification is completed
if (status.status == 'verified' || status.status == 'failed' || status.status == 'success') {
await _sotaIdPlugin.clearPendingSession();
setState(() {
_hasPendingVerification = false;
});
}
} catch (e) {
setState(() {
});
_showSnackBar('Error checking status: $e', backgroundColor: Colors.red);
}
}
/// Check for pending verification on app startup
Future<void> _checkPendingVerificationOnStartup() async {
try {
final pendingSession = await _sotaIdPlugin.getPendingSession();
if (pendingSession != null) {
setState(() {
_hasPendingVerification = true;
});
// Show a subtle notification about pending verification
_showSnackBar(
'Vous avez une vérification en cours. Cliquez sur l\'icône horloge ou la notification pour vérifier le statut.',
backgroundColor: Colors.orange,
);
}
} catch (e) {
print('Error checking pending verification on startup: $e');
}
}
/// Show dialog when user tries to start verification with pending manual verification
Future<bool> _showPendingVerificationDialog() async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.schedule,
color: Colors.orange,
size: 24,
),
SizedBox(width: 12),
Text(
'Vérification en cours',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vous avez une vérification en attente de traitement manuel.',
style: TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
SizedBox(height: 16),
Text(
'Que souhaitez-vous faire ?',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true); // Continue with new verification
},
child: Text(
'Nouvelle vérification',
style: TextStyle(
color: Colors.grey.shade600,
fontWeight: FontWeight.w600,
),
),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop(); // Close dialog first
// Get the pending session and check its status directly
final pendingSession = await _sotaIdPlugin.getPendingSession();
if (pendingSession != null && pendingSession['session_id'] != null) {
await _checkVerificationStatus(pendingSession['session_id']);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Vérifier le statut',
style: TextStyle(
fontWeight: FontWeight.w600,
),
),
),
],
);
},
) ?? false;
}
/// Check if there's a pending verification and handle accordingly
Future<void> _handlePendingVerificationCheck() async {
if (_hasPendingVerification) {
final result = await _showPendingVerificationDialog();
if (result == true) {
// User chose to continue with new verification, clear the pending state
await _sotaIdPlugin.clearPendingSession();
setState(() {
_hasPendingVerification = false;
});
}
// If result is false or null, the user either cancelled or chose to check status
// The status check is now handled directly in the button onPressed callback
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
if (_hasPendingVerification)
IconButton(
onPressed: () async {
final pendingSession = await _sotaIdPlugin.getPendingSession();
if (pendingSession != null && pendingSession['session_id'] != null) {
await _checkVerificationStatus(pendingSession['session_id']);
}
},
icon: Icon(
Icons.schedule,
color: Colors.orange,
),
tooltip: 'Vérifier le statut en cours',
),
],
),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Logo with better styling
Container(
padding: EdgeInsets.all(16),
child: Image.asset(
'assets/sotaid_logo.png',
height: 40,
fit: BoxFit.contain,
),
),
SizedBox(height: 5),
// Instructional text
Text(
'Veuillez sélectionner l\'un des trois processus pour commencer',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.black87,
height: 1.4,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 20),
// Middle section with verification options
// Full KYC Option
_buildVerificationOption(
title: 'Full KYC',
description: 'Vous permet de réaliser notre processus entier de KYC. Acquisition et validation d\'authenticité de document, facematching avec le titulaire de la pièce avec la réalisation de la détection du vivant.',
onTap: _startCompleteVerification,
customIconPath: 'assets/full_kyc_icon.svg',
),
SizedBox(height: 10),
// Face Matching Option
_buildVerificationOption(
title: 'Face Matching',
description: 'Réaliser le facematch entre un document de référence et une nouvelle vidéo acquise. La détection du vivant y est intégrée également.',
onTap: _startFaceVerification,
customIconPath: 'assets/face_macthin_icon.svg',
),
SizedBox(height: 10),
// Liveness Option
_buildVerificationOption(
title: 'Liveness',
description: 'Preuve de vie, détection de vivant sur une vidéo reçue.',
onTap: _startLivenessVerification,
customIconPath: 'assets/liveness_icon.svg',
),
SizedBox(height: 40),
// Bottom section with powered by text
Text(
'Powered by SotaBoost',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
SizedBox(height: 40),
],
),
),
),
),
);
}
Widget _buildVerificationOption({
IconData? icon,
required String title,
required String description,
required VoidCallback onTap,
String? customIconPath,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Icon with title badge
Container(
width: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon container
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Stack(
children: [
Center(
child: customIconPath != null
? SvgPicture.asset(
customIconPath,
width: 28,
height: 28,
fit: BoxFit.contain,
)
: Icon(
icon ?? Icons.person,
size: 28,
color: Colors.black87,
),
),
// Pending verification indicator
if (_hasPendingVerification)
Positioned(
top: -2,
right: -2,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Icon(
Icons.schedule,
size: 6,
color: Colors.white,
),
),
),
],
),
),
SizedBox(height: 8),
// Title badge
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade400,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade300, width: 1),
),
child: Text(
title,
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w700,
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
SizedBox(width: 14),
],
),
SizedBox(height: 4),
// Description with info icon
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
size: 16,
color: Colors.grey.shade600,
),
SizedBox(width: 8),
Expanded(
child: Text(
description,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
height: 1.4,
),
),
),
],
),
],
),
),
);
}
}
// Image Upload Page for Face Matching
class ImageUploadPage extends StatefulWidget {
final String bearerToken;
const ImageUploadPage({
super.key,
required this.bearerToken,
});
@override
State<ImageUploadPage> createState() => _ImageUploadPageState();
}
class _ImageUploadPageState extends State<ImageUploadPage> {
File? _selectedImage;
bool _isLoading = false;
final ImagePicker _picker = ImagePicker();
final SotaIdPlugin _sotaIdPlugin = SotaIdPlugin();
Future<void> _pickImage() async {
try {
setState(() {
_isLoading = true;
});
// Show option to pick from gallery or camera
final XFile? image = await showModalBottomSheet<XFile?>(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(Icons.photo_library),
title: Text('Galerie'),
onTap: () async {
Navigator.pop(context, await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
));
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Caméra'),
onTap: () async {
Navigator.pop(context, await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
));
},
),
ListTile(
leading: Icon(Icons.close),
title: Text('Annuler'),
onTap: () => Navigator.pop(context),
),
],
),
);
},
);
if (image != null) {
setState(() {
_selectedImage = File(image.path);
_isLoading = false;
});
_showSnackBar('Image sélectionnée avec succès', backgroundColor: Colors.green);
} else {
setState(() {
_isLoading = false;
});
}
} catch (e) {
setState(() {
_isLoading = false;
});
_showSnackBar('Erreur lors de la sélection de l\'image: $e', backgroundColor: Colors.red);
}
}
void _showSnackBar(String message, {Color? backgroundColor}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
),
);
}
void _startFaceVerification() async {
if (_selectedImage == null) {
_showSnackBar('Veuillez d\'abord sélectionner une image de référence', backgroundColor: Colors.orange);
return;
}
await _sotaIdPlugin.startFaceVerification(
context: context,
documentFile: _selectedImage!,
onVerificationComplete: (response) async {
// Handle different response types
if (response.status == 'check_pending_status') {
// Handle pending status check - this should be handled by the main app
if (response.sessionId != null) {
// Navigate back to main page to handle status check
Navigator.of(context).pop({
'action': 'check_pending_status',
'session_id': response.sessionId,
});
return;
}
return;
}
// Handle requires_manual_verification status
if (response.status == 'requires_manual_verification') {
// Navigate back to main page with pending verification info
Navigator.of(context).pop({
'action': 'requires_manual_verification',
'session_id': response.sessionId,
});
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Face Matching',
status: response.status,
message: response.message,
sessionId: response.sessionId, // Pass session ID to status page
isSuccess: response.status == 'success',
),
),
);
},
onError: (error) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerificationStatusPage(
verificationType: 'Face Matching',
status: 'error',
message: error,
isSuccess: false,
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header
Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.arrow_back, color: Colors.black87),
),
SizedBox(width: 16),
Text(
'Upload Image de Référence',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
SizedBox(height: 32),
// Instructions
Container(
width: double.infinity,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: Colors.blue.shade700, size: 20),
SizedBox(width: 8),
Text(
'Instructions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
],
),
SizedBox(height: 12),
Text(
'Pour le Face Matching, vous devez d\'abord fournir une image de référence (document d\'identité) qui sera comparée avec votre visage lors de la vérification.',
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade700,
height: 1.4,
),
),
],
),
),
SizedBox(height: 32),
// Image Upload Area
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedImage != null ? Colors.green.shade300 : Colors.grey.shade300,
width: 2,
),
),
child: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text(
'Chargement...',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
],
),
)
: _selectedImage != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
color: Colors.green,
size: 60,
),
SizedBox(height: 16),
Text(
'Image sélectionnée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.green,
),
),
SizedBox(height: 8),
Text(
'Document de référence prêt',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
SizedBox(height: 24),
TextButton(
onPressed: _pickImage,
child: Text(
'Changer l\'image',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w600,
),
),
),
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cloud_upload_outlined,
size: 80,
color: Colors.grey.shade400,
),
SizedBox(height: 16),
Text(
'Aucune image sélectionnée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey.shade600,
),
),
SizedBox(height: 8),
Text(
'Tapez pour sélectionner une image',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _pickImage,
icon: Icon(Icons.upload_file),
label: Text('Sélectionner Image'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
),
),
SizedBox(height: 32),
// Start Verification Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _selectedImage != null ? _startFaceVerification : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
'Commencer la Vérification',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
}
}