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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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