57377b1b21
Re-theme TasQ with a refined enterprise "Cyber/IT" identity on Material 3. Functionality is unchanged — only color/shape tokens and a few state widgets. Foundation: - Hand-tuned light + dark ColorSchemes (deep-slate surfaces, azure/teal/violet accents), tightened 12-16dp shape language, flat hairline-outlined cards. - New AppStatusColors theme extension (success/warning/danger/info/neutral, per-brightness) replacing scattered raw Colors.green/orange/red/grey so status color is consistent and legible in dark mode. - New TechChip mono primitive; StatusPill.kind; M3Card hairline variants; tightened AppSurfaces radii. Per-screen: - Tokenized status colors + radii across dashboard, tickets, tasks, IT service requests, attendance, whereabouts, network map, reports charts, auth, and shared/admin screens. Added _ticketStatusKind/_taskStatusKind/_isrStatusKind mappers so status carries meaning consistently. - Fixed white pie-slice borders for dark mode; dropped ad-hoc dialog radius overrides in favor of the theme default. Categorical palettes (leave-type icons, team color picker, distribution charts) intentionally preserved. Verified: flutter analyze lib clean; flutter build web green; visual QA in light and dark (dashboard, tickets, attendance). Co-Authored-By: claude-flow <ruv@ruv.net>
240 lines
6.3 KiB
Dart
240 lines
6.3 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;
|
|
import '../../theme/app_status_colors.dart';
|
|
|
|
/// 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: AppStatusColors.of(context).success.color,
|
|
),
|
|
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|