feat: Refactor signup process with role selection and update app routes

- Updated app routes for backward compatibility and renamed registration form route.
- Added new image constants for various animations and background images.
- Removed webview_flutter dependency from macOS and updated pubspec.lock.
- Introduced SignupWithRoleController to manage signup logic based on user roles.
- Created SignupWithRoleScreen for user interface, allowing role selection and form submission.
- Implemented RoleSelector widget for displaying available roles.
- Enhanced social login options and integrated privacy policy acceptance in the signup form.
This commit is contained in:
vergiLgood1 2025-05-20 00:02:35 +07:00
parent ce7d5f5cf4
commit 498b71c184
21 changed files with 1823 additions and 308 deletions

View File

@ -3,6 +3,7 @@ import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgo
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_screen.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart';
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
@ -25,7 +26,7 @@ class AppPages {
), ),
GetPage( GetPage(
name: AppRoutes.formRegistration, name: AppRoutes.registrationForm,
page: () => const FormRegistrationScreen(), page: () => const FormRegistrationScreen(),
), ),
@ -33,6 +34,11 @@ class AppPages {
GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()), GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()),
GetPage(
name: AppRoutes.signupWithRole,
page: () => const SignupWithRoleScreen(),
),
GetPage( GetPage(
name: AppRoutes.forgotPassword, name: AppRoutes.forgotPassword,
page: () => const ForgotPasswordScreen(), page: () => const ForgotPasswordScreen(),

View File

@ -1,8 +1,11 @@
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/models/profile_model.dart';
class UserMetadataModel { class UserMetadataModel {
final bool isOfficer; final bool isOfficer;
final String? userId;
final String? roleId;
final String? nik; final String? nik;
final String? email; final String? email;
final String? phone; final String? phone;
@ -10,109 +13,111 @@ class UserMetadataModel {
final OfficerModel? officerData; final OfficerModel? officerData;
final ProfileModel? profileData; final ProfileModel? profileData;
final Map<String, dynamic>? additionalData; final Map<String, dynamic>? additionalData;
// Emergency contact data frequently used in the app
final Map<String, dynamic>? emergencyContact;
UserMetadataModel({ const UserMetadataModel({
this.isOfficer = false, this.isOfficer = false,
this.userId,
this.roleId,
this.nik, this.nik,
this.email, this.email,
this.phone, this.phone,
this.name, this.name,
this.officerData, this.officerData,
this.profileData, this.profileData,
this.emergencyContact,
this.additionalData, this.additionalData,
}); });
/// Create a UserMetadataModel from raw Map data (from Supabase Auth) /// Create UserMetadataModel from raw Map data (from Supabase Auth)
factory UserMetadataModel.fromJson(Map<String, dynamic>? json) { factory UserMetadataModel.fromJson(Map<String, dynamic>? json) {
if (json == null) return UserMetadataModel(); if (json == null || json.isEmpty) {
return const UserMetadataModel();
}
// Extract officer data if available final bool isOfficer = json['is_officer'] == true;
// Parse officer data with better error handling
OfficerModel? officerData; OfficerModel? officerData;
if (json['officer_data'] != null && json['is_officer'] == true) { if (json['officer_data'] != null && isOfficer) {
try { try {
// Create temporary ID and role fields if missing
final officerJson = Map<String, dynamic>.from(json['officer_data']); final officerJson = Map<String, dynamic>.from(json['officer_data']);
if (!officerJson.containsKey('id')) {
officerJson['id'] = json['id'] ?? ''; // Only add missing required fields, allow null for optional ones
} officerJson.putIfAbsent('id', () => json['id']);
if (!officerJson.containsKey('role_id')) { officerJson.putIfAbsent('role_id', () => null);
officerJson['role_id'] = ''; officerJson.putIfAbsent('unit_id', () => null);
}
if (!officerJson.containsKey('unit_id')) {
officerJson['unit_id'] = officerJson['unit_id'] ?? '';
}
officerData = OfficerModel.fromJson(officerJson); officerData = OfficerModel.fromJson(officerJson);
} catch (e) { } catch (e) {
print('Error parsing officer data: $e'); // Use proper logging in production
Logger().e('Failed to parse officer data: $e');
// Consider rethrow for critical errors
} }
} }
// Extract profile data if available // Parse profile data with better error handling
ProfileModel? profileData; ProfileModel? profileData;
if (json['profile_data'] != null) { 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']);
if (!profileJson.containsKey('id')) {
profileJson['id'] = ''; profileJson.putIfAbsent('id', () => null);
} profileJson.putIfAbsent('user_id', () => json['id']);
if (!profileJson.containsKey('user_id')) { profileJson.putIfAbsent('nik', () => json['nik']);
profileJson['user_id'] = json['id'] ?? '';
}
if (!profileJson.containsKey('nik')) {
profileJson['nik'] = json['nik'] ?? '';
}
profileData = ProfileModel.fromJson(profileJson); profileData = ProfileModel.fromJson(profileJson);
} catch (e) { } catch (e) {
print('Error parsing profile data: $e'); Logger().e('Failed to parse profile data: $e');
} }
} }
// Create additionalData by excluding known fields
final excludedKeys = {
'is_officer',
'user_id',
'role_id',
'nik',
'email',
'phone',
'name',
'officer_data',
'profile_data',
'emergency_contact',
};
final additionalData = Map<String, dynamic>.from(json)
..removeWhere((key, _) => excludedKeys.contains(key));
return UserMetadataModel( return UserMetadataModel(
isOfficer: json['is_officer'] == true, isOfficer: isOfficer,
userId: json['user_id'] as String?,
roleId: json['role_id'] as String?,
nik: json['nik'] as String?, nik: json['nik'] as String?,
email: json['email'] as String?, email: json['email'] as String?,
phone: json['phone'] as String?, phone: json['phone'] as String?,
name: json['name'] as String?, name: json['name'] as String?,
officerData: officerData, officerData: officerData,
profileData: profileData, profileData: profileData,
emergencyContact: additionalData: additionalData.isNotEmpty ? additionalData : null,
json['emergency_contact'] != null
? Map<String, dynamic>.from(json['emergency_contact'])
: null,
additionalData: Map<String, dynamic>.from(json)..removeWhere(
(key, _) => [
'is_officer',
'nik',
'email',
'phone',
'name',
'officer_data',
'profile_data',
'emergency_contact',
].contains(key),
),
); );
} }
/// Convert the model to a json Map (for Supabase Auth) /// Convert model to JSON Map for Supabase Auth
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {'is_officer': isOfficer}; final data = <String, dynamic>{'is_officer': isOfficer};
// 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 (nik != null) data['nik'] = nik;
if (email != null) data['email'] = email; if (email != null) data['email'] = email;
if (phone != null) data['phone'] = phone; if (phone != null) data['phone'] = phone;
if (name != null) data['name'] = name; if (name != null) data['name'] = name;
// Add officer-specific data
if (officerData != null && isOfficer) { if (officerData != null && isOfficer) {
// Extract only the necessary fields for the officerData data['officer_data'] = {
// to prevent circular references and reduce data size
final officerJson = {
'nrp': officerData!.nrp, 'nrp': officerData!.nrp,
'name': officerData!.name, 'name': officerData!.name,
'rank': officerData!.rank, 'rank': officerData!.rank,
@ -120,24 +125,20 @@ class UserMetadataModel {
'phone': officerData!.phone, 'phone': officerData!.phone,
'unit_id': officerData!.unitId, 'unit_id': officerData!.unitId,
}; };
data['officer_data'] = officerJson;
} }
if (profileData != null) { // Add profile data for non-officers
// Extract only the necessary profile fields if (profileData != null && !isOfficer) {
final profileJson = { data['profile_data'] = {
'nik': profileData!.nik, 'nik': profileData!.nik,
'first_name': profileData!.firstName, 'first_name': profileData!.firstName,
'last_name': profileData!.lastName, 'last_name': profileData!.lastName,
'address': profileData!.address, 'address': profileData!.address,
}; };
data['profile_data'] = profileJson;
}
if (emergencyContact != null) {
data['emergency_contact'] = emergencyContact;
} }
// Add additional data
if (additionalData != null) { if (additionalData != null) {
data.addAll(additionalData!); data.addAll(additionalData!);
} }
@ -145,9 +146,11 @@ class UserMetadataModel {
return data; return data;
} }
/// Create a copy with updated fields /// Create copy with updated fields
UserMetadataModel copyWith({ UserMetadataModel copyWith({
bool? isOfficer, bool? isOfficer,
String? userId,
String? roleId,
String? nik, String? nik,
String? email, String? email,
String? phone, String? phone,
@ -159,26 +162,92 @@ class UserMetadataModel {
}) { }) {
return UserMetadataModel( return UserMetadataModel(
isOfficer: isOfficer ?? this.isOfficer, isOfficer: isOfficer ?? this.isOfficer,
userId: userId ?? this.userId,
roleId: roleId ?? this.roleId,
nik: nik ?? this.nik, nik: nik ?? this.nik,
email: email ?? this.email, email: email ?? this.email,
phone: phone ?? this.phone, phone: phone ?? this.phone,
name: name ?? this.name, name: name ?? this.name,
officerData: officerData ?? this.officerData, officerData: officerData ?? this.officerData,
profileData: profileData ?? this.profileData, profileData: profileData ?? this.profileData,
emergencyContact: emergencyContact ?? this.emergencyContact,
additionalData: additionalData ?? this.additionalData, additionalData: additionalData ?? this.additionalData,
); );
} }
/// Identifier for the user (NRP for officers, NIK for normal users) /// Primary identifier (NRP for officers, NIK for users)
String? get identifier => isOfficer ? officerData?.nrp : nik; String? get identifier => isOfficer ? officerData?.nrp : nik;
/// Get full name from name field, officer data, or profile data /// Get display name with fallback priority
String? get fullName { String? get displayName {
if (name != null) return name; // Priority: explicit name > officer name > profile name > email
if (isOfficer && officerData != null) return officerData!.name; if (name?.isNotEmpty == true) return name;
if (profileData != null) return profileData!.fullName; if (isOfficer && officerData?.name.isNotEmpty == true) {
return null; return officerData!.name;
}
if (!isOfficer && profileData?.fullName?.isNotEmpty == true) {
return profileData!.fullName;
}
return email?.split('@').first; // Fallback to email username
}
/// Validate required fields based on user type
List<String> validate() {
final errors = <String>[];
if (isOfficer) {
if (officerData?.nrp.isEmpty != false) {
errors.add('NRP is required for officers');
}
if (officerData?.unitId.isEmpty != false) {
errors.add('Unit ID is required for officers');
}
} else {
if (nik?.isEmpty != false) {
errors.add('NIK is required for regular users');
}
}
// Common validations
if (email?.isEmpty != false) {
errors.add('Email is required');
} else if (!_isValidEmail(email!)) {
errors.add('Invalid email format');
}
return errors;
}
bool _isValidEmail(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
/// Check if user data is complete
bool get isComplete {
return validate().isEmpty;
}
@override
String toString() {
return 'UserMetadataModel('
'isOfficer: $isOfficer, '
'identifier: $identifier, '
'displayName: $displayName'
')';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserMetadataModel &&
other.isOfficer == isOfficer &&
other.identifier == identifier &&
other.email == email;
}
@override
int get hashCode {
return isOfficer.hashCode ^ identifier.hashCode ^ email.hashCode;
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:sigap/src/cores/services/biometric_service.dart'; import 'package:sigap/src/cores/services/biometric_service.dart';
import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/cores/services/location_service.dart';
import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/cores/services/supabase_service.dart';
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/constants/app_routes.dart';
@ -512,6 +513,65 @@ class AuthenticationRepository extends GetxController {
} }
} }
// Add this method to the AuthenticationRepository class
/// Creates a new user with initial data but doesn't complete the profile setup
Future<AuthResponse> initialSignUp({
required String email,
required String password,
required UserMetadataModel initialData,
}) async {
try {
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,
},
);
return res;
} on AuthException catch (e) {
throw e.message;
} catch (e) {
throw 'Something went wrong. Please try again.';
}
}
/// Updates user profile after registration form completion
Future<void> completeUserProfile(UserMetadataModel completeData) async {
try {
// First update auth metadata
await _supabase.auth.updateUser(
UserAttributes(
data: {
'profile_status': 'complete',
'is_officer': completeData.isOfficer,
'name': completeData.name,
'phone': completeData.phone,
},
),
);
// Then update or insert relevant tables based on the role
if (completeData.isOfficer && completeData.officerData != null) {
await _supabase
.from('officers')
.insert(completeData.officerData!.toJson());
} else if (!completeData.isOfficer && completeData.profileData != null) {
await _supabase
.from('profiles')
.insert(completeData.profileData!.toJson());
}
} catch (e) {
throw 'Failed to update profile: ${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,
@ -544,6 +604,36 @@ 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 // BIOMETRIC AUTHENTICATION
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,9 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/email_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/email_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/forgot_password_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/forgot_password_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup_with_role_controller.dart';
class AuthControllerBindings extends Bindings { class AuthControllerBindings extends Bindings {
@override @override
@ -11,6 +12,7 @@ class AuthControllerBindings extends Bindings {
// Register all feature auth controllers // Register all feature auth controllers
Get.lazyPut(() => SignInController(), fenix: true); Get.lazyPut(() => SignInController(), fenix: true);
Get.lazyPut(() => SignUpController(), fenix: true); Get.lazyPut(() => SignUpController(), fenix: true);
Get.lazyPut(() => SignupWithRoleController(), fenix: true);
Get.lazyPut(() => FormRegistrationController(), fenix: true); Get.lazyPut(() => FormRegistrationController(), fenix: true);
Get.lazyPut(() => EmailVerificationController(), fenix: true); Get.lazyPut(() => EmailVerificationController(), fenix: true);
Get.lazyPut(() => ForgotPasswordController(), fenix: true); Get.lazyPut(() => ForgotPasswordController(), fenix: true);

View File

@ -1,5 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart';
@ -41,25 +42,36 @@ class FormRegistrationController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Get role from arguments // Get role and initial data from arguments
final arguments = Get.arguments; final arguments = Get.arguments;
if (arguments != null && arguments['role'] != null) { if (arguments != null) {
selectedRole.value = arguments['role'] as RoleModel; if (arguments['role'] != null) {
selectedRole.value = arguments['role'] as RoleModel;
}
if (arguments['initialData'] != null) {
// Initialize with data from signup
userMetadata.value = arguments['initialData'] as UserMetadataModel;
}
// Store userId if provided
if (arguments['userId'] != null) {
userMetadata.value = userMetadata.value.copyWith(
userId: arguments['userId'],
);
}
// Initialize userMetadata with the selected role information // Initialize userMetadata with the selected role information
userMetadata.value = UserMetadataModel( if ((userMetadata.value.roleId?.isEmpty ?? true) &&
isOfficer: selectedRole.value?.isOfficer ?? false, selectedRole.value != null) {
); userMetadata.value = userMetadata.value.copyWith(
roleId: selectedRole.value!.id,
isOfficer: selectedRole.value!.isOfficer,
);
}
_initializeControllers(); _initializeControllers();
} else { } else {
// Get.snackbar(
// 'Error',
// 'No role selected. Please go back and select a role.',
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.red,
// colorText: Colors.white,
// );
TLoaders.errorSnackBar( TLoaders.errorSnackBar(
title: 'Error', title: 'Error',
message: 'No role selected. Please go back and select a role.', message: 'No role selected. Please go back and select a role.',
@ -232,65 +244,26 @@ class FormRegistrationController extends GetxController {
if (!isValid) return; if (!isValid) return;
try { try {
isLoading.value = true; isLoading.value = false;
// Prepare UserMetadataModel based on role // Prepare UserMetadataModel with all collected data
if (selectedRole.value?.isOfficer == true) { collectAllFormData();
// Officer role - create OfficerModel with the data
final officerData = OfficerModel(
id: '', // Will be assigned by backend
unitId: unitInfoController!.unitIdController.text,
roleId: selectedRole.value!.id,
nrp: officerInfoController!.nrpController.text,
name: personalInfoController.nameController.text,
rank: officerInfoController!.rankController.text,
position: unitInfoController!.positionController.text,
phone: personalInfoController.phoneController.text,
);
userMetadata.value = UserMetadataModel( // Complete the user profile using AuthenticationRepository
isOfficer: true, await AuthenticationRepository.instance.completeUserProfile(
name: personalInfoController.nameController.text, userMetadata.value,
phone: personalInfoController.phoneController.text, );
officerData: officerData,
// idCardImagePath: idCardVerificationController.idCardImage.value?.path,
// selfieImagePath: selfieVerificationController.selfieImage.value?.path,
additionalData: {
'address': personalInfoController.addressController.text,
},
);
} else {
// Regular user - create profile-related data
userMetadata.value = UserMetadataModel(
isOfficer: false,
nik: identityController.nikController.text,
name: personalInfoController.nameController.text,
phone: personalInfoController.phoneController.text,
// idCardImagePath: idCardVerificationController.idCardImage.value?.path,
// selfieImagePath: selfieVerificationController.selfieImage.value?.path,
profileData: ProfileModel(
id: '', // Will be assigned by backend
userId: '', // Will be assigned by backend
nik: identityController.nikController.text,
firstName: personalInfoController.firstNameController.text.trim(),
lastName: personalInfoController.lastNameController.text.trim(),
bio: identityController.bioController.text,
birthDate: _parseBirthDate(
identityController.birthDateController.text,
),
),
additionalData: {
'address': personalInfoController.addressController.text,
},
);
}
// Navigate to the signup screen with the prepared metadata // Show success
Get.toNamed( Get.toNamed(
AppRoutes.signUp, AppRoutes.stateScreen,
arguments: { arguments: {
'userMetadata': userMetadata.value, 'type': 'success',
'role': selectedRole.value, 'title': 'Registration Completed',
'message': 'Your profile has been successfully created.',
'buttonText': 'Continue',
'onButtonPressed':
() => AuthenticationRepository.instance.screenRedirect(),
}, },
); );
} catch (e) { } catch (e) {
@ -298,9 +271,9 @@ class FormRegistrationController extends GetxController {
AppRoutes.stateScreen, AppRoutes.stateScreen,
arguments: { arguments: {
'type': 'error', 'type': 'error',
'title': 'Data Preparation Failed', 'title': 'Registration Failed',
'message': 'message':
'There was an error preparing your profile: ${e.toString()}', 'There was an error completing your profile: ${e.toString()}',
'buttonText': 'Try Again', 'buttonText': 'Try Again',
'onButtonPressed': () => Get.back(), 'onButtonPressed': () => Get.back(),
}, },
@ -310,6 +283,59 @@ class FormRegistrationController extends GetxController {
} }
} }
// Add this method to collect all form data
void collectAllFormData() {
final isOfficerRole = selectedRole.value?.isOfficer ?? false;
if (isOfficerRole) {
// Officer role - create OfficerModel with the data
final officerData = OfficerModel(
id: '', // Will be assigned by backend
unitId: unitInfoController!.unitIdController.text,
roleId: selectedRole.value!.id,
nrp: officerInfoController!.nrpController.text,
name: personalInfoController.nameController.text,
rank: officerInfoController!.rankController.text,
position: unitInfoController!.positionController.text,
phone: personalInfoController.phoneController.text,
);
userMetadata.value = userMetadata.value.copyWith(
isOfficer: true,
name: personalInfoController.nameController.text,
phone: personalInfoController.phoneController.text,
roleId: selectedRole.value!.id,
officerData: officerData,
additionalData: {
'address': personalInfoController.addressController.text,
},
);
} 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),
);
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,
additionalData: {
'address': personalInfoController.addressController.text,
},
);
}
}
// Parse birth date string to DateTime // Parse birth date string to DateTime
DateTime? _parseBirthDate(String dateStr) { DateTime? _parseBirthDate(String dateStr) {
try { try {

View File

@ -0,0 +1,506 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:logger/logger.dart';
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
import 'package:sigap/src/features/personalization/data/models/index.dart';
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
import 'package:sigap/src/utils/constants/app_routes.dart';
import 'package:sigap/src/utils/helpers/network_manager.dart';
import 'package:sigap/src/utils/popups/loaders.dart';
import 'package:sigap/src/utils/validators/validation.dart';
// Define the role types
enum RoleType { viewer, officer }
class SignupWithRoleController extends GetxController {
static SignupWithRoleController get instance => Get.find();
// Variables
final storage = GetStorage();
final signupFormKey = GlobalKey<FormState>();
final roleRepository = Get.find<RolesRepository>();
// Role type (Viewer or Officer)
final Rx<RoleType> roleType = RoleType.viewer.obs;
// Controllers for signup form fields
final emailController = TextEditingController();
final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();
// Observable error messages
final emailError = ''.obs;
final passwordError = ''.obs;
final confirmPasswordError = ''.obs;
// Role related
final RxList<RoleModel> availableRoles = <RoleModel>[].obs;
final RxString selectedRoleId = ''.obs;
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
// Observable states
final isPasswordVisible = false.obs;
final isConfirmPasswordVisible = false.obs;
final privacyPolicy = false.obs;
final isLoading = false.obs;
final currentTabIndex = 0.obs;
// Check if Apple Sign In is available (only on iOS)
final RxBool isAppleSignInAvailable = RxBool(Platform.isIOS);
@override
void onInit() {
super.onInit();
loadRoles();
}
@override
void onClose() {
emailController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
super.onClose();
}
// Load available roles
Future<void> loadRoles() async {
try {
isLoading.value = true;
final roles = await roleRepository.getAllRoles();
availableRoles.assignAll(roles);
// Pre-select the default roles based on roleType
_updateSelectedRoleBasedOnType();
} catch (e) {
Logger().e('Error loading roles: $e');
TLoaders.errorSnackBar(
title: 'Error',
message: 'Failed to load roles. Please try again.',
);
} finally {
isLoading.value = false;
}
}
// Set role type (viewer or officer)
void setRoleType(RoleType type) {
roleType.value = type;
_updateSelectedRoleBasedOnType();
}
// Update selected role based on roleType
void _updateSelectedRoleBasedOnType() {
if (availableRoles.isNotEmpty) {
if (roleType.value == RoleType.officer) {
// Find an officer role
final officerRole = availableRoles.firstWhere(
(role) => role.isOfficer,
orElse: () => availableRoles.first,
);
selectedRoleId.value = officerRole.id;
selectedRole.value = officerRole;
} else {
// Find a viewer role
final viewerRole = availableRoles.firstWhere(
(role) => !role.isOfficer,
orElse: () => availableRoles.first,
);
selectedRoleId.value = viewerRole.id;
selectedRole.value = viewerRole;
}
}
}
// Validators
String? validateEmail(String? value) {
final error = TValidators.validateEmail(value);
emailError.value = error ?? '';
return error;
}
String? validatePassword(String? value) {
final error = TValidators.validatePassword(value);
passwordError.value = error ?? '';
return error;
}
String? validateConfirmPassword(String? value) {
final error = TValidators.validateConfirmPassword(
passwordController.text,
value,
);
confirmPasswordError.value = error ?? '';
return error;
}
// Toggle password visibility
void togglePasswordVisibility() {
isPasswordVisible.value = !isPasswordVisible.value;
}
// Toggle confirm password visibility
void toggleConfirmPasswordVisibility() {
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
}
// Validate signup form
bool validateSignupForm() {
if (!signupFormKey.currentState!.validate()) {
return false;
}
// Check privacy policy
if (!privacyPolicy.value) {
TLoaders.warningSnackBar(
title: 'Privacy Policy',
message: 'Please accept the privacy policy to continue.',
);
return false;
}
return true;
}
// Sign up function
void signUp(bool isOfficer) async {
if (!validateSignupForm()) {
return;
}
try {
isLoading.value = true;
// Check connection
final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) {
TLoaders.errorSnackBar(
title: 'No Internet Connection',
message: 'Please check your internet connection and try again.',
);
return;
}
// Make sure we have a role selected
if (selectedRoleId.value.isEmpty) {
// Find a role based on the selected role type
_updateSelectedRoleBasedOnType();
}
// Create initial user metadata
final initialMetadata = UserMetadataModel(
email: emailController.text.trim(),
roleId: selectedRoleId.value,
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,
);
// 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 (e) {
Logger().e('Error during signup: $e');
TLoaders.errorSnackBar(
title: 'Registration Failed',
message: e.toString(),
);
} finally {
isLoading.value = false;
}
}
// Sign in with Google
Future<void> signInWithGoogle() async {
try {
isLoading.value = true;
// Check network connection
final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) {
TLoaders.errorSnackBar(
title: 'No Internet Connection',
message: 'Please check your internet connection and try again.',
);
return;
}
// Get selected role info before authentication
final roleType = this.roleType.value;
final isOfficer = roleType == RoleType.officer;
// Make sure we have a role selected
if (selectedRoleId.value.isEmpty) {
_updateSelectedRoleBasedOnType();
}
// Authenticate with Google
final authResponse =
await AuthenticationRepository.instance.signInWithGoogle();
// Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId;
// Create or update user metadata with role information
final userMetadata = UserMetadataModel(
isOfficer: isOfficer,
roleId: selectedRoleId.value,
);
// Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!,
metadata: userMetadata,
);
// Navigate to registration form to complete profile
Get.offNamed(
AppRoutes.registrationForm,
arguments: {
'role': selectedRole.value,
'userId': userId,
'initialData': userMetadata,
},
);
} catch (e) {
Logger().e('Error during Google Sign-In: $e');
TLoaders.errorSnackBar(
title: 'Authentication Failed',
message: e.toString(),
);
} finally {
isLoading.value = false;
}
}
// Sign in with Apple
Future<void> signInWithApple() async {
try {
if (!Platform.isIOS) {
TLoaders.warningSnackBar(
title: 'Not Supported',
message: 'Apple Sign-In is only available on iOS devices.',
);
return;
}
isLoading.value = true;
// Check network connection
final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) {
TLoaders.errorSnackBar(
title: 'No Internet Connection',
message: 'Please check your internet connection and try again.',
);
return;
}
// Get selected role info before authentication
final roleType = this.roleType.value;
final isOfficer = roleType == RoleType.officer;
// Make sure we have a role selected
if (selectedRoleId.value.isEmpty) {
_updateSelectedRoleBasedOnType();
}
// Authenticate with Apple
final authResponse =
await AuthenticationRepository.instance.signInWithApple();
// Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId;
// Create or update user metadata with role information
final userMetadata = UserMetadataModel(
isOfficer: isOfficer,
roleId: selectedRoleId.value,
);
// Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!,
metadata: userMetadata,
);
// Navigate to registration form to complete profile
Get.offNamed(
AppRoutes.registrationForm,
arguments: {
'role': selectedRole.value,
'userId': userId,
'initialData': userMetadata,
},
);
} catch (e) {
Logger().e('Error during Apple Sign-In: $e');
TLoaders.errorSnackBar(
title: 'Authentication Failed',
message: e.toString(),
);
} finally {
isLoading.value = false;
}
}
// Sign in with facebook
Future<void> signInWithFacebook() async {
try {
isLoading.value = true;
// Check network connection
final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) {
TLoaders.errorSnackBar(
title: 'No Internet Connection',
message: 'Please check your internet connection and try again.',
);
return;
}
// Get selected role info before authentication
final roleType = this.roleType.value;
final isOfficer = roleType == RoleType.officer;
// Make sure we have a role selected
if (selectedRoleId.value.isEmpty) {
_updateSelectedRoleBasedOnType();
}
// Authenticate with Facebook
final authResponse =
await AuthenticationRepository.instance.signInWithFacebook();
// Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId;
// Create or update user metadata with role information
final userMetadata = UserMetadataModel(
isOfficer: isOfficer,
roleId: selectedRoleId.value,
);
// Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!,
metadata: userMetadata,
);
// Navigate to registration form to complete profile
Get.offNamed(
AppRoutes.registrationForm,
arguments: {
'role': selectedRole.value,
'userId': userId,
'initialData': userMetadata,
},
);
} catch (e) {
Logger().e('Error during Facebook Sign-In: $e');
TLoaders.errorSnackBar(
title: 'Authentication Failed',
message: e.toString(),
);
} finally {
isLoading.value = false;
}
}
// Sign in with email and password
Future<void> signInWithEmail() async {
try {
isLoading.value = true;
// Check network connection
final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) {
TLoaders.errorSnackBar(
title: 'No Internet Connection',
message: 'Please check your internet connection and try again.',
);
return;
}
// Get selected role info before authentication
final roleType = this.roleType.value;
final isOfficer = roleType == RoleType.officer;
// Make sure we have a role selected
if (selectedRoleId.value.isEmpty) {
_updateSelectedRoleBasedOnType();
}
// Authenticate with email and password
final authResponse = await AuthenticationRepository.instance
.signInWithEmailPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
// Check if authResponse has a user property
final userId = AuthenticationRepository.instance.currentUserId;
// Create or update user metadata with role information
final userMetadata = UserMetadataModel(
isOfficer: isOfficer,
roleId: selectedRoleId.value,
);
// Update user metadata in the database
await AuthenticationRepository.instance.updateUserRoleOAuth(
userId: userId!,
metadata: userMetadata,
);
// Navigate to registration form to complete profile
Get.offNamed(
AppRoutes.registrationForm,
arguments: {
'role': selectedRole.value,
'userId': userId,
'initialData': userMetadata,
},
);
} catch (e) {
Logger().e('Error during Email Sign-In: $e');
TLoaders.errorSnackBar(
title: 'Authentication Failed',
message: e.toString(),
);
} finally {
isLoading.value = false;
}
}
// Navigate to forgot password screen
void goToForgotPassword() {
Get.toNamed(AppRoutes.forgotPassword);
}
// Navigate to sign in screen
void goToSignIn() {
Get.offNamed(AppRoutes.signIn);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart';
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
@ -106,7 +107,11 @@ class SignInScreen extends StatelessWidget {
// Social sign in buttons // Social sign in buttons
SocialButton( SocialButton(
text: 'Continue with Google', text: 'Continue with Google',
icon: Icons.g_mobiledata, icon: Icon(
TablerIcons.brand_google,
color: TColors.light,
size: 20,
),
onPressed: () => controller.googleSignIn(), onPressed: () => controller.googleSignIn(),
), ),

View File

@ -0,0 +1,488 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup_with_role_controller.dart';
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart';
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/image_strings.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
class SignupWithRoleScreen extends StatelessWidget {
const SignupWithRoleScreen({super.key});
@override
Widget build(BuildContext context) {
// Get the controller
final controller = Get.find<SignupWithRoleController>();
final theme = Theme.of(context);
final isDark = THelperFunctions.isDarkMode(context);
// Set system overlay style
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
),
);
return Scaffold(
body: Obx(
() => Column(
children: [
// Top section with image and role information
_buildTopImageSection(controller, context),
// Bottom section with form
Expanded(
child: Container(
decoration: BoxDecoration(
color: isDark ? TColors.dark : TColors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(TSizes.borderRadiusLg),
topRight: Radius.circular(TSizes.borderRadiusLg),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Column(
children: [
// Tab bar for switching between viewer and officer
_buildTabBar(context, controller),
// Form content
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(TSizes.defaultSpace),
child: _buildSignupForm(context, controller),
),
),
),
],
),
),
),
],
),
),
);
}
Widget _buildTopImageSection(
SignupWithRoleController controller,
BuildContext context,
) {
bool isOfficer = controller.roleType.value == RoleType.officer;
final isDark = THelperFunctions.isDarkMode(context);
return Container(
height:
MediaQuery.of(context).size.height *
0.35, // Take 35% of screen height
color: isDark ? TColors.dark : TColors.primary,
child: Stack(
children: [
// Background gradient
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
isDark ? Colors.black : TColors.primary,
isDark ? TColors.dark : TColors.primary.withOpacity(0.8),
],
),
),
),
),
// Back button
Positioned(
top: MediaQuery.of(context).padding.top + TSizes.sm,
left: TSizes.sm,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(TSizes.sm),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
),
// Role image and text content
Align(
alignment: Alignment.center,
child: LayoutBuilder(
builder: (context, constraints) {
// Responsive image size based on available height/width
final double maxImageHeight = constraints.maxHeight * 0.9;
final double maxImageWidth = constraints.maxWidth * 0.9;
final double imageSize =
maxImageHeight < maxImageWidth
? maxImageHeight
: maxImageWidth;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Role image
SizedBox(
height: imageSize,
width: imageSize,
child: SvgPicture.asset(
isOfficer
? (isDark
? TImages.communicationDark
: TImages.communication)
: (isDark ? TImages.fallingDark : TImages.falling),
fit: BoxFit.contain,
),
),
],
);
},
),
),
],
),
);
}
Widget _buildTabBar(
BuildContext context,
SignupWithRoleController controller,
) {
final isDark = THelperFunctions.isDarkMode(context);
return Padding(
padding: const EdgeInsets.fromLTRB(
TSizes.defaultSpace,
TSizes.md,
TSizes.defaultSpace,
0,
),
child: Container(
// Increase height from 50 to 60 or more
height: 60,
decoration: BoxDecoration(
color: isDark ? TColors.darkContainer : TColors.lightContainer,
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
),
child: Row(
children: [
// Viewer Tab
_buildTab(
context: context,
controller: controller,
roleType: RoleType.viewer,
label: 'Viewer',
icon: Icons.person,
),
// Officer Tab
_buildTab(
context: context,
controller: controller,
roleType: RoleType.officer,
label: 'Officer',
icon: Icons.security,
),
],
),
),
);
}
Widget _buildTab({
required BuildContext context,
required SignupWithRoleController controller,
required RoleType roleType,
required String label,
required IconData icon,
}) {
final theme = Theme.of(context);
bool isSelected = controller.roleType.value == roleType;
Color selectedColor =
roleType == RoleType.viewer ? TColors.primary : TColors.primary;
return Expanded(
child: GestureDetector(
onTap: () => controller.setRoleType(roleType),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
color: isSelected ? selectedColor : Colors.transparent,
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
),
// Add padding to make content larger
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color:
isSelected
? Colors.white
: theme.colorScheme.onSurfaceVariant,
// Increase icon size from 18 to 22 or 24
size: 22,
),
const SizedBox(width: 10), // Increased from 8
Text(
label,
style: theme.textTheme.bodyMedium?.copyWith(
color:
isSelected
? Colors.white
: theme.colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
// Increase text size
fontSize: 16, // Add explicit font size
),
),
],
),
),
),
);
}
Widget _buildSignupForm(
BuildContext context,
SignupWithRoleController controller,
) {
final theme = Theme.of(context);
bool isOfficer = controller.roleType.value == RoleType.officer;
Color themeColor = isOfficer ? TColors.primary : TColors.primary;
final isDark = THelperFunctions.isDarkMode(context);
return Form(
key: controller.signupFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create Your Account',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: themeColor,
),
),
const SizedBox(height: TSizes.sm),
Text(
isOfficer
? 'Sign up as a security officer to access all features'
: 'Sign up as a viewer to explore the application',
style: theme.textTheme.bodyMedium?.copyWith(
color: isDark ? TColors.textSecondary : Colors.grey.shade600,
),
),
const SizedBox(height: TSizes.spaceBtwSections),
// Social Login Buttons
_buildSocialLoginButtons(controller, themeColor, isDark),
// Or divider
const AuthDivider(text: 'OR'),
const SizedBox(height: TSizes.spaceBtwItems),
// Email field
Obx(
() => CustomTextField(
label: 'Email',
controller: controller.emailController,
validator: controller.validateEmail,
keyboardType: TextInputType.emailAddress,
errorText: controller.emailError.value,
textInputAction: TextInputAction.next,
prefixIcon: const Icon(Icons.email_outlined),
hintText: 'Enter your email',
accentColor: themeColor,
),
),
// Password field
Obx(
() => PasswordField(
label: 'Password',
controller: controller.passwordController,
validator: controller.validatePassword,
isVisible: controller.isPasswordVisible,
errorText: controller.passwordError.value,
onToggleVisibility: controller.togglePasswordVisibility,
textInputAction: TextInputAction.next,
prefixIcon: const Icon(Icons.lock_outlined),
hintText: 'Enter your password',
accentColor: themeColor,
),
),
// Confirm password field
Obx(
() => PasswordField(
label: 'Confirm Password',
controller: controller.confirmPasswordController,
validator: controller.validateConfirmPassword,
isVisible: controller.isConfirmPasswordVisible,
errorText: controller.confirmPasswordError.value,
onToggleVisibility: controller.toggleConfirmPasswordVisibility,
textInputAction: TextInputAction.done,
prefixIcon: const Icon(Icons.lock_outlined),
hintText: 'Confirm your password',
accentColor: themeColor,
),
),
// Privacy policy checkbox with theme-based styling
Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return themeColor;
}
return isDark ? Colors.grey.shade700 : Colors.grey.shade300;
}),
),
),
child: Row(
children: [
Obx(
() => Checkbox(
value: controller.privacyPolicy.value,
onChanged:
(value) => controller.privacyPolicy.value = value!,
),
),
Expanded(
child: Text(
'I agree to the Terms of Service and Privacy Policy',
style: theme.textTheme.bodySmall?.copyWith(
color:
isDark ? TColors.textSecondary : Colors.grey.shade700,
),
),
),
],
),
),
const SizedBox(height: TSizes.spaceBtwItems),
// Sign up button
Obx(
() => AuthButton(
text: 'Sign Up',
onPressed: () => controller.signUp(isOfficer),
isLoading: controller.isLoading.value,
backgroundColor: themeColor,
),
),
const SizedBox(height: TSizes.spaceBtwItems),
// Already have an account row
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Already have an account?',
style: theme.textTheme.bodyMedium?.copyWith(
color: isDark ? TColors.textSecondary : Colors.grey.shade700,
),
),
TextButton(
onPressed: controller.goToSignIn,
child: Text(
'Sign In',
style: theme.textTheme.bodyMedium?.copyWith(
color: themeColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
);
}
Widget _buildSocialLoginButtons(
SignupWithRoleController controller,
Color themeColor,
bool isDark,
) {
return Column(
children: [
// Google button
Row(
children: [
// Google button
Expanded(
child: SocialButton(
text: 'Google',
iconImage: TImages.googleIcon,
onPressed: () => controller.signInWithGoogle(),
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
foregroundColor: isDark ? TColors.white : TColors.dark,
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
),
),
const SizedBox(width: TSizes.sm),
// Facebook button
Expanded(
child: SocialButton(
text: 'Facebook',
iconImage: TImages.facebookIcon,
onPressed: () => controller.signInWithFacebook(),
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
foregroundColor: isDark ? TColors.white : TColors.dark,
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
),
),
],
),
const SizedBox(height: TSizes.spaceBtwItems),
// Apple button (visible only on iOS)
SocialButton(
text: 'Continue with Apple',
icon: Icon(
FontAwesomeIcons.apple,
), // Use the correct FontAwesome apple icon
onPressed: () => controller.signInWithApple(),
backgroundColor: isDark ? Colors.white : Colors.black,
foregroundColor: isDark ? Colors.black : Colors.white,
isVisible: controller.isAppleSignInAvailable.value,
),
if (controller.isAppleSignInAvailable.value)
const SizedBox(height: TSizes.spaceBtwItems),
],
);
}
}

