From 98e82cb6dbcc5a1bcadc5bf22bc45ec81ffa259b Mon Sep 17 00:00:00 2001 From: shaulascr Date: Tue, 20 May 2025 08:32:33 +0700 Subject: [PATCH] update addrses in register --- .../data/api/dto/CreateAddressRequest.kt | 4 +- .../data/api/dto/RegisterRequest.kt | 5 +- .../data/repository/UserRepository.kt | 5 +- .../ui/auth/RegisterActivity.kt | 293 ++------------ .../auth/fragments/RegisterStep1Fragment.kt | 268 +++++++++++++ .../auth/fragments/RegisterStep2Fragment.kt | 291 ++++++++++++++ .../auth/fragments/RegisterStep3Fragment.kt | 360 ++++++++++++++++++ .../utils/viewmodel/RegisterViewModel.kt | 216 ++++++++++- app/src/main/res/layout/activity_register.xml | 271 ++----------- .../res/layout/fragment_register_step1.xml | 245 ++++++++++++ .../res/layout/fragment_register_step2.xml | 108 ++++++ .../res/layout/fragment_register_step3.xml | 229 +++++++++++ 12 files changed, 1775 insertions(+), 520 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt create mode 100644 app/src/main/res/layout/fragment_register_step1.xml create mode 100644 app/src/main/res/layout/fragment_register_step2.xml create mode 100644 app/src/main/res/layout/fragment_register_step3.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CreateAddressRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CreateAddressRequest.kt index 658ce61..8aefbe3 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CreateAddressRequest.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CreateAddressRequest.kt @@ -4,10 +4,10 @@ import com.google.gson.annotations.SerializedName data class CreateAddressRequest ( @SerializedName("latitude") - val lat: Double, + val lat: Double? = null, @SerializedName("longitude") - val long: Double, + val long: Double? = null, @SerializedName("street") val street: String, diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt index d1a4482..ccea7d6 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt @@ -1,7 +1,10 @@ package com.alya.ecommerce_serang.data.api.dto +import android.os.Parcelable import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize +@Parcelize data class RegisterRequest ( val name: String?, val email: String?, @@ -15,4 +18,4 @@ data class RegisterRequest ( val image: String? = null, val otp: String? = null -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt index cdffe08..2c0416e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt @@ -14,6 +14,7 @@ import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse @@ -59,12 +60,12 @@ class UserRepository(private val apiService: ApiService) { return if (response.isSuccessful) response.body() else null } - suspend fun registerUser(request: RegisterRequest): String { + suspend fun registerUser(request: RegisterRequest): RegisterResponse { val response = apiService.register(request) // API call if (response.isSuccessful) { val responseBody = response.body() ?: throw Exception("Empty response body") - return responseBody.message // Get the message from RegisterResponse + return responseBody // Get the message from RegisterResponse } else { throw Exception("Registration failed: ${response.errorBody()?.string()}") } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index f07dcd4..e399590 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -1,49 +1,39 @@ package com.alya.ecommerce_serang.ui.auth -import android.app.DatePickerDialog import android.content.Intent import android.os.Bundle import android.util.Log -import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.dto.RegisterRequest -import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig -import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding import com.alya.ecommerce_serang.ui.MainActivity +import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep1Fragment +import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep2Fragment +import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep3Fragment import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale class RegisterActivity : AppCompatActivity() { private lateinit var binding: ActivityRegisterBinding private lateinit var sessionManager: SessionManager - private var isEmailValid = false - private var isPhoneValid = false - - // Track which validation was last performed - private var lastCheckField = "" - - // Counter for signup validation - private var signupValidationsComplete = 0 - private var signupInProgress = false private val registerViewModel: RegisterViewModel by viewModels{ BaseViewModelFactory { val apiService = ApiConfig.getUnauthenticatedApiService() + val orderRepository = OrderRepository(apiService) val userRepository = UserRepository(apiService) - RegisterViewModel(userRepository) + RegisterViewModel(userRepository, orderRepository, this) } } @@ -88,264 +78,27 @@ class RegisterActivity : AppCompatActivity() { windowInsets } - setupObservers() - - // Set up field validations - setupFieldValidations() - - binding.btnSignup.setOnClickListener { - handleSignUp() - } - - binding.tvLoginAlt.setOnClickListener { - val intent = Intent(this, LoginActivity::class.java) - startActivity(intent) - } - - binding.etBirthDate.setOnClickListener { - showDatePicker() + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, RegisterStep1Fragment.newInstance()) + .commit() } } - private fun setupFieldValidations() { - // Validate email when focus changes - binding.etEmail.setOnFocusChangeListener { _, hasFocus -> - if (!hasFocus) { - val email = binding.etEmail.text.toString() - if (email.isNotEmpty()) { - validateEmail(email, false) - } - } + // Function to navigate to the next fragment + fun navigateToStep(step: Int, userData: RegisterRequest?) { + val fragment = when (step) { + 1 -> RegisterStep1Fragment.newInstance() + 2 -> RegisterStep2Fragment.newInstance(userData) + 3 -> RegisterStep3Fragment.newInstance() + else -> null } - // Validate phone when focus changes - binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus -> - if (!hasFocus) { - val phone = binding.etNumberPhone.text.toString() - if (phone.isNotEmpty()) { - validatePhone(phone, false) - } - } + fragment?.let { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, it) + .addToBackStack(null) + .commit() } } - - private fun validateEmail(email: String, isSignup: Boolean) { - lastCheckField = "email" - Log.d("RegisterActivity", "Validating email: $email (signup: $isSignup)") - - val checkValueEmail = VerifRegisReq( - fieldRegis = "email", - valueRegis = email - ) - registerViewModel.checkValueReg(checkValueEmail) - } - - private fun validatePhone(phone: String, isSignup: Boolean) { - lastCheckField = "phone" - Log.d("RegisterActivity", "Validating phone: $phone (signup: $isSignup)") - - val checkValuePhone = VerifRegisReq( - fieldRegis = "phone", - valueRegis = phone - ) - registerViewModel.checkValueReg(checkValuePhone) - } - - private fun setupObservers() { - - registerViewModel.checkValue.observe(this) { result -> - when (result) { - is Result.Loading -> { - // Show loading if needed - } - is Result.Success -> { - val isValid = (result.data as? Boolean) ?: false - - when (lastCheckField) { - "email" -> { - isEmailValid = isValid - if (!isValid) { - Toast.makeText(this, "Email is already registered", Toast.LENGTH_SHORT).show() - } else { - Log.d("RegisterActivity", "Email is valid") - } - } - "phone" -> { - isPhoneValid = isValid - if (!isValid) { - Toast.makeText(this, "Phone number is already registered", Toast.LENGTH_SHORT).show() - } else { - Log.d("RegisterActivity", "Phone is valid") - } - } - } - - // Check if we're in signup process - if (signupInProgress) { - signupValidationsComplete++ - - // Check if both validations completed - if (signupValidationsComplete >= 2) { - signupInProgress = false - signupValidationsComplete = 0 - - // If both validations passed, request OTP - if (isEmailValid && isPhoneValid) { - requestOtp() - } - } - } - } - is Result.Error -> { - val fieldType = if (lastCheckField == "email") "Email" else "Phone" - Toast.makeText(this, "$fieldType validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() - - // Mark validation as invalid - if (lastCheckField == "email") { - isEmailValid = false - } else if (lastCheckField == "phone") { - isPhoneValid = false - } - - // Update signup validation counter if in signup process - if (signupInProgress) { - signupValidationsComplete++ - - // Check if both validations completed - if (signupValidationsComplete >= 2) { - signupInProgress = false - signupValidationsComplete = 0 - } - } - } - else -> { - Log.e("RegisterActivity", "Unexpected result type: $result") - } - } - } - registerViewModel.otpState.observe(this) { result -> - when (result) { - is Result.Loading -> { - binding.progressBarOtp.visibility = android.view.View.VISIBLE - } - is Result.Success -> { - binding.progressBarOtp.visibility = android.view.View.GONE - Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.") - - // Create user data before showing OTP dialog - val userData = createUserData() - - // Show OTP dialog - val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData -> - Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.") - registerViewModel.registerUser(fullUserData) - } - otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet") - } - is Result.Error -> { - binding.progressBarOtp.visibility = android.view.View.GONE - Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() - } - else -> { - Log.e("RegisterActivity", "Unexpected result type: $result") - } - } - } - registerViewModel.registerState.observe(this) { result -> - when (result) { - is Result.Loading -> { - // Show loading indicator for registration - binding.progressBarRegister.visibility = android.view.View.VISIBLE - } - is Result.Success -> { - // Hide loading indicator and show success message - binding.progressBarRegister.visibility = android.view.View.GONE - Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show() - val intent = Intent(this, LoginActivity::class.java) - startActivity(intent) - // Navigate to another screen if needed - } - is com.alya.ecommerce_serang.data.repository.Result.Error -> { - // Hide loading indicator and show error message - binding.progressBarRegister.visibility = android.view.View.GONE - Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() - } - } - } - } - - private fun handleSignUp() { - // Basic validation first - val email = binding.etEmail.text.toString() - val password = binding.etPassword.text.toString() - val confirmPassword = binding.etConfirmPassword.text.toString() - val phone = binding.etNumberPhone.text.toString() - val username = binding.etUsername.text.toString() - val name = binding.etFullname.text.toString() - val birthDate = binding.etBirthDate.text.toString() - - // Check if fields are filled - if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || - phone.isEmpty() || username.isEmpty() || name.isEmpty() || birthDate.isEmpty()) { - Toast.makeText(this, "Please fill all required fields", Toast.LENGTH_SHORT).show() - return - } - - // Check if passwords match - if (password != confirmPassword) { - Toast.makeText(this, "Passwords do not match", Toast.LENGTH_SHORT).show() - return - } - - // If both validations are already done and successful, just request OTP - if (isEmailValid && isPhoneValid) { - requestOtp() - return - } - - // Reset validation counters - signupInProgress = true - signupValidationsComplete = 0 - - // Start validations in parallel - validateEmail(email, true) - validatePhone(phone, true) - } - - private fun requestOtp() { - val email = binding.etEmail.text.toString() - Log.d("RegisterActivity", "Requesting OTP for email: $email") - registerViewModel.requestOtp(email) - } - - private fun createUserData(): RegisterRequest { - // Get all form values - val birthDate = binding.etBirthDate.text.toString() - val email = binding.etEmail.text.toString() - val password = binding.etPassword.text.toString() - val phone = binding.etNumberPhone.text.toString() - val username = binding.etUsername.text.toString() - val name = binding.etFullname.text.toString() - val image = null - - // Create and return user data object - return RegisterRequest(name, email, password, username, phone, birthDate, image) - } - - private fun showDatePicker() { - val calendar = Calendar.getInstance() - val year = calendar.get(Calendar.YEAR) - val month = calendar.get(Calendar.MONTH) - val day = calendar.get(Calendar.DAY_OF_MONTH) - - DatePickerDialog( - this, - { _, selectedYear, selectedMonth, selectedDay -> - calendar.set(selectedYear, selectedMonth, selectedDay) - val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - binding.etBirthDate.setText(sdf.format(calendar.time)) - }, - year, month, day - ).show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt new file mode 100644 index 0000000..911d2e5 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt @@ -0,0 +1,268 @@ +package com.alya.ecommerce_serang.ui.auth.fragments + +import android.app.DatePickerDialog +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.RegisterRequest +import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.OrderRepository +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.databinding.FragmentRegisterStep1Binding +import com.alya.ecommerce_serang.ui.auth.LoginActivity +import com.alya.ecommerce_serang.ui.auth.RegisterActivity +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel +import com.google.android.material.progressindicator.LinearProgressIndicator +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class RegisterStep1Fragment : Fragment() { + private var _binding: FragmentRegisterStep1Binding? = null + private val binding get() = _binding!! + + private val registerViewModel: RegisterViewModel by activityViewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getUnauthenticatedApiService() + val orderRepository = OrderRepository(apiService) + val userRepository = UserRepository(apiService) + RegisterViewModel(userRepository, orderRepository, requireContext()) + } + } + private var isEmailValid = false + private var isPhoneValid = false + + companion object { + private const val TAG = "RegisterStep1Fragment" + + fun newInstance() = RegisterStep1Fragment() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRegisterStep1Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Set step progress and description + (activity as? RegisterActivity)?.let { + it.findViewById(R.id.registration_progress)?.progress = 33 + it.findViewById(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info" + it.findViewById(R.id.tv_step_description)?.text = + "Fill in your account and personal details to create your profile." + } + + setupFieldValidations() + setupObservers() + setupDatePicker() + + binding.btnNext.setOnClickListener { + validateAndProceed() + } + + binding.tvLoginAlt.setOnClickListener { + startActivity(Intent(requireContext(), LoginActivity::class.java)) + } + } + + private fun setupDatePicker() { + binding.etBirthDate.setOnClickListener { + showDatePicker() + } + } + + private fun showDatePicker() { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + + DatePickerDialog( + requireContext(), + { _, selectedYear, selectedMonth, selectedDay -> + calendar.set(selectedYear, selectedMonth, selectedDay) + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + binding.etBirthDate.setText(sdf.format(calendar.time)) + }, + year, month, day + ).show() + } + + private fun setupFieldValidations() { + // Validate email when focus changes + binding.etEmail.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val email = binding.etEmail.text.toString() + if (email.isNotEmpty()) { + validateEmail(email) + } + } + } + + // Validate phone when focus changes + binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val phone = binding.etNumberPhone.text.toString() + if (phone.isNotEmpty()) { + validatePhone(phone) + } + } + } + } + + private fun validateEmail(email: String) { + val checkValueEmail = VerifRegisReq( + fieldRegis = "email", + valueRegis = email + ) + registerViewModel.checkValueReg(checkValueEmail) + } + + private fun validatePhone(phone: String) { + val checkValuePhone = VerifRegisReq( + fieldRegis = "phone", + valueRegis = phone + ) + registerViewModel.checkValueReg(checkValuePhone) + } + + private fun setupObservers() { + registerViewModel.checkValue.observe(viewLifecycleOwner) { result -> + when (result) { + is com.alya.ecommerce_serang.data.repository.Result.Loading -> { + // Show loading if needed + } + is com.alya.ecommerce_serang.data.repository.Result.Success -> { + val isValid = (result.data as? Boolean) ?: false + when (val fieldType = registerViewModel.lastCheckedField) { + "email" -> { + isEmailValid = isValid + if (!isValid) { + Toast.makeText(requireContext(), "Email is already registered", Toast.LENGTH_SHORT).show() + } + } + "phone" -> { + isPhoneValid = isValid + if (!isValid) { + Toast.makeText(requireContext(), "Phone number is already registered", Toast.LENGTH_SHORT).show() + } + } + } + } + is com.alya.ecommerce_serang.data.repository.Result.Error -> { + Toast.makeText(requireContext(), "Validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + } + } + + registerViewModel.otpState.observe(viewLifecycleOwner) { result -> + when (result) { + is com.alya.ecommerce_serang.data.repository.Result.Loading -> { + binding.progressBar.visibility = View.VISIBLE + binding.btnNext.isEnabled = false + } + is com.alya.ecommerce_serang.data.repository.Result.Success -> { + binding.progressBar.visibility = View.GONE + binding.btnNext.isEnabled = true + + // Create user data with both account and personal info + val userData = RegisterRequest( + name = binding.etFullname.text.toString(), + email = binding.etEmail.text.toString(), + password = binding.etPassword.text.toString(), + username = binding.etUsername.text.toString(), + phone = binding.etNumberPhone.text.toString(), + birthDate = binding.etBirthDate.text.toString(), + otp = "" // Will be filled in step 2 + ) + + registerViewModel.updateUserData(userData) + registerViewModel.setStep(2) + (activity as? RegisterActivity)?.navigateToStep(2, userData) + } + is Result.Error -> { + binding.progressBar.visibility = View.GONE + binding.btnNext.isEnabled = true + Toast.makeText(requireContext(), "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun validateAndProceed() { + // Validate account information + val email = binding.etEmail.text.toString() + val password = binding.etPassword.text.toString() + val confirmPassword = binding.etConfirmPassword.text.toString() + val phone = binding.etNumberPhone.text.toString() + val username = binding.etUsername.text.toString() + + // Validate personal information + val fullName = binding.etFullname.text.toString() + val birthDate = binding.etBirthDate.text.toString() +// val gender = binding.etGender.text.toString() + + // Check if all fields are filled + if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() || + username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) { + Toast.makeText(requireContext(), "Please fill all required fields", Toast.LENGTH_SHORT).show() + return + } + + // Check if passwords match + if (password != confirmPassword) { + Toast.makeText(requireContext(), "Passwords do not match", Toast.LENGTH_SHORT).show() + return + } + + // If both validations are already done and successful, request OTP + if (isEmailValid && isPhoneValid) { + requestOtp(email) + return + } + + // Validate email and phone + validateEmail(email) + validatePhone(phone) + + // Only proceed if both are valid + if (isEmailValid && isPhoneValid) { + requestOtp(email) + } else { + Toast.makeText(requireContext(), "Please fix validation errors before proceeding", Toast.LENGTH_SHORT).show() + } + } + + private fun requestOtp(email: String) { + + registerViewModel.requestOtp(email) + + registerViewModel.message.observe(viewLifecycleOwner) { message -> + Log.d(TAG, "Message from server: $message") + // You can use the message here if needed, e.g., for showing in a specific UI element + // or for storing for later use + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt new file mode 100644 index 0000000..e464524 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt @@ -0,0 +1,291 @@ +package com.alya.ecommerce_serang.ui.auth.fragments + +import android.os.Build +import android.os.Bundle +import android.os.CountDownTimer +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.RegisterRequest +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.OrderRepository +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.databinding.FragmentRegisterStep2Binding +import com.alya.ecommerce_serang.ui.auth.RegisterActivity +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel +import com.google.android.material.progressindicator.LinearProgressIndicator + +class RegisterStep2Fragment : Fragment() { + private var _binding: FragmentRegisterStep2Binding? = null + private val binding get() = _binding!! + private lateinit var sessionManager: SessionManager + + // In RegisterStep2Fragment AND RegisterStep3Fragment: + private val registerViewModel: RegisterViewModel by activityViewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getUnauthenticatedApiService() + val orderRepository = OrderRepository(apiService) + val userRepository = UserRepository(apiService) + RegisterViewModel(userRepository, orderRepository, requireContext()) + } + } + private var countDownTimer: CountDownTimer? = null + private var timeRemaining = 30 // 30 seconds cooldown for resend + + companion object { + + private const val TAG = "RegisterStep2Fragment" + fun newInstance(userData: RegisterRequest?) = RegisterStep2Fragment().apply { + arguments = Bundle().apply { + putParcelable("userData", userData) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRegisterStep2Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sessionManager = SessionManager(requireContext()) + Log.d(TAG, "SessionManager initialized, token: ${sessionManager.getToken()}") + + // Set step progress and description + (activity as? RegisterActivity)?.let { + it.findViewById(R.id.registration_progress)?.progress = 66 + it.findViewById(R.id.tv_step_title)?.text = "Step 2: Verify Your Email" + it.findViewById(R.id.tv_step_description)?.text = + "Enter the verification code sent to your email to continue." + Log.d(TAG, "Step indicators updated to Step 2") + } + + + // Get the user data from arguments + val userData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arguments?.getParcelable("userData", RegisterRequest::class.java) + } else { + @Suppress("DEPRECATION") + arguments?.getParcelable("userData") as? RegisterRequest + } + Log.d(TAG, "User data retrieved from arguments: ${userData?.email}, ${userData?.name}") + + // Update the email sent message + userData?.let { + binding.tvEmailSent.text = "We've sent a verification code to ${it.email}" + } + + // Start the resend cooldown timer + startResendCooldown() + Log.d(TAG, "Resend cooldown timer started") + + // Set up button listeners + binding.btnVerify.setOnClickListener { + verifyOtp(userData) + } + + binding.tvResendOtp.setOnClickListener { + if (timeRemaining <= 0) { + Log.d(TAG, "Resend OTP clicked, remaining time: $timeRemaining") + resendOtp(userData?.email) + } else { + Log.d(TAG, "Resend OTP clicked but cooldown active, remaining time: $timeRemaining") + } + } + + observeRegistrationState() + observeLoginState() + Log.d(TAG, "Registration and login state observers set up") + } + + private fun verifyOtp(userData: RegisterRequest?) { + val otp = binding.etOtp.text.toString() + Log.d(TAG, "verifyOtp called with OTP: $otp") + + if (otp.isEmpty()) { + Toast.makeText(requireContext(), "Please enter the verification code", Toast.LENGTH_SHORT).show() + return + } + + // Update the user data with the OTP + userData?.let { + val updatedUserData = it.copy(otp = otp) + Log.d(TAG, "Updating user data with OTP: $otp") + registerViewModel.updateUserData(updatedUserData) + + // For demo purposes, we're just proceeding to Step 3 + // In a real app, you would verify the OTP with the server first +// registerViewModel.setStep(3) +// (activity as? RegisterActivity)?.navigateToStep(3, updatedUserData) + + registerViewModel.registerUser(updatedUserData) + } ?: Log.e(TAG, "userData is null, cannot proceed with verification") + } + + private fun resendOtp(email: String?) { + Log.d(TAG, "resendOtp called for email: $email") + email?.let { + binding.progressBar.visibility = View.VISIBLE + Log.d(TAG, "Requesting OTP for: $it") + registerViewModel.requestOtp(it) + + // Observe the OTP state + registerViewModel.otpState.observe(viewLifecycleOwner) { result -> + when (result) { + is com.alya.ecommerce_serang.data.repository.Result.Loading -> { + binding.progressBar.visibility = View.VISIBLE + } + is com.alya.ecommerce_serang.data.repository.Result.Success -> { + binding.progressBar.visibility = View.GONE + Toast.makeText(requireContext(), "Verification code resent", Toast.LENGTH_SHORT).show() + startResendCooldown() + } + is Result.Error -> { + Log.e(TAG, "OTP request: Error - ${result.exception.message}") + binding.progressBar.visibility = View.GONE + Toast.makeText(requireContext(), "Failed to resend code: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + else -> { + Log.d(TAG, "OTP request: Unknown state") + binding.progressBar.visibility = View.GONE + } + } + } + } ?: Log.e(TAG, "Cannot resend OTP: email is null") + } + + private fun startResendCooldown() { + Log.d(TAG, "startResendCooldown called") + timeRemaining = 30 + binding.tvResendOtp.isEnabled = false + binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.soft_gray)) + + countDownTimer?.cancel() + countDownTimer = object : CountDownTimer(30000, 1000) { + override fun onTick(millisUntilFinished: Long) { + timeRemaining = (millisUntilFinished / 1000).toInt() + binding.tvTimer.text = "Resend available in 00:${String.format("%02d", timeRemaining)}" + if (timeRemaining % 5 == 0) { + Log.d(TAG, "Cooldown remaining: $timeRemaining seconds") + } + } + + override fun onFinish() { + Log.d(TAG, "Cooldown finished, enabling resend button") + binding.tvTimer.text = "You can now resend the code" + binding.tvResendOtp.isEnabled = true + binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1)) + timeRemaining = 0 + } + }.start() + } + + private fun observeRegistrationState() { + registerViewModel.message.observe(viewLifecycleOwner) { message -> + Log.d(TAG, "Message from server: $message") + // You can use the message here if needed, e.g., for showing in a specific UI element + // or for storing for later use + } + registerViewModel.registerState.observe(viewLifecycleOwner) { result -> + when (result) { + is Result.Loading -> { + binding.progressBar.visibility = View.VISIBLE + binding.btnVerify.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Registration: Success - ${result.data}") + // Don't hide progress bar or re-enable button yet + // We'll wait for login to complete + + // Don't show success toast yet - wait until address is added + Log.d("RegisterStep2Fragment", "Registration successful, waiting for login") + } + is Result.Error -> { + Log.e(TAG, "Registration: Error - ${result.exception.message}", result.exception) + binding.progressBar.visibility = View.GONE + binding.btnVerify.isEnabled = true + + // Show error message + Toast.makeText(requireContext(), "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + else -> { + Log.d(TAG, "Registration: Unknown state") + binding.progressBar.visibility = View.GONE + binding.btnVerify.isEnabled = true + } + } + } + } + + private fun observeLoginState() { + registerViewModel.loginState.observe(viewLifecycleOwner) { result -> + when (result) { + is Result.Loading -> { + // Keep showing progress + binding.progressBar.visibility = View.VISIBLE + binding.btnVerify.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Login: Success - token received") + binding.progressBar.visibility = View.GONE + binding.btnVerify.isEnabled = true + + // Save the token in fragment + val accessToken = result.data.accessToken + sessionManager.saveToken(accessToken) + Log.d(TAG, "Token saved to SessionManager: $accessToken") + + // Also save user ID if available in the login response +// result.data.?.let { userId -> +// sessionManager.saveUserId(userId) +// } + + Log.d(TAG, "Login successful, token saved: $accessToken") + + // Proceed to Step 3 + Log.d(TAG, "Proceeding to Step 3 after successful login") + (activity as? RegisterActivity)?.navigateToStep(3, null ) + } + is Result.Error -> { + Log.e(TAG, "Login: Error - ${result.exception.message}", result.exception) + binding.progressBar.visibility = View.GONE + binding.btnVerify.isEnabled = true + + // Show error message but continue to Step 3 anyway + Log.e(TAG, "Login failed but proceeding to Step 3", result.exception) + Toast.makeText(requireContext(), "Note: Auto-login failed, but registration was successful", Toast.LENGTH_SHORT).show() + + // Proceed to Step 3 + (activity as? RegisterActivity)?.navigateToStep(3, null) + } + else -> { + Log.d(TAG, "Login: Unknown state") + binding.progressBar.visibility = View.GONE + binding.btnVerify.isEnabled = true + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + countDownTimer?.cancel() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt new file mode 100644 index 0000000..e44392f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt @@ -0,0 +1,360 @@ +package com.alya.ecommerce_serang.ui.auth.fragments + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.OrderRepository +import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.databinding.FragmentRegisterStep3Binding +import com.alya.ecommerce_serang.ui.auth.LoginActivity +import com.alya.ecommerce_serang.ui.auth.RegisterActivity +import com.alya.ecommerce_serang.ui.order.address.CityAdapter +import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter +import com.alya.ecommerce_serang.ui.order.address.ViewState +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel +import com.google.android.material.progressindicator.LinearProgressIndicator + +class RegisterStep3Fragment : Fragment() { + private var _binding: FragmentRegisterStep3Binding? = null + private val binding get() = _binding!! + private lateinit var sessionManager: SessionManager + + private val defaultLatitude = -6.200000 + private val defaultLongitude = 106.816666 + + // In RegisterStep2Fragment AND RegisterStep3Fragment: + private val registerViewModel: RegisterViewModel by activityViewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getUnauthenticatedApiService() + val orderRepository = OrderRepository(apiService) + val userRepository = UserRepository(apiService) + RegisterViewModel(userRepository, orderRepository, requireContext()) + } + } + // For province and city selection + private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) } + private val cityAdapter by lazy { CityAdapter(requireContext()) } + + companion object { + private const val TAG = "RegisterStep3Fragment" + + fun newInstance() = RegisterStep3Fragment() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRegisterStep3Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sessionManager = SessionManager(requireContext()) + Log.d(TAG, "SessionManager initialized, token: ${sessionManager.getToken()}") + + // Set step progress and description + (activity as? RegisterActivity)?.let { + it.findViewById(R.id.registration_progress)?.progress = 33 + it.findViewById(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info" + it.findViewById(R.id.tv_step_description)?.text = + "Fill in your account and personal details to create your profile." + Log.d(TAG, "Step indicators updated to Step 1") + } + + // Get registered user data + val user = registerViewModel.registeredUser.value + Log.d(TAG, "Retrieved user data: ${user?.name}, ID: ${user?.id}") + + // Auto-fill recipient name and phone if available + user?.let { + binding.etNamaPenerima.setText(it.name) + binding.etNomorHp.setText(it.phone) + Log.d(TAG, "Auto-filled name: ${it.name}, phone: ${it.phone}") + } + + // Set up province and city dropdowns + setupAutoComplete() + + // Set up button listeners + binding.btnPrevious.setOnClickListener { + // Go back to the previous step + parentFragmentManager.popBackStack() + } + + binding.btnRegister.setOnClickListener { + submitAddress() + } + + // If user skips address entry +// binding.btnSkip.setOnClickListener { +// showRegistrationSuccess() +// } + + // Observe address submission state + observeAddressSubmissionState() + + // Load provinces + Log.d(TAG, "Requesting provinces data") + registerViewModel.getProvinces() + setupProvinceObserver() + setupCityObserver() + } + + private fun setupAutoComplete() { + // Same implementation as before + binding.autoCompleteProvinsi.setAdapter(provinceAdapter) + binding.autoCompleteKabupaten.setAdapter(cityAdapter) + + binding.autoCompleteProvinsi.setOnClickListener { + binding.autoCompleteProvinsi.showDropDown() + } + + binding.autoCompleteKabupaten.setOnClickListener { + if (cityAdapter.count > 0) { + Log.d(TAG, "City dropdown clicked, showing ${cityAdapter.count} cities") + binding.autoCompleteKabupaten.showDropDown() + } else { + Toast.makeText(requireContext(), "Pilih provinsi terlebih dahulu", Toast.LENGTH_SHORT).show() + } + } + + binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ -> + val provinceId = provinceAdapter.getProvinceId(position) + Log.d(TAG, "Province selected at position $position, ID: $provinceId") + + provinceId?.let { id -> + registerViewModel.selectedProvinceId = id + Log.d(TAG, "Requesting cities for province ID: $id") + registerViewModel.getCities(id) + binding.autoCompleteKabupaten.text.clear() + } + } + + binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ -> + val cityId = cityAdapter.getCityId(position) + Log.d(TAG, "City selected at position $position, ID: $cityId") + + cityId?.let { id -> + Log.d(TAG, "Selected city ID set to: $id") + registerViewModel.selectedCityId = id + } + } + } + + private fun setupProvinceObserver() { + // Same implementation as before + registerViewModel.provincesState.observe(viewLifecycleOwner) { state -> + when (state) { + is ViewState.Loading -> { + binding.progressBarProvinsi.visibility = View.VISIBLE + } + is ViewState.Success -> { + Log.d(TAG, "Provinces: Success - received ${state.data.size} provinces") + binding.progressBarProvinsi.visibility = View.GONE + if (state.data.isNotEmpty()) { + provinceAdapter.updateData(state.data) + } else { + showError("No provinces available") + } + } + is ViewState.Error -> { + Log.e(TAG, "Provinces: Error - ${state.message}") + binding.progressBarProvinsi.visibility = View.GONE + showError("Failed to load provinces: ${state.message}") + } + } + } + } + + private fun setupCityObserver() { + // Same implementation as before + registerViewModel.citiesState.observe(viewLifecycleOwner) { state -> + when (state) { + is ViewState.Loading -> { + binding.progressBarKabupaten.visibility = View.VISIBLE + } + is ViewState.Success -> { + Log.d(TAG, "Cities: Success - received ${state.data.size} cities") + binding.progressBarKabupaten.visibility = View.GONE + cityAdapter.updateData(state.data) + Log.d(TAG, "Updated city adapter with ${state.data.size} items") + } + is ViewState.Error -> { + Log.e(TAG, "Cities: Error - ${state.message}") + binding.progressBarKabupaten.visibility = View.GONE + showError("Failed to load cities: ${state.message}") + } + } + } + } + + private fun submitAddress() { + Log.d(TAG, "submitAddress called") + if (!validateAddressForm()) { + Log.w(TAG, "Address form validation failed") + return + } + + val user = registerViewModel.registeredUser.value + if (user == null) { + Log.e(TAG, "User data not available") + showError("User data not available. Please try again.") + return + } + + val userId = user.id + Log.d(TAG, "Using user ID: $userId") + + val street = binding.etDetailAlamat.text.toString().trim() + val subDistrict = binding.etKecamatan.text.toString().trim() + val postalCode = binding.etKodePos.text.toString().trim() + val recipient = binding.etNamaPenerima.text.toString().trim() + val phone = binding.etNomorHp.text.toString().trim() + + val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0 + val cityId = registerViewModel.selectedCityId?.toInt() ?: 0 + + Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode") + Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone") + Log.d(TAG, "Address data - ProvinceId: $provinceId, CityId: $cityId") + Log.d(TAG, "Address data - Lat: $defaultLatitude, Long: $defaultLongitude") + + // Create address request + val addressRequest = CreateAddressRequest( + lat = defaultLatitude, + long = defaultLongitude, + street = street, + subDistrict = subDistrict, + cityId = cityId, + provId = provinceId, + postCode = postalCode, + detailAddress = street, + userId = userId, + recipient = recipient, + phone = phone, + isStoreLocation = false + ) + + Log.d(TAG, "Address request created: $addressRequest") + + // Show loading + binding.progressBar.visibility = View.VISIBLE + binding.btnRegister.isEnabled = false +// binding.btnSkip.isEnabled = false + + // Submit address + registerViewModel.addAddress(addressRequest) + } + + private fun validateAddressForm(): Boolean { + val street = binding.etDetailAlamat.text.toString().trim() + val subDistrict = binding.etKecamatan.text.toString().trim() + val postalCode = binding.etKodePos.text.toString().trim() + val recipient = binding.etNamaPenerima.text.toString().trim() + val phone = binding.etNomorHp.text.toString().trim() + + val provinceId = registerViewModel.selectedProvinceId + val cityId = registerViewModel.selectedCityId + + Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode") + Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone") + Log.d(TAG, "Validating - ProvinceId: $provinceId, CityId: $cityId") + + + // Validate required fields + if (street.isBlank()) { + binding.etDetailAlamat.error = "Alamat tidak boleh kosong" + binding.etDetailAlamat.requestFocus() + return false + } + + if (recipient.isBlank()) { + binding.etNamaPenerima.error = "Nama penerima tidak boleh kosong" + binding.etNamaPenerima.requestFocus() + return false + } + + if (phone.isBlank()) { + binding.etNomorHp.error = "Nomor HP tidak boleh kosong" + binding.etNomorHp.requestFocus() + return false + } + + if (provinceId == null) { + showError("Pilih provinsi terlebih dahulu") + binding.autoCompleteProvinsi.requestFocus() + return false + } + + if (cityId == null) { + showError("Pilih kota/kabupaten terlebih dahulu") + binding.autoCompleteKabupaten.requestFocus() + return false + } + + return true + } + + private fun observeAddressSubmissionState() { + registerViewModel.addressSubmissionState.observe(viewLifecycleOwner) { state -> + when (state) { + is ViewState.Loading -> { + binding.progressBar.visibility = View.VISIBLE + binding.btnRegister.isEnabled = false +// binding.btnSkip.isEnabled = false + } + is ViewState.Success -> { + Log.d(TAG, "Address submission: Success - ${state.data}") + binding.progressBar.visibility = View.GONE + showRegistrationSuccess() + } + is ViewState.Error -> { + Log.e(TAG, "Address submission: Error - ${state.message}") + binding.progressBar.visibility = View.GONE + binding.btnRegister.isEnabled = true +// binding.btnSkip.isEnabled = true + showError("Failed to add address: ${state.message}") + } + } + } + } + + private fun showRegistrationSuccess() { + // Now we can show the success message for the overall registration process + Toast.makeText(requireContext(), "Registration completed successfully!", Toast.LENGTH_LONG).show() + + // Navigate to login screen + startActivity(Intent(requireContext(), LoginActivity::class.java)) + Log.d(TAG, "Navigating to LoginActivity") + activity?.finish() + } + + private fun showError(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +// +// // Data classes for province and city +// data class Province(val id: String, val name: String) +// data class City(val id: String, val name: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/RegisterViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/RegisterViewModel.kt index 34f75ae..9a47cc0 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/RegisterViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/RegisterViewModel.kt @@ -1,20 +1,49 @@ package com.alya.ecommerce_serang.utils.viewmodel +import android.content.Context import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq +import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse +import com.alya.ecommerce_serang.data.api.response.auth.User import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem +import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.ui.order.address.ViewState +import com.alya.ecommerce_serang.utils.SessionManager import kotlinx.coroutines.launch -class RegisterViewModel(private val repository: UserRepository) : ViewModel() { +class RegisterViewModel(private val repository: UserRepository, private val orderRepo: OrderRepository, private val context: Context) : ViewModel() { + private val _loginState = MutableLiveData>() + val loginState: LiveData> get() = _loginState + + // To track if user is authenticated + private val _isAuthenticated = MutableLiveData(false) + val isAuthenticated: LiveData = _isAuthenticated + + private var _lastCheckedField = MutableLiveData() + val lastCheckedField: String + get() = _lastCheckedField.value ?: "" + + private val _userData = MutableLiveData() + val userData: LiveData = _userData + + // Current step in the registration process + private val _currentStep = MutableLiveData(1) + val currentStep: LiveData = _currentStep // MutableLiveData for handling register state (Loading, Success, or Error) private val _registerState = MutableLiveData>() val registerState: LiveData> = _registerState @@ -30,19 +59,55 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() { private val _message = MutableLiveData() val message: LiveData = _message + private val _registeredUser = MutableLiveData() + val registeredUser: LiveData = _registeredUser + + // For address data + var selectedProvinceId: Int? = null + var selectedCityId: Int? = null + + // For provinces and cities + private val _provincesState = MutableLiveData>>() + val provincesState: LiveData>> = _provincesState + + private val _citiesState = MutableLiveData>>() + val citiesState: LiveData>> = _citiesState + + // For address submission + private val _addressSubmissionState = MutableLiveData>() + val addressSubmissionState: LiveData> = _addressSubmissionState + + private val sessionManager by lazy { SessionManager(context) } + + // For authenticated API calls + private fun getAuthenticatedApiService(): ApiService { + return ApiConfig.getApiService(sessionManager) + } + + fun updateUserData(updatedData: RegisterRequest) { + _userData.value = updatedData + } + + // Set current step + fun setStep(step: Int) { + _currentStep.value = step + } /** * Function to request OTP by sending an email to the API. * - It sets the OTP state to `Loading` before calling the repository. * - If successful, it updates `_message` with the response message and signals success. * - If an error occurs, it updates `_otpState` with `Result.Error` and logs the failure. */ + fun requestOtp(email: String) { viewModelScope.launch { _otpState.value = Result.Loading // Indicating API call in progress try { // Call the repository function to request OTP - val response: OtpResponse = repository.requestOtpRep(email) + val authenticatedApiService = getAuthenticatedApiService() + val authenticatedOrderRepo = UserRepository(authenticatedApiService) + val response: OtpResponse = authenticatedOrderRepo.requestOtpRep(email) // Log and store success message Log.d("RegisterViewModel", "OTP Response: ${response.message}") @@ -75,16 +140,43 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() { try { // Call repository function to register the user - val message = repository.registerUser(request) + val response: RegisterResponse = repository.registerUser(request) - // Store and display success message - _message.value = message - _registerState.value = Result.Success(message) // Store success result + Log.d(TAG, "Registration API call successful") + Log.d(TAG, "Response message: ${response.message}") + Log.d(TAG, "User ID received: ${response.user.id}") + Log.d(TAG, "User details - Name: ${response.user.name}, Email: ${response.user.email}, Username: ${response.user.username}") + + // Store the user data + _registeredUser.value = response.user + Log.d(TAG, "User data stored in ViewModel") + + // Store success message + _message.value = response.message + Log.d(TAG, "Success message stored: ${response.message}") + + _registerState.value = Result.Success(response.message) + + // Automatically login after successful registration + request.email?.let { email -> + + request.password?.let { password -> + Log.d(TAG, "Attempting auto-login with email: $email") + + login(email, password) + } + } } catch (exception: Exception) { + Log.e(TAG, "Registration failed with exception: ${exception.javaClass.simpleName}", exception) + Log.e(TAG, "Exception message: ${exception.message}") + Log.e(TAG, "Exception cause: ${exception.cause}") // Handle any errors and update state _registerState.value = Result.Error(exception) + _message.value = exception.localizedMessage ?: "Registration failed" + Log.d(TAG, "Error message stored: ${exception.localizedMessage ?: "Registration failed"}") + // Log the error for debugging Log.e("RegisterViewModel", "User registration failed", exception) @@ -92,7 +184,26 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() { } } + fun login(email: String, password: String) { + viewModelScope.launch { + _loginState.value = Result.Loading + try { + val result = repository.login(email, password) + _loginState.value = result + + // Update authentication status if login was successful + if (result is Result.Success) { + _isAuthenticated.value = true + } + } catch (exception: Exception) { + _loginState.value = Result.Error(exception) + Log.e("RegisterViewModel", "Login failed", exception) + } + } + } + fun checkValueReg(request: VerifRegisReq){ + _lastCheckedField.value = request.fieldRegis viewModelScope.launch { try { // Call the repository function to request OTP @@ -111,4 +222,97 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() { } } } + + fun getProvinces() { + _provincesState.value = ViewState.Loading + viewModelScope.launch { + try { + val result = repository.getListProvinces() + if (result?.provinces != null) { + _provincesState.postValue(ViewState.Success(result.provinces)) + Log.d(TAG, "Provinces loaded: ${result.provinces.size}") + } else { + _provincesState.postValue(ViewState.Error("Failed to load provinces")) + Log.e(TAG, "Province result was null or empty") + } + } catch (e: Exception) { + _provincesState.postValue(ViewState.Error(e.message ?: "Error loading provinces")) + Log.e(TAG, "Error fetching provinces", e) + } + } + } + + fun getCities(provinceId: Int) { + _citiesState.value = ViewState.Loading + viewModelScope.launch { + try { + + selectedProvinceId = provinceId + val result = repository.getListCities(provinceId) + result?.let { + _citiesState.postValue(ViewState.Success(it.cities)) + Log.d(TAG, "Cities loaded for province $provinceId: ${it.cities.size}") + } ?: run { + _citiesState.postValue(ViewState.Error("Failed to load cities")) + Log.e(TAG, "City result was null for province $provinceId") + } + } catch (e: Exception) { + _citiesState.postValue(ViewState.Error(e.message ?: "Error loading cities")) + Log.e(TAG, "Error fetching cities for province $provinceId", e) + } + } + } + + fun setSelectedProvinceId(id: Int) { + selectedProvinceId = id + } + + fun setSelectedCityId(id: Int) { + selectedCityId = id + } + + fun addAddress(request: CreateAddressRequest) { + Log.d(TAG, "Starting address submission process") + _addressSubmissionState.value = ViewState.Loading + viewModelScope.launch { + try { + val authenticatedApiService = getAuthenticatedApiService() + val authenticatedOrderRepo = OrderRepository(authenticatedApiService) + Log.d(TAG, "Calling repository.addAddress with request: $request") + val result = authenticatedOrderRepo.addAddress(request) + + when (result) { + is Result.Success -> { + val message = result.data.message + Log.d(TAG, "Address added successfully: $message") + _addressSubmissionState.postValue(ViewState.Success(message)) + } + is Result.Error -> { + val errorMsg = result.exception.message ?: "Unknown error" + Log.e(TAG, "Error from repository: $errorMsg", result.exception) + _addressSubmissionState.postValue(ViewState.Error(errorMsg)) + } + is Result.Loading -> { + Log.d(TAG, "Repository returned Loading state") + // We already set Loading at the beginning + } + } + } catch (e: Exception) { + Log.e(TAG, "Exception occurred during address submission", e) + val errorMessage = e.message ?: "Unknown error occurred" + Log.e(TAG, "Error message: $errorMessage") + + // Log the exception stack trace + e.printStackTrace() + + _addressSubmissionState.postValue(ViewState.Error(errorMessage)) + } + } + } + + companion object { + private const val TAG = "RegisterViewModel" + } + + //require auth } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml index 6485770..eac2bf7 100644 --- a/app/src/main/res/layout/activity_register.xml +++ b/app/src/main/res/layout/activity_register.xml @@ -1,256 +1,49 @@ - - + android:layout_marginTop="16dp" + app:trackThickness="8dp" /> - - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_register_step1.xml b/app/src/main/res/layout/fragment_register_step1.xml new file mode 100644 index 0000000..ba9fa9c --- /dev/null +++ b/app/src/main/res/layout/fragment_register_step1.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_register_step2.xml b/app/src/main/res/layout/fragment_register_step2.xml new file mode 100644 index 0000000..ca89814 --- /dev/null +++ b/app/src/main/res/layout/fragment_register_step2.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_register_step3.xml b/app/src/main/res/layout/fragment_register_step3.xml new file mode 100644 index 0000000..9d9bac9 --- /dev/null +++ b/app/src/main/res/layout/fragment_register_step3.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +