diff --git a/sigap-mobile/lib/src/cores/routes/app_pages.dart b/sigap-mobile/lib/src/cores/routes/app_pages.dart index 8245239..32022b6 100644 --- a/sigap-mobile/lib/src/cores/routes/app_pages.dart +++ b/sigap-mobile/lib/src/cores/routes/app_pages.dart @@ -3,6 +3,7 @@ import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgo import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_screen.dart'; +import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart'; @@ -25,7 +26,7 @@ class AppPages { ), GetPage( - name: AppRoutes.formRegistration, + name: AppRoutes.registrationForm, page: () => const FormRegistrationScreen(), ), @@ -33,6 +34,11 @@ class AppPages { GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()), + GetPage( + name: AppRoutes.signupWithRole, + page: () => const SignupWithRoleScreen(), + ), + GetPage( name: AppRoutes.forgotPassword, page: () => const ForgotPasswordScreen(), diff --git a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart index 96664fa..aa29ac2 100644 --- a/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart +++ b/sigap-mobile/lib/src/features/auth/data/models/user_metadata_model.dart @@ -1,8 +1,11 @@ +import 'package:logger/logger.dart'; import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; class UserMetadataModel { final bool isOfficer; + final String? userId; + final String? roleId; final String? nik; final String? email; final String? phone; @@ -10,109 +13,111 @@ class UserMetadataModel { final OfficerModel? officerData; final ProfileModel? profileData; final Map? additionalData; - - // Emergency contact data frequently used in the app - final Map? emergencyContact; + - UserMetadataModel({ + const UserMetadataModel({ this.isOfficer = false, + this.userId, + this.roleId, this.nik, this.email, this.phone, this.name, this.officerData, this.profileData, - this.emergencyContact, + this.additionalData, }); - /// Create a UserMetadataModel from raw Map data (from Supabase Auth) + /// Create UserMetadataModel from raw Map data (from Supabase Auth) factory UserMetadataModel.fromJson(Map? json) { - if (json == null) return UserMetadataModel(); + if (json == null || json.isEmpty) { + return const UserMetadataModel(); + } - // Extract officer data if available + final bool isOfficer = json['is_officer'] == true; + + // Parse officer data with better error handling OfficerModel? officerData; - if (json['officer_data'] != null && json['is_officer'] == true) { + if (json['officer_data'] != null && isOfficer) { try { - // Create temporary ID and role fields if missing final officerJson = Map.from(json['officer_data']); - if (!officerJson.containsKey('id')) { - officerJson['id'] = json['id'] ?? ''; - } - if (!officerJson.containsKey('role_id')) { - officerJson['role_id'] = ''; - } - if (!officerJson.containsKey('unit_id')) { - officerJson['unit_id'] = officerJson['unit_id'] ?? ''; - } + + // Only add missing required fields, allow null for optional ones + officerJson.putIfAbsent('id', () => json['id']); + officerJson.putIfAbsent('role_id', () => null); + officerJson.putIfAbsent('unit_id', () => null); officerData = OfficerModel.fromJson(officerJson); } catch (e) { - print('Error parsing officer data: $e'); + // Use proper logging in production + Logger().e('Failed to parse officer data: $e'); + // Consider rethrow for critical errors } } - // Extract profile data if available + // Parse profile data with better error handling ProfileModel? profileData; - if (json['profile_data'] != null) { + if (json['profile_data'] != null && !isOfficer) { try { final profileJson = Map.from(json['profile_data']); - if (!profileJson.containsKey('id')) { - profileJson['id'] = ''; - } - if (!profileJson.containsKey('user_id')) { - profileJson['user_id'] = json['id'] ?? ''; - } - if (!profileJson.containsKey('nik')) { - profileJson['nik'] = json['nik'] ?? ''; - } + + profileJson.putIfAbsent('id', () => null); + profileJson.putIfAbsent('user_id', () => json['id']); + profileJson.putIfAbsent('nik', () => json['nik']); profileData = ProfileModel.fromJson(profileJson); } catch (e) { - print('Error parsing profile data: $e'); + Logger().e('Failed to parse profile data: $e'); } } + // Create additionalData by excluding known fields + final excludedKeys = { + 'is_officer', + 'user_id', + 'role_id', + 'nik', + 'email', + 'phone', + 'name', + 'officer_data', + 'profile_data', + 'emergency_contact', + }; + + final additionalData = Map.from(json) + ..removeWhere((key, _) => excludedKeys.contains(key)); + return UserMetadataModel( - isOfficer: json['is_officer'] == true, + isOfficer: isOfficer, + userId: json['user_id'] as String?, + roleId: json['role_id'] as String?, nik: json['nik'] as String?, 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.from(json['emergency_contact']) - : null, - additionalData: Map.from(json)..removeWhere( - (key, _) => [ - 'is_officer', - 'nik', - 'email', - 'phone', - 'name', - 'officer_data', - 'profile_data', - 'emergency_contact', - ].contains(key), - ), + additionalData: additionalData.isNotEmpty ? additionalData : null, ); } - /// Convert the model to a json Map (for Supabase Auth) + /// Convert model to JSON Map for Supabase Auth Map toJson() { - final Map data = {'is_officer': isOfficer}; + final data = {'is_officer': isOfficer}; + // Add basic user data + if (roleId != null) data['role_id'] = roleId; + if (userId != null) data['user_id'] = userId; if (nik != null) data['nik'] = nik; if (email != null) data['email'] = email; if (phone != null) data['phone'] = phone; if (name != null) data['name'] = name; + // Add officer-specific data if (officerData != null && isOfficer) { - // Extract only the necessary fields for the officerData - // to prevent circular references and reduce data size - final officerJson = { + data['officer_data'] = { 'nrp': officerData!.nrp, 'name': officerData!.name, 'rank': officerData!.rank, @@ -120,24 +125,20 @@ class UserMetadataModel { 'phone': officerData!.phone, 'unit_id': officerData!.unitId, }; - data['officer_data'] = officerJson; } - if (profileData != null) { - // Extract only the necessary profile fields - final profileJson = { + // Add profile data for non-officers + if (profileData != null && !isOfficer) { + data['profile_data'] = { 'nik': profileData!.nik, 'first_name': profileData!.firstName, 'last_name': profileData!.lastName, 'address': profileData!.address, }; - data['profile_data'] = profileJson; - } - - if (emergencyContact != null) { - data['emergency_contact'] = emergencyContact; } + + // Add additional data if (additionalData != null) { data.addAll(additionalData!); } @@ -145,9 +146,11 @@ class UserMetadataModel { return data; } - /// Create a copy with updated fields + /// Create copy with updated fields UserMetadataModel copyWith({ bool? isOfficer, + String? userId, + String? roleId, String? nik, String? email, String? phone, @@ -159,26 +162,92 @@ class UserMetadataModel { }) { return UserMetadataModel( isOfficer: isOfficer ?? this.isOfficer, + userId: userId ?? this.userId, + roleId: roleId ?? this.roleId, nik: nik ?? this.nik, email: email ?? this.email, phone: phone ?? this.phone, name: name ?? this.name, officerData: officerData ?? this.officerData, profileData: profileData ?? this.profileData, - emergencyContact: emergencyContact ?? this.emergencyContact, + additionalData: additionalData ?? this.additionalData, ); } - /// Identifier for the user (NRP for officers, NIK for normal users) + /// Primary identifier (NRP for officers, NIK for users) String? get identifier => isOfficer ? officerData?.nrp : nik; - /// Get full name from name field, officer data, or profile data - String? get fullName { - if (name != null) return name; - if (isOfficer && officerData != null) return officerData!.name; - if (profileData != null) return profileData!.fullName; - return null; + /// Get display name with fallback priority + String? get displayName { + // Priority: explicit name > officer name > profile name > email + if (name?.isNotEmpty == true) return name; + if (isOfficer && officerData?.name.isNotEmpty == true) { + return officerData!.name; + } + if (!isOfficer && profileData?.fullName?.isNotEmpty == true) { + return profileData!.fullName; + } + return email?.split('@').first; // Fallback to email username + } + + /// Validate required fields based on user type + List validate() { + final errors = []; + + 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; } } - diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index 184c878..fd4ea16 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -4,6 +4,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:sigap/src/cores/services/biometric_service.dart'; import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; @@ -512,6 +513,65 @@ class AuthenticationRepository extends GetxController { } } + // Add this method to the AuthenticationRepository class + + /// Creates a new user with initial data but doesn't complete the profile setup + Future 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 completeUserProfile(UserMetadataModel completeData) async { + try { + // First update auth metadata + await _supabase.auth.updateUser( + UserAttributes( + data: { + 'profile_status': 'complete', + 'is_officer': completeData.isOfficer, + 'name': completeData.name, + 'phone': completeData.phone, + }, + ), + ); + + // Then update or insert relevant tables based on the role + if (completeData.isOfficer && completeData.officerData != null) { + await _supabase + .from('officers') + .insert(completeData.officerData!.toJson()); + } else if (!completeData.isOfficer && completeData.profileData != null) { + await _supabase + .from('profiles') + .insert(completeData.profileData!.toJson()); + } + } catch (e) { + throw 'Failed to update profile: ${e.toString()}'; + } + } + // Update user role (officer/user) and metadata Future updateUserRole({ required bool isOfficer, @@ -544,6 +604,36 @@ class AuthenticationRepository extends GetxController { } } + // Add these methods to the AuthenticationRepository class + + /// Update user role after social authentication + Future 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 // --------------------------------------------------------------------------- diff --git a/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart b/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart index de51241..b884ea0 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart @@ -1,9 +1,10 @@ import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/email_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/forgot_password_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup_with_role_controller.dart'; class AuthControllerBindings extends Bindings { @override @@ -11,6 +12,7 @@ class AuthControllerBindings extends Bindings { // Register all feature auth controllers Get.lazyPut(() => SignInController(), fenix: true); Get.lazyPut(() => SignUpController(), fenix: true); + Get.lazyPut(() => SignupWithRoleController(), fenix: true); Get.lazyPut(() => FormRegistrationController(), fenix: true); Get.lazyPut(() => EmailVerificationController(), fenix: true); Get.lazyPut(() => ForgotPasswordController(), fenix: true); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart index 2b4e475..bbd8b70 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/registration_form_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart'; +import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/steps/officer_info_controller.dart'; @@ -41,25 +42,36 @@ class FormRegistrationController extends GetxController { void onInit() { super.onInit(); - // Get role from arguments + // Get role and initial data from arguments final arguments = Get.arguments; - if (arguments != null && arguments['role'] != null) { - selectedRole.value = arguments['role'] as RoleModel; + if (arguments != null) { + if (arguments['role'] != null) { + selectedRole.value = arguments['role'] as RoleModel; + } + + if (arguments['initialData'] != null) { + // Initialize with data from signup + userMetadata.value = arguments['initialData'] as UserMetadataModel; + } + + // Store userId if provided + if (arguments['userId'] != null) { + userMetadata.value = userMetadata.value.copyWith( + userId: arguments['userId'], + ); + } // Initialize userMetadata with the selected role information - userMetadata.value = UserMetadataModel( - isOfficer: selectedRole.value?.isOfficer ?? false, - ); + if ((userMetadata.value.roleId?.isEmpty ?? true) && + selectedRole.value != null) { + userMetadata.value = userMetadata.value.copyWith( + roleId: selectedRole.value!.id, + isOfficer: selectedRole.value!.isOfficer, + ); + } _initializeControllers(); } else { - // Get.snackbar( - // 'Error', - // 'No role selected. Please go back and select a role.', - // snackPosition: SnackPosition.BOTTOM, - // backgroundColor: Colors.red, - // colorText: Colors.white, - // ); TLoaders.errorSnackBar( title: 'Error', message: 'No role selected. Please go back and select a role.', @@ -232,65 +244,26 @@ class FormRegistrationController extends GetxController { if (!isValid) return; try { - isLoading.value = true; + isLoading.value = false; - // Prepare UserMetadataModel based on role - if (selectedRole.value?.isOfficer == true) { - // Officer role - create OfficerModel with the data - final officerData = OfficerModel( - id: '', // Will be assigned by backend - unitId: unitInfoController!.unitIdController.text, - roleId: selectedRole.value!.id, - nrp: officerInfoController!.nrpController.text, - name: personalInfoController.nameController.text, - rank: officerInfoController!.rankController.text, - position: unitInfoController!.positionController.text, - phone: personalInfoController.phoneController.text, - ); + // Prepare UserMetadataModel with all collected data + collectAllFormData(); - userMetadata.value = UserMetadataModel( - isOfficer: true, - name: personalInfoController.nameController.text, - phone: personalInfoController.phoneController.text, - officerData: officerData, - // idCardImagePath: idCardVerificationController.idCardImage.value?.path, - // selfieImagePath: selfieVerificationController.selfieImage.value?.path, - additionalData: { - 'address': personalInfoController.addressController.text, - }, - ); - } else { - // Regular user - create profile-related data - userMetadata.value = UserMetadataModel( - isOfficer: false, - nik: identityController.nikController.text, - name: personalInfoController.nameController.text, - phone: personalInfoController.phoneController.text, - // idCardImagePath: idCardVerificationController.idCardImage.value?.path, - // selfieImagePath: selfieVerificationController.selfieImage.value?.path, - profileData: ProfileModel( - id: '', // Will be assigned by backend - userId: '', // Will be assigned by backend - nik: identityController.nikController.text, - firstName: personalInfoController.firstNameController.text.trim(), - lastName: personalInfoController.lastNameController.text.trim(), - bio: identityController.bioController.text, - birthDate: _parseBirthDate( - identityController.birthDateController.text, - ), - ), - additionalData: { - 'address': personalInfoController.addressController.text, - }, - ); - } + // Complete the user profile using AuthenticationRepository + await AuthenticationRepository.instance.completeUserProfile( + userMetadata.value, + ); - // Navigate to the signup screen with the prepared metadata + // Show success Get.toNamed( - AppRoutes.signUp, + AppRoutes.stateScreen, arguments: { - 'userMetadata': userMetadata.value, - 'role': selectedRole.value, + 'type': 'success', + 'title': 'Registration Completed', + 'message': 'Your profile has been successfully created.', + 'buttonText': 'Continue', + 'onButtonPressed': + () => AuthenticationRepository.instance.screenRedirect(), }, ); } catch (e) { @@ -298,9 +271,9 @@ class FormRegistrationController extends GetxController { AppRoutes.stateScreen, arguments: { 'type': 'error', - 'title': 'Data Preparation Failed', + 'title': 'Registration Failed', 'message': - 'There was an error preparing your profile: ${e.toString()}', + 'There was an error completing your profile: ${e.toString()}', 'buttonText': 'Try Again', 'onButtonPressed': () => Get.back(), }, @@ -310,6 +283,59 @@ class FormRegistrationController extends GetxController { } } + // Add this method to collect all form data + void collectAllFormData() { + final isOfficerRole = selectedRole.value?.isOfficer ?? false; + + if (isOfficerRole) { + // Officer role - create OfficerModel with the data + final officerData = OfficerModel( + id: '', // Will be assigned by backend + unitId: unitInfoController!.unitIdController.text, + roleId: selectedRole.value!.id, + nrp: officerInfoController!.nrpController.text, + name: personalInfoController.nameController.text, + rank: officerInfoController!.rankController.text, + position: unitInfoController!.positionController.text, + phone: personalInfoController.phoneController.text, + ); + + userMetadata.value = userMetadata.value.copyWith( + isOfficer: true, + name: personalInfoController.nameController.text, + phone: personalInfoController.phoneController.text, + roleId: selectedRole.value!.id, + officerData: officerData, + additionalData: { + 'address': personalInfoController.addressController.text, + }, + ); + } else { + // Regular user - create profile-related data + final profileData = ProfileModel( + id: '', // Will be assigned by backend + userId: userMetadata.value.userId ?? '', + nik: identityController.nikController.text, + firstName: personalInfoController.firstNameController.text.trim(), + lastName: personalInfoController.lastNameController.text.trim(), + bio: identityController.bioController.text, + birthDate: _parseBirthDate(identityController.birthDateController.text), + ); + + userMetadata.value = userMetadata.value.copyWith( + isOfficer: false, + nik: identityController.nikController.text, + name: personalInfoController.nameController.text, + phone: personalInfoController.phoneController.text, + roleId: selectedRole.value!.id, + profileData: profileData, + additionalData: { + 'address': personalInfoController.addressController.text, + }, + ); + } + } + // Parse birth date string to DateTime DateTime? _parseBirthDate(String dateStr) { try { diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart new file mode 100644 index 0000000..859a4a9 --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup_with_role_controller.dart @@ -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(); + final roleRepository = Get.find(); + + // Role type (Viewer or Officer) + final Rx 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 availableRoles = [].obs; + final RxString selectedRoleId = ''.obs; + final Rx selectedRole = Rx(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 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 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 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 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 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); + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart index adb6888..af9c70e 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signin_controller.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; @@ -106,7 +107,11 @@ class SignInScreen extends StatelessWidget { // Social sign in buttons SocialButton( text: 'Continue with Google', - icon: Icons.g_mobiledata, + icon: Icon( + TablerIcons.brand_google, + color: TColors.light, + size: 20, + ), onPressed: () => controller.googleSignIn(), ), diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart new file mode 100644 index 0000000..4d2a33b --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart @@ -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(); + 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(( + Set 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), + ], + ); + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart index 45d5c41..e3e0dd0 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart @@ -7,6 +7,9 @@ class AuthButton extends StatelessWidget { final VoidCallback onPressed; final bool isLoading; final bool isPrimary; + final bool isDisabled; + final Color? backgroundColor; + final Color? textColor; const AuthButton({ super.key, @@ -14,76 +17,47 @@ class AuthButton extends StatelessWidget { required this.onPressed, this.isLoading = false, this.isPrimary = true, + this.isDisabled = false, + this.backgroundColor, + this.textColor, }); @override Widget build(BuildContext context) { + final effectiveBackgroundColor = + backgroundColor ?? (isPrimary ? TColors.primary : Colors.grey.shade200); + final effectiveTextColor = + textColor ?? (isPrimary ? Colors.white : TColors.textPrimary); + return SizedBox( width: double.infinity, - height: TSizes.buttonHeight * 3, // Using consistent button height - child: - isPrimary - ? ElevatedButton( - onPressed: isLoading ? null : onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: TColors.primary, - foregroundColor: TColors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(TSizes.buttonRadius), + child: ElevatedButton( + onPressed: (isLoading || isDisabled) ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: effectiveBackgroundColor, + foregroundColor: effectiveTextColor, + padding: const EdgeInsets.symmetric(vertical: TSizes.md), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(TSizes.buttonRadius), + ), + elevation: 1, + disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5), + ), + child: + isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + effectiveTextColor, + ), ), - elevation: TSizes.buttonElevation, - disabledBackgroundColor: TColors.primary.withOpacity(0.6), - ), - child: - isLoading - ? SizedBox( - width: TSizes.iconMd, - height: TSizes.iconMd, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - TColors.white, - ), - ), - ) - : Text( - text, - style: const TextStyle( - fontSize: TSizes.fontSizeMd, - fontWeight: FontWeight.bold, - ), - ), - ) - : OutlinedButton( - onPressed: isLoading ? null : onPressed, - style: OutlinedButton.styleFrom( - foregroundColor: TColors.primary, - side: BorderSide(color: TColors.primary), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(TSizes.buttonRadius), - ), - disabledForegroundColor: TColors.primary.withOpacity(0.6), - ), - child: - isLoading - ? SizedBox( - width: TSizes.iconMd, - height: TSizes.iconMd, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - TColors.primary, - ), - ), - ) - : Text( - text, - style: const TextStyle( - fontSize: TSizes.fontSizeMd, - fontWeight: FontWeight.bold, - ), - ), - ), + ) + : Text(text, + style: TextStyle(fontWeight: FontWeight.bold)), + ), ); } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_divider.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_divider.dart index e0bfbcb..5307c51 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_divider.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_divider.dart @@ -1,35 +1,42 @@ import 'package:flutter/material.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; +import 'package:sigap/src/utils/helpers/helper_functions.dart'; class AuthDivider extends StatelessWidget { final String text; - const AuthDivider({super.key, required this.text}); + const AuthDivider({ + Key? key, + required this.text, + }) : super(key: key); @override Widget build(BuildContext context) { + final isDark = THelperFunctions.isDarkMode(context); + final dividerColor = isDark ? Colors.grey.shade700 : Colors.grey.shade300; + return Row( children: [ Expanded( child: Divider( - color: TColors.borderPrimary, - thickness: TSizes.dividerHeight, + color: dividerColor, + thickness: 1.0, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: TSizes.md), child: Text( text, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isDark ? TColors.textSecondary : Colors.grey.shade600, + ), ), ), Expanded( child: Divider( - color: TColors.borderPrimary, - thickness: TSizes.dividerHeight, + color: dividerColor, + thickness: 1.0, ), ), ], diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart index 4b52567..6cd4f35 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; - -import '../../../../shared/widgets/text/custom_text_field.dart'; +import 'package:sigap/src/utils/helpers/helper_functions.dart'; class PasswordField extends StatelessWidget { final String label; @@ -11,39 +10,100 @@ class PasswordField extends StatelessWidget { final String? Function(String?)? validator; final RxBool isVisible; final String? errorText; + final Function() onToggleVisibility; final TextInputAction textInputAction; - final Function()? onToggleVisibility; + final String? hintText; + final Widget? prefixIcon; + final Color? accentColor; const PasswordField({ super.key, required this.label, required this.controller, - this.validator, + required this.validator, required this.isVisible, this.errorText, + required this.onToggleVisibility, this.textInputAction = TextInputAction.done, - this.onToggleVisibility, + this.hintText, + this.prefixIcon, + this.accentColor, }); @override Widget build(BuildContext context) { - return Obx( - () => CustomTextField( - label: label, - controller: controller, - validator: validator, - obscureText: !isVisible.value, - errorText: errorText, - textInputAction: textInputAction, - suffixIcon: IconButton( - icon: Icon( - isVisible.value ? Icons.visibility_off : Icons.visibility, - color: TColors.textSecondary, - size: TSizes.iconMd, + final Color effectiveAccentColor = accentColor ?? TColors.primary; + final isDark = THelperFunctions.isDarkMode(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: isDark ? TColors.white : TColors.textPrimary, ), - onPressed: onToggleVisibility, ), - ), + const SizedBox(height: TSizes.sm), + TextFormField( + controller: controller, + validator: validator, + obscureText: !isVisible.value, + textInputAction: textInputAction, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: isDark ? TColors.white : TColors.textPrimary, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary), + errorText: + errorText != null && errorText!.isNotEmpty ? errorText : null, + errorStyle: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: TColors.error), + contentPadding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + prefixIcon: prefixIcon, + suffixIcon: IconButton( + onPressed: onToggleVisibility, + icon: Icon( + isVisible.value ? Icons.visibility_off : Icons.visibility, + color: effectiveAccentColor, + semanticLabel: + isVisible.value ? 'Hide password' : 'Show password', + ), + ), + filled: true, + fillColor: isDark ? TColors.darkContainer : TColors.lightContainer, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.borderPrimary, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.borderPrimary, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: effectiveAccentColor, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.error, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + borderSide: BorderSide(color: TColors.error, width: 1.5), + ), + ), + ), + const SizedBox(height: TSizes.spaceBtwInputFields), + ], ); } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/role_selector.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/role_selector.dart new file mode 100644 index 0000000..a0d6a92 --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/role_selector.dart @@ -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 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, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/social_button.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/social_button.dart index 6389e67..acbff18 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/social_button.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/social_button.dart @@ -1,43 +1,76 @@ import 'package:flutter/material.dart'; -import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; -import 'package:sigap/src/utils/helpers/helper_functions.dart'; class SocialButton extends StatelessWidget { final String text; - final IconData icon; + final Icon? icon; + final String? iconImage; + final double? iconImageWidth; + final double? iconImageHeight; final VoidCallback onPressed; + final Color? backgroundColor; + final Color? foregroundColor; + final Color? borderColor; + final bool isVisible; const SocialButton({ super.key, required this.text, - required this.icon, + this.icon, + this.iconImage, + this.iconImageWidth, + this.iconImageHeight, required this.onPressed, + this.backgroundColor, + this.foregroundColor, + this.borderColor, + this.isVisible = true, }); @override Widget build(BuildContext context) { - final dark = THelperFunctions.isDarkMode(context); + if (!isVisible) return const SizedBox.shrink(); return SizedBox( width: double.infinity, - height: TSizes.buttonHeight * 3, - child: OutlinedButton.icon( + child: OutlinedButton( onPressed: onPressed, - icon: Icon(icon, color: TColors.textPrimary, size: TSizes.iconMd), - label: Text( - text, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: TColors.textPrimary, - fontWeight: FontWeight.w500, - ), - ), style: OutlinedButton.styleFrom( - foregroundColor: TColors.textPrimary, - side: BorderSide(color: TColors.borderPrimary), + backgroundColor: backgroundColor ?? Colors.white, + foregroundColor: foregroundColor ?? Colors.black, + side: BorderSide(color: borderColor ?? Colors.grey.shade300), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(TSizes.buttonRadius), ), + padding: const EdgeInsets.symmetric( + vertical: TSizes.md, + horizontal: TSizes.md, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 24, + width: 24, + child: + iconImage == null + ? icon + : Image.asset( + iconImage!, + width: iconImageWidth, + height: iconImageHeight, + ), + ), + const SizedBox(width: TSizes.md), + Text( + text, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: foregroundColor ?? Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ); diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart index f71befd..cde44ce 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart @@ -110,7 +110,7 @@ class OnboardingController extends GetxController if (isLocationValid) { // If location is valid, proceed to role selection - Get.offAllNamed(AppRoutes.roleSelection); + Get.offAllNamed(AppRoutes.signupWithRole); TLoaders.successSnackBar( title: 'Location Valid', diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart index e5d42e4..291ccb6 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart @@ -1,6 +1,6 @@ import 'package:get/get.dart'; -import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart'; import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart'; +import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; @@ -88,7 +88,7 @@ class RoleSelectionController extends GetxController { // Navigate directly to step form with selected role Get.toNamed( - AppRoutes.formRegistration, + AppRoutes.registrationForm, arguments: {'role': selectedRole.value}, ); } catch (e) { diff --git a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart index 5c6dba5..a434dd5 100644 --- a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart +++ b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart @@ -7,34 +7,39 @@ class CustomTextField extends StatelessWidget { final String label; final TextEditingController controller; final String? Function(String?)? validator; - final bool obscureText; - final Widget? suffixIcon; - final TextInputType keyboardType; + final TextInputType? keyboardType; + final TextInputAction? textInputAction; final String? errorText; - final bool autofocus; - final int maxLines; + final int? maxLines; final String? hintText; - final TextInputAction textInputAction; - final Function(String)? onChanged; + final Widget? prefixIcon; + final Widget? suffixIcon; + final bool? enabled; + final bool obscureText; + final void Function(String)? onChanged; + final Color? accentColor; const CustomTextField({ super.key, required this.label, required this.controller, this.validator, - this.obscureText = false, - this.suffixIcon, - this.keyboardType = TextInputType.text, + this.keyboardType, + this.textInputAction, this.errorText, - this.autofocus = false, this.maxLines = 1, this.hintText, - this.textInputAction = TextInputAction.next, + this.prefixIcon, + this.suffixIcon, + this.enabled = true, + this.obscureText = false, this.onChanged, + this.accentColor, }); @override Widget build(BuildContext context) { + final Color effectiveAccentColor = accentColor ?? TColors.primary; final isDark = THelperFunctions.isDarkMode(context); return Column( @@ -51,11 +56,11 @@ class CustomTextField extends StatelessWidget { TextFormField( controller: controller, validator: validator, - obscureText: obscureText, keyboardType: keyboardType, - autofocus: autofocus, textInputAction: textInputAction, maxLines: maxLines, + enabled: enabled, + obscureText: obscureText, onChanged: onChanged, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: isDark ? TColors.white : TColors.textPrimary, @@ -74,6 +79,7 @@ class CustomTextField extends StatelessWidget { horizontal: TSizes.md, vertical: TSizes.md, ), + prefixIcon: prefixIcon, suffixIcon: suffixIcon, filled: true, fillColor: isDark ? TColors.darkContainer : TColors.lightContainer, @@ -87,7 +93,7 @@ class CustomTextField extends StatelessWidget { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), - borderSide: BorderSide(color: TColors.primary, width: 1.5), + borderSide: BorderSide(color: effectiveAccentColor, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), diff --git a/sigap-mobile/lib/src/utils/constants/app_routes.dart b/sigap-mobile/lib/src/utils/constants/app_routes.dart index f1df6a6..ff36b80 100644 --- a/sigap-mobile/lib/src/utils/constants/app_routes.dart +++ b/sigap-mobile/lib/src/utils/constants/app_routes.dart @@ -2,7 +2,8 @@ class AppRoutes { static const String onboarding = '/onboarding'; static const String welcome = '/welcome'; static const String signIn = '/sign-in'; - static const String signUp = '/sign-up'; + static const String signUp = + '/sign-up'; // keep this for backward compatibility static const String emailVerification = '/email-verification'; static const String forgotPassword = '/forgot-password'; static const String explore = '/explore'; @@ -15,5 +16,6 @@ class AppRoutes { static const String roleSelection = '/role-selection'; static const String stateScreen = '/state-screen'; static const String locationWarning = '/location-warning'; - static const String formRegistration = '/form-registration'; + static const String registrationForm = '/registration-form'; + static const String signupWithRole = '/signup-with-role'; } diff --git a/sigap-mobile/lib/src/utils/constants/image_strings.dart b/sigap-mobile/lib/src/utils/constants/image_strings.dart index 2b689da..9b3e7de 100644 --- a/sigap-mobile/lib/src/utils/constants/image_strings.dart +++ b/sigap-mobile/lib/src/utils/constants/image_strings.dart @@ -122,4 +122,154 @@ class TImages { "assets/images/animations/world-map-dark.json"; // static const String characterExplore2 = // "assets/images/animations/char_explore2.json"; + + // -- Background Images + static const String viewerBackground = + 'assets/images/auth/viewer_background.jpg'; + static const String officerBackground = + 'assets/images/auth/officer_background.jpg'; + static const String namaKonstanta = "path/ke/file.png"; + + // -- Animations (tambahan dari assets/images/animations) + static const String thankYouAnimation = + "assets/images/animations/104368-thank-you.json"; + static const String paperPlaneAnimation = + "assets/images/animations/110052-paper-plane.json"; + static const String paymentSuccessfulAnimation = + "assets/images/animations/120978-payment-successful.json"; + static const String carRidesAnimation = + "assets/images/animations/136491-animation-lottie-car-rides.json"; + static const String pencilDrawingAnimation = + "assets/images/animations/140429-pencil-drawing.json"; + static const String loadingJuggleAnimation = + "assets/images/animations/141397-loading-juggle.json"; + static const String docerAnimation = + "assets/images/animations/141594-animation-of-docer.json"; + static const String emptyFileAnimation = + "assets/images/animations/53207-empty-file.json"; + static const String securityIconAnimation = + "assets/images/animations/67263-security-icon-transparent.json"; + static const String checkRegisterAnimation = + "assets/images/animations/72462-check-register.json"; + static const String searchingAnimation = + "assets/images/animations/72785-searching.json"; + static const String packagingInProgressAnimation = + "assets/images/animations/98783-packaging-in-progress.json"; + static const String animationCarMarquee = + "assets/images/animations/Animation - 1747602248003.json"; + static const String cloudUploadingAnimation = + "assets/images/animations/cloud-uploading-animation.json"; + static const String ladyAddingProductInCartAnimation = + "assets/images/animations/lady-adding-product-in-cart-animation.json"; + static const String loaderAnimation = + "assets/images/animations/loader-animation.json"; + static const String loaderJson = "assets/images/animations/loader.json"; + static const String orderCompleteCarDeliveryAnimation = + "assets/images/animations/order-complete-car-delivery-animation.json"; + static const String worldMapDarkAnimation = + "assets/images/animations/world-map-dark.json"; + static const String worldMapAnimation = + "assets/images/animations/world-map.json"; + static const String splashDarkAnimation = + "assets/images/animations/splash-dark.json"; + static const String splashLightAnimation = + "assets/images/animations/splash-light.json"; + + // -- Content Images (assets/images/content) + static const String backpackingDark = + "assets/images/content/backpacking-dark.svg"; + static const String backpacking = "assets/images/content/backpacking.svg"; + static const String callingHelpDark = + "assets/images/content/calling-help-dark.svg"; + static const String callingHelp = "assets/images/content/calling-help.svg"; + static const String communicationDark = + "assets/images/content/communication-dark.svg"; + static const String communication = "assets/images/content/communication.svg"; + static const String crashedErrorDark = + "assets/images/content/crashed-error-dark.svg"; + static const String crashedError = "assets/images/content/crashed-error.svg"; + static const String customerSupportDark = + "assets/images/content/customer-support-dark.svg"; + static const String customerSupport = + "assets/images/content/customer-support.svg"; + static const String fallingDark = "assets/images/content/falling-dark.svg"; + static const String falling = "assets/images/content/falling.svg"; + static const String hitchhikingDark = + "assets/images/content/hitchhiking-dark.svg"; + static const String hitchhiking = "assets/images/content/hitchhiking.svg"; + static const String homeOfficeDark = + "assets/images/content/home-office-dark.svg"; + static const String homeOffice = "assets/images/content/home-office.svg"; + static const String lookingAtTheMapDark = + "assets/images/content/looking-at-the-map-dark.svg"; + static const String lookingAtTheMap = + "assets/images/content/looking-at-the-map.svg"; + static const String onlineDatingDark = + "assets/images/content/online-dating-dark.svg"; + static const String onlineDating = "assets/images/content/online-dating.svg"; + static const String paperPlaneDark = + "assets/images/content/paper-plane-dark.svg"; + static const String paperPlane = "assets/images/content/paper-plane.svg"; + static const String questionMarkDark = + "assets/images/content/question-mark-dark.svg"; + static const String questionMark = "assets/images/content/question-mark.svg"; + static const String searchingLocationOnThePhoneDark = + "assets/images/content/searching-location-on-the-phone-dark.svg"; + static const String searchingLocationOnThePhone = + "assets/images/content/searching-location-on-the-phone.svg"; + static const String telephoneCallDark = + "assets/images/content/telephone-call-dark.svg"; + static const String telephoneCall = + "assets/images/content/telephone-call.svg"; + static const String travelingWithASuitcaseDark = + "assets/images/content/traveling-with-a-suitcase-dark.svg"; + static const String travelingWithASuitcase = + "assets/images/content/traveling-with-a-suitcase.svg"; + static const String userPng = "assets/images/content/user.png"; + static const String videoCallDark = + "assets/images/content/video-call-dark.svg"; + static const String videoCall = "assets/images/content/video-call.svg"; + static const String womanHuggingEarthDark = + "assets/images/content/woman-hugging-earth-dark.svg"; + static const String womanHuggingEarth = + "assets/images/content/woman-hugging-earth.svg"; + static const String womanTouristDark = + "assets/images/content/woman-tourist-dark.svg"; + static const String womanTourist = "assets/images/content/woman-tourist.svg"; + + // -- OnBoarding Images (assets/images/on_boarding_images) + static const String sammyLineDelivery = + "assets/images/on_boarding_images/sammy-line-delivery.gif"; + static const String sammyLineNoConnection = + "assets/images/on_boarding_images/sammy-line-no-connection.gif"; + static const String sammyLineSearching = + "assets/images/on_boarding_images/sammy-line-searching.gif"; + static const String sammyLineShopping = + "assets/images/on_boarding_images/sammy-line-shopping.gif"; + + // -- Reviews Images (assets/images/reviews) + static const String reviewProfileImage1 = + "assets/images/reviews/review_profile_image_1.jpg"; + static const String reviewProfileImage2 = + "assets/images/reviews/review_profile_image_2.jpeg"; + static const String reviewProfileImage3 = + "assets/images/reviews/review_profile_image_3.jpeg"; + + // -- Logos (assets/logos) + static const String facebookIcon = "assets/logos/facebook-icon.png"; + static const String googleIcon = "assets/logos/google-icon.png"; + static const String logoBgDarkPng = "assets/logos/logo-bg-dark.png"; + static const String logoBgDarkSvg = "assets/logos/logo-bg-dark.svg"; + static const String logoBgLightPng = "assets/logos/logo-bg-light.png"; + static const String logoBgLightSvg = "assets/logos/logo-bg-light.svg"; + static const String logoDarkPng = "assets/logos/logo-dark.png"; + static const String logoDarkSvg = "assets/logos/logo-dark.svg"; + static const String logoLightPng = "assets/logos/logo-light.png"; + static const String logoLightSvg = "assets/logos/logo-light.svg"; + static const String logoPng = "assets/logos/logo.png"; + static const String logoSvg = "assets/logos/logo.svg"; + static const String tStoreSplashLogoBlack = + "assets/logos/t-store-splash-logo-black.png"; + static const String tStoreSplashLogoWhite = + "assets/logos/t-store-splash-logo-white.png"; } diff --git a/sigap-mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/sigap-mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 2e85ecd..e624e6e 100644 --- a/sigap-mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/sigap-mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,7 +18,6 @@ import location import path_provider_foundation import shared_preferences_foundation import url_launcher_macos -import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -34,5 +33,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/sigap-mobile/pubspec.lock b/sigap-mobile/pubspec.lock index 00bb359..79f9129 100644 --- a/sigap-mobile/pubspec.lock +++ b/sigap-mobile/pubspec.lock @@ -491,6 +491,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + flutter_tabler_icons: + dependency: "direct main" + description: + name: flutter_tabler_icons + sha256: "657c2201e12fa9121a12ddb4edb74d69290f803868eb6526f04102e6d49ec882" + url: "https://pub.dev" + source: hosted + version: "1.43.0" flutter_test: dependency: "direct dev" description: flutter @@ -1586,38 +1594,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: "62d763c27ce7f6cef04b3bec01c85a28d60149bffd155884aa4b8fd4941ea2e4" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "5ece28a317a9f76ad5ee17c78dbacc8a491687cec85ee19c1643761bf8d678ef" - url: "https://pub.dev" - source: hosted - version: "4.6.0" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "6af7d1908c9c89311c2dffcc2c9b51b88a6f055ba16fa0aa8a04cbb1c3afc9ce" - url: "https://pub.dev" - source: hosted - version: "3.21.0" win32: dependency: transitive description: diff --git a/sigap-mobile/pubspec.yaml b/sigap-mobile/pubspec.yaml index bff421e..7e9bba6 100644 --- a/sigap-mobile/pubspec.yaml +++ b/sigap-mobile/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: flutter_launcher_icons: animated_splash_screen: ^1.3.0 flutter_native_splash: ^2.4.6 + flutter_tabler_icons: ^1.43.0 # --- UI & Animation --- smooth_page_indicator: @@ -54,7 +55,7 @@ dependencies: dropdown_search: dotted_border: flutter_svg: ^2.1.0 - webview_flutter: ^4.12.0 + # --- Input & Forms --- flutter_otp_text_field: