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/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(),
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -11,83 +14,69 @@ class UserMetadataModel {
|
||||||
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserMetadataModel(
|
// Create additionalData by excluding known fields
|
||||||
isOfficer: json['is_officer'] == true,
|
final excludedKeys = {
|
||||||
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, _) => [
|
|
||||||
'is_officer',
|
'is_officer',
|
||||||
|
'user_id',
|
||||||
|
'role_id',
|
||||||
'nik',
|
'nik',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
|
@ -95,24 +84,40 @@ class UserMetadataModel {
|
||||||
'officer_data',
|
'officer_data',
|
||||||
'profile_data',
|
'profile_data',
|
||||||
'emergency_contact',
|
'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() {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
if (arguments['role'] != null) {
|
||||||
selectedRole.value = arguments['role'] as RoleModel;
|
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,10 +244,50 @@ 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();
|
||||||
|
|
||||||
|
// 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
|
// Officer role - create OfficerModel with the data
|
||||||
final officerData = OfficerModel(
|
final officerData = OfficerModel(
|
||||||
id: '', // Will be assigned by backend
|
id: '', // Will be assigned by backend
|
||||||
|
@ -248,66 +300,40 @@ class FormRegistrationController extends GetxController {
|
||||||
phone: personalInfoController.phoneController.text,
|
phone: personalInfoController.phoneController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
userMetadata.value = UserMetadataModel(
|
userMetadata.value = userMetadata.value.copyWith(
|
||||||
isOfficer: true,
|
isOfficer: true,
|
||||||
name: personalInfoController.nameController.text,
|
name: personalInfoController.nameController.text,
|
||||||
phone: personalInfoController.phoneController.text,
|
phone: personalInfoController.phoneController.text,
|
||||||
|
roleId: selectedRole.value!.id,
|
||||||
officerData: officerData,
|
officerData: officerData,
|
||||||
// idCardImagePath: idCardVerificationController.idCardImage.value?.path,
|
|
||||||
// selfieImagePath: selfieVerificationController.selfieImage.value?.path,
|
|
||||||
additionalData: {
|
additionalData: {
|
||||||
'address': personalInfoController.addressController.text,
|
'address': personalInfoController.addressController.text,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Regular user - create profile-related data
|
// Regular user - create profile-related data
|
||||||
userMetadata.value = UserMetadataModel(
|
final profileData = ProfileModel(
|
||||||
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
|
id: '', // Will be assigned by backend
|
||||||
userId: '', // Will be assigned by backend
|
userId: userMetadata.value.userId ?? '',
|
||||||
nik: identityController.nikController.text,
|
nik: identityController.nikController.text,
|
||||||
firstName: personalInfoController.firstNameController.text.trim(),
|
firstName: personalInfoController.firstNameController.text.trim(),
|
||||||
lastName: personalInfoController.lastNameController.text.trim(),
|
lastName: personalInfoController.lastNameController.text.trim(),
|
||||||
bio: identityController.bioController.text,
|
bio: identityController.bioController.text,
|
||||||
birthDate: _parseBirthDate(
|
birthDate: _parseBirthDate(identityController.birthDateController.text),
|
||||||
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: {
|
additionalData: {
|
||||||
'address': personalInfoController.addressController.text,
|
'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
|
// 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/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(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
@ -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 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,75 +17,46 @@ 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
|
|
||||||
? ElevatedButton(
|
|
||||||
onPressed: isLoading ? null : onPressed,
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TColors.primary,
|
backgroundColor: effectiveBackgroundColor,
|
||||||
foregroundColor: TColors.white,
|
foregroundColor: effectiveTextColor,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: TSizes.md),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
),
|
),
|
||||||
elevation: TSizes.buttonElevation,
|
elevation: 1,
|
||||||
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
isLoading
|
isLoading
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
width: TSizes.iconMd,
|
height: 20,
|
||||||
height: TSizes.iconMd,
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
TColors.white,
|
effectiveTextColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(text,
|
||||||
text,
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,
|
|
||||||
|
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,
|
controller: controller,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
obscureText: !isVisible.value,
|
obscureText: !isVisible.value,
|
||||||
errorText: errorText,
|
|
||||||
textInputAction: textInputAction,
|
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(
|
suffixIcon: IconButton(
|
||||||
|
onPressed: onToggleVisibility,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
||||||
color: TColors.textSecondary,
|
color: effectiveAccentColor,
|
||||||
size: TSizes.iconMd,
|
semanticLabel:
|
||||||
),
|
isVisible.value ? 'Hide password' : 'Show password',
|
||||||
onPressed: onToggleVisibility,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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: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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue