Files
tasq/lib/screens/shared/mobile_verification_screen.dart
T
redz1029 57377b1b21 UI: Cyber/IT design overhaul with semantic status tokens (Material 3)
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>
2026-09-26 15:13:47 +08:00

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