|
|
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: '6–9 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: '6–9 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,
|
|
|
// });
|
|
|
// }
|