tasq/lib/screens/shared/mobile_verification_screen.dart

235 lines
6.2 KiB
Dart

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'),
),
],
);
}
}