View File

@ -7,6 +7,9 @@ class AuthButton extends StatelessWidget {
final VoidCallback onPressed; final VoidCallback onPressed;
final bool isLoading; final bool isLoading;
final bool isPrimary; final bool isPrimary;
final bool isDisabled;
final Color? backgroundColor;
final Color? textColor;
const AuthButton({ const AuthButton({
super.key, super.key,
@ -14,76 +17,47 @@ class AuthButton extends StatelessWidget {
required this.onPressed, required this.onPressed,
this.isLoading = false, this.isLoading = false,
this.isPrimary = true, this.isPrimary = true,
this.isDisabled = false,
this.backgroundColor,
this.textColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final effectiveBackgroundColor =
backgroundColor ?? (isPrimary ? TColors.primary : Colors.grey.shade200);
final effectiveTextColor =
textColor ?? (isPrimary ? Colors.white : TColors.textPrimary);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: TSizes.buttonHeight * 3, // Using consistent button height child: ElevatedButton(
child: onPressed: (isLoading || isDisabled) ? null : onPressed,
isPrimary style: ElevatedButton.styleFrom(
? ElevatedButton( backgroundColor: effectiveBackgroundColor,
onPressed: isLoading ? null : onPressed, foregroundColor: effectiveTextColor,
style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: TSizes.md),
backgroundColor: TColors.primary, shape: RoundedRectangleBorder(
foregroundColor: TColors.white, borderRadius: BorderRadius.circular(TSizes.buttonRadius),
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(TSizes.buttonRadius), elevation: 1,
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5),
),
child:
isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
effectiveTextColor,
),
), ),
elevation: TSizes.buttonElevation, )
disabledBackgroundColor: TColors.primary.withOpacity(0.6), : Text(text,
), style: TextStyle(fontWeight: FontWeight.bold)),
child: ),
isLoading
? SizedBox(
width: TSizes.iconMd,
height: TSizes.iconMd,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
TColors.white,
),
),
)
: Text(
text,
style: const TextStyle(
fontSize: TSizes.fontSizeMd,
fontWeight: FontWeight.bold,
),
),
)
: OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: TColors.primary,
side: BorderSide(color: TColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
),
disabledForegroundColor: TColors.primary.withOpacity(0.6),
),
child:
isLoading
? SizedBox(
width: TSizes.iconMd,
height: TSizes.iconMd,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
TColors.primary,
),
),
)
: Text(
text,
style: const TextStyle(
fontSize: TSizes.fontSizeMd,
fontWeight: FontWeight.bold,
),
),
),
); );
} }
} }

