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 ( data class CreateAddressRequest (
@SerializedName("latitude") @SerializedName("latitude")
val lat: Double, val lat: Double? = null,
@SerializedName("longitude") @SerializedName("longitude")
val long: Double, val long: Double? = null,
@SerializedName("street") @SerializedName("street")
val street: String, val street: String,

View File

@ -1,7 +1,10 @@
package com.alya.ecommerce_serang.data.api.dto package com.alya.ecommerce_serang.data.api.dto
import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
@Parcelize
data class RegisterRequest ( data class RegisterRequest (
val name: String?, val name: String?,
val email: String?, val email: String?,
@ -15,4 +18,4 @@ data class RegisterRequest (
val image: String? = null, val image: String? = null,
val otp: 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.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse 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.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.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse 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 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 val response = apiService.register(request) // API call
if (response.isSuccessful) { if (response.isSuccessful) {
val responseBody = response.body() ?: throw Exception("Empty response body") 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 { } else {
throw Exception("Registration failed: ${response.errorBody()?.string()}") throw Exception("Registration failed: ${response.errorBody()?.string()}")
} }

View File

@ -1,49 +1,39 @@
package com.alya.ecommerce_serang.ui.auth package com.alya.ecommerce_serang.ui.auth
import android.app.DatePickerDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat 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.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.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.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding
import com.alya.ecommerce_serang.ui.MainActivity 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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class RegisterActivity : AppCompatActivity() { class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding private lateinit var binding: ActivityRegisterBinding
private lateinit var sessionManager: SessionManager 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{ private val registerViewModel: RegisterViewModel by viewModels{
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService() val apiService = ApiConfig.getUnauthenticatedApiService()
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService) val userRepository = UserRepository(apiService)
RegisterViewModel(userRepository) RegisterViewModel(userRepository, orderRepository, this)
} }
} }
@ -88,264 +78,27 @@ class RegisterActivity : AppCompatActivity() {
windowInsets windowInsets
} }
setupObservers() if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
// Set up field validations .replace(R.id.fragment_container, RegisterStep1Fragment.newInstance())
setupFieldValidations() .commit()
binding.btnSignup.setOnClickListener {
handleSignUp()
}
binding.tvLoginAlt.setOnClickListener {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
binding.etBirthDate.setOnClickListener {
showDatePicker()
} }
} }
private fun setupFieldValidations() { // Function to navigate to the next fragment
// Validate email when focus changes fun navigateToStep(step: Int, userData: RegisterRequest?) {
binding.etEmail.setOnFocusChangeListener { _, hasFocus -> val fragment = when (step) {
if (!hasFocus) { 1 -> RegisterStep1Fragment.newInstance()
val email = binding.etEmail.text.toString() 2 -> RegisterStep2Fragment.newInstance(userData)
if (email.isNotEmpty()) { 3 -> RegisterStep3Fragment.newInstance()
validateEmail(email, false) else -> null
}
}
} }
// Validate phone when focus changes fragment?.let {
binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus -> supportFragmentManager.beginTransaction()
if (!hasFocus) { .replace(R.id.fragment_container, it)
val phone = binding.etNumberPhone.text.toString() .addToBackStack(null)
if (phone.isNotEmpty()) { .commit()
validatePhone(phone, false)
}
}
} }
} }
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 package com.alya.ecommerce_serang.utils.viewmodel
import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq 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.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.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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository 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 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) // MutableLiveData for handling register state (Loading, Success, or Error)
private val _registerState = MutableLiveData<Result<String>>() private val _registerState = MutableLiveData<Result<String>>()
val registerState: LiveData<Result<String>> = _registerState val registerState: LiveData<Result<String>> = _registerState
@ -30,19 +59,55 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
private val _message = MutableLiveData<String>() private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message 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. * Function to request OTP by sending an email to the API.
* - It sets the OTP state to `Loading` before calling the repository. * - It sets the OTP state to `Loading` before calling the repository.
* - If successful, it updates `_message` with the response message and signals success. * - 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. * - If an error occurs, it updates `_otpState` with `Result.Error` and logs the failure.
*/ */
fun requestOtp(email: String) { fun requestOtp(email: String) {
viewModelScope.launch { viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress _otpState.value = Result.Loading // Indicating API call in progress
try { try {
// Call the repository function to request OTP // 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 and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.message}") Log.d("RegisterViewModel", "OTP Response: ${response.message}")
@ -75,16 +140,43 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
try { try {
// Call repository function to register the user // Call repository function to register the user
val message = repository.registerUser(request) val response: RegisterResponse = repository.registerUser(request)
// Store and display success message Log.d(TAG, "Registration API call successful")
_message.value = message Log.d(TAG, "Response message: ${response.message}")
_registerState.value = Result.Success(message) // Store success result 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) { } 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 // Handle any errors and update state
_registerState.value = Result.Error(exception) _registerState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Registration failed" _message.value = exception.localizedMessage ?: "Registration failed"
Log.d(TAG, "Error message stored: ${exception.localizedMessage ?: "Registration failed"}")
// Log the error for debugging // Log the error for debugging
Log.e("RegisterViewModel", "User registration failed", exception) 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){ fun checkValueReg(request: VerifRegisReq){
_lastCheckedField.value = request.fieldRegis
viewModelScope.launch { viewModelScope.launch {
try { try {
// Call the repository function to request OTP // 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"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.auth.RegisterActivity"> tools:context=".ui.auth.RegisterActivity">
<LinearLayout <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/registration_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginVertical="16dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:padding="16dp"> android:layout_marginTop="16dp"
app:trackThickness="8dp" />
<TextView <!-- Step Title -->
android:layout_width="match_parent" <TextView
android:layout_height="wrap_content" android:id="@+id/tv_step_title"
android:text="Buat Akun" android:layout_width="match_parent"
android:textSize="24sp" android:layout_height="wrap_content"
android:textStyle="bold" android:layout_marginHorizontal="16dp"
android:textAlignment="center" android:layout_marginTop="16dp"
android:layout_marginBottom="24dp"/> android:textAlignment="center"
<TextView android:textSize="20sp"
android:layout_width="match_parent" android:textStyle="bold" />
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 <!-- Step Description -->
android:layout_width="match_parent" <TextView
android:layout_height="wrap_content" android:id="@+id/tv_step_description"
android:layout_marginBottom="12dp" android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:textAlignment="center" />
<com.google.android.material.textfield.TextInputEditText <!-- Fragment Container -->
android:id="@+id/et_email" <FrameLayout
android:layout_width="match_parent" android:id="@+id/fragment_container"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:hint="@string/hint_email" android:layout_height="0dp"
android:inputType="textEmailAddress"/> android:layout_weight="1" />
</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 </LinearLayout>
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"/>
<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>
<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>

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>