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.

428 lines
380 KiB
Dart

1 month ago
// ignore_for_file: unused_import
import 'dart:convert';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:finger_print_verification/core/network/network.dart';
import 'package:finger_print_verification/fingerprint/utils/finger_print_quality_helper.dart';
import '../models/auth_response_model.dart';
import '../models/auth_result_ui_model.dart';
import '../models/fingerprint_verify_response_model.dart';
import '../models/login_response_model.dart';
import '../utils/zkt_finger_print_scanner.dart';
part 'fingerprint_event.dart';
part 'fingerprint_state.dart';
// ── Credentials ────────────────────────────────────────────────────────────
/// TODO: move to a secure config / env file before shipping.
const _kLoginUsername = 'admin';
const _kLoginPassword = 'Admin@123';
class FingerprintBloc extends Bloc<FingerprintEvent, FingerprintFormState> {
final ZktFingerPrintScanner _fingerprintScanner;
FingerprintBloc({ZktFingerPrintScanner? fingerprintScanner})
: _fingerprintScanner = fingerprintScanner ?? ZktFingerPrintScanner(),
super(const FingerprintFormState()) {
on<AppStarted>(_onAppStarted);
on<DocumentTypeChanged>(_onDocumentTypeChanged);
on<FingerTypeChanged>(_onFingerTypeChanged);
on<NationalIdChanged>(_onNationalIdChanged);
on<NationalityChanged>(_onNationalityChanged);
on<PassportNumberChanged>(_onPassportNumberChanged);
on<ConnectDevice>(_onConnectDevice);
on<ScanFingerprintEvent>(_onScanFingerprint);
on<ResetBlocEvent>(_onResetBloc);
on<SendFingerprintEvent>(_onSendFingerprint);
on<VerifyFingerprintEvent>(_onVerifyFingerprint);
}
// ─── App startup: login ───────────────────────────────────────────────────
/// Called once at app startup to obtain the bearer token.
/// Runs silently in the background — the user sees no loading indicator.
Future<void> _onAppStarted(
AppStarted event,
Emitter<FingerprintFormState> emit,
) async {
emit(state.copyWith(status: FormStatus.loggingIn));
final result = await ApiClient.post<LoginResponseModel>(
url: 'api/Auth/login',
shouldKeepToken: false, // non-token endpoint — we're acquiring the token
body: {
'username': _kLoginUsername,
'password': _kLoginPassword,
},
fromJson: (json) =>
LoginResponseModel.fromJson(json as Map<String, dynamic>),
);
switch (result) {
case ApiSuccess(:final data):
if (data.success && (data.data?.token.isNotEmpty ?? false)) {
// Store the token so future calls can attach it automatically.
TokenManager.setToken(data.data!.token);
emit(state.copyWith(status: FormStatus.idle, clearError: true));
} else {
// Login returned success=false — surface the message but don't block UI.
emit(state.copyWith(
status: FormStatus.idle,
errorMessage: data.message.isNotEmpty
? data.message
: 'Login failed. Please restart the app.',
));
}
case ApiFailure(:final code, :final message):
if (code == -1) {
// Cancelled (e.g. BLoC closed during startup) — ignore silently.
emit(state.copyWith(status: FormStatus.idle));
return;
}
emit(state.copyWith(
status: FormStatus.idle,
errorMessage: 'Background login failed: $message',
));
}
}
// ─── Dropdown / field handlers ────────────────────────────────────────────
void _onDocumentTypeChanged(
DocumentTypeChanged event,
Emitter<FingerprintFormState> emit,
) {
emit(state.copyWith(
documentType: event.documentType,
status: FormStatus.idle,
clearError: true,
clearNationalIdError: true,
clearNationalityError: true,
clearPassportNumberError: true,
));
}
void _onFingerTypeChanged(
FingerTypeChanged event,
Emitter<FingerprintFormState> emit,
) {
emit(state.copyWith(selectedFingerIndex: event.fingerIndex));
}
void _onNationalIdChanged(
NationalIdChanged event,
Emitter<FingerprintFormState> emit,
) {
emit(state.copyWith(nationalIdInput: event.value, clearNationalIdError: true));
}
void _onNationalityChanged(
NationalityChanged event,
Emitter<FingerprintFormState> emit,
) {
emit(state.copyWith(nationality: event.value, clearNationalityError: true));
}
void _onPassportNumberChanged(
PassportNumberChanged event,
Emitter<FingerprintFormState> emit,
) {
emit(state.copyWith(
passportNumber: event.value, clearPassportNumberError: true));
}
// ─── Validation ───────────────────────────────────────────────────────────
/// 10 digits, must start with 1 or 2.
String? _validateNationalId(String value) {
if (value.isEmpty) return 'National ID is required';
if (!RegExp(r'^[12]\d{9}$').hasMatch(value)) {
return 'Must be 10 digits and start with 1 or 2';
}
return null;
}
/// Non-empty string.
String? _validateNationality(String value) {
if (value.trim().isEmpty) return 'Nationality is required';
return null;
}
/// 69 alphanumeric characters (standard passport format).
String? _validatePassportNumber(String value) {
if (value.isEmpty) return 'Passport number is required';
if (!RegExp(r'^[A-Za-z0-9]{6,9}$').hasMatch(value)) {
return 'Must be 69 alphanumeric characters';
}
return null;
}
/// Returns a state with field errors if invalid, null if everything is OK.
FingerprintFormState? _buildValidationErrorState() {
if (state.documentType == DocumentType.nationalId) {
final error = _validateNationalId(state.nationalIdInput);
if (error != null) return state.copyWith(nationalIdError: error);
} else {
final natError = _validateNationality(state.nationality);
final passError = _validatePassportNumber(state.passportNumber);
if (natError != null || passError != null) {
return state.copyWith(
nationalityError: natError,
passportNumberError: passError,
);
}
}
return null;
}
// ─── Device / scan handlers ───────────────────────────────────────────────
Future<void> _onConnectDevice(
ConnectDevice event,
Emitter<FingerprintFormState> emit,
) async {
emit(state.copyWith(
status: FormStatus.connecting,
isDeviceConnected: false,
isScanConnecting: false,
clearError: true,
clearDeviceStatus: true,
deviceStatusMessage: 'Connecting to device…',
));
try {
final connectFuture = _fingerprintScanner.connect();
int polls = 0;
const maxPolls = 50;
while (polls < maxPolls) {
final done = await Future.any([
connectFuture.then((_) => true).catchError((_) => false),
Future.delayed(const Duration(seconds: 1)).then((_) => false),
]);
if (done) break;
polls++;
final statusMsg = _fingerprintScanner.lastStatus?.message;
if (statusMsg != null) emit(state.copyWith(deviceStatusMessage: statusMsg));
}
await connectFuture;
emit(state.copyWith(
status: FormStatus.idle,
isDeviceConnected: true,
deviceStatusMessage:
'Device connected ${_fingerprintScanner.lastStatus?.message ?? "OK"}',
));
} catch (e) {
emit(state.copyWith(
status: FormStatus.error,
isDeviceConnected: false,
errorMessage: e.toString().replaceFirst('Exception: ', ''),
deviceStatusMessage:
_fingerprintScanner.lastStatus?.message ?? 'No status received',
));
}
}
Future<void> _onScanFingerprint(
ScanFingerprintEvent event,
Emitter<FingerprintFormState> emit,
) async {
final validationError = _buildValidationErrorState();
if (validationError != null) {
emit(validationError);
return;
}
try {
if (!_fingerprintScanner.isConnected) {
emit(state.copyWith(
status: FormStatus.connecting,
isScanConnecting: true,
isDeviceConnected: false,
clearError: true,
deviceStatusMessage: 'Connecting to device…',
));
final connectFuture = _fingerprintScanner.connect();
int polls = 0;
const maxPolls = 50;
while (polls < maxPolls) {
final done = await Future.any([
connectFuture.then((_) => true).catchError((_) => false),
Future.delayed(const Duration(seconds: 1)).then((_) => false),
]);
if (done) break;
polls++;
final statusMsg = _fingerprintScanner.lastStatus?.message;
if (statusMsg != null) emit(state.copyWith(deviceStatusMessage: statusMsg));
}
await connectFuture;
}
emit(state.copyWith(
status: FormStatus.scanning,
isScanConnecting: false,
isDeviceConnected: true,
clearError: true,
deviceStatusMessage: 'Connected',
));
try {
final base64Data = await _fingerprintScanner.scan();
if (base64Data == null || base64Data.isEmpty) {
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'No fingerprint data captured'));
return;
}
emit(state.copyWith(
status: FormStatus.scanned,
base64Data: base64Data,
isScanConnecting: false,
isDeviceConnected: false,
deviceStatusMessage: 'Disconnected',
));
} catch (e) {
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'Capture failed: ${e.toString()}'));
}
} catch (e) {
emit(state.copyWith(
status: FormStatus.error,
isScanConnecting: false,
isDeviceConnected: false,
errorMessage: e.toString().replaceFirst('Exception: ', ''),
deviceStatusMessage:
_fingerprintScanner.lastStatus?.message ?? 'No status received',
));
} finally {
try {
await _fingerprintScanner.disconnect();
} catch (_) {}
}
}
Future<void> _onResetBloc(
ResetBlocEvent event,
Emitter<FingerprintFormState> emit,
) async {
emit(const FingerprintFormState(
isDeviceConnected: false, isScanConnecting: false));
}
Future<void> _onSendFingerprint(
SendFingerprintEvent event,
Emitter<FingerprintFormState> emit,
) async {
try {
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(
status: FormStatus.idle, clearBase64: true, clearError: true));
} catch (e) {
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'Failed to send fingerprint: ${e.toString()}'));
}
}
// ─── Verify fingerprint ───────────────────────────────────────────────────
Future<void> _onVerifyFingerprint(
VerifyFingerprintEvent event,
Emitter<FingerprintFormState> emit,
) async {
// FingerprintValidation validation = validateFingerprintBase64(
// state.base64Data!.split(",").last,
// );
// if(validation.isComplete == false)
// {
// emit(state.copyWith(
// status: FormStatus.error,
// errorMessage: validation.reason));
// return;
// }
//
// if (state.base64Data == null || state.base64Data!.isEmpty) {
// emit(state.copyWith(
// status: FormStatus.error,
// errorMessage: 'No fingerprint data to verify'));
// return;
// }
emit(state.copyWith(status: FormStatus.verifying, clearError: true));
try {
final FingerprintVerifyResponseModel responseModel;
if (state.documentType == DocumentType.nationalId) {
final result = await ApiClient.post<FingerprintVerifyResponseModel>(
url: 'api/Fingerprint/authenticate',
shouldKeepToken: true,
body: {
'personId': state.nationalIdInput,
'fingerType': state.selectedFingerIndex,
'finger': "iVBORw0KGgoAAAANSUhEUgAAASwAAAF3CAIAAACQVuEWAAAAAXNSR0IArs4c6QAAAANzQklUBQYFMwuNgAAAIABJREFUeJyMvf2SJMlxJ/armsgaj57M3UxsF9AJ7oBbOMwCDZGmW0hnEmgyyu5e4u5hzvRIpwc4Gf8gZZQZIR1oXPIG4gzZI84QVdwubAamE52+PTG1+sPjwyOzhsfcttmqrMz48PBv9/BYTdMEAAYA4AGP8FU+xIvBBGJweMyADMkzDJY74V/kn5YtzJv68CXPEIg968ZzL6A0VAaTIfa5Zd1Xmk4aKhkKTUHNd/bv7EqQwQegJCNhzkAA2LN8TqPK0/fQP4WB6QdiR8vXNTwzhGPXup00Nvm1gI+BQCx/0M0mwC6aSvcDSBO0Pc+mMMME9ixzKQbmi4nMlux8jxFW4fXUi17BslMsEDX3ssSuEqPSwKCxDmVfCWH8fFIBDlAPxOflsVUgQiyQTCGuBrH8OkMLlNeS2Oa4Vb6YkdLMvyZI6Tv6xQSdWe96efQU0noUK50gIss5o7QlfFAAZ0b2eeSIr8w4WoLkubnrGc1+nYMozVdAlLhVZAGhu0jw4XXNwpbErJBjTkUaJSJgi6YUjS15RELN2fIVIIrTzAuEBTV6Jip5aNnLcmlma0qG5osyQ4DFfcF5fbO4llzbl4Cd8ZTylXWevFGrFX/WfJEMFfiN+DUuMIHCv/pFLYIAAPKiTDi1mYebHqPcflh1U7QgIJafQiMm3JQ5y3jSROSBJJqS0MiIi/Crhskcyr5AlzT4hDHyocC/cgwoKSQMO8FWTbBYSMbEUwL1nPElEMXWsLgmPw08TDyBiwlqamRwgI9alNzXgk4EyGfnNRMgCQHCB69AtBBcBegU4yhUsLQiRv0hPy9LHFDFRMw0C1pSS1xIVDUGaSHglQaFL0erf0qM0pTjPPfkGpRHnyWez3ipQaOJmz0HzCYloBUfDUMxJSVompcJpJElhFDLLH9pAAn/BF00nRc9quEhkT04rU1e9bScqZE4hQAHFBImDyYu9gxHNQPOg4/8WEaVdVHkoaZ20isCumEc9sf94AZmBjCNU14/RQOJbDR9SrPTOE08uaML5KcYblrWogW9Lr5clHRHmDIFsg+dmjm7zKwBBYM4o55BKS9pESM/LUhx9qJXf4rIM3lEzDwjnPXiKjycd5e4ldLI5Mqqk+LOAffiKwWylW3KT6vpXamO6g/Iz+GcUUeGmDkhfVaWDOlXZqSLuGxFawvbKVBpKehnmg8+cGlts9A8Sw535ms597lm/wGTbKaOzlX0ZcsavBHvg5SeKaUegxv2+701lojIRv3AAwbW2DPzWsBqGIebFzcw6Lc9DLq60xiWNfZkJ5eLrmG1ROIwi4XGUYxqptCe1Tn1miazSpnuS924sGOXarPqK9jDmmDKYZ+5OQPs0lib2S/LpvTzs8/ltSBCX8JRXWfMNv1iHFxBYKUNNuMi6dJLqxVXfMCRsDQvZz+dmeiH9PgS9IUB7BHQXb2uNbSz1qm2Q7RvJvU48QQDC3uGtS9gMvDg9m5/3AMgoq7uqCFh823dyvACKZ6dbITk/rDnO756emXJpvlmCRZ71IqlVogCfEhJxRLLE9lgRqiKD2ZSSTeX7rFy2HIFLNJLrJyCBbc1JY9WoqnQftO14I//7VHN6Oocv0iPaVGhKbYAlNFEqJVbLUPU+PTok32Vh0gAL7Rzj4IOo+2Oc06diSdrIpbMXCygJTSTjhqmelbQneVb+pkSLmGcrHwnpqCruTCMsAoPl+IxvM48jVMwomqaSbAlAWtGsD/u+Y4nnrq2I0vsmScWWPXbnurCvWRhJ6+IPE5wf9y3l62Fzeu70C9m6zv5iVlZ8kSW7Fz7WEp4BY3ME8uFOEPA6jpDBrqj2RJrNeRDrc2Mzw9LpDMdfUimLahj3r4epJb8M9kOQiEJEUnon2Uk52VafFL/pFGqcBPrwc0myYrdljNf8uyMEIobFWrqUsEou85wXBKhdEihO+jFXsA9Q8ODKHI7LrigoLW8YslqiOn2NDEnHjfx5G5du22Z2d06aceSveqv5greWNi9We7FiU9+CjelF+0r1kPliZn3w/7Vy1efffpZt+2IRB22xbAXCzR3Ly+1u6V6dc6hPQOv1lrPEMZZDXP2jIYDlCw6R3Uz3jGT4WcmJcuNudNVI1i6H3hxEkWG1vkNr/5NSlQ0zFi5zgorSwMl0t6MGXAMucwdNhpSHgCozp49BosrIjyP0lmKgqOkBufNmsVK4NxSncOMgh0o16t2oohmCHF4KH0+WXfhYQMA1lhLNtOAxgksZmqiG8PAku2f9vLr5CehLmoIYhYi9M4js2dmfv78+c3NjRsDuRaXCS0kbpuWTAOcmd2de/XyFTwOhwNPwYsTGuToe/CRB4kf0gRVOYM0LdAMVRRUEygCevwzyxRhlS3V2VUSwJlf4zKF0Ub0SHMRnJmJ4mJUyI/lXtK7UMuauCHyHU0gAaM8KyJUMw9DxPxzupMfllHKFV0LCY3SnCW2UwSvIpHLUGbmU6Cr6HcNeJwmORM+Mx1pyRpSp6zsnFngVftXkT2N2sab/STENvE08DD5aeIJkUPRLGghvVCxQnNtKnYdnKWK3wVSNBYG3baDQbtt+8te7liyCYmH20HIDwDegZkDyQETT0nD1HRYgMtg4mkYh/3tfjgO8pbIdjc5MMAY3BBeTyuLwGGTV3DmuM9EDi6QEhGdFGwDtBeoHPzVM/rxJUjjhzNqsP6LI0927xkn38yWQYGZGYWi3M6BE696WTCFHCFLs0OShErzLO4oYIWRJWii6CbL9yg2w9xUZgMUjsKUDyMvAyKTSF2nZSZQkFI60KcHqRhSdrV7hQFaQEV5WwgiM5fk0JhRZgvAy/t82B945CR5EokmzTnBPbPVMkaXR2UyOiZqz1qGR9/3Xd2lAQTi9zzcDofDgUfeH/aHN4fBDYn8hnFwzjnnbp7fHPaHTHg6xIKMnV0X2re1zQIQcINj5hmvzC4cH8eTRGVq3ytd5qyY0vBPbaLAw2Qqz93mHhrBCjmR+LW+InMsiDle81hxqXkVtL0UxR/6KQmwpQvapHlEwp07CXwUVinMrW2VZGAorSM8zDxfYB0BQxabCZqpx7n0jxMLCE3nAgCzCScRtNQilBhMemzha/ExrpWU5PKn3Dtjf9wPw8Ce+7YHBeUwQS+HyASMfh7wTV8ztKVhM49zyCAtWdvaPCmPCVMYJ8ONLqxRTVSTba24cGDgBjeN0/NXz/urfjgO4uCRWEVe0Mi/Ze5d2/VX/eAGW1sAbdMK+VFFACZMFlYGOCE72NkxACbu2m6GiAHICqSFFlo6CAoq8lEdiIs418USDHWE0+Sm2DP5c1kySwswoYr6MBeqJcWmSc3NtFLrmXsTUvumDFHM4h7ZrVKqTNBud6XLFSxHw6Vk7WlYBYNXoDnjxlTzTw7rfFP/isKllKFT5q/NJohzfhftrjh7iS7qbh17bru2a7viZ+29OGvbRBgGG128JsbOXj9v2xgAwX0ikN+/3h8OB3hQTfAgov6qp4b4jveHPXu+eXmz3+/7vr/+g+uu7a62V0GVjd2lxqdxYmaqQxCYY5oYj+wG127b4OBFkHvBJcbsJkcgsiT03NVdMvkKDl7iTAElc2ZNP3iZxTNn2fFZefUhIYZCBp6h29kDHxrPciIfHtWcCKGFgHo0E6RfJO+U054QMEm7QwtEL2MbZ7pbaIOzLubgiCPUDLKI5M5oafHucpwZPxaTTfdF2g9uSDZbpqJyLjM1rGBYcTDh3WhRaI2DTGGd5lWISo5QIwzcrZPP1394DQMe2d25m1/d3Ly5YebpbrKNvbq82v1ot9vtRMexxgaxloAkY+Ag4lK0I3w12bkqmjDEkXPr9oe9kO71s+tAqJQnUqy7lv9nAwwqLpXXaBaOmyE6/lmSWz529v4iNDD79Xwj+t2ZMPznCbJsWJmMPgvZFNbLxok531DgzeMEKmzfPL5SWM+lRFIhNBxnsisNVSk2+Y5SmKHyufQDaVSaxgoUMYWAQjQyzwcwDODRUTdh
},
fromJson: (json) => FingerprintVerifyResponseModel.fromJson(
json as Map<String, dynamic>),
);
log(state.base64Data!.split(",").last);
switch (result) {
case ApiSuccess(:final data):
responseModel = data;
case ApiFailure(:final code, :final message):
if (code == -1) return;
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'Verification failed: $message'));
return;
}
} else {
final result = await ApiClient.post<FingerprintVerifyResponseModel>(
url: 'api/Fingerprint/authenticate-passport',
shouldKeepToken: true,
body: {
'passportNo': state.passportNumber,
'nationaltyCode': state.nationality,
'fingerType': state.selectedFingerIndex,
'finger': state.base64Data!.split(",").last,
},
fromJson: (json) => FingerprintVerifyResponseModel.fromJson(
json as Map<String, dynamic>),
);
switch (result) {
case ApiSuccess(:final data):
responseModel = data;
case ApiFailure(:final code, :final message):
if (code == -1) return;
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'Verification failed: $message'));
return;
}
}
final uiModel =
AuthResultUiModel.fromFingerprintVerifyResponse(responseModel);
emit(state.copyWith(status:uiModel.isSuccess? FormStatus.verified : FormStatus.error, authResult: uiModel, errorMessage: uiModel.statusMessage, base64Data: "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
} catch (e) {
emit(state.copyWith(
status: FormStatus.error,
errorMessage: 'Verification failed: ${e.toString()}'));
}
}
/// Cancel any in-flight [ApiClient] requests when this BLoC is closed.
@override
Future<void> close() {
ApiClient.cancel();
return super.close();
}
}