View File

@ -1,35 +1,42 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
class AuthDivider extends StatelessWidget { class AuthDivider extends StatelessWidget {
final String text; final String text;
const AuthDivider({super.key, required this.text}); const AuthDivider({
Key? key,
required this.text,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = THelperFunctions.isDarkMode(context);
final dividerColor = isDark ? Colors.grey.shade700 : Colors.grey.shade300;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: Divider( child: Divider(
color: TColors.borderPrimary, color: dividerColor,
thickness: TSizes.dividerHeight, thickness: 1.0,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: TSizes.md), padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
child: Text( child: Text(
text, text,
style: Theme.of( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
context, color: isDark ? TColors.textSecondary : Colors.grey.shade600,
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), ),
), ),
), ),
Expanded( Expanded(
child: Divider( child: Divider(
color: TColors.borderPrimary, color: dividerColor,
thickness: TSizes.dividerHeight, thickness: 1.0,
), ),
), ),
], ],

View File

@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
import '../../../../shared/widgets/text/custom_text_field.dart';
class PasswordField extends StatelessWidget { class PasswordField extends StatelessWidget {
final String label; final String label;
@ -11,39 +10,100 @@ class PasswordField extends StatelessWidget {
final String? Function(String?)? validator; final String? Function(String?)? validator;
final RxBool isVisible; final RxBool isVisible;
final String? errorText; final String? errorText;
final Function() onToggleVisibility;
final TextInputAction textInputAction; final TextInputAction textInputAction;
final Function()? onToggleVisibility; final String? hintText;
final Widget? prefixIcon;
final Color? accentColor;
const PasswordField({ const PasswordField({
super.key, super.key,
required this.label, required this.label,
required this.controller, required this.controller,
this.validator, required this.validator,
required this.isVisible, required this.isVisible,
this.errorText, this.errorText,
required this.onToggleVisibility,
this.textInputAction = TextInputAction.done, this.textInputAction = TextInputAction.done,
this.onToggleVisibility, this.hintText,
this.prefixIcon,
this.accentColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx( final Color effectiveAccentColor = accentColor ?? TColors.primary;
() => CustomTextField( final isDark = THelperFunctions.isDarkMode(context);
label: label,
controller: controller, return Column(
validator: validator, crossAxisAlignment: CrossAxisAlignment.start,
obscureText: !isVisible.value, children: [
errorText: errorText, Text(
textInputAction: textInputAction, label,
suffixIcon: IconButton( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
icon: Icon( fontWeight: FontWeight.w500,
isVisible.value ? Icons.visibility_off : Icons.visibility, color: isDark ? TColors.white : TColors.textPrimary,
color: TColors.textSecondary,
size: TSizes.iconMd,
), ),
onPressed: onToggleVisibility,
), ),
), const SizedBox(height: TSizes.sm),
TextFormField(
controller: controller,
validator: validator,
obscureText: !isVisible.value,
textInputAction: textInputAction,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: isDark ? TColors.white : TColors.textPrimary,
),
decoration: InputDecoration(
hintText: hintText,
hintStyle: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
errorText:
errorText != null && errorText!.isNotEmpty ? errorText : null,
errorStyle: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: TColors.error),
contentPadding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.md,
),
prefixIcon: prefixIcon,
suffixIcon: IconButton(
onPressed: onToggleVisibility,
icon: Icon(
isVisible.value ? Icons.visibility_off : Icons.visibility,
color: effectiveAccentColor,
semanticLabel:
isVisible.value ? 'Hide password' : 'Show password',
),
),
filled: true,
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: TColors.error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: TColors.error, width: 1.5),
),
),
),
const SizedBox(height: TSizes.spaceBtwInputFields),
],
); );
} }
} }

