Create new user registration processes

This commit is contained in:
Z. Charles Dziura 2024-11-08 12:38:37 -05:00
parent 149ce13e3e
commit 4847c1bb21
32 changed files with 581 additions and 175 deletions

View file

@ -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"

View file

@ -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) {

View file

@ -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>>
}

View file

@ -1,3 +0,0 @@
package ing.bikeshedengineer.debtpirate.auth.data.remote.model
data class AuthLoginRequest(val emailAddress: String, val password: String)

View file

@ -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)

View file

@ -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
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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()
) {

View file

@ -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()
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.auth.domain.usecase
package ing.bikeshedengineer.debtpirate.auth.usecase
sealed class LoginCredentialsValidationResult {
object EmptyCredentials : LoginCredentialsValidationResult()

View file

@ -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()
}
}

View file

@ -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>>
}

View file

@ -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>>
}

View file

@ -0,0 +1,3 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
data class AuthLoginPostRequest(val emailAddress: String, val password: String)

View file

@ -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)

View file

@ -0,0 +1,3 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.user
data class UserCreatePostRequest(val email: String, val name: String, val password: String)

View file

@ -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
)

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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()

View file

@ -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!!)
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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"