feat: Update user metadata model and authentication flow; enhance ID card verification process and error handling

This commit is contained in:
vergiLgood1 2025-05-20 03:30:19 +07:00
parent 498b71c184
commit ac39366371
10 changed files with 377 additions and 206 deletions

View File

@ -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);
}
}

View File

@ -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<Map<String, dynamic>> _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<String, String> _extractKtpInfo(Map<String, dynamic> ocrResult) {
final Map<String, String> extractedInfo = {};
final List<String> allLines = _getAllTextLines(ocrResult);
final List<String> 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<String, String> _extractKtaInfo(Map<String, dynamic> ocrResult) {
final Map<String, String> extractedInfo = {};
final List<String> allLines = _getAllTextLines(ocrResult);
final List<String> 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<String> _getAllTextLinesFromReadAPI(Map<String, dynamic> ocrResult) {
final List<String> 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<String> _getAllTextLines(Map<String, dynamic> ocrResult) {
final List<String> allText = [];

View File

@ -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;
}
}

View File

@ -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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>.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<String, dynamic> toJson() {
final data = <String, dynamic>{'is_officer': isOfficer};
final data = <String, dynamic>{
'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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>? emergencyContact,
UserModel? viewerData,
Map<String, dynamic>? 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<String, dynamic>? 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<String> validate() {

View File

@ -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<void> 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<void> 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<UserResponse> 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<void> 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

View File

@ -35,6 +35,13 @@ class FormRegistrationController extends GetxController {
// User metadata model
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs;
// Vievewer data
final Rx<UserModel?> viewerModel = Rx<UserModel?>(null);
final Rx<ProfileModel?> profileModel = Rx<ProfileModel?>(null);
// Officer data
final Rx<OfficerModel?> officerModel = Rx<OfficerModel?>(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,
},

View File

@ -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;
}

View File

@ -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;

View File

@ -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(),
),
],
),
);

View File

@ -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.