Attendance validation involving Location Detection + Facial Recoginition with Liveness Detection
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../providers/verification_session_provider.dart';
|
||||
import '../../services/face_verification.dart' as face;
|
||||
|
||||
/// Screen opened on mobile when the user scans a QR code for cross-device
|
||||
/// face verification. Performs liveness detection and uploads the result.
|
||||
class MobileVerificationScreen extends ConsumerStatefulWidget {
|
||||
const MobileVerificationScreen({super.key, required this.sessionId});
|
||||
|
||||
final String sessionId;
|
||||
|
||||
@override
|
||||
ConsumerState<MobileVerificationScreen> createState() =>
|
||||
_MobileVerificationScreenState();
|
||||
}
|
||||
|
||||
class _MobileVerificationScreenState
|
||||
extends ConsumerState<MobileVerificationScreen> {
|
||||
bool _loading = true;
|
||||
bool _verifying = false;
|
||||
bool _done = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSession();
|
||||
}
|
||||
|
||||
Future<void> _loadSession() async {
|
||||
try {
|
||||
final controller = ref.read(verificationSessionControllerProvider);
|
||||
final session = await controller.getSession(widget.sessionId);
|
||||
if (session == null) {
|
||||
setState(() {
|
||||
_error = 'Verification session not found.';
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (session.isExpired) {
|
||||
setState(() {
|
||||
_error = 'This verification session has expired.';
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (session.isCompleted) {
|
||||
setState(() {
|
||||
_error = 'This session has already been completed.';
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = false);
|
||||
|
||||
// Automatically start liveness detection
|
||||
_startLiveness();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = 'Failed to load session: $e';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startLiveness() async {
|
||||
if (_verifying) return;
|
||||
setState(() {
|
||||
_verifying = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await face.runFaceLiveness(context);
|
||||
|
||||
if (result == null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = 'Liveness check cancelled.';
|
||||
_verifying = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the photo and complete the session
|
||||
final controller = ref.read(verificationSessionControllerProvider);
|
||||
await controller.completeSession(
|
||||
sessionId: widget.sessionId,
|
||||
bytes: result.imageBytes,
|
||||
fileName: 'verify.jpg',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_done = true;
|
||||
_verifying = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = 'Verification failed: $e';
|
||||
_verifying = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Face Verification')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: _buildContent(theme, colors),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ThemeData theme, ColorScheme colors) {
|
||||
if (_loading) {
|
||||
return const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Loading verification session...'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (_done) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 64, color: Colors.green),
|
||||
const SizedBox(height: 16),
|
||||
Text('Verification Complete', style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'You can close this screen and return to the web app.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: colors.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Verification Error',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(color: colors.error),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _startLiveness,
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (_verifying) {
|
||||
return const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Processing verification...'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: prompt to start
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.face, size: 64, color: colors.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text('Ready to Verify', style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap the button below to start face liveness detection.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _startLiveness,
|
||||
icon: const Icon(Icons.face),
|
||||
label: const Text('Start Verification'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user