feat: implement register feature

This commit is contained in:
Cutiful 2025-07-05 11:26:07 +07:00
parent 90df005a3f
commit fc9c9614ba
7 changed files with 88 additions and 34 deletions

View File

@ -0,0 +1,6 @@
package com.syaroful.agrilinkvocpro.data.model
data class RegisterResponse(
val message: String?,
val success: Boolean?
)

View File

@ -5,6 +5,7 @@ import com.syaroful.agrilinkvocpro.presentation.screen.home.DynamicModuleViewMod
import com.syaroful.agrilinkvocpro.presentation.screen.home.HomeViewModel import com.syaroful.agrilinkvocpro.presentation.screen.home.HomeViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.login.LoginViewModel import com.syaroful.agrilinkvocpro.presentation.screen.login.LoginViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.profile.ProfileViewModel import com.syaroful.agrilinkvocpro.presentation.screen.profile.ProfileViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.register.RegisterViewModel
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
@ -14,4 +15,5 @@ val viewModelModule = module {
viewModel { ProfileViewModel(get()) } viewModel { ProfileViewModel(get()) }
viewModel { HomeViewModel(get(), get()) } viewModel { HomeViewModel(get(), get()) }
viewModel { DetailViewModel(get(), get()) } viewModel { DetailViewModel(get(), get()) }
viewModel { RegisterViewModel(get()) }
} }

View File

@ -55,7 +55,7 @@ fun SetupNavigation(
} }
composable("register") { composable("register") {
RegisterScreen( RegisterScreen(
onLoginSuccess = { onRegisterSuccess = {
navController.navigate("login") { navController.navigate("login") {
popUpTo("login") { inclusive = true } popUpTo("login") { inclusive = true }
} }

View File

@ -3,6 +3,7 @@ package com.syaroful.agrilinkvocpro.presentation.screen.login
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.core.utils.ResultState import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.data.UserPreferences import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.data.model.LoginResponse import com.syaroful.agrilinkvocpro.data.model.LoginResponse
import com.syaroful.agrilinkvocpro.data.repository.AuthRepository import com.syaroful.agrilinkvocpro.data.repository.AuthRepository
@ -55,24 +56,4 @@ class LoginViewModel(
fun resetLoginState() { fun resetLoginState() {
_loginState.value = ResultState.Idle _loginState.value = ResultState.Idle
} }
private fun mapToUserFriendlyError(e: Exception): String {
return when (e) {
is java.net.UnknownHostException -> "Tidak dapat terhubung ke server. Periksa koneksi internet Anda."
is java.net.SocketTimeoutException -> "Waktu koneksi habis. Mohon coba lagi."
is java.io.IOException -> "Terjadi kesalahan jaringan. Silakan cek koneksi Anda."
is retrofit2.HttpException -> {
// Kamu bisa cek kode HTTP di sini juga kalau mau
when (e.code()) {
401 -> "Akses ditolak. Silakan periksa kredensial login Anda."
403 -> "Anda tidak memiliki izin untuk mengakses ini."
404 -> "Data tidak ditemukan."
500 -> "Terjadi kesalahan pada server. Silakan coba lagi nanti."
else -> "Terjadi kesalahan. Kode: ${e.code()}"
}
}
else -> "Terjadi kesalahan. Silakan coba lagi."
}
}
} }

View File

@ -12,16 +12,25 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -30,23 +39,43 @@ import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.AppButton import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.core.components.CustomSnackBarComponent
import com.syaroful.agrilinkvocpro.core.components.textTheme import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.presentation.theme.DarkGrey import com.syaroful.agrilinkvocpro.presentation.theme.DarkGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun RegisterScreen( fun RegisterScreen(
onLoginSuccess: () -> Unit, registerViewModel: RegisterViewModel = koinViewModel(),
onRegisterSuccess: () -> Unit,
onNavigateToLogin: () -> Unit, onNavigateToLogin: () -> Unit,
) { ) {
val signupState by registerViewModel.signupState.collectAsState()
val focusManager = LocalFocusManager.current
val snackBarHostState = remember { SnackbarHostState() }
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") } var email by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") } var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var phone by remember { mutableStateOf("") }
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackBarHostState) { data ->
CustomSnackBarComponent(
modifier = Modifier.padding(16.dp),
message = data.visuals.message,
icon = if (data.visuals.message.contains("Failed")) Icons.Default.Clear else Icons.Default.Check
)
}
},
modifier = Modifier.fillMaxSize()
) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
@ -108,8 +137,18 @@ fun RegisterScreen(
) )
AppButton( AppButton(
label = "Daftar", label = if (signupState is ResultState.Loading) "Memuat..." else "Daftar",
isEnable = signupState !is ResultState.Loading,
) { ) {
focusManager.clearFocus()
registerViewModel.signup(name, username, email, password)
}
if (signupState is ResultState.Error) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = (signupState as ResultState.Error).message,
color = Color.Red
)
} }
Row( Row(
@ -128,6 +167,17 @@ fun RegisterScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
LaunchedEffect(signupState) {
if (signupState is ResultState.Success<*>) {
snackBarHostState.showSnackbar(message = "Pendaftaran Akun Berhasil")
// Navigate to Home if Success
onRegisterSuccess()
registerViewModel.resetRegisterState()
} else if (signupState is ResultState.Error) {
snackBarHostState.showSnackbar(message = (signupState as ResultState.Error).message)
}
}
} }
} }
} }

View File

@ -3,26 +3,39 @@ package com.syaroful.agrilinkvocpro.presentation.screen.register
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.core.utils.ResultState import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.data.model.LoginResponse import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.data.model.RegisterResponse
import com.syaroful.agrilinkvocpro.data.repository.AuthRepository import com.syaroful.agrilinkvocpro.data.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RegisterViewModel( class RegisterViewModel(
authRepository: AuthRepository, private val authRepository: AuthRepository,
) : ViewModel() { ) : ViewModel() {
private val _signupState = MutableStateFlow<ResultState<LoginResponse>>(ResultState.Idle) private val _signupState = MutableStateFlow<ResultState<RegisterResponse>>(ResultState.Idle)
val signupState: StateFlow<ResultState<LoginResponse>> = _signupState val signupState: StateFlow<ResultState<RegisterResponse>> = _signupState
fun signup(name: String, username: String, email: String, password: String) { fun signup(name: String, username: String, email: String, password: String) {
_signupState.value = ResultState.Loading _signupState.value = ResultState.Loading
viewModelScope.launch { viewModelScope.launch {
try { try {
val response = authRepository.register(username, password, email, name)
if (response.isSuccessful) {
_signupState.value = ResultState.Success(response.body())
} else {
val errorMessage = response.body()?.message ?: "Register Failed"
_signupState.value = ResultState.Error(errorMessage)
}
} catch (e: Exception) { } catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_signupState.value = ResultState.Error(errorMessage)
}
}
}
fun resetRegisterState(){
_signupState.value = ResultState.Idle
} }
} }
}
}

View File

@ -1,9 +1,11 @@
package com.syaroful.agrilinkvocpro.control_feature.di package com.syaroful.agrilinkvocpro.control_feature.di
import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel
import com.syaroful.agrilinkvocpro.control_feature.presentation.history.ControlHistoryViewModel
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
viewModel { ControlViewModel(get(), get()) } viewModel { ControlViewModel(get(), get()) }
viewModel { ControlHistoryViewModel(get(), get()) }
} }