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 { class UtilityBindings extends Bindings {
Logger? get logger => Logger(); Logger? get logger => Logger();
NetworkManager? get networkManager => NetworkManager();
BackgroundService? get backgroundService => BackgroundService.instance; BackgroundService? get backgroundService => BackgroundService.instance;
@override @override
void dependencies() { void dependencies() {
Get.put(backgroundService, permanent: true); Get.put(backgroundService, permanent: true);
Get.put(networkManager, permanent: true); Get.put(NetworkManager(), permanent: true);
Get.put(logger, permanent: true); Get.put(logger, permanent: true);
} }
} }

View File

@ -28,7 +28,8 @@ class AzureOCRService {
'$endpoint$ocrApiPath?language=id&detectOrientation=true', '$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(), uri.toString(),
data: bytes, data: bytes,
options: Options( options: Options(
@ -40,14 +41,25 @@ class AzureOCRService {
), ),
); );
if (response.statusCode == 200) { if (submitResponse.statusCode == 202) {
final jsonResponse = json.decode(response.data); // 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 return isOfficer
? _extractKtaInfo(jsonResponse) ? _extractKtaInfo(ocrResult)
: _extractKtpInfo(jsonResponse); : _extractKtpInfo(ocrResult);
} else { } else {
throw Exception( throw Exception(
'Failed to process image: ${response.statusCode} - ${response.data}', 'Failed to submit image: ${submitResponse.statusCode} - ${submitResponse.data}',
); );
} }
} catch (e) { } 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) { Map<String, String> _extractKtpInfo(Map<String, dynamic> ocrResult) {
final Map<String, String> extractedInfo = {}; 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++) { for (int i = 0; i < allLines.length; i++) {
String line = allLines[i].toLowerCase(); String line = allLines[i].toLowerCase();
@ -129,10 +186,10 @@ class AzureOCRService {
return extractedInfo; 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) { Map<String, String> _extractKtaInfo(Map<String, dynamic> ocrResult) {
final Map<String, String> extractedInfo = {}; 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++) { for (int i = 0; i < allLines.length; i++) {
String line = allLines[i].toLowerCase(); String line = allLines[i].toLowerCase();
@ -196,7 +253,33 @@ class AzureOCRService {
return extractedInfo; 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) { List<String> _getAllTextLines(Map<String, dynamic> ocrResult) {
final List<String> allText = []; final List<String> allText = [];

View File

@ -36,7 +36,7 @@ class SupabaseService extends GetxService {
if (metadata.isOfficer == true && metadata.officerData != null) { if (metadata.isOfficer == true && metadata.officerData != null) {
return metadata.officerData?.nrp; return metadata.officerData?.nrp;
} else { } else {
return metadata.profileData?.nik; return metadata.viewerData?.profile?.nik;
} }
} }

View File

@ -1,31 +1,32 @@
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.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 { class UserMetadataModel {
// Core properties that define the user type
final bool isOfficer; final bool isOfficer;
final String? userId; final String? userId;
final String? roleId; final String? roleId;
final String? nik; final String profileStatus;
final String? email;
final String? phone;
final String? name;
final OfficerModel? officerData;
final ProfileModel? profileData;
final Map<String, dynamic>? additionalData;
// 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({ const UserMetadataModel({
this.isOfficer = false, this.isOfficer = false,
this.userId, this.userId,
this.roleId, this.roleId,
this.nik, this.profileStatus = 'incomplete',
this.email, this.email,
this.phone,
this.name,
this.officerData, this.officerData,
this.profileData, this.viewerData,
this.additionalData, this.additionalData,
}); });
@ -37,7 +38,7 @@ class UserMetadataModel {
final bool isOfficer = json['is_officer'] == true; final bool isOfficer = json['is_officer'] == true;
// Parse officer data with better error handling // Parse officer data
OfficerModel? officerData; OfficerModel? officerData;
if (json['officer_data'] != null && isOfficer) { if (json['officer_data'] != null && isOfficer) {
try { try {
@ -50,14 +51,12 @@ class UserMetadataModel {
officerData = OfficerModel.fromJson(officerJson); officerData = OfficerModel.fromJson(officerJson);
} catch (e) { } catch (e) {
// Use proper logging in production
Logger().e('Failed to parse officer data: $e'); Logger().e('Failed to parse officer data: $e');
// Consider rethrow for critical errors
} }
} }
// Parse profile data with better error handling // Parse profile data
ProfileModel? profileData; UserModel? viewerData;
if (json['profile_data'] != null && !isOfficer) { if (json['profile_data'] != null && !isOfficer) {
try { try {
final profileJson = Map<String, dynamic>.from(json['profile_data']); final profileJson = Map<String, dynamic>.from(json['profile_data']);
@ -66,7 +65,7 @@ class UserMetadataModel {
profileJson.putIfAbsent('user_id', () => json['id']); profileJson.putIfAbsent('user_id', () => json['id']);
profileJson.putIfAbsent('nik', () => json['nik']); profileJson.putIfAbsent('nik', () => json['nik']);
profileData = ProfileModel.fromJson(profileJson); viewerData = UserModel.fromJson(profileJson);
} catch (e) { } catch (e) {
Logger().e('Failed to parse profile data: $e'); Logger().e('Failed to parse profile data: $e');
} }
@ -77,10 +76,7 @@ class UserMetadataModel {
'is_officer', 'is_officer',
'user_id', 'user_id',
'role_id', 'role_id',
'nik',
'email', 'email',
'phone',
'name',
'officer_data', 'officer_data',
'profile_data', 'profile_data',
'emergency_contact', 'emergency_contact',
@ -92,52 +88,37 @@ class UserMetadataModel {
return UserMetadataModel( return UserMetadataModel(
isOfficer: isOfficer, isOfficer: isOfficer,
userId: json['user_id'] as String?, userId: json['user_id'] as String?,
roleId: json['role_id'] as String?, roleId: json['role_id'] ?? json['initial_role_id'] as String?,
nik: json['nik'] as String?, profileStatus: json['profile_status'] as String? ?? 'incomplete',
email: json['email'] as String?, email: json['email'] as String?,
phone: json['phone'] as String?,
name: json['name'] as String?,
officerData: officerData, officerData: officerData,
profileData: profileData, viewerData: viewerData,
additionalData: additionalData.isNotEmpty ? additionalData : null, additionalData: additionalData.isNotEmpty ? additionalData : null,
); );
} }
/// Convert model to JSON Map for Supabase Auth /// Convert model to JSON Map for Supabase Auth
Map<String, dynamic> toJson() { 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 // Add basic user data
if (roleId != null) data['role_id'] = roleId; if (roleId != null) data['role_id'] = roleId;
if (userId != null) data['user_id'] = userId; if (userId != null) data['user_id'] = userId;
if (nik != null) data['nik'] = nik;
if (email != null) data['email'] = email; if (email != null) data['email'] = email;
if (phone != null) data['phone'] = phone;
if (name != null) data['name'] = name;
// Add officer-specific data // Add officer-specific data
if (officerData != null && isOfficer) { if (officerData != null && isOfficer) {
data['officer_data'] = { data['officer_data'] = officerData!.toJson();
'nrp': officerData!.nrp,
'name': officerData!.name,
'rank': officerData!.rank,
'position': officerData!.position,
'phone': officerData!.phone,
'unit_id': officerData!.unitId,
};
} }
// Add profile data for non-officers // Add profile data for non-officers
if (profileData != null && !isOfficer) { if (viewerData != null && !isOfficer) {
data['profile_data'] = { data['profile_data'] = viewerData!.toJson();
'nik': profileData!.nik,
'first_name': profileData!.firstName,
'last_name': profileData!.lastName,
'address': profileData!.address,
};
} }
// Add additional data // Add additional data
if (additionalData != null) { if (additionalData != null) {
data.addAll(additionalData!); data.addAll(additionalData!);
@ -146,51 +127,112 @@ class UserMetadataModel {
return data; 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 /// Create copy with updated fields
UserMetadataModel copyWith({ UserMetadataModel copyWith({
bool? isOfficer, bool? isOfficer,
String? userId, String? userId,
String? roleId, String? roleId,
String? nik, String? profileStatus,
String? email, String? email,
String? phone,
String? name,
OfficerModel? officerData, OfficerModel? officerData,
ProfileModel? profileData, UserModel? viewerData,
Map<String, dynamic>? emergencyContact,
Map<String, dynamic>? additionalData, Map<String, dynamic>? additionalData,
}) { }) {
return UserMetadataModel( return UserMetadataModel(
isOfficer: isOfficer ?? this.isOfficer, isOfficer: isOfficer ?? this.isOfficer,
userId: userId ?? this.userId, userId: userId ?? this.userId,
roleId: roleId ?? this.roleId, roleId: roleId ?? this.roleId,
nik: nik ?? this.nik, profileStatus: profileStatus ?? this.profileStatus,
email: email ?? this.email, email: email ?? this.email,
phone: phone ?? this.phone,
name: name ?? this.name,
officerData: officerData ?? this.officerData, officerData: officerData ?? this.officerData,
profileData: profileData ?? this.profileData, viewerData: viewerData ?? this.viewerData,
additionalData: additionalData ?? this.additionalData, additionalData: additionalData ?? this.additionalData,
); );
} }
// MARK: - Computed properties (getters)
/// Primary identifier (NRP for officers, NIK for users) /// Primary identifier (NRP for officers, NIK for users)
String? get identifier => isOfficer ? officerData?.nrp : nik; String? get identifier => isOfficer ? officerData?.nrp : nik;
/// Get display name with fallback priority /// User's NIK (delegated to viewerData if available)
String? get displayName { String? get nik => viewerData?.profile?.nik;
// Priority: explicit name > officer name > profile name > email
if (name?.isNotEmpty == true) return name; /// 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) { if (isOfficer && officerData?.name.isNotEmpty == true) {
return officerData!.name; return officerData!.name;
} }
if (!isOfficer && profileData?.fullName?.isNotEmpty == true) { if (!isOfficer && viewerData?.profile?.fullName?.isNotEmpty == true) {
return profileData!.fullName; return viewerData!.profile?.fullName;
} }
return email?.split('@').first; // Fallback to email username 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 /// Validate required fields based on user type
List<String> validate() { List<String> validate() {
final errors = <String>[]; final errors = <String>[];

View File

@ -65,19 +65,36 @@ class AuthenticationRepository extends GetxController {
// Redirect user to appropriate screen on app start // Redirect user to appropriate screen on app start
screenRedirect() async { screenRedirect() async {
final user = _supabase.auth.currentUser; final session = _supabase.auth.currentSession;
if (user != null) {
// local storage // Check if onboarding has been shown before
storage.writeIfNull('isFirstTime', true); final isFirstTime = storage.read('isFirstTime') ?? false;
// check if user is already logged in
storage.read('isFirstTime') != true if (session != null) {
? Get.offAll(() => const SignInScreen()) if (session.user.emailConfirmedAt == null) {
: Get.offAll(() => const OnboardingScreen()); // 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 { } else {
// Try biometric login first // Try biometric login first
bool biometricSuccess = await attemptBiometricLogin(); bool biometricSuccess = await attemptBiometricLogin();
if (!biometricSuccess) { 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, required UserMetadataModel initialData,
}) async { }) async {
try { try {
// Convert to UserModel for more functionality
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
initialData,
email: email,
);
final AuthResponse res = await _supabase.auth.signUp( final AuthResponse res = await _supabase.auth.signUp(
email: email, email: email,
password: password, password: password,
data: { data: userMetadataModel.toInitialSignupJson(),
'name': initialData.name,
'phone': initialData.phone,
'is_officer': initialData.isOfficer,
'profile_status': 'incomplete', // Mark profile as incomplete
'initial_role_id': initialData.roleId,
},
); );
return res; return res;
@ -545,15 +562,16 @@ class AuthenticationRepository extends GetxController {
/// Updates user profile after registration form completion /// Updates user profile after registration form completion
Future<void> completeUserProfile(UserMetadataModel completeData) async { Future<void> completeUserProfile(UserMetadataModel completeData) async {
try { try {
// Convert to UserModel
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
completeData,
profileStatus: 'complete',
);
// First update auth metadata // First update auth metadata
await _supabase.auth.updateUser( await _supabase.auth.updateUser(
UserAttributes( UserAttributes(
data: { data: userMetadataModel.toProfileCompletionJson(),
'profile_status': 'complete',
'is_officer': completeData.isOfficer,
'name': completeData.name,
'phone': completeData.phone,
},
), ),
); );
@ -562,16 +580,43 @@ class AuthenticationRepository extends GetxController {
await _supabase await _supabase
.from('officers') .from('officers')
.insert(completeData.officerData!.toJson()); .insert(completeData.officerData!.toJson());
} else if (!completeData.isOfficer && completeData.profileData != null) { } else if (!completeData.isOfficer && completeData.viewerData != null) {
await _supabase await _supabase
.from('profiles') .from('profiles')
.insert(completeData.profileData!.toJson()); .insert(completeData.viewerData!.toJson());
} }
} catch (e) { } catch (e) {
throw 'Failed to update profile: ${e.toString()}'; 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 // Update user role (officer/user) and metadata
Future<UserResponse> updateUserRole({ Future<UserResponse> updateUserRole({
required bool isOfficer, required bool isOfficer,
@ -606,33 +651,6 @@ class AuthenticationRepository extends GetxController {
// Add these methods to the AuthenticationRepository class // 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 // BIOMETRIC AUTHENTICATION

View File

@ -35,6 +35,13 @@ class FormRegistrationController extends GetxController {
// User metadata model // User metadata model
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs; 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 // Loading state
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -289,8 +296,7 @@ class FormRegistrationController extends GetxController {
if (isOfficerRole) { if (isOfficerRole) {
// Officer role - create OfficerModel with the data // Officer role - create OfficerModel with the data
final officerData = OfficerModel( final officerData = officerModel.value?.copyWith(
id: '', // Will be assigned by backend
unitId: unitInfoController!.unitIdController.text, unitId: unitInfoController!.unitIdController.text,
roleId: selectedRole.value!.id, roleId: selectedRole.value!.id,
nrp: officerInfoController!.nrpController.text, nrp: officerInfoController!.nrpController.text,
@ -302,8 +308,6 @@ class FormRegistrationController extends GetxController {
userMetadata.value = userMetadata.value.copyWith( userMetadata.value = userMetadata.value.copyWith(
isOfficer: true, isOfficer: true,
name: personalInfoController.nameController.text,
phone: personalInfoController.phoneController.text,
roleId: selectedRole.value!.id, roleId: selectedRole.value!.id,
officerData: officerData, officerData: officerData,
additionalData: { additionalData: {
@ -311,24 +315,25 @@ class FormRegistrationController extends GetxController {
}, },
); );
} else { } else {
// Regular user - create profile-related data // Regular user - create profile-related data
final profileData = ProfileModel( final viewerData = viewerModel.value?.copyWith(
id: '', // Will be assigned by backend phone: personalInfoController.phoneController.text,
userId: userMetadata.value.userId ?? '', profile: profileModel.value?.copyWith(
nik: identityController.nikController.text, firstName: personalInfoController.firstNameController.text,
firstName: personalInfoController.firstNameController.text.trim(), lastName: personalInfoController.lastNameController.text,
lastName: personalInfoController.lastNameController.text.trim(), nik: identityController.nikController.text,
bio: identityController.bioController.text, birthDate: _parseBirthDate(
birthDate: _parseBirthDate(identityController.birthDateController.text), identityController.birthDateController.text,
),
address: {'address': personalInfoController.addressController.text},
),
); );
userMetadata.value = userMetadata.value.copyWith( userMetadata.value = userMetadata.value.copyWith(
isOfficer: false, isOfficer: false,
nik: identityController.nikController.text,
name: personalInfoController.nameController.text,
phone: personalInfoController.phoneController.text,
roleId: selectedRole.value!.id, roleId: selectedRole.value!.id,
profileData: profileData, viewerData: viewerData,
additionalData: { additionalData: {
'address': personalInfoController.addressController.text, 'address': personalInfoController.addressController.text,
}, },

View File

@ -197,35 +197,52 @@ class SignupWithRoleController extends GetxController {
isOfficer: isOfficer, isOfficer: isOfficer,
); );
// First create the basic account with email/password try {
final authResponse = await AuthenticationRepository.instance // First create the basic account with email/password
.initialSignUp( final authResponse = await AuthenticationRepository.instance
email: emailController.text.trim(), .initialSignUp(
password: passwordController.text.trim(), email: emailController.text.trim(),
initialData: initialMetadata, password: passwordController.text.trim(),
); initialData: initialMetadata,
);
// Store email for verification // Check if authResponse has a user property
storage.write('CURRENT_USER_EMAIL', emailController.text.trim()); if (authResponse.user == null || authResponse.session == null) {
storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken); throw Exception('Failed to create account. Please try again.');
storage.write('TEMP_USER_ID', authResponse.user?.id); }
storage.write('TEMP_ROLE_ID', selectedRoleId.value);
// Navigate to registration form // Store email for verification
Get.offNamed( storage.write('CURRENT_USER_EMAIL', emailController.text.trim());
AppRoutes.registrationForm, storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken);
arguments: { storage.write('TEMP_USER_ID', authResponse.user?.id);
'role': selectedRole.value, storage.write('TEMP_ROLE_ID', selectedRoleId.value);
'userId': authResponse.user?.id,
'initialData': initialMetadata, // 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) { } catch (e) {
Logger().e('Error during signup: $e'); Logger().e('Error during signup: $e');
TLoaders.errorSnackBar( TLoaders.errorSnackBar(
title: 'Registration Failed', title: 'Registration Failed',
message: e.toString(), message: e.toString(),
); );
// No navigation on error
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -262,6 +279,10 @@ class SignupWithRoleController extends GetxController {
// Check if authResponse has a user property // Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId; final userId = AuthenticationRepository.instance.currentUserId;
if (userId == null) {
throw Exception("Failed to authenticate. Please try again.");
}
// Create or update user metadata with role information // Create or update user metadata with role information
final userMetadata = UserMetadataModel( final userMetadata = UserMetadataModel(
isOfficer: isOfficer, isOfficer: isOfficer,
@ -270,7 +291,7 @@ class SignupWithRoleController extends GetxController {
// Update user metadata in the database // Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth( await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!, userId: userId,
metadata: userMetadata, metadata: userMetadata,
); );
@ -333,6 +354,12 @@ class SignupWithRoleController extends GetxController {
// Check if authResponse has a user property // Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId; 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 // Create or update user metadata with role information
final userMetadata = UserMetadataModel( final userMetadata = UserMetadataModel(
isOfficer: isOfficer, isOfficer: isOfficer,
@ -341,7 +368,7 @@ class SignupWithRoleController extends GetxController {
// Update user metadata in the database // Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth( await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!, userId: userId,
metadata: userMetadata, metadata: userMetadata,
); );
@ -360,6 +387,7 @@ class SignupWithRoleController extends GetxController {
title: 'Authentication Failed', title: 'Authentication Failed',
message: e.toString(), message: e.toString(),
); );
// No redirection on error - user stays on current page
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -396,6 +424,12 @@ class SignupWithRoleController extends GetxController {
// Check if authResponse has a user property // Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId; 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 // Create or update user metadata with role information
final userMetadata = UserMetadataModel( final userMetadata = UserMetadataModel(
isOfficer: isOfficer, isOfficer: isOfficer,
@ -404,7 +438,7 @@ class SignupWithRoleController extends GetxController {
// Update user metadata in the database // Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth( await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!, userId: userId,
metadata: userMetadata, metadata: userMetadata,
); );
@ -423,6 +457,7 @@ class SignupWithRoleController extends GetxController {
title: 'Authentication Failed', title: 'Authentication Failed',
message: e.toString(), message: e.toString(),
); );
// No redirection on error - user stays on current page
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -462,6 +497,12 @@ class SignupWithRoleController extends GetxController {
// Check if authResponse has a user property // Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId; 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 // Create or update user metadata with role information
final userMetadata = UserMetadataModel( final userMetadata = UserMetadataModel(
isOfficer: isOfficer, isOfficer: isOfficer,
@ -470,7 +511,7 @@ class SignupWithRoleController extends GetxController {
// Update user metadata in the database // Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth( await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!, userId: userId,
metadata: userMetadata, metadata: userMetadata,
); );
@ -489,6 +530,7 @@ class SignupWithRoleController extends GetxController {
title: 'Authentication Failed', title: 'Authentication Failed',
message: e.toString(), message: e.toString(),
); );
// No redirection on error - user stays on current page
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -105,6 +105,7 @@ class IdCardVerificationController extends GetxController {
idCardImage.value!, idCardImage.value!,
isOfficer, isOfficer,
); );
// If we get here without an exception, the image is likely valid // If we get here without an exception, the image is likely valid
isImageValid = result.isNotEmpty; isImageValid = result.isNotEmpty;

View File

@ -47,31 +47,23 @@ class IdCardVerificationStep extends StatelessWidget {
), ),
const SizedBox(height: TSizes.spaceBtwItems), 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 // ID Card Upload Widget
_buildIdCardUploader(controller, isOfficer), _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 // Verification Message for ID Card
Obx( Obx(
() => () =>
@ -178,19 +170,7 @@ class IdCardVerificationStep extends StatelessWidget {
: const SizedBox.shrink(), : 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. # addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in. # 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. # If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.