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:
parent
ce7d5f5cf4
commit
498b71c184
|
@ -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/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_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/onboarding/onboarding_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
||||
|
@ -25,7 +26,7 @@ class AppPages {
|
|||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.formRegistration,
|
||||
name: AppRoutes.registrationForm,
|
||||
page: () => const FormRegistrationScreen(),
|
||||
),
|
||||
|
||||
|
@ -33,6 +34,11 @@ class AppPages {
|
|||
|
||||
GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.signupWithRole,
|
||||
page: () => const SignupWithRoleScreen(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.forgotPassword,
|
||||
page: () => const ForgotPasswordScreen(),
|
||||
|
|
|
@ -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/personalization/data/models/models/profile_model.dart';
|
||||
|
||||
class UserMetadataModel {
|
||||
final bool isOfficer;
|
||||
final String? userId;
|
||||
final String? roleId;
|
||||
final String? nik;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
@ -11,83 +14,69 @@ class UserMetadataModel {
|
|||
final ProfileModel? profileData;
|
||||
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.userId,
|
||||
this.roleId,
|
||||
this.nik,
|
||||
this.email,
|
||||
this.phone,
|
||||
this.name,
|
||||
this.officerData,
|
||||
this.profileData,
|
||||
this.emergencyContact,
|
||||
|
||||
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) {
|
||||
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;
|
||||
if (json['officer_data'] != null && json['is_officer'] == true) {
|
||||
if (json['officer_data'] != null && isOfficer) {
|
||||
try {
|
||||
// Create temporary ID and role fields if missing
|
||||
final officerJson = Map<String, dynamic>.from(json['officer_data']);
|
||||
if (!officerJson.containsKey('id')) {
|
||||
officerJson['id'] = json['id'] ?? '';
|
||||
}
|
||||
if (!officerJson.containsKey('role_id')) {
|
||||
officerJson['role_id'] = '';
|
||||
}
|
||||
if (!officerJson.containsKey('unit_id')) {
|
||||
officerJson['unit_id'] = officerJson['unit_id'] ?? '';
|
||||
}
|
||||
|
||||
// Only add missing required fields, allow null for optional ones
|
||||
officerJson.putIfAbsent('id', () => json['id']);
|
||||
officerJson.putIfAbsent('role_id', () => null);
|
||||
officerJson.putIfAbsent('unit_id', () => null);
|
||||
|
||||
officerData = OfficerModel.fromJson(officerJson);
|
||||
} 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;
|
||||
if (json['profile_data'] != null) {
|
||||
if (json['profile_data'] != null && !isOfficer) {
|
||||
try {
|
||||
final profileJson = Map<String, dynamic>.from(json['profile_data']);
|
||||
if (!profileJson.containsKey('id')) {
|
||||
profileJson['id'] = '';
|
||||
}
|
||||
if (!profileJson.containsKey('user_id')) {
|
||||
profileJson['user_id'] = json['id'] ?? '';
|
||||
}
|
||||
if (!profileJson.containsKey('nik')) {
|
||||
profileJson['nik'] = json['nik'] ?? '';
|
||||
}
|
||||
|
||||
profileJson.putIfAbsent('id', () => null);
|
||||
profileJson.putIfAbsent('user_id', () => json['id']);
|
||||
profileJson.putIfAbsent('nik', () => json['nik']);
|
||||
|
||||
profileData = ProfileModel.fromJson(profileJson);
|
||||
} catch (e) {
|
||||
print('Error parsing profile data: $e');
|
||||
Logger().e('Failed to parse profile data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return UserMetadataModel(
|
||||
isOfficer: json['is_officer'] == true,
|
||||
nik: json['nik'] as String?,
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
name: json['name'] as String?,
|
||||
officerData: officerData,
|
||||
profileData: profileData,
|
||||
emergencyContact:
|
||||
json['emergency_contact'] != null
|
||||
? Map<String, dynamic>.from(json['emergency_contact'])
|
||||
: null,
|
||||
additionalData: Map<String, dynamic>.from(json)..removeWhere(
|
||||
(key, _) => [
|
||||
// Create additionalData by excluding known fields
|
||||
final excludedKeys = {
|
||||
'is_officer',
|
||||
'user_id',
|
||||
'role_id',
|
||||
'nik',
|
||||
'email',
|
||||
'phone',
|
||||
|
@ -95,24 +84,40 @@ class UserMetadataModel {
|
|||
'officer_data',
|
||||
'profile_data',
|
||||
'emergency_contact',
|
||||
].contains(key),
|
||||
),
|
||||
};
|
||||
|
||||
final additionalData = Map<String, dynamic>.from(json)
|
||||
..removeWhere((key, _) => excludedKeys.contains(key));
|
||||
|
||||
return UserMetadataModel(
|
||||
isOfficer: isOfficer,
|
||||
userId: json['user_id'] as String?,
|
||||
roleId: json['role_id'] as String?,
|
||||
nik: json['nik'] as String?,
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
name: json['name'] as String?,
|
||||
officerData: officerData,
|
||||
profileData: profileData,
|
||||
additionalData: additionalData.isNotEmpty ? additionalData : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert the model to a json Map (for Supabase Auth)
|
||||
/// Convert model to JSON Map for Supabase Auth
|
||||
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 (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) {
|
||||
// Extract only the necessary fields for the officerData
|
||||
// to prevent circular references and reduce data size
|
||||
final officerJson = {
|
||||
data['officer_data'] = {
|
||||
'nrp': officerData!.nrp,
|
||||
'name': officerData!.name,
|
||||
'rank': officerData!.rank,
|
||||
|
@ -120,24 +125,20 @@ class UserMetadataModel {
|
|||
'phone': officerData!.phone,
|
||||
'unit_id': officerData!.unitId,
|
||||
};
|
||||
data['officer_data'] = officerJson;
|
||||
}
|
||||
|
||||
if (profileData != null) {
|
||||
// Extract only the necessary profile fields
|
||||
final profileJson = {
|
||||
// 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,
|
||||
};
|
||||
data['profile_data'] = profileJson;
|
||||
}
|
||||
|
||||
if (emergencyContact != null) {
|
||||
data['emergency_contact'] = emergencyContact;
|
||||
}
|
||||
|
||||
// Add additional data
|
||||
if (additionalData != null) {
|
||||
data.addAll(additionalData!);
|
||||
}
|
||||
|
@ -145,9 +146,11 @@ class UserMetadataModel {
|
|||
return data;
|
||||
}
|
||||
|
||||
/// Create a copy with updated fields
|
||||
/// Create copy with updated fields
|
||||
UserMetadataModel copyWith({
|
||||
bool? isOfficer,
|
||||
String? userId,
|
||||
String? roleId,
|
||||
String? nik,
|
||||
String? email,
|
||||
String? phone,
|
||||
|
@ -159,26 +162,92 @@ class UserMetadataModel {
|
|||
}) {
|
||||
return UserMetadataModel(
|
||||
isOfficer: isOfficer ?? this.isOfficer,
|
||||
userId: userId ?? this.userId,
|
||||
roleId: roleId ?? this.roleId,
|
||||
nik: nik ?? this.nik,
|
||||
email: email ?? this.email,
|
||||
phone: phone ?? this.phone,
|
||||
name: name ?? this.name,
|
||||
officerData: officerData ?? this.officerData,
|
||||
profileData: profileData ?? this.profileData,
|
||||
emergencyContact: emergencyContact ?? this.emergencyContact,
|
||||
|
||||
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;
|
||||
|
||||
/// Get full name from name field, officer data, or profile data
|
||||
String? get fullName {
|
||||
if (name != null) return name;
|
||||
if (isOfficer && officerData != null) return officerData!.name;
|
||||
if (profileData != null) return profileData!.fullName;
|
||||
return null;
|
||||
/// Get display name with fallback priority
|
||||
String? get displayName {
|
||||
// Priority: explicit name > officer name > profile name > email
|
||||
if (name?.isNotEmpty == true) return name;
|
||||
if (isOfficer && officerData?.name.isNotEmpty == true) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/location_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/onboarding/presentasion/pages/onboarding/onboarding_screen.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
|
||||
Future<UserResponse> updateUserRole({
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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/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/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 {
|
||||
@override
|
||||
|
@ -11,6 +12,7 @@ class AuthControllerBindings extends Bindings {
|
|||
// Register all feature auth controllers
|
||||
Get.lazyPut(() => SignInController(), fenix: true);
|
||||
Get.lazyPut(() => SignUpController(), fenix: true);
|
||||
Get.lazyPut(() => SignupWithRoleController(), fenix: true);
|
||||
Get.lazyPut(() => FormRegistrationController(), fenix: true);
|
||||
Get.lazyPut(() => EmailVerificationController(), fenix: true);
|
||||
Get.lazyPut(() => ForgotPasswordController(), fenix: true);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:get/get.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/identity_verification_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() {
|
||||
super.onInit();
|
||||
|
||||
// Get role from arguments
|
||||
// Get role and initial data from arguments
|
||||
final arguments = Get.arguments;
|
||||
if (arguments != null && arguments['role'] != null) {
|
||||
if (arguments != null) {
|
||||
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
|
||||
userMetadata.value = UserMetadataModel(
|
||||
isOfficer: selectedRole.value?.isOfficer ?? false,
|
||||
if ((userMetadata.value.roleId?.isEmpty ?? true) &&
|
||||
selectedRole.value != null) {
|
||||
userMetadata.value = userMetadata.value.copyWith(
|
||||
roleId: selectedRole.value!.id,
|
||||
isOfficer: selectedRole.value!.isOfficer,
|
||||
);
|
||||
}
|
||||
|
||||
_initializeControllers();
|
||||
} 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(
|
||||
title: 'Error',
|
||||
message: 'No role selected. Please go back and select a role.',
|
||||
|
@ -232,10 +244,50 @@ class FormRegistrationController extends GetxController {
|
|||
if (!isValid) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
isLoading.value = false;
|
||||
|
||||
// Prepare UserMetadataModel based on role
|
||||
if (selectedRole.value?.isOfficer == true) {
|
||||
// Prepare UserMetadataModel with all collected data
|
||||
collectAllFormData();
|
||||
|
||||
// Complete the user profile using AuthenticationRepository
|
||||
await AuthenticationRepository.instance.completeUserProfile(
|
||||
userMetadata.value,
|
||||
);
|
||||
|
||||
// Show success
|
||||
Get.toNamed(
|
||||
AppRoutes.stateScreen,
|
||||
arguments: {
|
||||
'type': 'success',
|
||||
'title': 'Registration Completed',
|
||||
'message': 'Your profile has been successfully created.',
|
||||
'buttonText': 'Continue',
|
||||
'onButtonPressed':
|
||||
() => AuthenticationRepository.instance.screenRedirect(),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Get.toNamed(
|
||||
AppRoutes.stateScreen,
|
||||
arguments: {
|
||||
'type': 'error',
|
||||
'title': 'Registration Failed',
|
||||
'message':
|
||||
'There was an error completing your profile: ${e.toString()}',
|
||||
'buttonText': 'Try Again',
|
||||
'onButtonPressed': () => Get.back(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -248,66 +300,40 @@ class FormRegistrationController extends GetxController {
|
|||
phone: personalInfoController.phoneController.text,
|
||||
);
|
||||
|
||||
userMetadata.value = UserMetadataModel(
|
||||
userMetadata.value = userMetadata.value.copyWith(
|
||||
isOfficer: true,
|
||||
name: personalInfoController.nameController.text,
|
||||
phone: personalInfoController.phoneController.text,
|
||||
roleId: selectedRole.value!.id,
|
||||
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(
|
||||
final profileData = ProfileModel(
|
||||
id: '', // Will be assigned by backend
|
||||
userId: '', // 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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to the signup screen with the prepared metadata
|
||||
Get.toNamed(
|
||||
AppRoutes.signUp,
|
||||
arguments: {
|
||||
'userMetadata': userMetadata.value,
|
||||
'role': selectedRole.value,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Get.toNamed(
|
||||
AppRoutes.stateScreen,
|
||||
arguments: {
|
||||
'type': 'error',
|
||||
'title': 'Data Preparation Failed',
|
||||
'message':
|
||||
'There was an error preparing your profile: ${e.toString()}',
|
||||
'buttonText': 'Try Again',
|
||||
'onButtonPressed': () => Get.back(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse birth date string to DateTime
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||
|
@ -106,7 +107,11 @@ class SignInScreen extends StatelessWidget {
|
|||
// Social sign in buttons
|
||||
SocialButton(
|
||||
text: 'Continue with Google',
|
||||
icon: Icons.g_mobiledata,
|
||||
icon: Icon(
|
||||
TablerIcons.brand_google,
|
||||
color: TColors.light,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => controller.googleSignIn(),
|
||||
),
|
||||
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ class AuthButton extends StatelessWidget {
|
|||
final VoidCallback onPressed;
|
||||
final bool isLoading;
|
||||
final bool isPrimary;
|
||||
final bool isDisabled;
|
||||
final Color? backgroundColor;
|
||||
final Color? textColor;
|
||||
|
||||
const AuthButton({
|
||||
super.key,
|
||||
|
@ -14,75 +17,46 @@ class AuthButton extends StatelessWidget {
|
|||
required this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.isPrimary = true,
|
||||
this.isDisabled = false,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBackgroundColor =
|
||||
backgroundColor ?? (isPrimary ? TColors.primary : Colors.grey.shade200);
|
||||
final effectiveTextColor =
|
||||
textColor ?? (isPrimary ? Colors.white : TColors.textPrimary);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: TSizes.buttonHeight * 3, // Using consistent button height
|
||||
child:
|
||||
isPrimary
|
||||
? ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
child: ElevatedButton(
|
||||
onPressed: (isLoading || isDisabled) ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: TColors.white,
|
||||
backgroundColor: effectiveBackgroundColor,
|
||||
foregroundColor: effectiveTextColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: TSizes.md),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
elevation: TSizes.buttonElevation,
|
||||
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
||||
elevation: 1,
|
||||
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? SizedBox(
|
||||
width: TSizes.iconMd,
|
||||
height: TSizes.iconMd,
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
TColors.white,
|
||||
effectiveTextColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
: Text(text,
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
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/helpers/helper_functions.dart';
|
||||
|
||||
class AuthDivider extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const AuthDivider({super.key, required this.text});
|
||||
const AuthDivider({
|
||||
Key? key,
|
||||
required this.text,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
final dividerColor = isDark ? Colors.grey.shade700 : Colors.grey.shade300;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: TColors.borderPrimary,
|
||||
thickness: TSizes.dividerHeight,
|
||||
color: dividerColor,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: isDark ? TColors.textSecondary : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: TColors.borderPrimary,
|
||||
thickness: TSizes.dividerHeight,
|
||||
color: dividerColor,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
import '../../../../shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class PasswordField extends StatelessWidget {
|
||||
final String label;
|
||||
|
@ -11,39 +10,100 @@ class PasswordField extends StatelessWidget {
|
|||
final String? Function(String?)? validator;
|
||||
final RxBool isVisible;
|
||||
final String? errorText;
|
||||
final Function() onToggleVisibility;
|
||||
final TextInputAction textInputAction;
|
||||
final Function()? onToggleVisibility;
|
||||
final String? hintText;
|
||||
final Widget? prefixIcon;
|
||||
final Color? accentColor;
|
||||
|
||||
const PasswordField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.validator,
|
||||
required this.validator,
|
||||
required this.isVisible,
|
||||
this.errorText,
|
||||
required this.onToggleVisibility,
|
||||
this.textInputAction = TextInputAction.done,
|
||||
this.onToggleVisibility,
|
||||
this.hintText,
|
||||
this.prefixIcon,
|
||||
this.accentColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => CustomTextField(
|
||||
label: label,
|
||||
final Color effectiveAccentColor = accentColor ?? TColors.primary;
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? TColors.white : TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: !isVisible.value,
|
||||
errorText: errorText,
|
||||
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: TColors.textSecondary,
|
||||
size: TSizes.iconMd,
|
||||
),
|
||||
onPressed: onToggleVisibility,
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,43 +1,76 @@
|
|||
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/helpers/helper_functions.dart';
|
||||
|
||||
class SocialButton extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData icon;
|
||||
final Icon? icon;
|
||||
final String? iconImage;
|
||||
final double? iconImageWidth;
|
||||
final double? iconImageHeight;
|
||||
final VoidCallback onPressed;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final Color? borderColor;
|
||||
final bool isVisible;
|
||||
|
||||
const SocialButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.icon,
|
||||
this.icon,
|
||||
this.iconImage,
|
||||
this.iconImageWidth,
|
||||
this.iconImageHeight,
|
||||
required this.onPressed,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.isVisible = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
if (!isVisible) return const SizedBox.shrink();
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: TSizes.buttonHeight * 3,
|
||||
child: OutlinedButton.icon(
|
||||
child: OutlinedButton(
|
||||
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(
|
||||
foregroundColor: TColors.textPrimary,
|
||||
side: BorderSide(color: TColors.borderPrimary),
|
||||
backgroundColor: backgroundColor ?? Colors.white,
|
||||
foregroundColor: foregroundColor ?? Colors.black,
|
||||
side: BorderSide(color: borderColor ?? Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -110,7 +110,7 @@ class OnboardingController extends GetxController
|
|||
|
||||
if (isLocationValid) {
|
||||
// If location is valid, proceed to role selection
|
||||
Get.offAllNamed(AppRoutes.roleSelection);
|
||||
Get.offAllNamed(AppRoutes.signupWithRole);
|
||||
|
||||
TLoaders.successSnackBar(
|
||||
title: 'Location Valid',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/repositories/roles_repository.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.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
|
||||
Get.toNamed(
|
||||
AppRoutes.formRegistration,
|
||||
AppRoutes.registrationForm,
|
||||
arguments: {'role': selectedRole.value},
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
@ -7,34 +7,39 @@ class CustomTextField extends StatelessWidget {
|
|||
final String label;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?)? validator;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
final TextInputType keyboardType;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final String? errorText;
|
||||
final bool autofocus;
|
||||
final int maxLines;
|
||||
final int? maxLines;
|
||||
final String? hintText;
|
||||
final TextInputAction textInputAction;
|
||||
final Function(String)? onChanged;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final bool? enabled;
|
||||
final bool obscureText;
|
||||
final void Function(String)? onChanged;
|
||||
final Color? accentColor;
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.validator,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.errorText,
|
||||
this.autofocus = false,
|
||||
this.maxLines = 1,
|
||||
this.hintText,
|
||||
this.textInputAction = TextInputAction.next,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.enabled = true,
|
||||
this.obscureText = false,
|
||||
this.onChanged,
|
||||
this.accentColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveAccentColor = accentColor ?? TColors.primary;
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
|
@ -51,11 +56,11 @@ class CustomTextField extends StatelessWidget {
|
|||
TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
autofocus: autofocus,
|
||||
textInputAction: textInputAction,
|
||||
maxLines: maxLines,
|
||||
enabled: enabled,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? TColors.white : TColors.textPrimary,
|
||||
|
@ -74,6 +79,7 @@ class CustomTextField extends StatelessWidget {
|
|||
horizontal: TSizes.md,
|
||||
vertical: TSizes.md,
|
||||
),
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
filled: true,
|
||||
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||
|
@ -87,7 +93,7 @@ class CustomTextField extends StatelessWidget {
|
|||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.primary, width: 1.5),
|
||||
borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
|
|
|
@ -2,7 +2,8 @@ class AppRoutes {
|
|||
static const String onboarding = '/onboarding';
|
||||
static const String welcome = '/welcome';
|
||||
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 forgotPassword = '/forgot-password';
|
||||
static const String explore = '/explore';
|
||||
|
@ -15,5 +16,6 @@ class AppRoutes {
|
|||
static const String roleSelection = '/role-selection';
|
||||
static const String stateScreen = '/state-screen';
|
||||
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';
|
||||
}
|
||||
|
|
|
@ -122,4 +122,154 @@ class TImages {
|
|||
"assets/images/animations/world-map-dark.json";
|
||||
// static const String characterExplore2 =
|
||||
// "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";
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import location
|
|||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
|
@ -34,5 +33,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
|
|
@ -491,6 +491,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -1586,38 +1594,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -42,6 +42,7 @@ dependencies:
|
|||
flutter_launcher_icons:
|
||||
animated_splash_screen: ^1.3.0
|
||||
flutter_native_splash: ^2.4.6
|
||||
flutter_tabler_icons: ^1.43.0
|
||||
|
||||
# --- UI & Animation ---
|
||||
smooth_page_indicator:
|
||||
|
@ -54,7 +55,7 @@ dependencies:
|
|||
dropdown_search:
|
||||
dotted_border:
|
||||
flutter_svg: ^2.1.0
|
||||
webview_flutter: ^4.12.0
|
||||
|
||||
|
||||
# --- Input & Forms ---
|
||||
flutter_otp_text_field:
|
||||
|
|
Loading…
Reference in New Issue