View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:sigap/src/features/personalization/data/models/index.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
class RoleSelector extends StatelessWidget {
final List<RoleModel> roles;
final String selectedRoleId;
final Function(String) onRoleSelected;
const RoleSelector({
super.key,
required this.roles,
required this.selectedRoleId,
required this.onRoleSelected,
});
@override
Widget build(BuildContext context) {
if (roles.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: roles.length,
itemBuilder: (context, index) {
final role = roles[index];
final isSelected = role.id == selectedRoleId;
return GestureDetector(
onTap: () => onRoleSelected(role.id),
child: Container(
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color:
isSelected ? TColors.primary.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
border: Border.all(
color: isSelected ? TColors.primary : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 5,
spreadRadius: 1,
),
],
),
child: Row(
children: [
// Role Icon
Container(
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color: isSelected ? TColors.primary : Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
role.isOfficer ? Icons.security : Icons.person,
color: isSelected ? Colors.white : Colors.grey.shade600,
),
),
const SizedBox(width: TSizes.md),
// Role Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
role.name,
style: TextStyle(
fontSize: TSizes.fontSizeMd,
fontWeight: FontWeight.bold,
color:
isSelected
? TColors.primary
: TColors.textPrimary,
),
),
const SizedBox(height: TSizes.xs),
Text(
(role.description ?? '').length > 70
? '${(role.description ?? '').substring(0, 70)}...'
: (role.description ?? ''),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: TSizes.fontSizeSm,
color: TColors.textSecondary,
),
),
],
),
),
// Selected indicator
if (isSelected)
const Icon(
Icons.check_circle,
color: TColors.primary,
size: TSizes.iconMd,
),
],
),
),
);
},
);
}
}

