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 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.Arrangement
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.AppTextField
|
||||
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.DarkGrey
|
||||
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
fun LoginScreen() {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
|
||||
fun LoginScreen(
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.padding(innerPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.greenhouse_banner),
|
||||
|
|
@ -48,46 +70,72 @@ fun LoginScreen() {
|
|||
.align(Alignment.CenterHorizontally),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Halo! yuk masuk ke dalam akunmu",
|
||||
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AppTextField(
|
||||
hint = "Username", keyboardType = KeyboardType.Email, leadingIcon = painterResource(
|
||||
hint = "Username",
|
||||
keyboardType = KeyboardType.Email,
|
||||
leadingIcon = painterResource(
|
||||
R.drawable.icon_email
|
||||
)
|
||||
),
|
||||
value = username,
|
||||
onValueChange = { username = it }
|
||||
)
|
||||
AppPasswordField(
|
||||
hint = "Password",
|
||||
keyboardType = KeyboardType.Password
|
||||
keyboardType = KeyboardType.Password,
|
||||
value = password,
|
||||
onValueChange = { password = it }
|
||||
)
|
||||
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))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Belum punya akun? ",
|
||||
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.clickable { onNavigateToRegister() },
|
||||
text = "Daftar disini",
|
||||
style = textTheme.titleSmall.copy(color = MainGreen),
|
||||
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
|
||||
fun LoginScreenPreview() {
|
||||
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
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -14,29 +13,48 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.syaroful.agrilinkvocpro.R
|
||||
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.MainGreen
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@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(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
|
@ -76,19 +94,19 @@ fun ProfileScreen() {
|
|||
) {
|
||||
RowItemWidget(
|
||||
label = "Nama",
|
||||
value = "Muhamad Syaroful Anam"
|
||||
value = profileState?.name ?: "-"
|
||||
)
|
||||
RowItemWidget(
|
||||
label = "Username",
|
||||
value = "syaroful"
|
||||
value = profileState?.username ?: "-"
|
||||
)
|
||||
RowItemWidget(
|
||||
label = "Email",
|
||||
value = "syaroful@gamil.com"
|
||||
value = profileState?.email ?: "-"
|
||||
)
|
||||
RowItemWidget(
|
||||
label = "No. Telepon",
|
||||
value = "08123456789"
|
||||
value = profileState?.phone ?: "-"
|
||||
)
|
||||
}
|
||||
Text(text = "Informasi", style = textTheme.titleMedium)
|
||||
|
|
@ -117,10 +135,43 @@ fun ProfileScreen() {
|
|||
RowItemWidgetWithIcon(
|
||||
label = "Log 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
|
||||
|
|
@ -161,13 +212,4 @@ private fun RowItemWidget(
|
|||
Text(text = label)
|
||||
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