Create new user registration processes
This commit is contained in:
parent
149ce13e3e
commit
4847c1bb21
32 changed files with 581 additions and 175 deletions
|
@ -31,12 +31,12 @@ protobuf {
|
|||
|
||||
android {
|
||||
namespace = "ing.bikeshedengineer.debtpirate"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ing.bikeshedengineer.debtpirate"
|
||||
minSdk = 33
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
|
|
|
@ -16,15 +16,11 @@ import androidx.navigation.compose.rememberNavController
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import ing.bikeshedengineer.debtpirate.auth.presentation.login.LoginScreen
|
||||
import ing.bikeshedengineer.debtpirate.auth.presentation.register.RegistrationScreen
|
||||
import ing.bikeshedengineer.debtpirate.domain.usecase.pref.UpdateCurrentRouteUseCase
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Destination
|
||||
import ing.bikeshedengineer.debtpirate.navigation.NavigationAction
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Navigator
|
||||
import ing.bikeshedengineer.debtpirate.theme.DebtPirateTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,33 +29,12 @@ class MainActivity : ComponentActivity() {
|
|||
@Inject
|
||||
lateinit var navigator: Navigator
|
||||
|
||||
@Inject
|
||||
lateinit var updateCurrentRoute: UpdateCurrentRouteUseCase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val navController = rememberNavController()
|
||||
|
||||
ObserveAsEvents(navigator.navigationActions) { action ->
|
||||
when (action) {
|
||||
is NavigationAction.Navigate -> {
|
||||
navController.navigate(action.destination) {
|
||||
val scope = CoroutineScope(context = Dispatchers.IO)
|
||||
scope.launch {
|
||||
updateCurrentRoute(action.destination)
|
||||
}
|
||||
action.navOptions(this)
|
||||
}
|
||||
}
|
||||
|
||||
NavigationAction.NavigateUp -> {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DebtPirateTheme {
|
||||
NavHost(navController = navController, startDestination = Destination.AuthGraph) {
|
||||
navigation<Destination.AuthGraph>(startDestination = Destination.RegistrationScreen) {
|
||||
|
@ -73,7 +48,7 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> ObserveAsEvents(flow: Flow<T>, onEvent: (T) -> Unit) {
|
||||
private fun <T> _ObserveAsEvents(flow: Flow<T>, onEvent: (T) -> Unit) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
LaunchedEffect(lifecycleOwner.lifecycle) {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.data.remote.endpoint
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthEndpoint {
|
||||
@POST("auth/login")
|
||||
suspend fun submitLoginRequest(@Body credentials: AuthLoginRequest): Response<ApiResponse<AuthLoginResponse>>
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.data.remote.model
|
||||
|
||||
data class AuthLoginRequest(val emailAddress: String, val password: String)
|
|
@ -1,9 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.data.remote.model
|
||||
|
||||
data class AuthLoginResponse(
|
||||
val userId: Int,
|
||||
val session: AuthLoginTokenData,
|
||||
val auth: AuthLoginTokenData
|
||||
)
|
||||
|
||||
data class AuthLoginTokenData(val token: String, val expiresAt: String)
|
|
@ -1,8 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.data.remote.repository
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse
|
||||
|
||||
interface AuthRepository {
|
||||
suspend fun submitLoginRequest(credentials: AuthLoginRequest): AuthLoginResponse
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.repository.AuthRepositoryImpl
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
object AuthDiModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideAuthRepository(httpClient: Retrofit): AuthRepository {
|
||||
return AuthRepositoryImpl(httpClient)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSubmitLoginCredentialsUseCase(authRepository: AuthRepository): SubmitLoginCredentialsUseCase {
|
||||
return SubmitLoginCredentialsUseCase(authRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideValidateLoginCredentialsUseCase() = ValidateLoginCredentialsUseCase()
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.SubmitAccountRegistrationRequestUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.SubmitLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.ValidateLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.ValidateNewAccountRegistrationUseCase
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
object AuthScreensDiModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSubmitLoginCredentialsUseCase(authRepository: AuthRepository): SubmitLoginCredentialsUseCase {
|
||||
return SubmitLoginCredentialsUseCase(authRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideValidateLoginCredentialsUseCase() = ValidateLoginCredentialsUseCase()
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideValidateNewAccountRegistrationUseCase() = ValidateNewAccountRegistrationUseCase()
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSubmitAccountRegistrationUseCase(userRepository: UserRepository): SubmitAccountRegistrationRequestUseCase {
|
||||
return SubmitAccountRegistrationRequestUseCase(userRepository)
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.domain.usecase
|
||||
|
||||
import android.util.Log
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
|
||||
|
||||
class SubmitLoginCredentialsUseCase(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(emailAddress: String, password: String): AuthLoginResponse {
|
||||
val credentials = AuthLoginRequest(emailAddress, password)
|
||||
|
||||
try {
|
||||
val response = authRepository.submitLoginRequest(credentials)
|
||||
Log.d("AuthScreen", "Login successful! $response")
|
||||
return response
|
||||
} catch (err: Throwable) {
|
||||
// TODO...
|
||||
Log.e("AuthScreen", err.message!!)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,7 +39,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import ing.bikeshedengineer.debtpirate.R
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
|
@ -67,7 +66,7 @@ fun LoginScreen(
|
|||
|
||||
LoginComponent(
|
||||
emailAddress = emailAddress,
|
||||
onUpdateEmailAddress = viewModel::updateUsername,
|
||||
onUpdateEmailAddress = viewModel::updateEmailAddress,
|
||||
password = password,
|
||||
onUpdatePassword = viewModel::updatePassword,
|
||||
submitLoginRequest = viewModel::submitLoginRequest
|
||||
|
@ -128,6 +127,7 @@ private fun LoginComponent(
|
|||
},
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Send
|
||||
|
|
|
@ -5,10 +5,9 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import ing.bikeshedengineer.debtpirate.PrefsDataStore
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.LoginCredentialsValidationResult
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.LoginCredentialsValidationResult
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.SubmitLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.ValidateLoginCredentialsUseCase
|
||||
import ing.bikeshedengineer.debtpirate.domain.model.Token
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Destination
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Navigator
|
||||
|
@ -50,8 +49,8 @@ class LoginScreenViewModel @Inject constructor(
|
|||
private val _password = MutableStateFlow("")
|
||||
val password = _password.asStateFlow()
|
||||
|
||||
fun updateUsername(username: String) {
|
||||
_emailAddress.value = username
|
||||
fun updateEmailAddress(emailAddress: String) {
|
||||
_emailAddress.value = emailAddress
|
||||
}
|
||||
|
||||
fun updatePassword(password: String) {
|
||||
|
|
|
@ -26,6 +26,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
@ -35,6 +37,7 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
|
@ -59,7 +62,41 @@ fun RegistrationScreen(viewModel: RegistrationScreenViewModel = hiltViewModel<Re
|
|||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
RegistrationComponent()
|
||||
val emailAddress = viewModel.emailAddress.collectAsState()
|
||||
val emailAddressError = viewModel.emailAddressError.collectAsState()
|
||||
val isInvalidEmailAddress = viewModel.isInvalidEmailAddress.collectAsState(false)
|
||||
|
||||
val name = viewModel.name.collectAsState()
|
||||
val nameError = viewModel.nameError.collectAsState()
|
||||
val isInvalidName = viewModel.isInvalidName.collectAsState(false)
|
||||
|
||||
val password = viewModel.password.collectAsState()
|
||||
val passwordError = viewModel.passwordError.collectAsState()
|
||||
val isInvalidPassword = viewModel.isInvalidPassword.collectAsState(false)
|
||||
|
||||
val confirmPassword = viewModel.confirmPassword.collectAsState()
|
||||
val confirmPasswordError = viewModel.confirmPasswordError.collectAsState()
|
||||
val isInvalidConfirmPassword = viewModel.isInvalidConfirmPassword.collectAsState(false)
|
||||
|
||||
RegistrationComponent(
|
||||
emailAddress = emailAddress,
|
||||
emailAddressError = emailAddressError,
|
||||
isInvalidEmailAddress = isInvalidEmailAddress,
|
||||
updateEmailAddress = viewModel::updateEmailAddress,
|
||||
name = name,
|
||||
nameError = nameError,
|
||||
isInvalidName = isInvalidName,
|
||||
updateName = viewModel::updateName,
|
||||
password = password,
|
||||
passwordError = passwordError,
|
||||
isInvalidPassword = isInvalidPassword,
|
||||
updatePassword = viewModel::updatePassword,
|
||||
confirmPassword = confirmPassword,
|
||||
confirmPasswordError = confirmPasswordError,
|
||||
isInvalidConfirmPassword = isInvalidConfirmPassword,
|
||||
updateConfirmPassword = viewModel::updateConfirmPassword,
|
||||
registerNewAccount = viewModel::registerNewAccount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,44 +128,68 @@ private fun RegistrationTopAppBar(onNavigateUp: () -> Unit, modifier: Modifier =
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RegistrationComponent(modifier: Modifier = Modifier) {
|
||||
private fun RegistrationComponent(
|
||||
emailAddress: State<String>,
|
||||
emailAddressError: State<String>,
|
||||
isInvalidEmailAddress: State<Boolean>,
|
||||
updateEmailAddress: (String) -> Unit,
|
||||
name: State<String>,
|
||||
nameError: State<String>,
|
||||
isInvalidName: State<Boolean>,
|
||||
updateName: (String) -> Unit,
|
||||
password: State<String>,
|
||||
passwordError: State<String>,
|
||||
isInvalidPassword: State<Boolean>,
|
||||
updatePassword: (String) -> Unit,
|
||||
confirmPassword: State<String>,
|
||||
confirmPasswordError: State<String>,
|
||||
isInvalidConfirmPassword: State<Boolean>,
|
||||
updateConfirmPassword: (String) -> Unit,
|
||||
registerNewAccount: (String, String, String, String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
value = emailAddress.value,
|
||||
label = { Text(text = stringResource(R.string.auth__email)) },
|
||||
placeholder = { Text(text = stringResource(R.string.auth__email)) },
|
||||
supportingText = { Text(text = emailAddressError.value) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Mail, stringResource(R.string.auth__email)) },
|
||||
singleLine = true,
|
||||
isError = isInvalidEmailAddress.value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
onValueChange = {},
|
||||
onValueChange = updateEmailAddress,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
value = name.value,
|
||||
label = { Text(text = stringResource(R.string.auth__name)) },
|
||||
placeholder = { Text(text = stringResource(R.string.auth__name)) },
|
||||
supportingText = { Text(text = nameError.value) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Person, stringResource(R.string.auth__name)) },
|
||||
singleLine = true,
|
||||
isError = isInvalidName.value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Words,
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
onValueChange = {},
|
||||
onValueChange = updateName,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(PaddingValues(top = 4.dp))
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
value = password.value,
|
||||
label = { Text(text = stringResource(R.string.auth__password)) },
|
||||
placeholder = { Text(text = stringResource(R.string.auth__password)) },
|
||||
supportingText = { Text(text = passwordError.value) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Password,
|
||||
|
@ -136,21 +197,24 @@ private fun RegistrationComponent(modifier: Modifier = Modifier) {
|
|||
)
|
||||
},
|
||||
singleLine = true,
|
||||
isError = isInvalidPassword.value,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
onValueChange = {},
|
||||
onValueChange = updatePassword,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(PaddingValues(top = 4.dp))
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
value = confirmPassword.value,
|
||||
label = { Text(text = stringResource(R.string.auth__confirmPassword)) },
|
||||
placeholder = { Text(text = stringResource(R.string.auth__confirmPassword)) },
|
||||
supportingText = { Text(text = confirmPasswordError.value) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Password,
|
||||
|
@ -158,12 +222,14 @@ private fun RegistrationComponent(modifier: Modifier = Modifier) {
|
|||
)
|
||||
},
|
||||
singleLine = true,
|
||||
isError = isInvalidConfirmPassword.value,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
onValueChange = {},
|
||||
onValueChange = updateConfirmPassword,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(PaddingValues(top = 4.dp))
|
||||
|
@ -190,14 +256,33 @@ private fun RegistrationComponent(modifier: Modifier = Modifier) {
|
|||
softWrap = true
|
||||
)
|
||||
|
||||
RegisterButton()
|
||||
RegisterButton(
|
||||
emailAddress,
|
||||
name,
|
||||
password,
|
||||
confirmPassword,
|
||||
registerNewAccount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RegisterButton() {
|
||||
private fun RegisterButton(
|
||||
emailAddress: State<String>,
|
||||
name: State<String>,
|
||||
password: State<String>,
|
||||
confirmPassword: State<String>,
|
||||
registerNewAccount: (String, String, String, String) -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = { },
|
||||
onClick = {
|
||||
registerNewAccount(
|
||||
emailAddress.value,
|
||||
name.value,
|
||||
password.value,
|
||||
confirmPassword.value
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
|
|
|
@ -3,17 +3,140 @@ package ing.bikeshedengineer.debtpirate.auth.presentation.register
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.NewAccountRegistrationValidationResult
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.SubmitAccountRegistrationRequestUseCase
|
||||
import ing.bikeshedengineer.debtpirate.auth.usecase.ValidateNewAccountRegistrationUseCase
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Navigator
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RegistrationScreenViewModel @Inject constructor(
|
||||
private val navigator: Navigator
|
||||
private val navigator: Navigator,
|
||||
private val validateNewAccount: ValidateNewAccountRegistrationUseCase,
|
||||
private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase
|
||||
) : ViewModel() {
|
||||
fun navigateUp() {
|
||||
viewModelScope.launch {
|
||||
navigator.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
private val _emailAddress = MutableStateFlow("")
|
||||
val emailAddress = _emailAddress.asStateFlow()
|
||||
|
||||
private val _emailAddressError = MutableStateFlow("")
|
||||
val emailAddressError = _emailAddressError.asStateFlow()
|
||||
val isInvalidEmailAddress = _emailAddressError.asStateFlow().map { it.isNotEmpty() }
|
||||
|
||||
private val _name = MutableStateFlow("")
|
||||
val name = _name.asStateFlow()
|
||||
|
||||
private val _nameError = MutableStateFlow("")
|
||||
val nameError = _nameError.asStateFlow()
|
||||
val isInvalidName = _nameError.asStateFlow().map { it.isNotEmpty() }
|
||||
|
||||
private val _password = MutableStateFlow("")
|
||||
val password = _password.asStateFlow()
|
||||
|
||||
private val _passwordError = MutableStateFlow("")
|
||||
val passwordError = _passwordError.asStateFlow()
|
||||
val isInvalidPassword = _passwordError.asStateFlow().map { it.isNotEmpty() }
|
||||
|
||||
private val _confirmPassword = MutableStateFlow("")
|
||||
val confirmPassword = _confirmPassword.asStateFlow()
|
||||
|
||||
private val _confirmPasswordError = MutableStateFlow("")
|
||||
val confirmPasswordError = _confirmPasswordError.asStateFlow()
|
||||
val isInvalidConfirmPassword = _confirmPasswordError.asStateFlow().map { it.isNotEmpty() }
|
||||
|
||||
fun updateEmailAddress(emailAddress: String) {
|
||||
_emailAddress.value = emailAddress
|
||||
}
|
||||
|
||||
fun updateName(name: String) {
|
||||
_name.value = name
|
||||
}
|
||||
|
||||
fun updatePassword(password: String) {
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
fun updateConfirmPassword(confirmPassword: String) {
|
||||
_confirmPassword.value = confirmPassword
|
||||
}
|
||||
|
||||
fun registerNewAccount(
|
||||
emailAddress: String,
|
||||
name: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
) {
|
||||
val fieldsAreValid =
|
||||
validateRegistrationFields(emailAddress, name, password, confirmPassword)
|
||||
if (fieldsAreValid) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = submitAccountRegistrationRequest(emailAddress, name, confirmPassword)
|
||||
} catch (err: Throwable) {
|
||||
// TODO...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateRegistrationFields(
|
||||
emailAddress: String,
|
||||
name: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
): Boolean {
|
||||
val validationResults = validateNewAccount(emailAddress, name, password, confirmPassword)
|
||||
validationResults.forEach {
|
||||
if (it == NewAccountRegistrationValidationResult.EmptyEmailAddressField) {
|
||||
_emailAddressError.value = "Enter an email address"
|
||||
} else if (it == NewAccountRegistrationValidationResult.InvalidEmailAddress) {
|
||||
_emailAddressError.value = "Enter a valid email address"
|
||||
} else if (it == NewAccountRegistrationValidationResult.ValidEmailAddress) {
|
||||
_emailAddressError.value = ""
|
||||
}
|
||||
|
||||
if (it == NewAccountRegistrationValidationResult.EmptyNameField) {
|
||||
_nameError.value = "Enter your name"
|
||||
} else if (it == NewAccountRegistrationValidationResult.ValidName) {
|
||||
_nameError.value = ""
|
||||
}
|
||||
|
||||
if (it == NewAccountRegistrationValidationResult.EmptyPasswordField) {
|
||||
_passwordError.value = "Enter a password"
|
||||
} else if (it == NewAccountRegistrationValidationResult.PasswordTooShort) {
|
||||
_passwordError.value = "Password must be more than 8 characters"
|
||||
} else if (it == NewAccountRegistrationValidationResult.ValidPassword) {
|
||||
_passwordError.value = ""
|
||||
}
|
||||
|
||||
if (it == NewAccountRegistrationValidationResult.EmptyConfirmPasswordField) {
|
||||
_confirmPasswordError.value = "Enter a password"
|
||||
} else if (it == NewAccountRegistrationValidationResult.PasswordsDontMatch) {
|
||||
_confirmPasswordError.value = "Passwords don't match"
|
||||
} else if (it == NewAccountRegistrationValidationResult.ValidConfirmPassword) {
|
||||
_confirmPasswordError.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
return validationResults.filter {
|
||||
when (it) {
|
||||
NewAccountRegistrationValidationResult.ValidEmailAddress,
|
||||
NewAccountRegistrationValidationResult.ValidName,
|
||||
NewAccountRegistrationValidationResult.ValidPassword,
|
||||
NewAccountRegistrationValidationResult.ValidConfirmPassword
|
||||
-> false
|
||||
|
||||
else -> true
|
||||
}
|
||||
}.isEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.usecase
|
||||
|
||||
import android.util.Log
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
||||
|
||||
class SubmitAccountRegistrationRequestUseCase(private val userRepository: UserRepository) {
|
||||
suspend operator fun invoke(
|
||||
emailAddress: String,
|
||||
name: String,
|
||||
password: String
|
||||
): UserCreatePostResponse {
|
||||
val request = UserCreatePostRequest(emailAddress, name, password)
|
||||
|
||||
try {
|
||||
val response = userRepository.submitCreateUserRequest(request)
|
||||
Log.d("RegistrationScreen", "Account registration successful! $response")
|
||||
return response
|
||||
} catch (err: Throwable) {
|
||||
// TODO...
|
||||
Log.e("RegistrationScreen", "$err")
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.usecase
|
||||
|
||||
import android.util.Log
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||
|
||||
class SubmitLoginCredentialsUseCase(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(emailAddress: String, password: String): AuthLoginPostResponse {
|
||||
val credentials = AuthLoginPostRequest(emailAddress, password)
|
||||
|
||||
try {
|
||||
val response = authRepository.submitAuthLoginRequest(credentials)
|
||||
Log.d("AuthScreen", "Login successful! $response")
|
||||
return response
|
||||
} catch (err: Throwable) {
|
||||
// TODO...
|
||||
Log.e("AuthScreen", err.message!!)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.domain.usecase
|
||||
package ing.bikeshedengineer.debtpirate.auth.usecase
|
||||
|
||||
sealed class LoginCredentialsValidationResult {
|
||||
object EmptyCredentials : LoginCredentialsValidationResult()
|
|
@ -0,0 +1,59 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.usecase
|
||||
|
||||
import android.util.Patterns
|
||||
|
||||
enum class NewAccountRegistrationValidationResult {
|
||||
EmptyEmailAddressField,
|
||||
InvalidEmailAddress,
|
||||
ValidEmailAddress,
|
||||
EmptyNameField,
|
||||
ValidName,
|
||||
EmptyPasswordField,
|
||||
PasswordTooShort,
|
||||
ValidPassword,
|
||||
EmptyConfirmPasswordField,
|
||||
PasswordsDontMatch,
|
||||
ValidConfirmPassword,
|
||||
}
|
||||
|
||||
class ValidateNewAccountRegistrationUseCase {
|
||||
operator fun invoke(
|
||||
emailAddress: String,
|
||||
name: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
): List<NewAccountRegistrationValidationResult> {
|
||||
var resultsList = mutableListOf<NewAccountRegistrationValidationResult>()
|
||||
if (emailAddress.isEmpty()) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.EmptyEmailAddressField)
|
||||
} else if (!Patterns.EMAIL_ADDRESS.matcher(emailAddress).matches()) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.InvalidEmailAddress)
|
||||
} else {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.ValidEmailAddress)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.EmptyNameField)
|
||||
} else {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.ValidName)
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.EmptyPasswordField)
|
||||
} else if (password.length <= 8) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.PasswordTooShort)
|
||||
} else {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.ValidPassword)
|
||||
}
|
||||
|
||||
if (confirmPassword.isEmpty()) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.EmptyConfirmPasswordField)
|
||||
} else if (confirmPassword != password) {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.PasswordsDontMatch)
|
||||
} else {
|
||||
resultsList.add(NewAccountRegistrationValidationResult.ValidConfirmPassword)
|
||||
}
|
||||
|
||||
return resultsList.toList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.endpoint
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthEndpoint {
|
||||
@POST("auth/login")
|
||||
suspend fun submitAuthLoginRequest(@Body credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>>
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.endpoint
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface UserEndpoint {
|
||||
@POST("user")
|
||||
suspend fun submitUserPostRequest(@Body request: UserCreatePostRequest): Response<ApiResponse<UserCreatePostResponse>>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
|
||||
|
||||
data class AuthLoginPostRequest(val emailAddress: String, val password: String)
|
|
@ -0,0 +1,9 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
|
||||
|
||||
data class AuthLoginPostResponse(
|
||||
val userId: Int,
|
||||
val session: AuthLoginPostResponseTokenData,
|
||||
val auth: AuthLoginPostResponseTokenData
|
||||
)
|
||||
|
||||
data class AuthLoginPostResponseTokenData(val token: String, val expiresAt: String)
|
|
@ -0,0 +1,3 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.model.user
|
||||
|
||||
data class UserCreatePostRequest(val email: String, val name: String, val password: String)
|
|
@ -0,0 +1,12 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.remote.model.user
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import ing.bikeshedengineer.debtpirate.domain.adapter.OffsetDateTimeAdapter
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class UserCreatePostResponse(
|
||||
val userId: Int,
|
||||
@JsonAdapter(OffsetDateTimeAdapter::class)
|
||||
val expiresAt: OffsetDateTime,
|
||||
val sessionToken: String? = null
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.repository
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||
|
||||
|
||||
interface AuthRepository {
|
||||
suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): AuthLoginPostResponse
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package ing.bikeshedengineer.debtpirate.data.repository
|
||||
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
||||
|
||||
interface UserRepository {
|
||||
suspend fun submitCreateUserRequest(request: UserCreatePostRequest): UserCreatePostResponse
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ing.bikeshedengineer.debtpirate.di.repository
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class AuthRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideAuthRepository(httpClient: Retrofit): AuthRepository {
|
||||
return AuthRepositoryImpl(httpClient)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ing.bikeshedengineer.debtpirate.di.repository
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
||||
import ing.bikeshedengineer.debtpirate.domain.repository.UserRepositoryImpl
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class UserRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideUserRepository(retrofit: Retrofit): UserRepository {
|
||||
return UserRepositoryImpl(retrofit)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package ing.bikeshedengineer.debtpirate.domain.adapter
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
|
||||
class OffsetDateTimeAdapter : TypeAdapter<OffsetDateTime>() {
|
||||
val rfc3339 = DateTimeFormatterBuilder()
|
||||
.append(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
.appendLiteral('T')
|
||||
.append(DateTimeFormatter.ISO_LOCAL_TIME)
|
||||
.optionalStart()
|
||||
.appendOffset("+HHMM", "Z")
|
||||
.optionalEnd()
|
||||
.toFormatter()
|
||||
|
||||
override fun write(out: JsonWriter?, value: OffsetDateTime?) {
|
||||
if (value == null) {
|
||||
out?.nullValue()
|
||||
} else {
|
||||
out?.value(rfc3339.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(reader: JsonReader?): OffsetDateTime? {
|
||||
if (reader?.peek() == JsonToken.NULL) {
|
||||
reader.nextNull()
|
||||
return null;
|
||||
} else {
|
||||
return OffsetDateTime.parse(reader?.nextString(), rfc3339)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package ing.bikeshedengineer.debtpirate.auth.domain.repository
|
||||
package ing.bikeshedengineer.debtpirate.domain.repository
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.endpoint.AuthEndpoint
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse
|
||||
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.Retrofit
|
||||
|
@ -14,9 +14,9 @@ import retrofit2.Retrofit
|
|||
class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
|
||||
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
|
||||
|
||||
override suspend fun submitLoginRequest(credentials: AuthLoginRequest): AuthLoginResponse {
|
||||
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): AuthLoginPostResponse {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = authEndpoint.submitLoginRequest(credentials)
|
||||
val response = authEndpoint.submitAuthLoginRequest(credentials)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body()
|
|
@ -0,0 +1,36 @@
|
|||
package ing.bikeshedengineer.debtpirate.domain.repository
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.UserEndpoint
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.Retrofit
|
||||
|
||||
class UserRepositoryImpl(httpClient: Retrofit) : UserRepository {
|
||||
private val userEndpoint: UserEndpoint = httpClient.create(UserEndpoint::class.java)
|
||||
|
||||
override suspend fun submitCreateUserRequest(request: UserCreatePostRequest): UserCreatePostResponse {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = userEndpoint.submitUserPostRequest(request)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body()
|
||||
Log.d("Registration", "$body")
|
||||
return@withContext body!!.data!!
|
||||
} else {
|
||||
val gson = Gson()
|
||||
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
||||
val body =
|
||||
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
||||
|
||||
throw Throwable(body!!.error!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package ing.bikeshedengineer.debtpirate.domain.usecase.pref
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import ing.bikeshedengineer.debtpirate.PrefsDataStore
|
||||
import ing.bikeshedengineer.debtpirate.navigation.Destination
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateCurrentRouteUseCase @Inject constructor(
|
||||
private val store: DataStore<PrefsDataStore>
|
||||
) {
|
||||
suspend operator fun invoke(destination: Destination) {
|
||||
store.updateData { data ->
|
||||
data.toBuilder()
|
||||
.setCurrentRoute(destination.toString())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
[versions]
|
||||
activityCompose = "1.9.3"
|
||||
agp = "8.7.1"
|
||||
agp = "8.7.2"
|
||||
appcompat = "1.7.0"
|
||||
composeBom = "2024.10.00"
|
||||
coreKtx = "1.13.1"
|
||||
composeBom = "2024.10.01"
|
||||
coreKtx = "1.15.0"
|
||||
datastore = "1.1.1"
|
||||
espressoCore = "3.6.1"
|
||||
hilt = "2.51.1"
|
||||
iconsExtended = "1.7.3"
|
||||
iconsExtended = "1.7.5"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
kotlin = "2.0.10"
|
||||
kotlinxSerializationJson = "1.7.1"
|
||||
lifecycleRuntimeCompose = "2.8.6"
|
||||
lifecycleRuntimeKtx = "2.8.6"
|
||||
lifecycleViewModelKtx = "2.8.6"
|
||||
lifecycleViewmodelCompose = "2.8.6"
|
||||
lifecycleRuntimeCompose = "2.8.7"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
lifecycleViewModelKtx = "2.8.7"
|
||||
lifecycleViewmodelCompose = "2.8.7"
|
||||
material = "1.12.0"
|
||||
material3 = "1.3.0"
|
||||
material3 = "1.4.0-alpha03"
|
||||
navigation = "2.8.3"
|
||||
protobuf = "0.9.4"
|
||||
protoLite = "3.21.11"
|
||||
|
|
Loading…
Add table
Reference in a new issue