View File

@ -1,43 +1,76 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
class SocialButton extends StatelessWidget { class SocialButton extends StatelessWidget {
final String text; final String text;
final IconData icon; final Icon? icon;
final String? iconImage;
final double? iconImageWidth;
final double? iconImageHeight;
final VoidCallback onPressed; final VoidCallback onPressed;
final Color? backgroundColor;
final Color? foregroundColor;
final Color? borderColor;
final bool isVisible;
const SocialButton({ const SocialButton({
super.key, super.key,
required this.text, required this.text,
required this.icon, this.icon,
this.iconImage,
this.iconImageWidth,
this.iconImageHeight,
required this.onPressed, required this.onPressed,
this.backgroundColor,
this.foregroundColor,
this.borderColor,
this.isVisible = true,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dark = THelperFunctions.isDarkMode(context); if (!isVisible) return const SizedBox.shrink();
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: TSizes.buttonHeight * 3, child: OutlinedButton(
child: OutlinedButton.icon(
onPressed: onPressed, onPressed: onPressed,
icon: Icon(icon, color: TColors.textPrimary, size: TSizes.iconMd),
label: Text(
text,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: TColors.textPrimary,
fontWeight: FontWeight.w500,
),
),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: TColors.textPrimary, backgroundColor: backgroundColor ?? Colors.white,
side: BorderSide(color: TColors.borderPrimary), foregroundColor: foregroundColor ?? Colors.black,
side: BorderSide(color: borderColor ?? Colors.grey.shade300),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(TSizes.buttonRadius), borderRadius: BorderRadius.circular(TSizes.buttonRadius),
), ),
padding: const EdgeInsets.symmetric(
vertical: TSizes.md,
horizontal: TSizes.md,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 24,
width: 24,
child:
iconImage == null
? icon
: Image.asset(
iconImage!,
width: iconImageWidth,
height: iconImageHeight,
),
),
const SizedBox(width: TSizes.md),
Text(
text,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: foregroundColor ?? Colors.black,
fontWeight: FontWeight.w500,
),
),
],
), ),
), ),
); );

