feat: implement user authentication and profile display
This commit is contained in:
parent
27bc8f4100
commit
39224f66c4
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.core.utils
|
||||||
|
|
||||||
|
sealed class ResultState<out T> {
|
||||||
|
data object Idle : ResultState<Nothing>()
|
||||||
|
data object Loading : ResultState<Nothing>()
|
||||||
|
data class Success<T>(val data: T?) : ResultState<T>()
|
||||||
|
data class Error(val message: String) : ResultState<Nothing>()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.exceptions.JWTDecodeException
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class UserPreferences(private val context: Context) {
|
||||||
|
companion object {
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
|
||||||
|
private val TOKEN_KEY = stringPreferencesKey("auth_token")
|
||||||
|
private val JWT_TOKEN = stringPreferencesKey("jwt_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow untuk token biasa
|
||||||
|
val tokenFlow: Flow<String?> = context.dataStore.data.map { preferences ->
|
||||||
|
preferences[TOKEN_KEY]
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveToken(token: String) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[TOKEN_KEY] = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearToken() {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences.remove(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow untuk JWT token
|
||||||
|
val jwtTokenFlow: Flow<String?> = context.dataStore.data.map { preferences ->
|
||||||
|
preferences[JWT_TOKEN]
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveJwtToken(token: String) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[JWT_TOKEN] = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearJwtToken() {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences.remove(JWT_TOKEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk cek apakah user sudah login berdasarkan JWT token valid dan belum expired
|
||||||
|
fun isUserLoggedIn(): Flow<Boolean> = jwtTokenFlow.map { token ->
|
||||||
|
!token.isNullOrEmpty() && !isTokenExpired(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi private untuk cek expired JWT token menggunakan library Auth0 java-jwt
|
||||||
|
private fun isTokenExpired(token: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val jwt = JWT.decode(token)
|
||||||
|
val expiresAt: Date? = jwt.expiresAt
|
||||||
|
expiresAt?.let { Date().after(it) } != false
|
||||||
|
} catch (e: JWTDecodeException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data.model
|
||||||
|
|
||||||
|
data class ErrorLoginResponse(
|
||||||
|
val errors: Int,
|
||||||
|
val message: String,
|
||||||
|
val statusCode: Int
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data.model
|
||||||
|
|
||||||
|
data class LoginResponse(
|
||||||
|
val data: Token?,
|
||||||
|
val message: String?,
|
||||||
|
val statusCode: Int?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data.model
|
||||||
|
|
||||||
|
data class Token(
|
||||||
|
val jwtToken: String?,
|
||||||
|
val token: String?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data.model
|
||||||
|
|
||||||
|
data class UserProfile(
|
||||||
|
val name: String?,
|
||||||
|
val username: String?,
|
||||||
|
val email: String?,
|
||||||
|
val phone: String? = null
|
||||||
|
)
|
||||||
|
|
@ -2,9 +2,10 @@ package com.syaroful.agrilinkvocpro.ui.screen.login
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -13,9 +14,17 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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
|
||||||
|
|
@ -26,19 +35,32 @@ 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.textTheme
|
import com.syaroful.agrilinkvocpro.core.components.textTheme
|
||||||
|
import com.syaroful.agrilinkvocpro.core.utils.ResultState
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
|
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
|
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen() {
|
fun LoginScreen(
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
|
loginViewModel: LoginViewModel,
|
||||||
|
onLoginSuccess: () -> Unit,
|
||||||
|
onNavigateToRegister: () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val loginState by loginViewModel.loginState.collectAsState()
|
||||||
|
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(paddingValues)
|
.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.greenhouse_banner),
|
painter = painterResource(id = R.drawable.greenhouse_banner),
|
||||||
|
|
@ -48,46 +70,72 @@ fun LoginScreen() {
|
||||||
.align(Alignment.CenterHorizontally),
|
.align(Alignment.CenterHorizontally),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = "Halo! yuk masuk ke dalam akunmu",
|
text = "Halo! yuk masuk ke dalam akunmu",
|
||||||
style = textTheme.titleSmall.copy(color = DarkGrey),
|
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
hint = "Username", keyboardType = KeyboardType.Email, leadingIcon = painterResource(
|
hint = "Username",
|
||||||
|
keyboardType = KeyboardType.Email,
|
||||||
|
leadingIcon = painterResource(
|
||||||
R.drawable.icon_email
|
R.drawable.icon_email
|
||||||
)
|
),
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it }
|
||||||
)
|
)
|
||||||
AppPasswordField(
|
AppPasswordField(
|
||||||
hint = "Password",
|
hint = "Password",
|
||||||
keyboardType = KeyboardType.Password
|
keyboardType = KeyboardType.Password,
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it }
|
||||||
)
|
)
|
||||||
AppButton(
|
AppButton(
|
||||||
label = "Login"
|
label = if (loginState is ResultState.Loading) "Memuat..." else "Login",
|
||||||
) { }
|
isEnable = loginState !is ResultState.Loading,
|
||||||
|
) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
loginViewModel.login(username = username, password = password)
|
||||||
|
|
||||||
|
}
|
||||||
|
// Error Message
|
||||||
|
if (loginState is ResultState.Error) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
text = (loginState as ResultState.Error).message,
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Belum punya akun? ",
|
text = "Belum punya akun? ",
|
||||||
style = textTheme.titleSmall.copy(color = DarkGrey),
|
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.clickable { onNavigateToRegister() },
|
||||||
text = "Daftar disini",
|
text = "Daftar disini",
|
||||||
style = textTheme.titleSmall.copy(color = MainGreen),
|
style = textTheme.titleSmall.copy(color = MainGreen),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Navigate to Home if Success
|
||||||
|
LaunchedEffect(loginState) {
|
||||||
|
if (loginState is ResultState.Success<*>) {
|
||||||
|
onLoginSuccess()
|
||||||
|
loginViewModel.resetLoginState()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +144,10 @@ fun LoginScreen() {
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreenPreview() {
|
fun LoginScreenPreview() {
|
||||||
AgrilinkVocproTheme {
|
AgrilinkVocproTheme {
|
||||||
LoginScreen()
|
LoginScreen(
|
||||||
|
loginViewModel = koinViewModel(),
|
||||||
|
onLoginSuccess = {},
|
||||||
|
onNavigateToRegister = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.ui.screen.login
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.syaroful.agrilinkvocpro.core.utils.ResultState
|
||||||
|
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
||||||
|
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.data.repository.AuthRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LoginViewModel(
|
||||||
|
private val repository: AuthRepository,
|
||||||
|
private val userPreferences: UserPreferences
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _loginState = MutableStateFlow<ResultState<LoginResponse>>(ResultState.Idle)
|
||||||
|
val loginState: StateFlow<ResultState<LoginResponse>> = _loginState
|
||||||
|
|
||||||
|
fun login(username: String, password: String) {
|
||||||
|
_loginState.value = ResultState.Loading
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = repository.login(username, password)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val token = response.body()?.data?.token
|
||||||
|
val jwtToken = response.body()?.data?.jwtToken
|
||||||
|
if (token != null && jwtToken != null) {
|
||||||
|
userPreferences.saveToken(token)
|
||||||
|
userPreferences.saveJwtToken(jwtToken)
|
||||||
|
_loginState.value = ResultState.Success(response.body())
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val errorMessage = response.body()?.message ?: "Login Failed: Token is null"
|
||||||
|
_loginState.value = ResultState.Error(errorMessage)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string()
|
||||||
|
val errorMessage = if (errorBody != null) {
|
||||||
|
// Assuming errorBody is a JSON string with a "message" key
|
||||||
|
org.json.JSONObject(errorBody)
|
||||||
|
.optString("message", "Login Failed: ${response.code()}")
|
||||||
|
} else {
|
||||||
|
"Login Failed: ${response.code()}"
|
||||||
|
}
|
||||||
|
_loginState.value = ResultState.Error(errorMessage)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val errorMessage = mapToUserFriendlyError(e)
|
||||||
|
_loginState.value = ResultState.Error(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetLoginState() {
|
||||||
|
_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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.syaroful.agrilinkvocpro.ui.screen.profile
|
package com.syaroful.agrilinkvocpro.ui.screen.profile
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -14,29 +13,48 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.syaroful.agrilinkvocpro.R
|
import com.syaroful.agrilinkvocpro.R
|
||||||
import com.syaroful.agrilinkvocpro.core.components.textTheme
|
import com.syaroful.agrilinkvocpro.core.components.textTheme
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
|
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
|
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
|
||||||
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen() {
|
fun ProfileScreen(
|
||||||
|
profileViewModel: ProfileViewModel = koinViewModel(),
|
||||||
|
onNavigateToLogin: () -> Unit,
|
||||||
|
) {
|
||||||
|
val profileState by profileViewModel.profileState.collectAsState()
|
||||||
|
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(profileState) {
|
||||||
|
profileViewModel.loadUserProfile()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|
@ -76,19 +94,19 @@ fun ProfileScreen() {
|
||||||
) {
|
) {
|
||||||
RowItemWidget(
|
RowItemWidget(
|
||||||
label = "Nama",
|
label = "Nama",
|
||||||
value = "Muhamad Syaroful Anam"
|
value = profileState?.name ?: "-"
|
||||||
)
|
)
|
||||||
RowItemWidget(
|
RowItemWidget(
|
||||||
label = "Username",
|
label = "Username",
|
||||||
value = "syaroful"
|
value = profileState?.username ?: "-"
|
||||||
)
|
)
|
||||||
RowItemWidget(
|
RowItemWidget(
|
||||||
label = "Email",
|
label = "Email",
|
||||||
value = "syaroful@gamil.com"
|
value = profileState?.email ?: "-"
|
||||||
)
|
)
|
||||||
RowItemWidget(
|
RowItemWidget(
|
||||||
label = "No. Telepon",
|
label = "No. Telepon",
|
||||||
value = "08123456789"
|
value = profileState?.phone ?: "-"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(text = "Informasi", style = textTheme.titleMedium)
|
Text(text = "Informasi", style = textTheme.titleMedium)
|
||||||
|
|
@ -117,10 +135,43 @@ fun ProfileScreen() {
|
||||||
RowItemWidgetWithIcon(
|
RowItemWidgetWithIcon(
|
||||||
label = "Log Out",
|
label = "Log Out",
|
||||||
icon = painterResource(id = R.drawable.ic_out)
|
icon = painterResource(id = R.drawable.ic_out)
|
||||||
)
|
) {
|
||||||
|
showLogoutDialog = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showLogoutDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLogoutDialog = false },
|
||||||
|
title = { Text("Logout") },
|
||||||
|
text = { Text("Apakah Anda yakin ingin logout?") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
profileViewModel.logout {
|
||||||
|
onNavigateToLogin()
|
||||||
|
}
|
||||||
|
showLogoutDialog = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = Color.Red.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Logout")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showLogoutDialog = false },
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = Color.Gray
|
||||||
|
)
|
||||||
|
) { Text("Batal") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -161,13 +212,4 @@ private fun RowItemWidget(
|
||||||
Text(text = label)
|
Text(text = label)
|
||||||
Text(text = value, color = LightGrey)
|
Text(text = value, color = LightGrey)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, name = "Light Mode")
|
|
||||||
@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
fun ProfilePreview() {
|
|
||||||
AgrilinkVocproTheme {
|
|
||||||
ProfileScreen()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.ui.screen.profile
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
||||||
|
import com.syaroful.agrilinkvocpro.data.model.UserProfile
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private const val TAG = "ProfileViewModel"
|
||||||
|
|
||||||
|
class ProfileViewModel(
|
||||||
|
private val userPreferences: UserPreferences
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _profileState = MutableStateFlow<UserProfile?>(null)
|
||||||
|
val profileState: StateFlow<UserProfile?> = _profileState
|
||||||
|
|
||||||
|
fun loadUserProfile() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val token = userPreferences.jwtTokenFlow.first()
|
||||||
|
|
||||||
|
if (!token.isNullOrEmpty()) {
|
||||||
|
Log.d(TAG, "loadUserProfile: $token")
|
||||||
|
try {
|
||||||
|
val jwt = JWT.decode(token)
|
||||||
|
val userMap = jwt.getClaim("user").asMap()
|
||||||
|
val username = userMap["username"] as? String
|
||||||
|
val email = userMap["email"] as? String
|
||||||
|
val fullname = userMap["fullname"] as? String
|
||||||
|
|
||||||
|
Log.d(TAG, "loadUserProfile: $username")
|
||||||
|
|
||||||
|
val profile =
|
||||||
|
UserProfile(name = fullname, username = username, email = email)
|
||||||
|
_profileState.value = profile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "loadUserProfile: ${e.message}")
|
||||||
|
_profileState.value = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "loadUserProfile: null")
|
||||||
|
_profileState.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout(onLogoutCompleted: () -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
userPreferences.clearToken()
|
||||||
|
userPreferences.clearJwtToken()
|
||||||
|
}
|
||||||
|
onLogoutCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.ui.screen.register
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.syaroful.agrilinkvocpro.R
|
||||||
|
import com.syaroful.agrilinkvocpro.core.components.AppButton
|
||||||
|
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
|
||||||
|
import com.syaroful.agrilinkvocpro.core.components.AppTextField
|
||||||
|
import com.syaroful.agrilinkvocpro.core.components.textTheme
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RegisterScreen(
|
||||||
|
onLoginSuccess: () -> Unit,
|
||||||
|
onNavigateToLogin: () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var email by remember { mutableStateOf("") }
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var phone by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.greenhouse_banner),
|
||||||
|
contentDescription = "Banner",
|
||||||
|
modifier = Modifier
|
||||||
|
.height(180.dp)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = "Halo! yuk masuk ke dalam akunmu",
|
||||||
|
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
AppTextField(
|
||||||
|
hint = "Username",
|
||||||
|
keyboardType = KeyboardType.Email,
|
||||||
|
leadingIcon = painterResource(
|
||||||
|
R.drawable.icon_email
|
||||||
|
),
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it }
|
||||||
|
)
|
||||||
|
AppPasswordField(
|
||||||
|
hint = "Password",
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it }
|
||||||
|
)
|
||||||
|
AppButton(
|
||||||
|
label = "Daftar",
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Error Message
|
||||||
|
// if (loginState is ResultState.Error) {
|
||||||
|
// Text(
|
||||||
|
// modifier = Modifier.padding(16.dp),
|
||||||
|
// text = (loginState as ResultState.Error).message,
|
||||||
|
// color = Color.Red
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Navigate to Home if Success
|
||||||
|
// LaunchedEffect(loginState) {
|
||||||
|
// if (loginState is ResultState.Success<*>) {
|
||||||
|
// onLoginSuccess()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Sudah punya akun? ",
|
||||||
|
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.clickable { onNavigateToLogin() },
|
||||||
|
text = "Masuk",
|
||||||
|
style = textTheme.titleSmall.copy(color = MainGreen),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.ui.screen.splash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.syaroful.agrilinkvocpro.R
|
||||||
|
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.DarkGreen
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplashScreen(
|
||||||
|
userPreferences: UserPreferences,
|
||||||
|
onNavigate: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val isLoggedIn by userPreferences.isUserLoggedIn()
|
||||||
|
.collectAsState(initial = false)
|
||||||
|
|
||||||
|
LaunchedEffect(isLoggedIn) {
|
||||||
|
delay(1000)
|
||||||
|
if (isLoggedIn) {
|
||||||
|
onNavigate("home")
|
||||||
|
} else {
|
||||||
|
onNavigate("login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(DarkGreen)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(top = 100.dp)
|
||||||
|
.blur(radius = 150.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||||
|
.width(120.dp)
|
||||||
|
.height(120.dp)
|
||||||
|
.background(color = MainGreen)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(bottom = 100.dp)
|
||||||
|
.blur(radius = 150.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||||
|
.width(180.dp)
|
||||||
|
.height(180.dp)
|
||||||
|
.background(color = MainGreen)
|
||||||
|
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(120.dp),
|
||||||
|
painter = painterResource(id = R.drawable.app_logo),
|
||||||
|
contentDescription = "App Logo"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user