Refactor identity verification process and enhance registration summary

- Updated IdentityVerificationController to streamline data loading and validation.
- Introduced a new VerificationSummary widget for displaying registration details.
- Improved form validation logic to ensure all required fields are checked before submission.
- Enhanced user profile update functionality in UserRepository to include additional registration data.
- Added verification progress card to provide visual feedback on the status of verification steps.
- Cleaned up code for better readability and maintainability.
This commit is contained in:
vergiLgood1 2025-05-23 23:07:25 +07:00
parent 5c3faac8c3
commit 7ca33cdaa3
5 changed files with 1009 additions and 137 deletions

View File

@ -14,6 +14,7 @@ import 'package:sigap/src/features/daily-ops/data/models/index.dart';
import 'package:sigap/src/features/personalization/data/models/index.dart';
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
import 'package:sigap/src/utils/constants/num_int.dart';
import 'package:sigap/src/utils/popups/loaders.dart';
@ -83,7 +84,7 @@ class FormRegistrationController extends GetxController {
try {
Logger().d('Fetching user data safely without redirects');
// Get user session directly without going through AuthRepository methods that might trigger redirects
// Get user session directly without going through UserRepositorysitory methods that might trigger redirects
final session = SupabaseService.instance.client.auth.currentSession;
if (session?.user != null) {
@ -691,8 +692,7 @@ class FormRegistrationController extends GetxController {
submitMessage.value = 'Submitting your registration...';
// Save all registration data using the identity verification controller
final identityController = Get.find<IdentityVerificationController>();
final result = await identityController.saveRegistrationData();
final result = await saveRegistrationData();
if (result) {
isSubmitSuccess.value = true;
@ -712,4 +712,155 @@ class FormRegistrationController extends GetxController {
isSubmitting.value = false;
}
}
/// Save registration data by collecting information from all steps
Future<bool> saveRegistrationData({Map<String, dynamic>? summaryData}) async {
try {
Logger().d('Starting registration data save process');
// Collect all data from forms if not provided in summaryData
if (summaryData == null) {
collectAllFormData();
Logger().d('Collected data from all form controllers');
} else {
Logger().d('Using provided summary data for registration');
}
// Create the final data object for submission based on user models
Map<String, dynamic> finalData = {};
// Basic user information (matching UserModel structure)
finalData['user_id'] = userMetadata.value.userId;
finalData['email'] = userMetadata.value.email;
finalData['role_id'] =
userMetadata.value.roleId ?? selectedRole.value?.id;
finalData['is_officer'] = userMetadata.value.isOfficer;
finalData['phone'] = personalInfoController.phoneController.text;
finalData['profile_status'] = 'pending_approval';
// Structure user profile data according to ProfileModel
Map<String, dynamic> profileData = {};
if (!userMetadata.value.isOfficer) {
// Regular user profile data (matching ProfileModel structure)
profileData = {
'user_id': userMetadata.value.userId,
'nik': identityController.nikController.text,
'first_name': personalInfoController.firstNameController.text,
'last_name': personalInfoController.lastNameController.text,
'place_of_birth': identityController.placeOfBirthController.text,
'birth_date': identityController.birthDateController.text,
'address': {'address': personalInfoController.addressController.text},
};
finalData['profile'] = profileData;
}
// For officers, structure data according to OfficerModel
if (userMetadata.value.isOfficer) {
// Officer data (matching OfficerModel structure)
Map<String, dynamic> officerData = {
'id': userMetadata.value.userId,
'unit_id': unitInfoController?.unitIdController.text ?? '',
'role_id': finalData['role_id'],
'nrp':
identityController
.nikController
.text, // NRP is stored in NIK field for officers
'name': personalInfoController.nameController.text,
'rank': officerInfoController?.rankController.text,
'position': unitInfoController?.positionController.text,
'phone': personalInfoController.phoneController.text,
'email': finalData['email'],
'place_of_birth': identityController.placeOfBirthController.text,
'date_of_birth': identityController.birthDateController.text,
};
finalData['officer'] = officerData;
}
// Store ID card verification data (KTP or KTA)
if (userMetadata.value.isOfficer) {
// KTA data
finalData['id_card'] = {
'type': 'KTA',
'nrp': identityController.nikController.text,
'name': identityController.fullNameController.text,
'birth_date': identityController.birthDateController.text,
'gender': identityController.selectedGender.value,
};
} else {
// KTP data
finalData['id_card'] = {
'type': 'KTP',
'nik': identityController.nikController.text,
'name': identityController.fullNameController.text,
'place_of_birth': identityController.placeOfBirthController.text,
'birth_date': identityController.birthDateController.text,
'gender': identityController.selectedGender.value,
'address': identityController.addressController.text,
};
}
// Face verification data
finalData['face_verification'] = {
'selfie_valid': selfieVerificationController.isSelfieValid.value,
'liveness_check_passed':
selfieVerificationController.isLivenessCheckPassed.value,
'face_match_result':
selfieVerificationController.isMatchWithIDCard.value,
'match_confidence': selfieVerificationController.matchConfidence.value,
};
// Image paths
finalData['images'] = {
'id_card': idCardVerificationController.idCardImage.value?.path,
'selfie': selfieVerificationController.selfieImage.value?.path,
};
// Merge with provided summary data if available
if (summaryData != null && summaryData.isNotEmpty) {
// Merge summaryData into profile or officer data based on user type
if (userMetadata.value.isOfficer) {
if (summaryData['fullName'] != null) {
finalData['officer']['name'] = summaryData['fullName'];
}
if (summaryData['birthDate'] != null) {
finalData['officer']['date_of_birth'] = summaryData['birthDate'];
}
} else {
if (summaryData['fullName'] != null) {
final names = summaryData['fullName'].toString().split(' ');
if (names.isNotEmpty) {
finalData['profile']['first_name'] = names.first;
if (names.length > 1) {
finalData['profile']['last_name'] = names.sublist(1).join(' ');
}
}
}
if (summaryData['placeOfBirth'] != null) {
finalData['profile']['place_of_birth'] =
summaryData['placeOfBirth'];
}
if (summaryData['birthDate'] != null) {
finalData['profile']['birth_date'] = summaryData['birthDate'];
}
if (summaryData['address'] != null) {
finalData['profile']['address'] = {
'address': summaryData['address'],
};
}
}
}
Logger().d('Registration data prepared and ready for submission');
// Submit to user repository
final userRepo = Get.find<UserRepository>();
final result = await userRepo.updateUserProfile(finalData);
Logger().d('Registration submission result: $result');
return result;
} catch (e) {
Logger().e('Error saving registration data: $e');
return false;
}
}
}

View File

@ -4,15 +4,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart';
import 'package:sigap/src/features/auth/data/bindings/registration_binding.dart';
import 'package:sigap/src/features/auth/data/models/face_model.dart';
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
import 'package:sigap/src/features/auth/data/services/registration_service.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart';
class IdentityVerificationController extends GetxController {
// Singleton instance
@ -21,23 +21,22 @@ class IdentityVerificationController extends GetxController {
// Dependencies
final bool isOfficer;
final AzureOCRService _ocrService = AzureOCRService();
// Use FacialVerificationService instead of direct EdgeFunction
final FacialVerificationService _faceService =
FacialVerificationService.instance;
// Local storage keys (matching those in IdCardVerificationController)
// Local storage keys
static const String _kOcrResultsKey = 'ocr_results';
static const String _kOcrModelKey = 'ocr_model';
static const String _kIdCardTypeKey = 'id_card_type';
// Controllers
// Controllers for form fields
final TextEditingController nikController = TextEditingController();
final TextEditingController fullNameController = TextEditingController();
final TextEditingController placeOfBirthController = TextEditingController();
final TextEditingController birthDateController = TextEditingController();
final TextEditingController addressController = TextEditingController();
// Error variables
// Form validation errors
final RxString nikError = RxString('');
final RxString fullNameError = RxString('');
final RxString placeOfBirthError = RxString('');
@ -45,45 +44,51 @@ class IdentityVerificationController extends GetxController {
final RxString genderError = RxString('');
final RxString addressError = RxString('');
// Verification states
// ID verification states
final RxBool isVerifying = RxBool(false);
final RxBool isVerified = RxBool(false);
final RxString verificationMessage = RxString('');
// Face verification
// Face verification states
final RxBool isVerifyingFace = RxBool(false);
final RxBool isFaceVerified = RxBool(false);
final RxString faceVerificationMessage = RxString('');
// Use FaceComparisonResult for face verification
final Rx<FaceComparisonResult?> faceComparisonResult =
Rx<FaceComparisonResult?>(null);
// Gender selection - initialize with a default value
// Gender selection dropdown
final Rx<String?> selectedGender = Rx<String?>('Male');
// Form validation
// Form validation state
final RxBool isFormValid = RxBool(true);
// Flag to prevent infinite loop
bool _isApplyingData = false;
// NIK field readonly status
// UI control states
final RxBool isNikReadOnly = RxBool(false);
// Properties to store extracted ID card data
final String? extractedIdCardNumber;
final String? extractedName;
final RxBool isPreFilledNik = false.obs;
// Store the loaded OCR data
// Storage for extracted data
final RxMap<String, dynamic> ocrData = RxMap<String, dynamic>({});
final String? extractedIdCardNumber;
final String? extractedName;
// Status of data saving
// Data saving states
final RxBool isSavingData = RxBool(false);
final RxBool isDataSaved = RxBool(false);
final RxString dataSaveMessage = RxString('');
// Summary data for review page
final RxMap<String, dynamic> summaryData = RxMap<String, dynamic>({});
// Verification status of different sections
final RxBool isBasicInfoVerified = RxBool(false);
final RxBool isIdCardVerified = RxBool(false);
final RxBool isSelfieVerified = RxBool(false);
final RxBool isContactInfoVerified = RxBool(false);
final RxBool isLoadingSummary = RxBool(false);
IdentityVerificationController({
this.extractedIdCardNumber = '',
this.extractedName = '',
@ -93,14 +98,173 @@ class IdentityVerificationController extends GetxController {
@override
void onInit() {
super.onInit();
// Make sure selectedGender has a default value
// Set default gender value
selectedGender.value = selectedGender.value ?? 'Male';
// Load OCR data from local storage with debug info
print(
'Initializing IdentityVerificationController and loading OCR data...',
);
loadOcrDataFromLocalStorage();
// Load data in sequence
_initializeData();
}
// Initialize all data in sequence
Future<void> _initializeData() async {
try {
// First load OCR data
await loadOcrDataFromLocalStorage();
// Then load data from previous steps for summary
await loadAllStepsData();
} catch (e) {
print('Error initializing data: $e');
}
}
// Load data from all previous steps
Future<void> loadAllStepsData() async {
try {
isLoadingSummary.value = true;
// Get references to all controllers
final registrationController = Get.find<FormRegistrationController>();
final idCardController = Get.find<IdCardVerificationController>();
final selfieController = Get.find<SelfieVerificationController>();
// Load data from each controller
_loadBasicInfoData(registrationController);
_loadIdCardData(idCardController);
_loadSelfieData(selfieController);
// Pre-fill form with extracted data
_prefillFormWithExtractedData(idCardController);
} catch (e) {
print('Error loading steps data: $e');
} finally {
isLoadingSummary.value = false;
}
}
// Load basic information
void _loadBasicInfoData(FormRegistrationController controller) {
try {
// Add basic info to summary
summaryData['email'] = SignupWithRoleController().emailController.text;
summaryData['phone'] = PersonalInfoController.instance.phoneController.text;
summaryData['role'] = controller.selectedRole.value?.name ?? 'Unknown';
isBasicInfoVerified.value = true;
} catch (e) {
print('Error loading basic info: $e');
isBasicInfoVerified.value = false;
}
}
// Load ID card data
void _loadIdCardData(IdCardVerificationController controller) {
try {
// Add ID card info to summary
summaryData['idCardType'] = isOfficer ? 'KTA' : 'KTP';
summaryData['idCardValid'] = controller.isIdCardValid.value;
summaryData['idCardConfirmed'] = controller.hasConfirmedIdCard.value;
summaryData['extractedInfo'] = controller.extractedInfo;
// Add model data
if (isOfficer) {
summaryData['ktaModel'] = controller.ktaModel.value?.toJson();
} else {
summaryData['ktpModel'] = controller.ktpModel.value?.toJson();
}
isIdCardVerified.value =
controller.isIdCardValid.value && controller.hasConfirmedIdCard.value;
} catch (e) {
print('Error loading ID card data: $e');
isIdCardVerified.value = false;
}
}
// Load selfie data
void _loadSelfieData(SelfieVerificationController controller) {
try {
// Add selfie verification info to summary
summaryData['selfieValid'] = controller.isSelfieValid.value;
summaryData['selfieConfirmed'] = controller.hasConfirmedSelfie.value;
summaryData['livenessCheckPassed'] =
controller.isLivenessCheckPassed.value;
summaryData['faceMatchResult'] = controller.isMatchWithIDCard.value;
summaryData['faceMatchConfidence'] = controller.matchConfidence.value;
isSelfieVerified.value =
controller.isSelfieValid.value && controller.hasConfirmedSelfie.value;
} catch (e) {
print('Error loading selfie data: $e');
isSelfieVerified.value = false;
}
}
// Pre-fill form with extracted data
void _prefillFormWithExtractedData(IdCardVerificationController controller) {
try {
if (!isOfficer && controller.ktpModel.value != null) {
// Extract KTP data
final ktp = controller.ktpModel.value!;
if (ktp.nik.isNotEmpty) {
nikController.text = ktp.nik;
summaryData['nik'] = ktp.nik;
}
if (ktp.name.isNotEmpty) {
fullNameController.text = ktp.name;
summaryData['fullName'] = ktp.name;
}
if (ktp.birthPlace.isNotEmpty) {
placeOfBirthController.text = ktp.birthPlace;
summaryData['placeOfBirth'] = ktp.birthPlace;
}
if (ktp.birthDate.isNotEmpty) {
birthDateController.text = ktp.birthDate;
summaryData['birthDate'] = ktp.birthDate;
}
if (ktp.gender.isNotEmpty) {
// Convert gender to the format expected by the dropdown
String gender = ktp.gender.toLowerCase();
if (gender.contains('laki') || gender == 'male') {
selectedGender.value = 'Male';
} else if (gender.contains('perempuan') || gender == 'female') {
selectedGender.value = 'Female';
}
summaryData['gender'] = selectedGender.value;
}
if (ktp.address.isNotEmpty) {
addressController.text = ktp.address;
summaryData['address'] = ktp.address;
}
// Make NIK field read-only since it's extracted from KTP
isNikReadOnly.value = true;
} else if (isOfficer && controller.ktaModel.value != null) {
// Extract KTA data
final kta = controller.ktaModel.value!;
if (kta.name.isNotEmpty) {
fullNameController.text = kta.name;
summaryData['fullName'] = kta.name;
}
// Extract birth date from extra data if available
if (kta.extraData != null &&
kta.extraData!.containsKey('tanggal_lahir') &&
kta.extraData!['tanggal_lahir'] != null) {
birthDateController.text = kta.extraData!['tanggal_lahir'];
summaryData['birthDate'] = kta.extraData!['tanggal_lahir'];
}
}
} catch (e) {
print('Error prefilling form with extracted data: $e');
}
}
// Load OCR data from local storage
@ -108,7 +272,6 @@ class IdentityVerificationController extends GetxController {
try {
final prefs = await SharedPreferences.getInstance();
// Load stored ID card type to verify it matches current flow
final String? storedIdCardType = prefs.getString(_kIdCardTypeKey);
print(
'Stored ID card type: $storedIdCardType, Current isOfficer: $isOfficer',
@ -121,7 +284,6 @@ class IdentityVerificationController extends GetxController {
return;
}
// Load OCR results
final String? jsonData = prefs.getString(_kOcrResultsKey);
if (jsonData != null) {
print('Found OCR data in storage: ${jsonData.length} chars');
@ -129,11 +291,8 @@ class IdentityVerificationController extends GetxController {
ocrData.assignAll(results);
print('OCR data loaded: ${results.length} items');
// Load OCR model
final String? modelJson = prefs.getString(_kOcrModelKey);
if (modelJson != null) {
print('Found OCR model in storage: ${modelJson.length} chars');
try {
if (isOfficer) {
final ktaModel = KtaModel.fromJson(jsonDecode(modelJson));
@ -145,7 +304,6 @@ class IdentityVerificationController extends GetxController {
applyKtpDataToForm(ktpModel);
}
isNikReadOnly.value = true;
print('NIK field set to read-only');
} catch (e) {
print('Error parsing model JSON: $e');
}
@ -156,7 +314,6 @@ class IdentityVerificationController extends GetxController {
} catch (e) {
print('Error loading OCR data from local storage: $e');
} finally {
// If data wasn't loaded from local storage, try from FormRegistrationController
if (ocrData.isEmpty) {
print('Falling back to FormRegistrationController data');
_safeApplyIdCardData();
@ -166,24 +323,14 @@ class IdentityVerificationController extends GetxController {
// Apply KTP data to form
void applyKtpDataToForm(KtpModel ktpModel) {
if (ktpModel.nik.isNotEmpty) {
nikController.text = ktpModel.nik;
}
if (ktpModel.name.isNotEmpty) {
fullNameController.text = ktpModel.name;
}
if (ktpModel.birthPlace.isNotEmpty) {
if (ktpModel.nik.isNotEmpty) nikController.text = ktpModel.nik;
if (ktpModel.name.isNotEmpty) fullNameController.text = ktpModel.name;
if (ktpModel.birthPlace.isNotEmpty)
placeOfBirthController.text = ktpModel.birthPlace;
}
if (ktpModel.birthDate.isNotEmpty) {
if (ktpModel.birthDate.isNotEmpty)
birthDateController.text = ktpModel.birthDate;
}
if (ktpModel.gender.isNotEmpty) {
// Convert gender to the format expected by the dropdown
String gender = ktpModel.gender.toLowerCase();
if (gender.contains('laki') || gender == 'male') {
selectedGender.value = 'Male';
@ -192,55 +339,41 @@ class IdentityVerificationController extends GetxController {
}
}
if (ktpModel.address.isNotEmpty) {
addressController.text = ktpModel.address;
}
if (ktpModel.address.isNotEmpty) addressController.text = ktpModel.address;
// Mark as verified since we have validated KTP data
// Mark as verified
isVerified.value = true;
verificationMessage.value = 'KTP information loaded successfully';
}
// Apply KTA data to form
void applyKtaDataToForm(KtaModel ktaModel) {
// For officer, we'd fill in different fields as needed
if (ktaModel.name.isNotEmpty) {
fullNameController.text = ktaModel.name;
}
if (ktaModel.name.isNotEmpty) fullNameController.text = ktaModel.name;
// If birthDate is available in extra data
if (ktaModel.extraData != null &&
ktaModel.extraData!.containsKey('tanggal_lahir') &&
ktaModel.extraData!['tanggal_lahir'] != null) {
birthDateController.text = ktaModel.extraData!['tanggal_lahir'];
}
// Mark as verified
isVerified.value = true;
verificationMessage.value = 'KTA information loaded successfully';
}
// Safely apply ID card data without risking stack overflow (fallback method)
// Safe method to apply ID card data without risk of stack overflow
void _safeApplyIdCardData() {
if (_isApplyingData) return; // Guard against recursive calls
if (_isApplyingData) return;
try {
_isApplyingData = true;
// Check if FormRegistrationController is ready
if (!Get.isRegistered<FormRegistrationController>()) {
return;
}
if (!Get.isRegistered<FormRegistrationController>()) return;
final formController = Get.find<FormRegistrationController>();
if (formController.idCardData.value == null) {
return;
}
if (formController.idCardData.value == null) return;
final idCardData = formController.idCardData.value;
if (idCardData != null) {
// Fill the form with the extracted data
if (!isOfficer && idCardData is KtpModel) {
applyKtpDataToForm(idCardData);
isNikReadOnly.value = true;
@ -256,11 +389,13 @@ class IdentityVerificationController extends GetxController {
}
// Validate form inputs
bool validate(GlobalKey<FormState> formKey) {
bool validate(GlobalKey<FormState>? formKey) {
isFormValid.value = true;
clearErrors();
// For non-officers, we need to validate NIK and other KTP-related fields
// Validate required fields based on officer status
if (!isOfficer) {
// KTP validation
if (nikController.text.isEmpty) {
nikError.value = 'NIK is required';
isFormValid.value = false;
@ -278,37 +413,64 @@ class IdentityVerificationController extends GetxController {
placeOfBirthError.value = 'Place of birth is required';
isFormValid.value = false;
}
} else {
// KTA validation
if (fullNameController.text.isEmpty) {
fullNameError.value = 'Full name is required';
isFormValid.value = false;
}
}
// These validations apply to both officers and non-officers
// Common validations
if (birthDateController.text.isEmpty) {
birthDateError.value = 'Birth date is required';
isFormValid.value = false;
}
// if (addressController.text.isEmpty) {
// addressError.value = 'Address is required';
// isFormValid.value = false;
// }
if (selectedGender.value == null) {
genderError.value = 'Gender is required';
isFormValid.value = false;
}
// Verify previous steps completion
bool allPreviousStepsCompleted =
isBasicInfoVerified.value &&
isIdCardVerified.value &&
isSelfieVerified.value;
if (!allPreviousStepsCompleted) {
isFormValid.value = false;
verificationMessage.value =
'Please complete all previous steps before submitting';
}
// Update summary data
_updateSummaryWithFormData();
return isFormValid.value;
}
// Verify ID card information against OCR results
// Update summary with current form data
void _updateSummaryWithFormData() {
summaryData['nik'] = nikController.text;
summaryData['fullName'] = fullNameController.text;
summaryData['placeOfBirth'] = placeOfBirthController.text;
summaryData['birthDate'] = birthDateController.text;
summaryData['gender'] = selectedGender.value;
summaryData['address'] = addressController.text;
}
// Verify ID card with OCR data
void verifyIdCardWithOCR() {
try {
isVerifying.value = true;
// Compare form input with OCR results
final formController = Get.find<FormRegistrationController>();
final idCardData = formController.idCardData.value;
if (idCardData != null) {
if (!isOfficer && idCardData is KtpModel) {
// Verify NIK matches
bool nikMatches = nikController.text == idCardData.nik;
// Verify name is similar (accounting for slight differences in formatting)
bool nameMatches = _compareNames(
fullNameController.text,
idCardData.name,
@ -324,7 +486,6 @@ class IdentityVerificationController extends GetxController {
'Information doesn\'t match with KTP. Please check and try again.';
}
} else if (isOfficer && idCardData is KtaModel) {
// For officers, verify that the name matches
bool nameMatches = _compareNames(
fullNameController.text,
idCardData.name,
@ -354,9 +515,8 @@ class IdentityVerificationController extends GetxController {
}
}
// Simple name comparison function (ignores case, spaces)
// Compare names accounting for formatting differences
bool _compareNames(String name1, String name2) {
// Normalize names for comparison
String normalizedName1 = name1.toLowerCase().trim().replaceAll(
RegExp(r'\s+'),
' ',
@ -366,19 +526,15 @@ class IdentityVerificationController extends GetxController {
' ',
);
// Check exact match
if (normalizedName1 == normalizedName2) return true;
// Check if one name is contained within the other
if (normalizedName1.contains(normalizedName2) ||
normalizedName2.contains(normalizedName1))
return true;
// Split names into parts and check for partial matches
var parts1 = normalizedName1.split(' ');
var parts2 = normalizedName2.split(' ');
// Count matching name parts
int matches = 0;
for (var part1 in parts1) {
for (var part2 in parts2) {
@ -391,25 +547,22 @@ class IdentityVerificationController extends GetxController {
}
}
// If more than half of the name parts match, consider it a match
return matches >= (parts1.length / 2).floor();
}
// Face verification function using EdgeFunction instead of AWS directly
// Verify face match using FacialVerificationService
void verifyFaceMatch() {
// Set quick verification status for development
if (_faceService.skipFaceVerification) {
// Development mode - use dummy data
isFaceVerified.value = true;
faceVerificationMessage.value =
'Face verification skipped (development mode)';
// Create dummy comparison result
final idCardController = Get.find<IdCardVerificationController>();
final selfieController = Get.find<SelfieVerificationController>();
if (idCardController.idCardImage.value != null &&
selfieController.selfieImage.value != null) {
// Set dummy result
faceComparisonResult.value = FaceComparisonResult(
sourceFace: FaceModel(
imagePath: idCardController.idCardImage.value!.path,
@ -428,18 +581,14 @@ class IdentityVerificationController extends GetxController {
message: 'Face verification passed (development mode)',
);
}
return;
}
isVerifyingFace.value = true;
// Get ID card and selfie images
final formController = Get.find<FormRegistrationController>();
final idCardController = Get.find<IdCardVerificationController>();
final selfieController = Get.find<SelfieVerificationController>();
// Check if we have both images
if (idCardController.idCardImage.value == null ||
selfieController.selfieImage.value == null) {
isFaceVerified.value = false;
@ -449,17 +598,13 @@ class IdentityVerificationController extends GetxController {
return;
}
// Use FacialVerificationService to compare faces
_faceService
.compareFaces(
idCardController.idCardImage.value!,
selfieController.selfieImage.value!,
)
.then((result) {
// Store the comparison result
faceComparisonResult.value = result;
// Update verification status
isFaceVerified.value = result.isMatch;
faceVerificationMessage.value = result.message;
})
@ -473,7 +618,7 @@ class IdentityVerificationController extends GetxController {
});
}
// Clear all error messages
// Clear all validation errors
void clearErrors() {
nikError.value = '';
fullNameError.value = '';
@ -481,21 +626,10 @@ class IdentityVerificationController extends GetxController {
birthDateError.value = '';
genderError.value = '';
addressError.value = '';
isFormValid.value = true;
}
@override
void onClose() {
nikController.dispose();
fullNameController.dispose();
placeOfBirthController.dispose();
birthDateController.dispose();
addressController.dispose();
super.onClose();
}
// Method to pre-fill NIK and Name from the extracted data
// Prefill form with extracted data
void prefillExtractedData() {
if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) {
nikController.text = extractedIdCardNumber!;
@ -514,13 +648,38 @@ class IdentityVerificationController extends GetxController {
isSavingData.value = true;
dataSaveMessage.value = 'Saving your registration data...';
// Ensure the registration service is available();
if (!Get.isRegistered<RegistrationService>()) {
await Get.putAsync(() async => RegistrationService());
} // Get registration service
// Final validation
if (!validate(null)) {
dataSaveMessage.value = 'Please fix the errors before submitting';
return false;
}
final registrationService = Get.find<RegistrationService>();
final result = await registrationService.saveRegistrationData();
// Update summary with final data
_updateSummaryWithFormData();
// Format data according to models
Map<String, dynamic> formattedData = {
// Match format from summaryData to match ProfileModel and OfficerModel
'nik': nikController.text,
'fullName': fullNameController.text,
'placeOfBirth': placeOfBirthController.text,
'birthDate': birthDateController.text,
'gender': selectedGender.value,
'address': addressController.text,
};
// Add all other summary data for completeness
summaryData.forEach((key, value) {
if (!formattedData.containsKey(key)) {
formattedData[key] = value;
}
});
// Use FormRegistrationController for actual submission
final formController = Get.find<FormRegistrationController>();
final result = await formController.saveRegistrationData(
summaryData: formattedData,
);
if (result) {
isDataSaved.value = true;
@ -541,4 +700,15 @@ class IdentityVerificationController extends GetxController {
isSavingData.value = false;
}
}
@override
void onClose() {
// Dispose controllers to prevent memory leaks
nikController.dispose();
fullNameController.dispose();
placeOfBirthController.dispose();
birthDateController.dispose();
addressController.dispose();
super.onClose();
}
}

View File

@ -3,7 +3,9 @@ import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart';
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/verification_summary.dart';
import 'package:sigap/src/shared/widgets/form/form_section_header.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
class IdentityVerificationStep extends StatelessWidget {
@ -23,21 +25,305 @@ class IdentityVerificationStep extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Header
FormSectionHeader(
title: 'Additional Information',
subtitle: isOfficer
? 'Please provide additional personal details'
: 'Please verify your KTP information below. NIK field cannot be edited.',
title: 'Review & Verification',
subtitle:
'Please review and confirm your information before submitting',
),
const SizedBox(height: TSizes.spaceBtwItems),
// Personal Information Form Section
// Verification Progress Card
Obx(() => _buildVerificationProgressCard(controller)),
const SizedBox(height: TSizes.spaceBtwItems),
// Registration Summary
Obx(
() => VerificationSummary(
summaryData: controller.summaryData,
isOfficer: isOfficer,
),
),
const SizedBox(height: TSizes.spaceBtwItems),
// Form section header
Container(
padding: const EdgeInsets.only(
top: TSizes.spaceBtwItems,
bottom: TSizes.sm,
),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.withOpacity(0.2), width: 1),
),
),
child: FormSectionHeader(
title: 'Confirm Identity Information',
subtitle:
isOfficer
? 'Please verify the pre-filled information from your KTA'
: 'Please verify the pre-filled information from your KTP',
),
),
const SizedBox(height: TSizes.spaceBtwItems),
// ID Card Info Form (with pre-filled data)
IdInfoForm(controller: controller, isOfficer: isOfficer),
const SizedBox(height: TSizes.spaceBtwSections),
// Save & Submit Button
Obx(
() => ElevatedButton(
onPressed:
controller.isSavingData.value
? null
: () => _submitRegistrationData(controller, context),
style: ElevatedButton.styleFrom(
backgroundColor: TColors.primary,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
),
disabledBackgroundColor: TColors.primary.withOpacity(0.3),
),
child:
controller.isSavingData.value
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
SizedBox(width: TSizes.sm),
Text('Submitting...'),
],
)
: const Text('Submit Registration'),
),
),
// Save Result Message
Obx(
() =>
controller.dataSaveMessage.value.isNotEmpty
? Padding(
padding: const EdgeInsets.only(top: TSizes.sm),
child: Text(
controller.dataSaveMessage.value,
style: TextStyle(
color:
controller.isDataSaved.value
? Colors.green
: TColors.error,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
)
: const SizedBox.shrink(),
),
],
),
);
}
// Build Verification Progress Card
Widget _buildVerificationProgressCard(
IdentityVerificationController controller,
) {
final bool allVerified =
controller.isBasicInfoVerified.value &&
controller.isIdCardVerified.value &&
controller.isSelfieVerified.value;
return Container(
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color:
allVerified
? Colors.green.withOpacity(0.1)
: TColors.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
border: Border.all(
color:
allVerified
? Colors.green.withOpacity(0.5)
: TColors.warning.withOpacity(0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
allVerified ? Icons.verified : Icons.info_outline,
color: allVerified ? Colors.green : TColors.warning,
size: TSizes.iconMd,
),
const SizedBox(width: TSizes.sm),
Text(
allVerified
? 'All verification steps completed!'
: 'Verification Status',
style: TextStyle(
fontWeight: FontWeight.bold,
color: allVerified ? Colors.green : TColors.warning,
),
),
],
),
const SizedBox(height: TSizes.sm),
// Basic Info status
_buildVerificationItem(
'Basic Information',
controller.isBasicInfoVerified.value,
),
// ID Card status
_buildVerificationItem(
'ID Card Verification',
controller.isIdCardVerified.value,
),
// Selfie status
_buildVerificationItem(
'Selfie Verification',
controller.isSelfieVerified.value,
),
// Identity Verification (this step)
_buildVerificationItem(
'Identity Confirmation',
controller.isFormValid.value,
isCurrentStep: true,
),
],
),
);
}
// Build verification step item
Widget _buildVerificationItem(
String title,
bool isVerified, {
bool isCurrentStep = false,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: TSizes.xs),
child: Row(
children: [
Icon(
isVerified
? Icons.check_circle
: isCurrentStep
? Icons.edit
: Icons.error_outline,
color:
isVerified
? Colors.green
: isCurrentStep
? TColors.primary
: TColors.error,
size: TSizes.iconSm,
),
const SizedBox(width: TSizes.xs),
Text(
title,
style: TextStyle(
fontSize: TSizes.fontSizeSm,
color:
isVerified
? Colors.green
: isCurrentStep
? TColors.primary
: TColors.textSecondary,
fontWeight: isCurrentStep ? FontWeight.bold : FontWeight.normal,
),
),
const Spacer(),
Text(
isVerified
? 'Verified'
: isCurrentStep
? 'In Progress'
: 'Not Verified',
style: TextStyle(
fontSize: TSizes.fontSizeXs,
color:
isVerified
? Colors.green
: isCurrentStep
? TColors.primary
: TColors.textSecondary,
),
),
],
),
);
}
// Submit registration data
void _submitRegistrationData(
IdentityVerificationController controller,
BuildContext context,
) async {
final formKey = FormRegistrationController().formKey;
// Validate form
if (!controller.validate(formKey)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please complete all required fields'),
backgroundColor: TColors.error,
),
);
return;
}
// Save registration data
final result = await controller.saveRegistrationData();
if (result) {
// Navigate to success page or show success dialog
showDialog(
context: context,
barrierDismissible: false,
builder:
(context) => AlertDialog(
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: TSizes.sm),
Text('Registration Successful'),
],
),
content: Text(
'Your registration has been submitted successfully. You will be notified once your account is verified.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Navigate to login or home page
Get.offAllNamed('/login');
},
child: Text('Go to Login'),
),
],
),
);
}
}
}

View File

@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
class VerificationSummary extends StatelessWidget {
final Map<String, dynamic> summaryData;
final bool isOfficer;
const VerificationSummary({
super.key,
required this.summaryData,
required this.isOfficer,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.05),
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color: TColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(TSizes.borderRadiusMd),
topRight: Radius.circular(TSizes.borderRadiusMd),
),
),
child: Row(
children: [
Icon(
Icons.summarize,
color: TColors.primary,
size: TSizes.iconMd,
),
const SizedBox(width: TSizes.sm),
Text(
'Registration Summary',
style: TextStyle(
fontWeight: FontWeight.bold,
color: TColors.primary,
fontSize: TSizes.fontSizeMd,
),
),
],
),
),
// Basic Info Section
_buildSectionHeader(context, 'Basic Information'),
_buildInfoItem('Email', summaryData['email'] ?? '-'),
_buildInfoItem('Phone', summaryData['phone'] ?? '-'),
_buildInfoItem('Role', summaryData['role'] ?? '-'),
// ID Card Section
_buildSectionHeader(
context,
'${isOfficer ? 'KTA' : 'KTP'} Verification',
),
_buildInfoItem(
'${isOfficer ? 'KTA' : 'KTP'} Verified',
(summaryData['idCardValid'] ?? false) ? 'Yes' : 'No',
),
if (!isOfficer) _buildInfoItem('NIK', summaryData['nik'] ?? '-'),
_buildInfoItem('Full Name', summaryData['fullName'] ?? '-'),
if (!isOfficer)
_buildInfoItem(
'Place of Birth',
summaryData['placeOfBirth'] ?? '-',
),
_buildInfoItem('Birth Date', summaryData['birthDate'] ?? '-'),
_buildInfoItem('Gender', summaryData['gender'] ?? '-'),
if (!isOfficer)
_buildInfoItem('Address', summaryData['address'] ?? '-'),
// Selfie Verification Section
_buildSectionHeader(context, 'Selfie Verification'),
_buildInfoItem(
'Liveness Check',
(summaryData['livenessCheckPassed'] ?? false)
? 'Passed'
: 'Not Verified',
),
_buildInfoItem(
'Face Match with ID Card',
(summaryData['faceMatchResult'] ?? false)
? 'Matched (${((summaryData['faceMatchConfidence'] ?? 0.0) * 100).toStringAsFixed(1)}% confidence)'
: 'Not Matched',
),
],
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.sm,
),
color: Colors.grey.withOpacity(0.1),
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
),
);
}
Widget _buildInfoItem(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.sm,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
label,
style: TextStyle(
color: TColors.textSecondary,
fontSize: TSizes.fontSizeSm,
),
),
),
const SizedBox(width: TSizes.sm),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: TSizes.fontSizeSm,
),
),
),
],
),
);
}
}

View File

@ -340,4 +340,117 @@ class UserRepository extends GetxController {
return false; // Default to not banned
}
}
// Update user profile with registration data
Future<bool> updateUserProfile(Map<String, dynamic> data) async {
try {
if (!isAuthenticated) {
throw 'User not authenticated';
}
final userId = data['user_id'] ?? currentUserId;
if (userId == null) {
throw 'User ID is required';
}
// Update user metadata
final authData = {
'email': data['email'],
'phone': data['phone'],
'is_officer': data['is_officer'] ?? false,
'role_id': data['role_id'],
'profile_status': data['profile_status'] ?? 'pending_approval',
};
// Add ID card and verification data to user metadata
if (data['id_card'] != null) {
authData['id_card'] = data['id_card'];
}
if (data['face_verification'] != null) {
authData['face_verification'] = data['face_verification'];
}
// Add officer-specific data if user is an officer
if (data['is_officer'] == true && data['officer'] != null) {
authData['officer_data'] = data['officer'];
}
// Update user metadata in auth
await _supabase.auth.updateUser(UserAttributes(data: authData));
// Update the database tables based on user type
if (data['is_officer'] == true) {
// Handle officer data
if (data['officer'] != null) {
final officerData = Map<String, dynamic>.from(data['officer']);
// Check if officer exists
final existingOfficer =
await _supabase
.from('officers')
.select('id')
.eq('id', userId)
.maybeSingle();
if (existingOfficer != null) {
// Update existing officer
await _supabase
.from('officers')
.update(officerData)
.eq('id', userId);
} else {
// Create new officer
await _supabase.from('officers').insert(officerData);
}
}
} else {
// Handle regular user data
if (data['profile'] != null) {
final profileData = Map<String, dynamic>.from(data['profile']);
// Check if profile exists
final existingProfile =
await _supabase
.from('profiles')
.select('id')
.eq('user_id', userId)
.maybeSingle();
if (existingProfile != null) {
// Update existing profile
await _supabase
.from('profiles')
.update(profileData)
.eq('user_id', userId);
} else {
// Create new profile
await _supabase.from('profiles').insert(profileData);
}
}
}
// Update users table with common data
await _supabase
.from('users')
.update({
'phone': data['phone'],
'roles_id': data['role_id'],
'updated_at': DateTime.now().toIso8601String(),
})
.eq('id', userId);
_logger.d('User profile updated successfully');
return true;
} on PostgrestException catch (error) {
_logger.e('PostgrestException in updateUserProfile: ${error.message}');
return false;
} on AuthException catch (e) {
_logger.e('AuthException in updateUserProfile: ${e.message}');
return false;
} catch (e) {
_logger.e('Exception in updateUserProfile: $e');
return false;
}
}
}