update addrses in register

This commit is contained in:
shaulascr
2025-05-20 08:32:33 +07:00
parent ae6ca21a98
commit 98e82cb6db
12 changed files with 1775 additions and 520 deletions

View File

@ -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,

View File

@ -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
)
): Parcelable

View File

@ -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()}")
}

View File

@ -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()
}
}

View File

@ -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<LinearProgressIndicator>(R.id.registration_progress)?.progress = 33
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info"
it.findViewById<TextView>(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
}
}

View File

@ -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<LinearProgressIndicator>(R.id.registration_progress)?.progress = 66
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 2: Verify Your Email"
it.findViewById<TextView>(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
}
}

View File

@ -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<LinearProgressIndicator>(R.id.registration_progress)?.progress = 33
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info"
it.findViewById<TextView>(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)
}

View File

@ -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<Result<LoginResponse>>()
val loginState: LiveData<Result<LoginResponse>> get() = _loginState
// To track if user is authenticated
private val _isAuthenticated = MutableLiveData<Boolean>(false)
val isAuthenticated: LiveData<Boolean> = _isAuthenticated
private var _lastCheckedField = MutableLiveData<String>()
val lastCheckedField: String
get() = _lastCheckedField.value ?: ""
private val _userData = MutableLiveData<RegisterRequest>()
val userData: LiveData<RegisterRequest> = _userData
// Current step in the registration process
private val _currentStep = MutableLiveData<Int>(1)
val currentStep: LiveData<Int> = _currentStep
// MutableLiveData for handling register state (Loading, Success, or Error)
private val _registerState = MutableLiveData<Result<String>>()
val registerState: LiveData<Result<String>> = _registerState
@ -30,19 +59,55 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
private val _registeredUser = MutableLiveData<User>()
val registeredUser: LiveData<User> = _registeredUser
// For address data
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
// For provinces and cities
private val _provincesState = MutableLiveData<ViewState<List<ProvincesItem>>>()
val provincesState: LiveData<ViewState<List<ProvincesItem>>> = _provincesState
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
// For address submission
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
val addressSubmissionState: LiveData<ViewState<String>> = _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
}

View File

