You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1998 lines
75 KiB
Dart

1 month ago
import 'dart:convert';
import 'dart:typed_data';
import 'package:finger_print_verification/fingerprint/view/result_banner.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../theme/app_theme.dart';
import '../bloc/fingerprint_bloc.dart';
import '../utils/const.dart' show countryAlpha2List, fingerIndexMap;
import 'auth_result_screen.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// FingerprintView — "Futuristic Hospital, KSA" verification kiosk screen.
///
/// Behaviour, BLoC events/state, and field validation are UNCHANGED from the
/// original implementation — only presentation has been redesigned for
/// Material 3 and tablet/desktop layouts (a two-column layout appears on
/// shortestSide >= 600, otherwise a single scrollable column as on phones).
/// ─────────────────────────────────────────────────────────────────────────
class FingerprintView extends StatefulWidget {
const FingerprintView({super.key});
@override
State<FingerprintView> createState() => _FingerprintViewState();
}
class _FingerprintViewState extends State<FingerprintView> {
final _nationalIdController = TextEditingController();
final _nationalityController = TextEditingController();
final _passportController = TextEditingController();
@override
void dispose() {
_nationalIdController.dispose();
_nationalityController.dispose();
_passportController.dispose();
super.dispose();
}
void _clearControllers() {
_nationalIdController.clear();
_nationalityController.clear();
_passportController.clear();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FingerprintBloc()..add(const AppStarted()),
child: _buildScaffold(),
);
}
Widget _buildScaffold() {
return Scaffold(
extendBodyBehindAppBar: false,
appBar: AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 34,
height: 34,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.cyan.withValues(alpha: 0.12),
border: Border.all(color: AppTheme.cyan.withValues(alpha: 0.4)),
),
child: const Icon(Icons.fingerprint, color: AppTheme.cyan, size: 20),
),
const SizedBox(width: 12),
const Text('Biometric Verification'),
],
),
actions: const [
Padding(
padding: EdgeInsets.only(right: 16),
child: _KsaBadge(),
),
],
),
body: TechGridBackground(
child: BlocConsumer<FingerprintBloc, FingerprintFormState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == FormStatus.error && state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: AppTheme.inkRaised2,
content: Row(
children: [
const Icon(Icons.error_outline, color: AppTheme.redAlert, size: 18),
const SizedBox(width: 10),
Expanded(child: Text(state.errorMessage!,
style: const TextStyle(color: AppTheme.textPrimary),
)),
],
),
),
);
} else if (state.status == FormStatus.scanned) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: AppTheme.inkRaised2,
content: Row(
children: const [
Icon(Icons.check_circle_outline, color: AppTheme.saudiGreen, size: 18),
SizedBox(width: 10),
Text('Fingerprint scanned successfully!',
style: const TextStyle(color: AppTheme.textPrimary),
),
],
),
),
);
} else if (state.status == FormStatus.verified &&
state.authResult != null) {
// ── Navigation mechanism ──────────────────────────────────
// Phones: not enough room for form + result side-by-side, so
// push the dedicated AuthResultScreen route as before.
// Tablets: the form and the result panel are already fused
// into one screen (see _TabletLayout's right-hand pane), so
// we deliberately do NOT navigate, ever — the inline panel
// below updates itself and the user never leaves this screen.
if (!isTablet(context)) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => AuthResultScreen(result: state.authResult!),
),
);
}
} else if (state.status == FormStatus.idle) {
_clearControllers();
}
},
builder: (context, state) {
final isBusy = state.status == FormStatus.scanning ||
state.status == FormStatus.connecting ||
state.status == FormStatus.verifying;
if (state.status == FormStatus.loggingIn) {
return const _LoggingInOverlay();
}
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_DeviceStatusBar(state: state),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final tablet = constraints.maxWidth >= 760;
if (tablet) {
return _TabletLayout(
state: state,
isBusy: isBusy,
nationalIdController: _nationalIdController,
passportController: _passportController,
onClearControllers: _clearControllers,
);
}
return _PhoneLayout(
state: state,
isBusy: isBusy,
nationalIdController: _nationalIdController,
passportController: _passportController,
onClearControllers: _clearControllers,
);
},
),
),
],
),
);
},
),
),
);
}
}
class _KsaBadge extends StatelessWidget {
const _KsaBadge();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppTheme.saudiGreen.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppTheme.saudiGreen.withValues(alpha: 0.4)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.health_and_safety_outlined, size: 14, color: AppTheme.saudiGreen),
SizedBox(width: 5),
Text('KSA MOH', style: TextStyle(fontSize: 11, color: AppTheme.saudiGreen, fontWeight: FontWeight.w700)),
],
),
);
}
}
class _LoggingInOverlay extends StatelessWidget {
const _LoggingInOverlay();
@override
Widget build(BuildContext context) {
return TechGridBackground(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 56,
height: 56,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: const AlwaysStoppedAnimation<Color>(AppTheme.cyan),
),
),
const SizedBox(height: 20),
const Text(
'Logging in…',
style: TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w600, fontSize: 16),
),
],
),
),
);
}
}
// ── Phone layout: single scrollable column ──────────────────────────────────
class _PhoneLayout extends StatelessWidget {
final FingerprintFormState state;
final bool isBusy;
final TextEditingController nationalIdController;
final TextEditingController passportController;
final VoidCallback onClearControllers;
const _PhoneLayout({
required this.state,
required this.isBusy,
required this.nationalIdController,
required this.passportController,
required this.onClearControllers,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_FormCard(
state: state,
isBusy: isBusy,
nationalIdController: nationalIdController,
passportController: passportController,
onClearControllers: onClearControllers,
),
const SizedBox(height: 16),
if ((state.status == FormStatus.scanned ||
state.status == FormStatus.verifying ||
state.status == FormStatus.verified) &&
state.base64Data != null)
_ScanResultCard(state: state),
],
),
);
}
}
// ── Tablet layout: form on the left, live scan/status panel on the right ──
class _TabletLayout extends StatelessWidget {
final FingerprintFormState state;
final bool isBusy;
final TextEditingController nationalIdController;
final TextEditingController passportController;
final VoidCallback onClearControllers;
const _TabletLayout({
required this.state,
required this.isBusy,
required this.nationalIdController,
required this.passportController,
required this.onClearControllers,
});
@override
Widget build(BuildContext context) {
final isVerified = state.status == FormStatus.verified && state.authResult != null;
final hasScan = !isVerified &&
(state.status == FormStatus.scanned ||
state.status == FormStatus.verifying ||
state.status == FormStatus.verified) &&
state.base64Data != null;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1180),
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 5,
child: SingleChildScrollView(
child: _FormCard(
state: state,
isBusy: isBusy,
nationalIdController: nationalIdController,
passportController: passportController,
onClearControllers: onClearControllers,
),
),
),
const SizedBox(width: 24),
Expanded(
flex: 4,
child: SingleChildScrollView(
child: isVerified
? _InlineResultPane(
state: state,
onNewVerification: () {
onClearControllers();
context.read<FingerprintBloc>().add(const ResetBlocEvent());
},
)
: hasScan
? _ScanResultCard(state: state)
: const _ScanPlaceholderPanel(),
),
),
],
),
),
),
);
}
}
/// The "fused screen" mechanism for tablets: instead of navigating to
/// AuthResultScreen, the verification result is rendered right here, in the
/// same pane that previously showed the live scan preview. A "New
/// Verification" action resets the BLoC in place so the user can run
/// another check without ever leaving this screen.
class _InlineResultPane extends StatelessWidget {
final FingerprintFormState state;
final VoidCallback onNewVerification;
const _InlineResultPane({required this.state, required this.onNewVerification});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ResultBody(result: state.authResult!),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: onNewVerification,
icon: const Icon(Icons.refresh),
label: const Text('New Verification'),
),
],
);
}
}
class _ScanPlaceholderPanel extends StatelessWidget {
const _ScanPlaceholderPanel();
@override
Widget build(BuildContext context) {
return GlowPanel(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 88,
height: 88,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.cyan.withValues(alpha: 0.07),
border: Border.all(color: AppTheme.cyan.withValues(alpha: 0.25)),
),
child: const Icon(Icons.fingerprint, color: AppTheme.cyan, size: 44),
),
const SizedBox(height: 20),
const Text(
'Awaiting scan',
style: TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w700, fontSize: 16),
),
const SizedBox(height: 6),
const Text(
'Connect the device and place a finger on the scanner to begin capture.',
textAlign: TextAlign.center,
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
),
],
),
);
}
}
// ── Form card: document type, finger, ID/passport fields, action buttons ──
class _FormCard extends StatelessWidget {
final FingerprintFormState state;
final bool isBusy;
final TextEditingController nationalIdController;
final TextEditingController passportController;
final VoidCallback onClearControllers;
const _FormCard({
required this.state,
required this.isBusy,
required this.nationalIdController,
required this.passportController,
required this.onClearControllers,
});
@override
Widget build(BuildContext context) {
final isVerified = state.status == FormStatus.verified;
final isVerifying = state.status == FormStatus.verifying;
return GlowPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _SectionLabel(icon: Icons.badge_outlined, text: 'IDENTITY DOCUMENT'),
const SizedBox(height: 14),
// ── Document Type ──────────────────────────────────────────────
DropdownButtonFormField<DocumentType>(
initialValue: state.documentType,
dropdownColor: AppTheme.inkRaised2,
style: const TextStyle(color: AppTheme.textPrimary),
decoration: const InputDecoration(
labelText: 'Document Type',
prefixIcon: Icon(Icons.description_outlined, color: AppTheme.textMuted),
),
items: const [
DropdownMenuItem(value: DocumentType.nationalId, child: Text('National ID')),
DropdownMenuItem(value: DocumentType.passport, child: Text('Passport')),
],
onChanged: isBusy
? null
: (val) {
if (val != null) {
context.read<FingerprintBloc>().add(DocumentTypeChanged(val));
}
},
),
const SizedBox(height: 16),
// ── Finger Selection ────────────────────────────────────────────
DropdownButtonFormField<int>(
value: state.selectedFingerIndex,
dropdownColor: AppTheme.inkRaised2,
style: const TextStyle(color: AppTheme.textPrimary),
decoration: const InputDecoration(
labelText: 'Finger',
prefixIcon: Icon(Icons.back_hand_outlined, color: AppTheme.textMuted),
),
items: fingerIndexMap.entries
.map((e) => DropdownMenuItem<int>(value: e.key, child: Text(e.value)))
.toList(),
onChanged: isBusy
? null
: (val) {
if (val != null) {
context.read<FingerprintBloc>().add(FingerTypeChanged(val));
}
},
),
const SizedBox(height: 16),
// ── Conditional document fields ─────────────────────────────────
if (state.documentType == DocumentType.nationalId) ...[
TextField(
controller: nationalIdController,
keyboardType: TextInputType.number,
style: const TextStyle(color: AppTheme.textPrimary,),
maxLength: 10,
decoration: InputDecoration(
labelText: 'National ID',
hintText: '10-digit number starting with 1 or 2',
prefixIcon: const Icon(Icons.pin_outlined, color: AppTheme.textMuted),
errorText: state.nationalIdError,
),
onChanged: (val) =>
context.read<FingerprintBloc>().add(NationalIdChanged(val)),
),
] else ...[
_SearchableCountryDropdown(
selectedCode: state.nationality.isEmpty ? null : state.nationality,
errorText: state.nationalityError,
onChanged: (code) =>
context.read<FingerprintBloc>().add(NationalityChanged(code)),
),
const SizedBox(height: 12),
TextField(
controller: passportController,
textCapitalization: TextCapitalization.characters,
decoration: InputDecoration(
labelText: 'Passport Number',
hintText: '69 alphanumeric characters',
prefixIcon: const Icon(Icons.numbers_outlined, color: AppTheme.textMuted),
errorText: state.passportNumberError,
),
onChanged: (val) =>
context.read<FingerprintBloc>().add(PassportNumberChanged(val)),
),
],
const SizedBox(height: 24),
const _SectionLabel(icon: Icons.sensors, text: 'SCANNER ACTIONS'),
const SizedBox(height: 14),
// ── Action buttons ──────────────────────────────────────────────
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: isBusy
? null
: () => context.read<FingerprintBloc>().add(const ConnectDevice()),
icon: state.status == FormStatus.connecting && !state.isScanConnecting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.cyan),
)
: const Icon(Icons.usb, color: AppTheme.cyan),
label: Text(
state.status == FormStatus.connecting && !state.isScanConnecting
? 'Connecting…'
: 'Connect',
),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton.icon(
onPressed: isBusy
? null
: () => context.read<FingerprintBloc>().add(const ScanFingerprintEvent()),
icon: state.isScanConnecting || state.status == FormStatus.scanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF002022)),
)
: const Icon(Icons.fingerprint),
label: Text(
state.isScanConnecting
? 'Connecting…'
: state.status == FormStatus.scanning
? 'Scanning…'
: 'Scan',
),
),
),
],
),
const SizedBox(height: 12),
// ── Reset ────────────────────────────────────────────────────────
if (state.status == FormStatus.scanned ||
state.status == FormStatus.verified ||
state.status == FormStatus.error)
TextButton.icon(
onPressed: () {
onClearControllers();
context.read<FingerprintBloc>().add(const ResetBlocEvent());
},
style: TextButton.styleFrom(foregroundColor: AppTheme.textMuted),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Reset'),
),
const SizedBox(height: 8),
// ── Verify button ────────────────────────────────────────────────
if (!isVerified)
FilledButton.icon(
onPressed: isVerifying
? null
: () => context.read<FingerprintBloc>().add(const VerifyFingerprintEvent()),
style: FilledButton.styleFrom(
backgroundColor: AppTheme.saudiGreen,
foregroundColor: Colors.white,
),
icon: isVerifying
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.verified_user_outlined),
label: Text(isVerifying ? 'Verifying…' : 'Verify Fingerprint'),
),
if (isVerified)
Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: AppTheme.saudiGreen.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppTheme.saudiGreen.withValues(alpha: 0.4)),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, color: AppTheme.saudiGreen, size: 20),
SizedBox(width: 8),
Text(
'Identity verified successfully',
style: TextStyle(color: AppTheme.saudiGreen, fontWeight: FontWeight.w700),
),
],
),
),
],
),
);
}
}
class _SectionLabel extends StatelessWidget {
final IconData icon;
final String text;
const _SectionLabel({required this.icon, required this.text});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, size: 16, color: AppTheme.cyan),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
color: AppTheme.cyan,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
],
);
}
}
// ── Searchable country dropdown ─────────────────────────────────────────────
class _SearchableCountryDropdown extends StatefulWidget {
final String? selectedCode;
final String? errorText;
final bool enabled;
final ValueChanged<String> onChanged;
const _SearchableCountryDropdown({
required this.onChanged,
this.selectedCode,
this.errorText,
this.enabled = true,
});
@override
State<_SearchableCountryDropdown> createState() => _SearchableCountryDropdownState();
}
class _SearchableCountryDropdownState extends State<_SearchableCountryDropdown> {
String? _displayLabel(String? code) {
if (code == null || code.isEmpty) return null;
final match = countryAlpha2List.firstWhere((c) => c['code'] == code, orElse: () => {});
if (match.isEmpty) return code;
return '${match['name']} (${match['code']})';
}
void _openSearch() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppTheme.inkRaised,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => _CountrySearchSheet(
selectedCode: widget.selectedCode,
onSelected: (code) {
Navigator.of(context).pop();
widget.onChanged(code);
},
),
);
}
@override
Widget build(BuildContext context) {
final label = _displayLabel(widget.selectedCode);
final hasError = widget.errorText != null;
return GestureDetector(
onTap: widget.enabled ? _openSearch : null,
child: InputDecorator(
decoration: InputDecoration(
labelText: 'Nationality',
hintText: 'Select country',
errorText: widget.errorText,
prefixIcon: const Icon(Icons.flag_outlined, color: AppTheme.textMuted),
suffixIcon: const Icon(Icons.arrow_drop_down, color: AppTheme.textMuted),
enabled: widget.enabled,
),
isEmpty: label == null,
child: Text(
label ?? '',
style: TextStyle(
fontSize: 16,
color: widget.enabled
? (hasError ? AppTheme.redAlert : AppTheme.textPrimary)
: AppTheme.textMuted,
),
overflow: TextOverflow.ellipsis,
),
),
);
}
}
// ── Country search bottom sheet ─────────────────────────────────────────────
class _CountrySearchSheet extends StatefulWidget {
final String? selectedCode;
final ValueChanged<String> onSelected;
const _CountrySearchSheet({required this.onSelected, this.selectedCode});
@override
State<_CountrySearchSheet> createState() => _CountrySearchSheetState();
}
class _CountrySearchSheetState extends State<_CountrySearchSheet> {
final _searchController = TextEditingController();
List<Map<String, String>> _filtered = List.from(countryAlpha2List);
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onSearch(String query) {
final lower = query.toLowerCase().trim();
setState(() {
_filtered = lower.isEmpty
? List.from(countryAlpha2List)
: countryAlpha2List
.where((c) =>
c['name']!.toLowerCase().contains(lower) ||
c['code']!.toLowerCase().contains(lower))
.toList();
});
}
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInsets.only(bottom: bottomInset),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: Column(
children: [
const SizedBox(height: 8),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.hairline,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Select Nationality',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppTheme.textPrimary),
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Search country…',
prefixIcon: const Icon(Icons.search, color: AppTheme.textMuted),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, color: AppTheme.textMuted),
onPressed: () {
_searchController.clear();
_onSearch('');
},
)
: null,
),
onChanged: _onSearch,
),
),
const SizedBox(height: 8),
const Divider(height: 1),
Expanded(
child: _filtered.isEmpty
? const Center(
child: Text('No countries found', style: TextStyle(color: AppTheme.textMuted)))
: ListView.builder(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
itemCount: _filtered.length,
itemBuilder: (_, index) {
final country = _filtered[index];
final code = country['code']!;
final name = country['name']!;
final isSelected = code == widget.selectedCode;
return ListTile(
dense: true,
leading: isSelected
? const Icon(Icons.check_circle, color: AppTheme.cyan, size: 20)
: const Icon(Icons.flag_outlined, color: AppTheme.textMuted, size: 20),
title: Text(
'$name ($code)',
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal,
color: isSelected ? AppTheme.cyan : AppTheme.textPrimary,
),
),
selected: isSelected,
onTap: () => widget.onSelected(code),
);
},
),
),
],
),
),
);
}
}
// ── Scan result card with image + chips ─────────────────────────────────────
class _ScanResultCard extends StatelessWidget {
final FingerprintFormState state;
const _ScanResultCard({required this.state});
static Uint8List _decodeBase64(String raw) {
final clean = raw.contains(',') ? raw.split(',').last : raw;
return base64Decode(clean);
}
@override
Widget build(BuildContext context) {
final isVerified = state.status == FormStatus.verified;
late final Uint8List imageBytes;
try {
imageBytes = _decodeBase64(state.base64Data!);
} catch (_) {
return const SizedBox.shrink();
}
final accent = isVerified ? AppTheme.saudiGreen : AppTheme.cyan;
return GlowPanel(
glowColor: accent,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.10),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Row(
children: [
Icon(isVerified ? Icons.verified : Icons.fingerprint, color: accent, size: 20),
const SizedBox(width: 8),
Text(
isVerified ? 'Fingerprint Verified' : 'Fingerprint Captured',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: accent),
),
if (isVerified) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.saudiGreen,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'VERIFIED',
style: TextStyle(
color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 0.8),
),
),
],
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
height: 220,
color: const Color(0xFF000000),
alignment: Alignment.center,
child: Image.memory(
imageBytes,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Center(
child: Text('Unable to render image', style: TextStyle(color: Colors.white54)),
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 18),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_InfoChip(
icon: Icons.badge_outlined,
label: state.documentType == DocumentType.nationalId ? 'National ID' : 'Passport',
),
_InfoChip(
icon: Icons.fingerprint,
label: fingerIndexMap[state.selectedFingerIndex] ?? 'Unknown',
),
if (state.documentType == DocumentType.nationalId)
_InfoChip(icon: Icons.numbers, label: state.nationalIdInput)
else ...[
_InfoChip(icon: Icons.flag_outlined, label: state.nationality),
_InfoChip(icon: Icons.book_outlined, label: state.passportNumber),
],
],
),
),
],
),
);
}
}
class _InfoChip extends StatelessWidget {
final IconData icon;
final String label;
const _InfoChip({required this.icon, required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.inkRaised2,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppTheme.hairline),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: AppTheme.cyan),
const SizedBox(width: 5),
Text(label, style: const TextStyle(fontSize: 12, color: AppTheme.textMuted)),
],
),
);
}
}
// ── Top device connection status bar ────────────────────────────────────────
class _DeviceStatusBar extends StatelessWidget {
final FingerprintFormState state;
const _DeviceStatusBar({required this.state});
@override
Widget build(BuildContext context) {
final cfg = _resolveConfig();
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
color: cfg.background,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: SafeArea(
top: false,
bottom: false,
child: Row(
children: [
cfg.showSpinner
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, valueColor: AlwaysStoppedAnimation<Color>(cfg.foreground)),
)
: Icon(cfg.icon, size: 16, color: cfg.foreground),
const SizedBox(width: 8),
Text(
cfg.label,
style: TextStyle(color: cfg.foreground, fontWeight: FontWeight.w700, fontSize: 13),
),
if (state.deviceStatusMessage != null) ...[
const SizedBox(width: 6),
Expanded(
child: Text(
' ${state.deviceStatusMessage}',
style: TextStyle(color: cfg.foreground.withValues(alpha: 0.85), fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
);
}
_StatusConfig _resolveConfig() {
final isConnecting = state.status == FormStatus.connecting;
if (isConnecting) {
return _StatusConfig(
background: AppTheme.amberAlert.withValues(alpha: 0.14),
foreground: AppTheme.amberAlert,
icon: Icons.sync,
showSpinner: true,
label: 'Connecting to device…',
);
}
if (state.isDeviceConnected) {
return _StatusConfig(
background: AppTheme.saudiGreen.withValues(alpha: 0.14),
foreground: AppTheme.saudiGreen,
icon: Icons.usb,
showSpinner: false,
label: 'Device connected',
);
}
if (state.status == FormStatus.error && state.deviceStatusMessage != null) {
return _StatusConfig(
background: AppTheme.redAlert.withValues(alpha: 0.14),
foreground: AppTheme.redAlert,
icon: Icons.usb_off,
showSpinner: false,
label: 'Device not connected',
);
}
return _StatusConfig(
background: AppTheme.inkRaised2,
foreground: AppTheme.textMuted,
icon: Icons.usb_off,
showSpinner: false,
label: 'Device disconnected',
);
}
}
class _StatusConfig {
final Color background;
final Color foreground;
final IconData icon;
final bool showSpinner;
final String label;
const _StatusConfig({
required this.background,
required this.foreground,
required this.icon,
required this.showSpinner,
required this.label,
});
}
// import 'dart:convert';
// import 'dart:typed_data';
// import 'package:flutter/material.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
// import '../bloc/fingerprint_bloc.dart';
// import '../utils/const.dart' show countryAlpha2List, fingerIndexMap;
// import 'auth_result_screen.dart';
//
// class FingerprintView extends StatefulWidget {
// const FingerprintView({super.key});
//
// @override
// State<FingerprintView> createState() => _FingerprintViewState();
// }
//
// class _FingerprintViewState extends State<FingerprintView> {
// final _nationalIdController = TextEditingController();
// final _nationalityController = TextEditingController();
// final _passportController = TextEditingController();
//
// @override
// void dispose() {
// _nationalIdController.dispose();
// _nationalityController.dispose();
// _passportController.dispose();
// super.dispose();
// }
//
// void _clearControllers() {
// _nationalIdController.clear();
// _nationalityController.clear();
// _passportController.clear();
// }
//
// @override
// Widget build(BuildContext context) {
// return BlocProvider(
// create: (_) => FingerprintBloc()..add(const AppStarted()),
// child: _buildScaffold(),
// );
// }
//
// Widget _buildScaffold() {
// return Scaffold(
// appBar: AppBar(title: const Text('Fingerprint Verification')),
// body: BlocConsumer<FingerprintBloc, FingerprintFormState>(
// listenWhen: (prev, curr) => prev.status != curr.status,
// listener: (context, state) {
// if (state.status == FormStatus.error && state.errorMessage != null) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text(state.errorMessage!)),
// );
// } else if (state.status == FormStatus.scanned) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Fingerprint scanned successfully!')),
// );
// } else if (state.status == FormStatus.verified &&
// state.authResult != null) {
// // Navigate to the result screen — keep the fingerprint view in the
// // back-stack so the user can return with the AppBar back arrow or
// // the "Return to Fingerprint" button.
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (_) => AuthResultScreen(result: state.authResult!),
// ),
// );
// } else if (state.status == FormStatus.idle) {
// _clearControllers();
// }
// },
// builder: (context, state) {
// final isBusy = state.status == FormStatus.scanning ||
// state.status == FormStatus.connecting ||
// state.status == FormStatus.verifying;
//
// return (state.status == FormStatus.loggingIn)?SizedBox(
// height: MediaQuery.sizeOf(context).height,
// child: Center(
// child: Column(
// children: [
// CircularProgressIndicator(
// ),
// Text("Logging in"),
// ],
// ),
// ),
// ):Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// // ── Top device connection status bar ───────────────────────
// _DeviceStatusBar(state: state),
//
// // ── Scrollable form body ───────────────────────────────────
// Expanded(
// child: SingleChildScrollView(
// padding: const EdgeInsets.all(16),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// // ── Document Type ────────────────────────────────
// DropdownButtonFormField<DocumentType>(
// initialValue: state.documentType,
// decoration: InputDecoration(
// labelText: 'Document Type',
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8)),
// ),
// items: const [
// DropdownMenuItem(
// value: DocumentType.nationalId,
// child: Text('National ID'),
// ),
// DropdownMenuItem(
// value: DocumentType.passport,
// child: Text('Passport'),
// ),
// ],
// onChanged: isBusy
// ? null
// : (val) {
// if (val != null) {
// context
// .read<FingerprintBloc>()
// .add(DocumentTypeChanged(val));
// }
// },
// ),
// const SizedBox(height: 16),
//
// // ── Finger Selection ──────────────────────────────
// DropdownButtonFormField<int>(
// value: state.selectedFingerIndex,
// decoration: InputDecoration(
// labelText: 'Finger',
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8)),
// ),
// items: fingerIndexMap.entries
// .map((e) => DropdownMenuItem<int>(
// value: e.key,
// child: Text(e.value),
// ))
// .toList(),
// onChanged: isBusy
// ? null
// : (val) {
// if (val != null) {
// context
// .read<FingerprintBloc>()
// .add(FingerTypeChanged(val));
// }
// },
// ),
// const SizedBox(height: 16),
//
// // ── Conditional form fields ───────────────────────
// if (state.documentType == DocumentType.nationalId) ...[
// TextField(
// controller: _nationalIdController,
// keyboardType: TextInputType.number,
// enabled: !isBusy,
// maxLength: 10,
// decoration: InputDecoration(
// labelText: 'National ID',
// hintText: '10-digit number starting with 1 or 2',
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8)),
// errorText: state.nationalIdError,
// ),
// onChanged: (val) => context
// .read<FingerprintBloc>()
// .add(NationalIdChanged(val)),
// ),
// ] else ...[
// _SearchableCountryDropdown(
// selectedCode: state.nationality.isEmpty ? null : state.nationality,
// errorText: state.nationalityError,
// enabled: !isBusy,
// onChanged: (code) => context
// .read<FingerprintBloc>()
// .add(NationalityChanged(code)),
// ),
//
// const SizedBox(height: 12),
// TextField(
// controller: _passportController,
// enabled: !isBusy,
// textCapitalization: TextCapitalization.characters,
// decoration: InputDecoration(
// labelText: 'Passport Number',
// hintText: '69 alphanumeric characters',
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8)),
// errorText: state.passportNumberError,
// ),
// onChanged: (val) => context
// .read<FingerprintBloc>()
// .add(PassportNumberChanged(val)),
// ),
// ],
// const SizedBox(height: 24),
//
// // ── Action buttons ────────────────────────────────
// Row(
// children: [
// Expanded(
// child: ElevatedButton.icon(
// onPressed: isBusy
// ? null
// : () => context
// .read<FingerprintBloc>()
// .add(const ConnectDevice()),
// icon: state.status == FormStatus.connecting &&
// !state.isScanConnecting
// ? const SizedBox(
// width: 16,
// height: 16,
// child: CircularProgressIndicator(
// strokeWidth: 2),
// )
// : const Icon(Icons.usb),
// label: Text(
// state.status == FormStatus.connecting &&
// !state.isScanConnecting
// ? 'Connecting…'
// : 'Connect',
// ),
// ),
// ),
// const SizedBox(width: 12),
// Expanded(
// child: ElevatedButton.icon(
// onPressed: isBusy
// ? null
// : () => context
// .read<FingerprintBloc>()
// .add(const ScanFingerprintEvent()),
// icon: state.isScanConnecting ||
// state.status == FormStatus.scanning
// ? const SizedBox(
// width: 16,
// height: 16,
// child: CircularProgressIndicator(
// strokeWidth: 2),
// )
// : const Icon(Icons.fingerprint),
// label: Text(
// state.isScanConnecting
// ? 'Connecting…'
// : state.status == FormStatus.scanning
// ? 'Scanning…'
// : 'Scan',
// ),
// ),
// ),
// ],
// ),
// const SizedBox(height: 12),
//
// // ── Reset button ──────────────────────────────────
// if (state.status == FormStatus.scanned ||
// state.status == FormStatus.verified ||
// state.status == FormStatus.error)
// ElevatedButton(
// onPressed: () {
// _clearControllers();
// context
// .read<FingerprintBloc>()
// .add(const ResetBlocEvent());
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.grey),
// child: const Text('Reset'),
// ),
// const SizedBox(height: 24),
//
// // ── Scan result ───────────────────────────────────
// if ((state.status == FormStatus.scanned ||
// state.status == FormStatus.verifying ||
// state.status == FormStatus.verified) &&
// state.base64Data != null)
// _ScanResultCard(state: state),
// ],
// ),
// ),
// ),
// ],
// );
// },
// ),
// );
// }
// }
//
// // ── Searchable country dropdown ────────────────────────────────────────────────
//
// class _SearchableCountryDropdown extends StatefulWidget {
// final String? selectedCode;
// final String? errorText;
// final bool enabled;
// final ValueChanged<String> onChanged;
//
// const _SearchableCountryDropdown({
// required this.onChanged,
// this.selectedCode,
// this.errorText,
// this.enabled = true,
// });
//
// @override
// State<_SearchableCountryDropdown> createState() =>
// _SearchableCountryDropdownState();
// }
//
// class _SearchableCountryDropdownState
// extends State<_SearchableCountryDropdown> {
// String? _displayLabel(String? code) {
// if (code == null || code.isEmpty) return null;
// final match = countryAlpha2List.firstWhere(
// (c) => c['code'] == code,
// orElse: () => {},
// );
// if (match.isEmpty) return code;
// return '${match['name']} (${match['code']})';
// }
//
// void _openSearch() {
// showModalBottomSheet(
// context: context,
// isScrollControlled: true,
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
// ),
// builder: (_) => _CountrySearchSheet(
// selectedCode: widget.selectedCode,
// onSelected: (code) {
// Navigator.of(context).pop();
// widget.onChanged(code);
// },
// ),
// );
// }
//
// @override
// Widget build(BuildContext context) {
// final label = _displayLabel(widget.selectedCode);
// final hasError = widget.errorText != null;
//
// return GestureDetector(
// onTap: widget.enabled ? _openSearch : null,
// child: InputDecorator(
// decoration: InputDecoration(
// labelText: 'Nationality',
// hintText: 'Select country',
// errorText: widget.errorText,
// border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
// suffixIcon: const Icon(Icons.arrow_drop_down),
// enabled: widget.enabled,
// errorBorder: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8),
// borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
// ),
// ),
// isEmpty: label == null,
// child: Text(
// label ?? '',
// style: TextStyle(
// fontSize: 16,
// color: widget.enabled
// ? (hasError
// ? Theme.of(context).colorScheme.error
// : Theme.of(context).textTheme.bodyLarge?.color)
// : Theme.of(context).disabledColor,
// ),
// overflow: TextOverflow.ellipsis,
// ),
// ),
// );
// }
// }
//
// // ── Country search bottom sheet ────────────────────────────────────────────────
//
// class _CountrySearchSheet extends StatefulWidget {
// final String? selectedCode;
// final ValueChanged<String> onSelected;
//
// const _CountrySearchSheet({
// required this.onSelected,
// this.selectedCode,
// });
//
// @override
// State<_CountrySearchSheet> createState() => _CountrySearchSheetState();
// }
//
// class _CountrySearchSheetState extends State<_CountrySearchSheet> {
// final _searchController = TextEditingController();
// List<Map<String, String>> _filtered = List.from(countryAlpha2List);
//
// @override
// void dispose() {
// _searchController.dispose();
// super.dispose();
// }
//
// void _onSearch(String query) {
// final lower = query.toLowerCase().trim();
// setState(() {
// _filtered = lower.isEmpty
// ? List.from(countryAlpha2List)
// : countryAlpha2List
// .where((c) =>
// c['name']!.toLowerCase().contains(lower) ||
// c['code']!.toLowerCase().contains(lower))
// .toList();
// });
// }
//
// @override
// Widget build(BuildContext context) {
// final bottomInset = MediaQuery.of(context).viewInsets.bottom;
//
// return Padding(
// padding: EdgeInsets.only(bottom: bottomInset),
// child: SizedBox(
// height: MediaQuery.of(context).size.height * 0.75,
// child: Column(
// children: [
// // ── Handle ──────────────────────────────────────────────────
// const SizedBox(height: 8),
// Container(
// width: 40,
// height: 4,
// decoration: BoxDecoration(
// color: Colors.grey.shade300,
// borderRadius: BorderRadius.circular(2),
// ),
// ),
// const SizedBox(height: 12),
//
// // ── Title ────────────────────────────────────────────────────
// const Padding(
// padding: EdgeInsets.symmetric(horizontal: 16),
// child: Align(
// alignment: Alignment.centerLeft,
// child: Text(
// 'Select Nationality',
// style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
// ),
// ),
// ),
// const SizedBox(height: 10),
//
// // ── Search field ─────────────────────────────────────────────
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 16),
// child: TextField(
// controller: _searchController,
// autofocus: true,
// decoration: InputDecoration(
// hintText: 'Search country…',
// prefixIcon: const Icon(Icons.search),
// suffixIcon: _searchController.text.isNotEmpty
// ? IconButton(
// icon: const Icon(Icons.clear),
// onPressed: () {
// _searchController.clear();
// _onSearch('');
// },
// )
// : null,
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8)),
// contentPadding: const EdgeInsets.symmetric(
// vertical: 10, horizontal: 12),
// ),
// onChanged: _onSearch,
// ),
// ),
// const SizedBox(height: 8),
// const Divider(height: 1),
//
// // ── List ─────────────────────────────────────────────────────
// Expanded(
// child: _filtered.isEmpty
// ? const Center(child: Text('No countries found'))
// : ListView.builder(
// keyboardDismissBehavior:
// ScrollViewKeyboardDismissBehavior.onDrag,
// itemCount: _filtered.length,
// itemBuilder: (_, index) {
// final country = _filtered[index];
// final code = country['code']!;
// final name = country['name']!;
// final isSelected = code == widget.selectedCode;
//
// return ListTile(
// dense: true,
// leading: isSelected
// ? const Icon(Icons.check_circle,
// color: Colors.indigo, size: 20)
// : const Icon(Icons.flag_outlined,
// color: Colors.grey, size: 20),
// title: Text(
// '$name ($code)',
// style: TextStyle(
// fontSize: 14,
// fontWeight: isSelected
// ? FontWeight.w600
// : FontWeight.normal,
// color: isSelected ? Colors.indigo : null,
// ),
// ),
// selected: isSelected,
// onTap: () => widget.onSelected(code),
// );
// },
// ),
// ),
// ],
// ),
// ),
// );
// }
// }
//
// // ── Scan result card with image + verify button ────────────────────────────
//
// class _ScanResultCard extends StatelessWidget {
// final FingerprintFormState state;
// const _ScanResultCard({required this.state});
//
// /// Strips the optional data-URI header (data:image/png;base64,...) and
// /// returns raw bytes ready for [Image.memory].
// static Uint8List _decodeBase64(String raw) {
// final clean = raw.contains(',') ? raw.split(',').last : raw;
// return base64Decode(clean);
// }
//
// @override
// Widget build(BuildContext context) {
// final isVerifying = state.status == FormStatus.verifying;
// final isVerified = state.status == FormStatus.verified;
//
// late final Uint8List imageBytes;
// try {
// imageBytes = _decodeBase64(state.base64Data!);
// } catch (_) {
// return const SizedBox.shrink();
// }
//
// return Container(
// decoration: BoxDecoration(
// color: isVerified ? Colors.green.shade50 : Colors.grey.shade50,
// borderRadius: BorderRadius.circular(12),
// border: Border.all(
// color: isVerified ? Colors.green.shade400 : Colors.grey.shade300,
// width: 1.5,
// ),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// // ── Header ──────────────────────────────────────────────────
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
// decoration: BoxDecoration(
// color: isVerified ? Colors.green.shade100 : Colors.blueGrey.shade50,
// borderRadius: const BorderRadius.vertical(top: Radius.circular(10)),
// ),
// child: Row(
// children: [
// Icon(
// isVerified ? Icons.verified : Icons.fingerprint,
// color: isVerified ? Colors.green.shade700 : Colors.blueGrey,
// size: 20,
// ),
// const SizedBox(width: 8),
// Text(
// isVerified ? 'Fingerprint Verified' : 'Fingerprint Captured',
// style: TextStyle(
// fontSize: 15,
// fontWeight: FontWeight.bold,
// color: isVerified
// ? Colors.green.shade800
// : Colors.blueGrey.shade800,
// ),
// ),
// if (isVerified) ...[
// const Spacer(),
// Container(
// padding:
// const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
// decoration: BoxDecoration(
// color: Colors.green.shade600,
// borderRadius: BorderRadius.circular(20),
// ),
// child: const Text(
// 'VERIFIED',
// style: TextStyle(
// color: Colors.white,
// fontSize: 11,
// fontWeight: FontWeight.bold,
// letterSpacing: 0.8),
// ),
// ),
// ],
// ],
// ),
// ),
//
// // ── Fingerprint image ────────────────────────────────────────
// Padding(
// padding: const EdgeInsets.all(16),
// child: ClipRRect(
// borderRadius: BorderRadius.circular(8),
// child: Container(
// height: 200,
// color: Colors.black,
// alignment: Alignment.center,
// child: Image.memory(
// imageBytes,
// fit: BoxFit.contain,
// errorBuilder: (_, __, ___) => const Center(
// child: Text(
// 'Unable to render image',
// style: TextStyle(color: Colors.white54),
// ),
// ),
// ),
// ),
// ),
// ),
//
// // ── Document info row ────────────────────────────────────────
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
// child: Wrap(
// spacing: 8,
// runSpacing: 6,
// children: [
// _InfoChip(
// icon: Icons.badge_outlined,
// label: state.documentType == DocumentType.nationalId
// ? 'National ID'
// : 'Passport',
// ),
// _InfoChip(
// icon: Icons.fingerprint,
// label: fingerIndexMap[state.selectedFingerIndex] ?? 'Unknown',
// ),
// if (state.documentType == DocumentType.nationalId)
// _InfoChip(
// icon: Icons.numbers,
// label: state.nationalIdInput,
// )
// else ...[
// _InfoChip(
// icon: Icons.flag_outlined,
// label: state.nationality,
// ),
// _InfoChip(
// icon: Icons.book_outlined,
// label: state.passportNumber,
// ),
// ],
// ],
// ),
// ),
//
// const SizedBox(height: 16),
//
// // ── Verify button ────────────────────────────────────────────
// if (!isVerified)
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
// child: ElevatedButton.icon(
// onPressed: isVerifying
// ? null
// : () => context
// .read<FingerprintBloc>()
// .add(const VerifyFingerprintEvent()),
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.indigo,
// foregroundColor: Colors.white,
// padding: const EdgeInsets.symmetric(vertical: 14),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8)),
// ),
// icon: isVerifying
// ? const SizedBox(
// width: 18,
// height: 18,
// child: CircularProgressIndicator(
// strokeWidth: 2,
// valueColor:
// AlwaysStoppedAnimation<Color>(Colors.white),
// ),
// )
// : const Icon(Icons.verified_user_outlined),
// label: Text(
// isVerifying ? 'Verifying…' : 'Verify Fingerprint',
// style: const TextStyle(
// fontSize: 15, fontWeight: FontWeight.w600),
// ),
// ),
// ),
//
// if (isVerified)
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
// child: Row(
// children: [
// const Icon(Icons.check_circle,
// color: Colors.green, size: 20),
// const SizedBox(width: 8),
// const Text(
// 'Identity verified successfully',
// style: TextStyle(
// color: Colors.green,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// );
// }
// }
//
// class _InfoChip extends StatelessWidget {
// final IconData icon;
// final String label;
// const _InfoChip({required this.icon, required this.label});
//
// @override
// Widget build(BuildContext context) {
// return Container(
// padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
// decoration: BoxDecoration(
// color: Colors.blueGrey.shade50,
// borderRadius: BorderRadius.circular(20),
// border: Border.all(color: Colors.blueGrey.shade200),
// ),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Icon(icon, size: 14, color: Colors.blueGrey),
// const SizedBox(width: 4),
// Text(
// label,
// style: TextStyle(fontSize: 12, color: Colors.blueGrey.shade700),
// ),
// ],
// ),
// );
// }
// }
//
// // ── Top connection status bar ──────────────────────────────────────────────────
//
// class _DeviceStatusBar extends StatelessWidget {
// final FingerprintFormState state;
// const _DeviceStatusBar({required this.state});
//
// @override
// Widget build(BuildContext context) {
// final _StatusConfig cfg = _resolveConfig();
//
// return AnimatedContainer(
// duration: const Duration(milliseconds: 300),
// color: cfg.background,
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
// child: SafeArea(
// top: false,
// bottom: false,
// child: Row(
// children: [
// // Icon / spinner
// cfg.showSpinner
// ? SizedBox(
// width: 16,
// height: 16,
// child: CircularProgressIndicator(
// strokeWidth: 2,
// valueColor:
// AlwaysStoppedAnimation<Color>(cfg.foreground),
// ),
// )
// : Icon(cfg.icon, size: 16, color: cfg.foreground),
// const SizedBox(width: 8),
// // Primary label
// Text(
// cfg.label,
// style: TextStyle(
// color: cfg.foreground,
// fontWeight: FontWeight.w600,
// fontSize: 13,
// ),
// ),
// // Live status message (during connect/retry)
// if (state.deviceStatusMessage != null) ...[
// const SizedBox(width: 6),
// Expanded(
// child: Text(
// ' ${state.deviceStatusMessage}',
// style: TextStyle(
// color: cfg.foreground.withValues(alpha: 0.85),
// fontSize: 12,
// ),
// overflow: TextOverflow.ellipsis,
// ),
// ),
// ],
// ],
// ),
// ),
// );
// }
//
// _StatusConfig _resolveConfig() {
// final isConnecting = state.status == FormStatus.connecting;
//
// if (isConnecting) {
// return _StatusConfig(
// background: const Color(0xFFFFF3CD), // amber-50
// foreground: const Color(0xFF7B5800),
// icon: Icons.sync,
// showSpinner: true,
// label: 'Connecting to device…',
// );
// }
//
// if (state.isDeviceConnected) {
// return _StatusConfig(
// background: const Color(0xFFD4EDDA), // green-50
// foreground: const Color(0xFF155724),
// icon: Icons.usb,
// showSpinner: false,
// label: 'Device connected',
// );
// }
//
// if (state.status == FormStatus.error &&
// state.deviceStatusMessage != null) {
// return _StatusConfig(
// background: const Color(0xFFF8D7DA), // red-50
// foreground: const Color(0xFF721C24),
// icon: Icons.usb_off,
// showSpinner: false,
// label: 'Device not connected',
// );
// }
//
// // Default: idle / not yet attempted
// return _StatusConfig(
// background: const Color(0xFFF0F0F0),
// foreground: const Color(0xFF555555),
// icon: Icons.usb_off,
// showSpinner: false,
// label: 'Device disconnected',
// );
// }
// }
//
// class _StatusConfig {
// final Color background;
// final Color foreground;
// final IconData icon;
// final bool showSpinner;
// final String label;
//
// const _StatusConfig({
// required this.background,
// required this.foreground,
// required this.icon,
// required this.showSpinner,
// required this.label,
// });
// }