View File

@ -110,7 +110,7 @@ class OnboardingController extends GetxController
if (isLocationValid) { if (isLocationValid) {
// If location is valid, proceed to role selection // If location is valid, proceed to role selection
Get.offAllNamed(AppRoutes.roleSelection); Get.offAllNamed(AppRoutes.signupWithRole);
TLoaders.successSnackBar( TLoaders.successSnackBar(
title: 'Location Valid', title: 'Location Valid',

View File

@ -1,6 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart'; import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/constants/app_routes.dart';
import 'package:sigap/src/utils/popups/loaders.dart'; import 'package:sigap/src/utils/popups/loaders.dart';
@ -88,7 +88,7 @@ class RoleSelectionController extends GetxController {
// Navigate directly to step form with selected role // Navigate directly to step form with selected role
Get.toNamed( Get.toNamed(
AppRoutes.formRegistration, AppRoutes.registrationForm,
arguments: {'role': selectedRole.value}, arguments: {'role': selectedRole.value},
); );
} catch (e) { } catch (e) {

View File

@ -7,34 +7,39 @@ class CustomTextField extends StatelessWidget {
final String label; final String label;
final TextEditingController controller; final TextEditingController controller;
final String? Function(String?)? validator; final String? Function(String?)? validator;
final bool obscureText; final TextInputType? keyboardType;
final Widget? suffixIcon; final TextInputAction? textInputAction;
final TextInputType keyboardType;
final String? errorText; final String? errorText;
final bool autofocus; final int? maxLines;
final int maxLines;
final String? hintText; final String? hintText;
final TextInputAction textInputAction; final Widget? prefixIcon;
final Function(String)? onChanged; final Widget? suffixIcon;
final bool? enabled;
final bool obscureText;
final void Function(String)? onChanged;
final Color? accentColor;
const CustomTextField({ const CustomTextField({
super.key, super.key,
required this.label, required this.label,
required this.controller, required this.controller,
this.validator, this.validator,
this.obscureText = false, this.keyboardType,
this.suffixIcon, this.textInputAction,
this.keyboardType = TextInputType.text,
this.errorText, this.errorText,
this.autofocus = false,
this.maxLines = 1, this.maxLines = 1,
this.hintText, this.hintText,
this.textInputAction = TextInputAction.next, this.prefixIcon,
this.suffixIcon,
this.enabled = true,
this.obscureText = false,
this.onChanged, this.onChanged,
this.accentColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color effectiveAccentColor = accentColor ?? TColors.primary;
final isDark = THelperFunctions.isDarkMode(context); final isDark = THelperFunctions.isDarkMode(context);
return Column( return Column(
@ -51,11 +56,11 @@ class CustomTextField extends StatelessWidget {
TextFormField( TextFormField(
controller: controller, controller: controller,
validator: validator, validator: validator,
obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
autofocus: autofocus,
textInputAction: textInputAction, textInputAction: textInputAction,
maxLines: maxLines, maxLines: maxLines,
enabled: enabled,
obscureText: obscureText,
onChanged: onChanged, onChanged: onChanged,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: isDark ? TColors.white : TColors.textPrimary, color: isDark ? TColors.white : TColors.textPrimary,
@ -74,6 +79,7 @@ class CustomTextField extends StatelessWidget {
horizontal: TSizes.md, horizontal: TSizes.md,
vertical: TSizes.md, vertical: TSizes.md,
), ),
prefixIcon: prefixIcon,
suffixIcon: suffixIcon, suffixIcon: suffixIcon,
filled: true, filled: true,
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer, fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
@ -87,7 +93,7 @@ class CustomTextField extends StatelessWidget {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
borderSide: BorderSide(color: TColors.primary, width: 1.5), borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),

View File

@ -2,7 +2,8 @@ class AppRoutes {
static const String onboarding = '/onboarding'; static const String onboarding = '/onboarding';
static const String welcome = '/welcome'; static const String welcome = '/welcome';
static const String signIn = '/sign-in'; static const String signIn = '/sign-in';
static const String signUp = '/sign-up'; static const String signUp =
'/sign-up'; // keep this for backward compatibility
static const String emailVerification = '/email-verification'; static const String emailVerification = '/email-verification';
static const String forgotPassword = '/forgot-password'; static const String forgotPassword = '/forgot-password';
static const String explore = '/explore'; static const String explore = '/explore';
@ -15,5 +16,6 @@ class AppRoutes {
static const String roleSelection = '/role-selection'; static const String roleSelection = '/role-selection';
static const String stateScreen = '/state-screen'; static const String stateScreen = '/state-screen';
static const String locationWarning = '/location-warning'; static const String locationWarning = '/location-warning';
static const String formRegistration = '/form-registration'; static const String registrationForm = '/registration-form';
static const String signupWithRole = '/signup-with-role';
} }

View File

@ -122,4 +122,154 @@ class TImages {
"assets/images/animations/world-map-dark.json"; "assets/images/animations/world-map-dark.json";
// static const String characterExplore2 = // static const String characterExplore2 =
// "assets/images/animations/char_explore2.json"; // "assets/images/animations/char_explore2.json";
// -- Background Images
static const String viewerBackground =
'assets/images/auth/viewer_background.jpg';
static const String officerBackground =
'assets/images/auth/officer_background.jpg';
static const String namaKonstanta = "path/ke/file.png";
// -- Animations (tambahan dari assets/images/animations)
static const String thankYouAnimation =
"assets/images/animations/104368-thank-you.json";
static const String paperPlaneAnimation =
"assets/images/animations/110052-paper-plane.json";
static const String paymentSuccessfulAnimation =
"assets/images/animations/120978-payment-successful.json";
static const String carRidesAnimation =
"assets/images/animations/136491-animation-lottie-car-rides.json";
static const String pencilDrawingAnimation =
"assets/images/animations/140429-pencil-drawing.json";
static const String loadingJuggleAnimation =
"assets/images/animations/141397-loading-juggle.json";
static const String docerAnimation =
"assets/images/animations/141594-animation-of-docer.json";
static const String emptyFileAnimation =
"assets/images/animations/53207-empty-file.json";
static const String securityIconAnimation =
"assets/images/animations/67263-security-icon-transparent.json";
static const String checkRegisterAnimation =
"assets/images/animations/72462-check-register.json";
static const String searchingAnimation =
"assets/images/animations/72785-searching.json";
static const String packagingInProgressAnimation =
"assets/images/animations/98783-packaging-in-progress.json";
static const String animationCarMarquee =
"assets/images/animations/Animation - 1747602248003.json";
static const String cloudUploadingAnimation =
"assets/images/animations/cloud-uploading-animation.json";
static const String ladyAddingProductInCartAnimation =
"assets/images/animations/lady-adding-product-in-cart-animation.json";
static const String loaderAnimation =
"assets/images/animations/loader-animation.json";
static const String loaderJson = "assets/images/animations/loader.json";
static const String orderCompleteCarDeliveryAnimation =
"assets/images/animations/order-complete-car-delivery-animation.json";
static const String worldMapDarkAnimation =
"assets/images/animations/world-map-dark.json";
static const String worldMapAnimation =
"assets/images/animations/world-map.json";
static const String splashDarkAnimation =
"assets/images/animations/splash-dark.json";
static const String splashLightAnimation =
"assets/images/animations/splash-light.json";
// -- Content Images (assets/images/content)
static const String backpackingDark =
"assets/images/content/backpacking-dark.svg";
static const String backpacking = "assets/images/content/backpacking.svg";
static const String callingHelpDark =
"assets/images/content/calling-help-dark.svg";
static const String callingHelp = "assets/images/content/calling-help.svg";
static const String communicationDark =
"assets/images/content/communication-dark.svg";
static const String communication = "assets/images/content/communication.svg";
static const String crashedErrorDark =
"assets/images/content/crashed-error-dark.svg";
static const String crashedError = "assets/images/content/crashed-error.svg";
static const String customerSupportDark =
"assets/images/content/customer-support-dark.svg";
static const String customerSupport =
"assets/images/content/customer-support.svg";
static const String fallingDark = "assets/images/content/falling-dark.svg";
static const String falling = "assets/images/content/falling.svg";
static const String hitchhikingDark =
"assets/images/content/hitchhiking-dark.svg";
static const String hitchhiking = "assets/images/content/hitchhiking.svg";
static const String homeOfficeDark =
"assets/images/content/home-office-dark.svg";
static const String homeOffice = "assets/images/content/home-office.svg";
static const String lookingAtTheMapDark =
"assets/images/content/looking-at-the-map-dark.svg";
static const String lookingAtTheMap =
"assets/images/content/looking-at-the-map.svg";
static const String onlineDatingDark =
"assets/images/content/online-dating-dark.svg";
static const String onlineDating = "assets/images/content/online-dating.svg";
static const String paperPlaneDark =
"assets/images/content/paper-plane-dark.svg";
static const String paperPlane = "assets/images/content/paper-plane.svg";
static const String questionMarkDark =
"assets/images/content/question-mark-dark.svg";
static const String questionMark = "assets/images/content/question-mark.svg";
static const String searchingLocationOnThePhoneDark =
"assets/images/content/searching-location-on-the-phone-dark.svg";
static const String searchingLocationOnThePhone =
"assets/images/content/searching-location-on-the-phone.svg";
static const String telephoneCallDark =
"assets/images/content/telephone-call-dark.svg";
static const String telephoneCall =
"assets/images/content/telephone-call.svg";
static const String travelingWithASuitcaseDark =
"assets/images/content/traveling-with-a-suitcase-dark.svg";
static const String travelingWithASuitcase =
"assets/images/content/traveling-with-a-suitcase.svg";
static const String userPng = "assets/images/content/user.png";
static const String videoCallDark =
"assets/images/content/video-call-dark.svg";
static const String videoCall = "assets/images/content/video-call.svg";
static const String womanHuggingEarthDark =
"assets/images/content/woman-hugging-earth-dark.svg";
static const String womanHuggingEarth =
"assets/images/content/woman-hugging-earth.svg";
static const String womanTouristDark =
"assets/images/content/woman-tourist-dark.svg";
static const String womanTourist = "assets/images/content/woman-tourist.svg";
// -- OnBoarding Images (assets/images/on_boarding_images)
static const String sammyLineDelivery =
"assets/images/on_boarding_images/sammy-line-delivery.gif";
static const String sammyLineNoConnection =
"assets/images/on_boarding_images/sammy-line-no-connection.gif";
static const String sammyLineSearching =
"assets/images/on_boarding_images/sammy-line-searching.gif";
static const String sammyLineShopping =
"assets/images/on_boarding_images/sammy-line-shopping.gif";
// -- Reviews Images (assets/images/reviews)
static const String reviewProfileImage1 =
"assets/images/reviews/review_profile_image_1.jpg";
static const String reviewProfileImage2 =
"assets/images/reviews/review_profile_image_2.jpeg";
static const String reviewProfileImage3 =
"assets/images/reviews/review_profile_image_3.jpeg";
// -- Logos (assets/logos)
static const String facebookIcon = "assets/logos/facebook-icon.png";
static const String googleIcon = "assets/logos/google-icon.png";
static const String logoBgDarkPng = "assets/logos/logo-bg-dark.png";
static const String logoBgDarkSvg = "assets/logos/logo-bg-dark.svg";
static const String logoBgLightPng = "assets/logos/logo-bg-light.png";
static const String logoBgLightSvg = "assets/logos/logo-bg-light.svg";
static const String logoDarkPng = "assets/logos/logo-dark.png";
static const String logoDarkSvg = "assets/logos/logo-dark.svg";
static const String logoLightPng = "assets/logos/logo-light.png";
static const String logoLightSvg = "assets/logos/logo-light.svg";
static const String logoPng = "assets/logos/logo.png";
static const String logoSvg = "assets/logos/logo.svg";
static const String tStoreSplashLogoBlack =
"assets/logos/t-store-splash-logo-black.png";
static const String tStoreSplashLogoWhite =
"assets/logos/t-store-splash-logo-white.png";
} }

View File

@ -18,7 +18,6 @@ import location
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
@ -34,5 +33,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
} }

View File

@ -491,6 +491,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
flutter_tabler_icons:
dependency: "direct main"
description:
name: flutter_tabler_icons
sha256: "657c2201e12fa9121a12ddb4edb74d69290f803868eb6526f04102e6d49ec882"
url: "https://pub.dev"
source: hosted
version: "1.43.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -1586,38 +1594,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: "62d763c27ce7f6cef04b3bec01c85a28d60149bffd155884aa4b8fd4941ea2e4"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "5ece28a317a9f76ad5ee17c78dbacc8a491687cec85ee19c1643761bf8d678ef"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "6af7d1908c9c89311c2dffcc2c9b51b88a6f055ba16fa0aa8a04cbb1c3afc9ce"
url: "https://pub.dev"
source: hosted
version: "3.21.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -42,6 +42,7 @@ dependencies:
flutter_launcher_icons: flutter_launcher_icons:
animated_splash_screen: ^1.3.0 animated_splash_screen: ^1.3.0
flutter_native_splash: ^2.4.6 flutter_native_splash: ^2.4.6
flutter_tabler_icons: ^1.43.0
# --- UI & Animation --- # --- UI & Animation ---
smooth_page_indicator: smooth_page_indicator:
@ -54,7 +55,7 @@ dependencies:
dropdown_search: dropdown_search:
dotted_border: dotted_border:
flutter_svg: ^2.1.0 flutter_svg: ^2.1.0
webview_flutter: ^4.12.0
# --- Input & Forms --- # --- Input & Forms ---
flutter_otp_text_field: flutter_otp_text_field: