feat: Update user metadata model and authentication flow; enhance ID card verification process and error handling
This commit is contained in:
parent
498b71c184
commit
ac39366371
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,52 +88,37 @@ 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) {
|
||||
data.addAll(additionalData!);
|
||||
|
@ -146,51 +127,112 @@ 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() {
|
||||
final errors = <String>[];
|
||||
|
|
|
@ -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) {
|
||||
// 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
|
||||
|
|
|
@ -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 ?? '',
|
||||
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,
|
||||
firstName: personalInfoController.firstNameController.text.trim(),
|
||||
lastName: personalInfoController.lastNameController.text.trim(),
|
||||
bio: identityController.bioController.text,
|
||||
birthDate: _parseBirthDate(identityController.birthDateController.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,
|
||||
},
|
||||
|
|
|
@ -197,6 +197,7 @@ class SignupWithRoleController extends GetxController {
|
|||
isOfficer: isOfficer,
|
||||
);
|
||||
|
||||
try {
|
||||
// First create the basic account with email/password
|
||||
final authResponse = await AuthenticationRepository.instance
|
||||
.initialSignUp(
|
||||
|
@ -205,6 +206,11 @@ class SignupWithRoleController extends GetxController {
|
|||
initialData: initialMetadata,
|
||||
);
|
||||
|
||||
// Check if authResponse has a user property
|
||||
if (authResponse.user == null || authResponse.session == null) {
|
||||
throw Exception('Failed to create account. Please try again.');
|
||||
}
|
||||
|
||||
// Store email for verification
|
||||
storage.write('CURRENT_USER_EMAIL', emailController.text.trim());
|
||||
storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken);
|
||||
|
@ -220,12 +226,23 @@ class SignupWithRoleController extends GetxController {
|
|||
'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;
|
||||
}
|
||||
|
@ -262,6 +279,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(
|
||||
isOfficer: isOfficer,
|
||||
|
@ -270,7 +291,7 @@ class SignupWithRoleController extends GetxController {
|
|||
|
||||
// Update user metadata in the database
|
||||
await AuthenticationRepository.instance.updateUserRoleOAuth(
|
||||
userId: userId!,
|
||||
userId: userId,
|
||||
metadata: userMetadata,
|
||||
);
|
||||
|
||||
|
@ -333,6 +354,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(
|
||||
isOfficer: isOfficer,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -396,6 +424,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(
|
||||
isOfficer: isOfficer,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -462,6 +497,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(
|
||||
isOfficer: isOfficer,
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue