From ac3936637159a64ad40b64ef87d77220200fff9f Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Tue, 20 May 2025 03:30:19 +0700 Subject: [PATCH] feat: Update user metadata model and authentication flow; enhance ID card verification process and error handling --- .../src/cores/bindings/general_bindings.dart | 4 +- .../src/cores/services/azure_ocr_service.dart | 105 +++++++++-- .../src/cores/services/supabase_service.dart | 2 +- .../auth/data/models/user_metadata_model.dart | 170 +++++++++++------- .../authentication_repository.dart | 120 +++++++------ .../registration_form_controller.dart | 37 ++-- .../signup_with_role_controller.dart | 92 +++++++--- .../id_card_verification_controller.dart | 1 + .../widgets/id_card_verification_step.dart | 50 ++---- sigap-website/supabase/config.toml | 2 +- 10 files changed, 377 insertions(+), 206 deletions(-) diff --git a/sigap-mobile/lib/src/cores/bindings/general_bindings.dart b/sigap-mobile/lib/src/cores/bindings/general_bindings.dart index 3707201..362b64a 100644 --- a/sigap-mobile/lib/src/cores/bindings/general_bindings.dart +++ b/sigap-mobile/lib/src/cores/bindings/general_bindings.dart @@ -5,13 +5,13 @@ import 'package:sigap/src/utils/helpers/network_manager.dart'; class UtilityBindings extends Bindings { Logger? get logger => Logger(); - NetworkManager? get networkManager => NetworkManager(); + BackgroundService? get backgroundService => BackgroundService.instance; @override void dependencies() { Get.put(backgroundService, permanent: true); - Get.put(networkManager, permanent: true); + Get.put(NetworkManager(), permanent: true); Get.put(logger, permanent: true); } } diff --git a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart index 78c88f7..1870c7d 100644 --- a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart +++ b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart @@ -28,7 +28,8 @@ class AzureOCRService { '$endpoint$ocrApiPath?language=id&detectOrientation=true', ); - final response = await DioClient().post( + // First request: Submit the image to the Read API + final submitResponse = await DioClient().post( uri.toString(), data: bytes, options: Options( @@ -40,14 +41,25 @@ class AzureOCRService { ), ); - if (response.statusCode == 200) { - final jsonResponse = json.decode(response.data); + if (submitResponse.statusCode == 202) { + // Get the operation-location header from the response + final operationLocation = submitResponse.headers.value( + 'operation-location', + ); + + if (operationLocation == null) { + throw Exception('Failed to get operation location from response'); + } + + // Poll for results + final ocrResult = await _pollForOcrResults(operationLocation); + return isOfficer - ? _extractKtaInfo(jsonResponse) - : _extractKtpInfo(jsonResponse); + ? _extractKtaInfo(ocrResult) + : _extractKtpInfo(ocrResult); } else { throw Exception( - 'Failed to process image: ${response.statusCode} - ${response.data}', + 'Failed to submit image: ${submitResponse.statusCode} - ${submitResponse.data}', ); } } catch (e) { @@ -55,10 +67,55 @@ class AzureOCRService { } } - // Extract KTP (Civilian ID) information + // Poll for OCR results until the operation completes + Future> _pollForOcrResults( + String operationLocation, + ) async { + const maxRetries = 10; + const pollingInterval = Duration(milliseconds: 1000); + + for (int i = 0; i < maxRetries; i++) { + try { + final response = await DioClient().get( + operationLocation, + options: Options( + headers: {'Ocp-Apim-Subscription-Key': subscriptionKey}, + ), + ); + + if (response.statusCode == 200) { + final result = json.decode(response.data); + final status = result['status']; + + if (status == 'succeeded') { + return result; + } else if (status == 'failed') { + throw Exception('OCR operation failed: ${result['error']}'); + } + // If status is 'running' or 'notStarted', continue polling + } else { + throw Exception( + 'Failed to get OCR results: ${response.statusCode} - ${response.data}', + ); + } + } catch (e) { + if (i == maxRetries - 1) { + throw Exception( + 'Failed to get OCR results after $maxRetries attempts: $e', + ); + } + } + + await Future.delayed(pollingInterval); + } + + throw Exception('Timeout while waiting for OCR results'); + } + + // Extract KTP (Civilian ID) information - updated for Read API response format Map _extractKtpInfo(Map ocrResult) { final Map extractedInfo = {}; - final List allLines = _getAllTextLines(ocrResult); + final List allLines = _getAllTextLinesFromReadAPI(ocrResult); for (int i = 0; i < allLines.length; i++) { String line = allLines[i].toLowerCase(); @@ -129,10 +186,10 @@ class AzureOCRService { return extractedInfo; } - // Extract KTA (Officer ID) information + // Extract KTA (Officer ID) information - updated for Read API response format Map _extractKtaInfo(Map ocrResult) { final Map extractedInfo = {}; - final List allLines = _getAllTextLines(ocrResult); + final List allLines = _getAllTextLinesFromReadAPI(ocrResult); for (int i = 0; i < allLines.length; i++) { String line = allLines[i].toLowerCase(); @@ -196,7 +253,33 @@ class AzureOCRService { return extractedInfo; } - // Helper method to extract all text lines from OCR result + // Updated helper method to extract text lines from Read API result format + List _getAllTextLinesFromReadAPI(Map ocrResult) { + final List allText = []; + + try { + // Navigate through the Read API response structure + if (ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('readResults')) { + final readResults = ocrResult['analyzeResult']['readResults']; + for (var page in readResults) { + if (page.containsKey('lines')) { + for (var line in page['lines']) { + if (line.containsKey('text')) { + allText.add(line['text']); + } + } + } + } + } + } catch (e) { + print('Error extracting text from OCR result: $e'); + } + + return allText; + } + + // Original helper method for old OCR API format (keeping for compatibility) List _getAllTextLines(Map ocrResult) { final List allText = []; diff --git a/sigap-mobile/lib/src/cores/services/supabase_service.dart b/sigap-mobile/lib/src/cores/services/supabase_service.dart index bf597d3..474d123 100644 --- a/sigap-mobile/lib/src/cores/services/supabase_service.dart +++ b/sigap-mobile/lib/src/cores/services/supabase_service.dart @@ -36,7 +36,7 @@ class SupabaseService extends GetxService { if (metadata.isOfficer == true && metadata.officerData != null) { return metadata.officerData?.nrp; } else { - return metadata.profileData?.nik; + return metadata.viewerData?.profile?.nik; } } diff --git a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart index aa29ac2..83f5075 100644 --- a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart +++ b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart @@ -1,31 +1,32 @@ import 'package:logger/logger.dart'; import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; -import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; +import 'package:sigap/src/features/personalization/data/models/index.dart'; +/// A model representing user metadata with a focus on single source of truth +/// by delegating properties to related models when possible class UserMetadataModel { + // Core properties that define the user type final bool isOfficer; final String? userId; final String? roleId; - final String? nik; - final String? email; - final String? phone; - final String? name; - final OfficerModel? officerData; - final ProfileModel? profileData; - final Map? additionalData; - + final String profileStatus; + // Related models that hold specific data + final OfficerModel? officerData; + final UserModel? viewerData; + + // Only store properties here that aren't available in related models + final String? email; + final Map? additionalData; + const UserMetadataModel({ this.isOfficer = false, this.userId, this.roleId, - this.nik, + this.profileStatus = 'incomplete', this.email, - this.phone, - this.name, this.officerData, - this.profileData, - + this.viewerData, this.additionalData, }); @@ -37,7 +38,7 @@ class UserMetadataModel { final bool isOfficer = json['is_officer'] == true; - // Parse officer data with better error handling + // Parse officer data OfficerModel? officerData; if (json['officer_data'] != null && isOfficer) { try { @@ -50,14 +51,12 @@ class UserMetadataModel { officerData = OfficerModel.fromJson(officerJson); } catch (e) { - // Use proper logging in production Logger().e('Failed to parse officer data: $e'); - // Consider rethrow for critical errors } } - // Parse profile data with better error handling - ProfileModel? profileData; + // Parse profile data + UserModel? viewerData; if (json['profile_data'] != null && !isOfficer) { try { final profileJson = Map.from(json['profile_data']); @@ -66,7 +65,7 @@ class UserMetadataModel { profileJson.putIfAbsent('user_id', () => json['id']); profileJson.putIfAbsent('nik', () => json['nik']); - profileData = ProfileModel.fromJson(profileJson); + viewerData = UserModel.fromJson(profileJson); } catch (e) { Logger().e('Failed to parse profile data: $e'); } @@ -77,10 +76,7 @@ class UserMetadataModel { 'is_officer', 'user_id', 'role_id', - 'nik', 'email', - 'phone', - 'name', 'officer_data', 'profile_data', 'emergency_contact', @@ -92,51 +88,36 @@ class UserMetadataModel { return UserMetadataModel( isOfficer: isOfficer, userId: json['user_id'] as String?, - roleId: json['role_id'] as String?, - nik: json['nik'] as String?, + roleId: json['role_id'] ?? json['initial_role_id'] as String?, + profileStatus: json['profile_status'] as String? ?? 'incomplete', email: json['email'] as String?, - phone: json['phone'] as String?, - name: json['name'] as String?, officerData: officerData, - profileData: profileData, + viewerData: viewerData, additionalData: additionalData.isNotEmpty ? additionalData : null, ); } /// Convert model to JSON Map for Supabase Auth Map toJson() { - final data = {'is_officer': isOfficer}; + final data = { + 'is_officer': isOfficer, + 'profile_status': profileStatus, + }; // Add basic user data if (roleId != null) data['role_id'] = roleId; if (userId != null) data['user_id'] = userId; - if (nik != null) data['nik'] = nik; if (email != null) data['email'] = email; - if (phone != null) data['phone'] = phone; - if (name != null) data['name'] = name; // Add officer-specific data if (officerData != null && isOfficer) { - data['officer_data'] = { - 'nrp': officerData!.nrp, - 'name': officerData!.name, - 'rank': officerData!.rank, - 'position': officerData!.position, - 'phone': officerData!.phone, - 'unit_id': officerData!.unitId, - }; + data['officer_data'] = officerData!.toJson(); } // Add profile data for non-officers - if (profileData != null && !isOfficer) { - data['profile_data'] = { - 'nik': profileData!.nik, - 'first_name': profileData!.firstName, - 'last_name': profileData!.lastName, - 'address': profileData!.address, - }; + if (viewerData != null && !isOfficer) { + data['profile_data'] = viewerData!.toJson(); } - // Add additional data if (additionalData != null) { @@ -146,50 +127,111 @@ class UserMetadataModel { return data; } + /// Convert to specialized JSON for initial signup + Map toInitialSignupJson() { + return { + 'is_officer': isOfficer, + if (email != null) 'email': email, + if (name != null) 'name': name, + if (phone != null) 'phone': phone, + 'profile_status': profileStatus, + 'role_id': roleId, + }; + } + + /// Convert to specialized JSON for profile completion + Map toProfileCompletionJson() { + return { + 'profile_status': 'complete', + 'is_officer': isOfficer, + if (name != null) 'name': name, + if (phone != null) 'phone': phone, + if (roleId != null) 'role_id': roleId, + }; + } + + /// Converts to JSON format for auth metadata update + Map toAuthMetadataJson() { + return { + 'is_officer': isOfficer, + 'role_id': roleId, + 'profile_status': profileStatus, + if (name != null) 'name': name, + }; + } + + factory UserMetadataModel.fromInitUserMetadata( + UserMetadataModel metadata, { + String? email, + String profileStatus = 'incomplete', + }) { + return UserMetadataModel( + email: email, + isOfficer: metadata.isOfficer, + roleId: metadata.roleId, + profileStatus: profileStatus, + viewerData: metadata.viewerData, + officerData: metadata.officerData, + ); + } + /// Create copy with updated fields UserMetadataModel copyWith({ bool? isOfficer, String? userId, String? roleId, - String? nik, + String? profileStatus, String? email, - String? phone, - String? name, OfficerModel? officerData, - ProfileModel? profileData, - Map? emergencyContact, + UserModel? viewerData, Map? additionalData, }) { return UserMetadataModel( isOfficer: isOfficer ?? this.isOfficer, userId: userId ?? this.userId, roleId: roleId ?? this.roleId, - nik: nik ?? this.nik, + profileStatus: profileStatus ?? this.profileStatus, email: email ?? this.email, - phone: phone ?? this.phone, - name: name ?? this.name, officerData: officerData ?? this.officerData, - profileData: profileData ?? this.profileData, - + viewerData: viewerData ?? this.viewerData, additionalData: additionalData ?? this.additionalData, ); } + // MARK: - Computed properties (getters) + /// Primary identifier (NRP for officers, NIK for users) String? get identifier => isOfficer ? officerData?.nrp : nik; - /// Get display name with fallback priority - String? get displayName { - // Priority: explicit name > officer name > profile name > email - if (name?.isNotEmpty == true) return name; + /// User's NIK (delegated to viewerData if available) + String? get nik => viewerData?.profile?.nik; + + /// User's phone number (delegated to appropriate model based on user type) + String? get phone => isOfficer ? officerData?.phone : viewerData?.phone; + + /// User's name (delegated to appropriate model or fallback to email) + String? get name { if (isOfficer && officerData?.name.isNotEmpty == true) { return officerData!.name; } - if (!isOfficer && profileData?.fullName?.isNotEmpty == true) { - return profileData!.fullName; + if (!isOfficer && viewerData?.profile?.fullName?.isNotEmpty == true) { + return viewerData!.profile?.fullName; } return email?.split('@').first; // Fallback to email username } + + /// User's address (only available for regular users) + Map? get address => + !isOfficer ? viewerData?.profile?.address : null; + + /// User's rank (only available for officers) + String? get rank => isOfficer ? officerData?.rank : null; + + /// User's unit ID (only available for officers) + String? get unitId => isOfficer ? officerData?.unitId : null; + + /// Get display name with fallback priority + String? get displayName => name; /// Validate required fields based on user type List validate() { diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index fd4ea16..4145597 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -65,19 +65,36 @@ class AuthenticationRepository extends GetxController { // Redirect user to appropriate screen on app start screenRedirect() async { - final user = _supabase.auth.currentUser; - if (user != null) { - // local storage - storage.writeIfNull('isFirstTime', true); - // check if user is already logged in - storage.read('isFirstTime') != true - ? Get.offAll(() => const SignInScreen()) - : Get.offAll(() => const OnboardingScreen()); + final session = _supabase.auth.currentSession; + + // Check if onboarding has been shown before + final isFirstTime = storage.read('isFirstTime') ?? false; + + if (session != null) { + if (session.user.emailConfirmedAt == null) { + // User is not verified, go to email verification screen + Get.offAllNamed(AppRoutes.emailVerification); + } else if (session.user.userMetadata!['profile_status'] == 'incomplete') { + // User is regular user, go to main app screen + Get.offAllNamed(AppRoutes.registrationForm); + } else if (session.user.userMetadata!['profile_status'] == 'complete' && + session.user.emailConfirmedAt != null) { + // Redirect to the main app screen + Get.offAllNamed(AppRoutes.panicButton); + } } else { // Try biometric login first bool biometricSuccess = await attemptBiometricLogin(); if (!biometricSuccess) { - Get.offAll(() => const SignInScreen()); + // If not first time, go to sign in directly + // If first time, show onboarding first + if (isFirstTime) { + Get.offAll(() => const SignInScreen()); + } else { + // Mark that onboarding has been shown + storage.write('isFirstTime', true); + Get.offAll(() => const OnboardingScreen()); + } } } } @@ -522,16 +539,16 @@ class AuthenticationRepository extends GetxController { required UserMetadataModel initialData, }) async { try { + // Convert to UserModel for more functionality + final userMetadataModel = UserMetadataModel.fromInitUserMetadata( + initialData, + email: email, + ); + final AuthResponse res = await _supabase.auth.signUp( email: email, password: password, - data: { - 'name': initialData.name, - 'phone': initialData.phone, - 'is_officer': initialData.isOfficer, - 'profile_status': 'incomplete', // Mark profile as incomplete - 'initial_role_id': initialData.roleId, - }, + data: userMetadataModel.toInitialSignupJson(), ); return res; @@ -545,15 +562,16 @@ class AuthenticationRepository extends GetxController { /// Updates user profile after registration form completion Future completeUserProfile(UserMetadataModel completeData) async { try { + // Convert to UserModel + final userMetadataModel = UserMetadataModel.fromInitUserMetadata( + completeData, + profileStatus: 'complete', + ); + // First update auth metadata await _supabase.auth.updateUser( UserAttributes( - data: { - 'profile_status': 'complete', - 'is_officer': completeData.isOfficer, - 'name': completeData.name, - 'phone': completeData.phone, - }, + data: userMetadataModel.toProfileCompletionJson(), ), ); @@ -562,16 +580,43 @@ class AuthenticationRepository extends GetxController { await _supabase .from('officers') .insert(completeData.officerData!.toJson()); - } else if (!completeData.isOfficer && completeData.profileData != null) { + } else if (!completeData.isOfficer && completeData.viewerData != null) { await _supabase .from('profiles') - .insert(completeData.profileData!.toJson()); + .insert(completeData.viewerData!.toJson()); } } catch (e) { throw 'Failed to update profile: ${e.toString()}'; } } + /// Update user role after social authentication + Future updateUserRoleOAuth({ + required String userId, + required UserMetadataModel metadata, + }) async { + try { + // Convert to UserModel for more functionality + final userMetadataModel = UserMetadataModel.fromInitUserMetadata( + metadata, + ); + + // Update user metadata in auth + await _supabase.auth.updateUser( + UserAttributes(data: userMetadataModel.toAuthMetadataJson()), + ); + + // Store role information for later use + final localStorage = GetStorage(); + localStorage.write('TEMP_USER_ID', userId); + localStorage.write('TEMP_ROLE_ID', metadata.roleId); + localStorage.write('IS_OFFICER', metadata.isOfficer); + } catch (e) { + throw 'Failed to update user role: ${e.toString()}'; + } + } + + // Update user role (officer/user) and metadata Future updateUserRole({ required bool isOfficer, @@ -606,33 +651,6 @@ class AuthenticationRepository extends GetxController { // Add these methods to the AuthenticationRepository class - /// Update user role after social authentication - Future updateUserRoleOAuth({ - required String userId, - required UserMetadataModel metadata, - }) async { - try { - // Update user metadata in auth - await _supabase.auth.updateUser( - UserAttributes( - data: { - 'is_officer': metadata.isOfficer, - 'role_id': metadata.roleId, - 'profile_status': - 'incomplete', // Mark as incomplete until registration form is filled - }, - ), - ); - - // Store role information for later use - final localStorage = GetStorage(); - localStorage.write('TEMP_USER_ID', userId); - localStorage.write('TEMP_ROLE_ID', metadata.roleId); - localStorage.write('IS_OFFICER', metadata.isOfficer); - } catch (e) { - throw 'Failed to update user role: ${e.toString()}'; - } - } // --------------------------------------------------------------------------- // BIOMETRIC AUTHENTICATION diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart index bbd8b70..fe7eea2 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart @@ -35,6 +35,13 @@ class FormRegistrationController extends GetxController { // User metadata model final Rx userMetadata = UserMetadataModel().obs; + // Vievewer data + final Rx viewerModel = Rx(null); + final Rx profileModel = Rx(null); + + // Officer data + final Rx officerModel = Rx(null); + // Loading state final RxBool isLoading = false.obs; @@ -289,8 +296,7 @@ class FormRegistrationController extends GetxController { if (isOfficerRole) { // Officer role - create OfficerModel with the data - final officerData = OfficerModel( - id: '', // Will be assigned by backend + final officerData = officerModel.value?.copyWith( unitId: unitInfoController!.unitIdController.text, roleId: selectedRole.value!.id, nrp: officerInfoController!.nrpController.text, @@ -302,8 +308,6 @@ class FormRegistrationController extends GetxController { userMetadata.value = userMetadata.value.copyWith( isOfficer: true, - name: personalInfoController.nameController.text, - phone: personalInfoController.phoneController.text, roleId: selectedRole.value!.id, officerData: officerData, additionalData: { @@ -311,24 +315,25 @@ class FormRegistrationController extends GetxController { }, ); } else { + // Regular user - create profile-related data - final profileData = ProfileModel( - id: '', // Will be assigned by backend - userId: userMetadata.value.userId ?? '', - nik: identityController.nikController.text, - firstName: personalInfoController.firstNameController.text.trim(), - lastName: personalInfoController.lastNameController.text.trim(), - bio: identityController.bioController.text, - birthDate: _parseBirthDate(identityController.birthDateController.text), + final viewerData = viewerModel.value?.copyWith( + phone: personalInfoController.phoneController.text, + profile: profileModel.value?.copyWith( + firstName: personalInfoController.firstNameController.text, + lastName: personalInfoController.lastNameController.text, + nik: identityController.nikController.text, + birthDate: _parseBirthDate( + identityController.birthDateController.text, + ), + address: {'address': personalInfoController.addressController.text}, + ), ); userMetadata.value = userMetadata.value.copyWith( isOfficer: false, - nik: identityController.nikController.text, - name: personalInfoController.nameController.text, - phone: personalInfoController.phoneController.text, roleId: selectedRole.value!.id, - profileData: profileData, + viewerData: viewerData, additionalData: { 'address': personalInfoController.addressController.text, }, diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart index 859a4a9..9d74df4 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart @@ -197,35 +197,52 @@ class SignupWithRoleController extends GetxController { isOfficer: isOfficer, ); - // First create the basic account with email/password - final authResponse = await AuthenticationRepository.instance - .initialSignUp( - email: emailController.text.trim(), - password: passwordController.text.trim(), - initialData: initialMetadata, - ); + try { + // First create the basic account with email/password + final authResponse = await AuthenticationRepository.instance + .initialSignUp( + email: emailController.text.trim(), + password: passwordController.text.trim(), + initialData: initialMetadata, + ); - // Store email for verification - storage.write('CURRENT_USER_EMAIL', emailController.text.trim()); - storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken); - storage.write('TEMP_USER_ID', authResponse.user?.id); - storage.write('TEMP_ROLE_ID', selectedRoleId.value); + // Check if authResponse has a user property + if (authResponse.user == null || authResponse.session == null) { + throw Exception('Failed to create account. Please try again.'); + } - // Navigate to registration form - Get.offNamed( - AppRoutes.registrationForm, - arguments: { - 'role': selectedRole.value, - 'userId': authResponse.user?.id, - 'initialData': initialMetadata, - }, - ); + // Store email for verification + storage.write('CURRENT_USER_EMAIL', emailController.text.trim()); + storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken); + storage.write('TEMP_USER_ID', authResponse.user?.id); + storage.write('TEMP_ROLE_ID', selectedRoleId.value); + + // Navigate to registration form + Get.offNamed( + AppRoutes.registrationForm, + arguments: { + 'role': selectedRole.value, + 'userId': authResponse.user?.id, + 'initialData': initialMetadata, + }, + ); + } catch (authError) { + // Handle specific authentication errors + Logger().e('Error during signup: $authError'); + TLoaders.errorSnackBar( + title: 'Registration Failed', + message: authError.toString(), + ); + // Important: Do not navigate or redirect on error + return; + } } catch (e) { Logger().e('Error during signup: $e'); TLoaders.errorSnackBar( title: 'Registration Failed', message: e.toString(), ); + // No navigation on error } finally { isLoading.value = false; } @@ -261,6 +278,10 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; + + if (userId == null) { + throw Exception("Failed to authenticate. Please try again."); + } // Create or update user metadata with role information final userMetadata = UserMetadataModel( @@ -270,7 +291,7 @@ class SignupWithRoleController extends GetxController { // Update user metadata in the database await AuthenticationRepository.instance.updateUserRoleOAuth( - userId: userId!, + userId: userId, metadata: userMetadata, ); @@ -332,6 +353,12 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; + + if (userId == null) { + throw Exception( + "Failed to authenticate with Apple ID. Please try again.", + ); + } // Create or update user metadata with role information final userMetadata = UserMetadataModel( @@ -341,7 +368,7 @@ class SignupWithRoleController extends GetxController { // Update user metadata in the database await AuthenticationRepository.instance.updateUserRoleOAuth( - userId: userId!, + userId: userId, metadata: userMetadata, ); @@ -360,6 +387,7 @@ class SignupWithRoleController extends GetxController { title: 'Authentication Failed', message: e.toString(), ); + // No redirection on error - user stays on current page } finally { isLoading.value = false; } @@ -395,6 +423,12 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; + + if (userId == null) { + throw Exception( + "Failed to authenticate with Facebook. Please try again.", + ); + } // Create or update user metadata with role information final userMetadata = UserMetadataModel( @@ -404,7 +438,7 @@ class SignupWithRoleController extends GetxController { // Update user metadata in the database await AuthenticationRepository.instance.updateUserRoleOAuth( - userId: userId!, + userId: userId, metadata: userMetadata, ); @@ -423,6 +457,7 @@ class SignupWithRoleController extends GetxController { title: 'Authentication Failed', message: e.toString(), ); + // No redirection on error - user stays on current page } finally { isLoading.value = false; } @@ -461,6 +496,12 @@ class SignupWithRoleController extends GetxController { // Check if authResponse has a user property final userId = AuthenticationRepository.instance.currentUserId; + + if (userId == null) { + throw Exception( + "Failed to sign in with email. Please check your credentials and try again.", + ); + } // Create or update user metadata with role information final userMetadata = UserMetadataModel( @@ -470,7 +511,7 @@ class SignupWithRoleController extends GetxController { // Update user metadata in the database await AuthenticationRepository.instance.updateUserRoleOAuth( - userId: userId!, + userId: userId, metadata: userMetadata, ); @@ -489,6 +530,7 @@ class SignupWithRoleController extends GetxController { title: 'Authentication Failed', message: e.toString(), ); + // No redirection on error - user stays on current page } finally { isLoading.value = false; } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart index 79dc95f..08167db 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart @@ -105,6 +105,7 @@ class IdCardVerificationController extends GetxController { idCardImage.value!, isOfficer, ); + // If we get here without an exception, the image is likely valid isImageValid = result.isNotEmpty; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart index 29dcb5b..efcff58 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/id_card_verification_step.dart @@ -47,31 +47,23 @@ class IdCardVerificationStep extends StatelessWidget { ), const SizedBox(height: TSizes.spaceBtwItems), + // Error Messages + Obx( + () => + controller.idCardError.value.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(top: TSizes.sm), + child: Text( + controller.idCardError.value, + style: const TextStyle(color: Colors.red), + ), + ) + : const SizedBox.shrink(), + ), + // ID Card Upload Widget _buildIdCardUploader(controller, isOfficer), - // Verification Status for ID Card - // Obx( - // () => - // controller.isVerifying.value && - // !controller.isUploadingIdCard.value - // ? const Padding( - // padding: EdgeInsets.symmetric( - // vertical: TSizes.spaceBtwItems, - // ), - // child: Center( - // child: Column( - // children: [ - // CircularProgressIndicator(), - // SizedBox(height: TSizes.sm), - // Text('Validating your ID card...'), - // ], - // ), - // ), - // ) - // : const SizedBox.shrink(), - // ), - // Verification Message for ID Card Obx( () => @@ -178,19 +170,7 @@ class IdCardVerificationStep extends StatelessWidget { : const SizedBox.shrink(), ), - // Error Messages - Obx( - () => - controller.idCardError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: TSizes.sm), - child: Text( - controller.idCardError.value, - style: const TextStyle(color: Colors.red), - ), - ) - : const SizedBox.shrink(), - ), + ], ), ); diff --git a/sigap-website/supabase/config.toml b/sigap-website/supabase/config.toml index 2d125bf..39b2f24 100644 --- a/sigap-website/supabase/config.toml +++ b/sigap-website/supabase/config.toml @@ -154,7 +154,7 @@ enable_signup = true # addresses. If disabled, only the new email is required to confirm. double_confirm_changes = true # If enabled, users need to confirm their email address before signing in. -enable_confirmations = false +enable_confirmations = true # If enabled, users will need to reauthenticate or have logged in recently to change their password. secure_password_change = false # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.