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 createState() => _MobileVerificationScreenState(); } class _MobileVerificationScreenState extends ConsumerState { bool _loading = true; bool _verifying = false; bool _done = false; String? _error; @override void initState() { super.initState(); _loadSession(); } Future _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 _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'), ), ], ); } }