@ -1,256 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.auth.RegisterActivity">
<LinearLayout
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/registration_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginVertical="16dp"
android:layout_marginHorizontal="16dp"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Buat Akun"
android:textSize="24sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/email"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_email"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/username"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/full_name"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_fullname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_fullname"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/password"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/confirm_password"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_confirmation_password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/birth_date"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"
style="@style/SharpedBorderStyleOutline"
app:endIconMode="custom"
app:endIconDrawable="@drawable/outline_calendar_today_24">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_birth_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Pilih tanggal"
android:focusable="false"
android:clickable="true"
android:minHeight="50dp"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/gender"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/et_gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_gender"
android:inputType="textFilter"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="@string/number_phone"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_number_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_number_phone"
android:inputType="phone"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_signup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/signup"
android:layout_marginTop="16dp"
app:cornerRadius="8dp"/>
app:trackThickness="8dp" />
<LinearLayout
<!-- Step Title -->
<TextView
android:id="@+id/tv_step_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:textAlignment="center"
android:textSize="20sp"
android:textStyle="bold" />
<!-- Step Description -->
<TextView
android:layout_width="wrap_content"
android:id="@+id/tv_step_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_account"/>
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:textAlignment="center" />
<TextView
android:id="@+id/tv_login_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login"
android:textColor="@color/blue1"
android:textStyle="bold"/>
</LinearLayout>
<!-- Fragment Container -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ProgressBar
android:id="@+id/progressBarOtp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center"/>
<!-- ProgressBar for Registration -->
<ProgressBar
android:id="@+id/progressBarRegister"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Account Information Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/email"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_email"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Username Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/username"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Password Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/password"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Confirm Password Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/confirm_password"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_confirmation_password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Phone Number Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/number_phone"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_number_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_number_phone"
android:inputType="phone"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Full Name Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/full_name"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_fullname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_fullname"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Birth Date Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="14sp"
android:text="@string/birth_date"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"
style="@style/SharpedBorderStyleOutline"
app:endIconMode="custom"
app:endIconDrawable="@drawable/outline_calendar_today_24">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_birth_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Pilih tanggal"
android:focusable="false"
android:clickable="true"
android:minHeight="50dp"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- &lt;!&ndash; Gender Field &ndash;&gt;-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="4dp"-->
<!-- android:fontFamily="@font/dmsans_medium"-->
<!-- android:textSize="14sp"-->
<!-- android:text="@string/gender"/>-->
<!-- <com.google.android.material.textfield.TextInputLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginBottom="12dp"-->
<!-- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">-->
<!-- <AutoCompleteTextView-->
<!-- android:id="@+id/et_gender"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:hint="@string/hint_gender"-->
<!-- android:inputType="textFilter"/>-->
<!-- </com.google.android.material.textfield.TextInputLayout>-->
<!-- Next Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Next"
android:layout_marginTop="16dp"
app:cornerRadius="8dp"/>
<!-- Login Alternative -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_account"/>
<TextView
android:id="@+id/tv_login_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login"
android:textColor="@color/blue1"
android:textStyle="bold"/>
</LinearLayout>
<!-- Progress Bar -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center"/>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- OTP Verification Image -->
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:src="@drawable/outline_notifications_24"
android:contentDescription="OTP Verification" />
<!-- OTP Input Field -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:text="Enter Verification Code"
android:textAlignment="center" />
<TextView
android:id="@+id/tv_email_sent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="We've sent a verification code to your email"
android:textAlignment="center" />
<!-- OTP Input Layout -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_otp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter OTP"
android:inputType="number"
android:textAlignment="center"
android:maxLength="6" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Verify Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_verify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Verify"
app:cornerRadius="8dp" />
<!-- Resend OTP -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Didn't receive the code? " />
<TextView
android:id="@+id/tv_resend_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Resend"
android:textColor="@color/blue1"
android:textStyle="bold" />
</LinearLayout>
<!-- Timer for resend cooldown -->
<TextView
android:id="@+id/tv_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Resend available in 00:30"
android:textAlignment="center"
android:visibility="visible" />
<!-- Progress Bar -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:visibility="gone" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/btn_register"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama Penerima"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/et_nama_penerima"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nama penerima"
android:inputType="textPersonName"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Nomor Hp"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/et_nomor_hp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nomor handphone aktif"
android:inputType="phone"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Detail Alamat"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/et_detail_alamat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:gravity="top"
android:hint="Isi detail alamat (nomor rumah, lantai, dll)"
android:inputType="textMultiLine"
android:lines="3"
android:padding="12dp"
android:textSize="14sp" />
<!-- Provinsi -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Provinsi"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Pilih Provinsi"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteProvinsi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progress_bar_provinsi"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- Kabupaten / Kota -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kabupaten / Kota"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Pilih Kabupaten / Kota"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteKabupaten"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progress_bar_kabupaten"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- Kecamatan / Desa -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kecamatan / Desa"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Isi Kecamatan / Desa"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_kecamatan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textSize="14sp"
android:inputType="textCapWords" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Kode Pos -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kode Pos"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/et_kode_pos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi kode pos"
android:inputType="number"
android:padding="12dp"
android:textSize="14sp" />
<!-- Navigation Button (Previous) -->
<Button
android:id="@+id/btn_previous"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/bg_button_outline"
android:text="Previous"
android:textAllCaps="false"
android:textColor="@color/blue1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
</LinearLayout>
</ScrollView>
<!-- Register Button -->
<Button
android:id="@+id/btn_register"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_margin="16dp"
android:background="@drawable/button_address_background"
android:text="@string/signup"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>